|
|
@@ -18,6 +18,18 @@ def client(tmp_path):
|
|
|
return DSClient(conf_path=str(conf))
|
|
|
|
|
|
|
|
|
+def _mock_resp(status_code=200, json_data=None, text=''):
|
|
|
+ """构造 requests.Response mock。json_data=None 表示 .json() 抛 ValueError。"""
|
|
|
+ r = MagicMock(status_code=status_code, text=text)
|
|
|
+ if json_data is None:
|
|
|
+ r.json.side_effect = ValueError('not json')
|
|
|
+ else:
|
|
|
+ r.json.return_value = json_data
|
|
|
+ return r
|
|
|
+
|
|
|
+
|
|
|
+# --- 初始化 ---
|
|
|
+
|
|
|
def test_init_loads_conf(client):
|
|
|
assert client.base_url == 'http://example/dolphinscheduler'
|
|
|
assert client.token == 'TEST_TOKEN'
|
|
|
@@ -41,40 +53,48 @@ def test_init_strips_trailing_slash(tmp_path):
|
|
|
assert c.base_url == 'http://example/dolphinscheduler'
|
|
|
|
|
|
|
|
|
+# --- GET ---
|
|
|
+
|
|
|
def test_get_2xx_returns_json(client):
|
|
|
- with patch.object(client.session, 'get') as mock_get:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.return_value = {'code': 0, 'data': 'ok'}
|
|
|
- mock_get.return_value = resp
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'code': 0, 'data': 'ok'})
|
|
|
assert client.get('/projects') == {'code': 0, 'data': 'ok'}
|
|
|
- mock_get.assert_called_once_with(
|
|
|
- 'http://example/dolphinscheduler/projects', params=None)
|
|
|
+ # 不传 params 时不应在 kwargs 出现 params
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'GET', 'http://example/dolphinscheduler/projects')
|
|
|
+
|
|
|
+
|
|
|
+def test_get_with_params(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {})
|
|
|
+ client.get('/projects', params={'pageNo': 1, 'pageSize': 10})
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'GET', 'http://example/dolphinscheduler/projects',
|
|
|
+ params={'pageNo': 1, 'pageSize': 10})
|
|
|
|
|
|
|
|
|
def test_get_strips_leading_slash(client):
|
|
|
"""path 带或不带前导 / 都拼成相同 URL。"""
|
|
|
- with patch.object(client.session, 'get') as mock_get:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.return_value = {}
|
|
|
- mock_get.return_value = resp
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {})
|
|
|
client.get('projects')
|
|
|
- assert mock_get.call_args[0][0] == 'http://example/dolphinscheduler/projects'
|
|
|
+ assert mock_req.call_args[0][1] == 'http://example/dolphinscheduler/projects'
|
|
|
|
|
|
|
|
|
def test_get_non_2xx_raises(client):
|
|
|
- with patch.object(client.session, 'get') as mock_get:
|
|
|
- mock_get.return_value = MagicMock(status_code=500, text='Internal Server Error')
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(500, json_data={}, text='Internal Server Error')
|
|
|
+ # 改 status_code 不影响 json mock
|
|
|
+ mock_req.return_value.status_code = 500
|
|
|
with pytest.raises(RuntimeError, match='GET .*failed rc=500'):
|
|
|
client.get('/projects')
|
|
|
|
|
|
|
|
|
def test_get_2xx_non_json_raises_friendly(client):
|
|
|
- """新特性:path 错被 SPA fallback 时抛 RuntimeError 含 raw text 前 200 字符。"""
|
|
|
- with patch.object(client.session, 'get') as mock_get:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.side_effect = ValueError('Expecting value')
|
|
|
- resp.text = '<!DOCTYPE html><html>fallback</html>'
|
|
|
- mock_get.return_value = resp
|
|
|
+ """path 错被 SPA fallback 时抛 RuntimeError 含 raw text 前 200 字符。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, json_data=None,
|
|
|
+ text='<!DOCTYPE html><html>fallback</html>')
|
|
|
with pytest.raises(RuntimeError) as exc:
|
|
|
client.get('/wrong/path')
|
|
|
msg = str(exc.value)
|
|
|
@@ -84,56 +104,147 @@ def test_get_2xx_non_json_raises_friendly(client):
|
|
|
|
|
|
|
|
|
def test_get_2xx_non_json_truncates_to_200(client):
|
|
|
- with patch.object(client.session, 'get') as mock_get:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.side_effect = ValueError()
|
|
|
- resp.text = 'X' * 500
|
|
|
- mock_get.return_value = resp
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, json_data=None, text='X' * 500)
|
|
|
with pytest.raises(RuntimeError) as exc:
|
|
|
client.get('/p')
|
|
|
- # 错误信息含 200 个 X,不含 500 个
|
|
|
assert 'X' * 200 in str(exc.value)
|
|
|
assert 'X' * 201 not in str(exc.value)
|
|
|
|
|
|
|
|
|
-def test_post_2xx_returns_json(client):
|
|
|
- with patch.object(client.session, 'post') as mock_post:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.return_value = {'ok': True}
|
|
|
- mock_post.return_value = resp
|
|
|
+# --- POST ---
|
|
|
+
|
|
|
+def test_post_json_body(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'ok': True})
|
|
|
assert client.post('/foo', json_body={'k': 'v'}) == {'ok': True}
|
|
|
- mock_post.assert_called_once_with(
|
|
|
- 'http://example/dolphinscheduler/foo', json={'k': 'v'})
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'POST', 'http://example/dolphinscheduler/foo', json={'k': 'v'})
|
|
|
+
|
|
|
+
|
|
|
+def test_post_form_data_uses_data_kwarg(client):
|
|
|
+ """form_data 走 requests data= kwarg(form-encoded)。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'ok': True})
|
|
|
+ client.post('/foo', form_data={'name': 'x', 'desc': 'hi'})
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'POST', 'http://example/dolphinscheduler/foo',
|
|
|
+ data={'name': 'x', 'desc': 'hi'})
|
|
|
+
|
|
|
+
|
|
|
+def test_post_no_body(client):
|
|
|
+ """不传 body 时 kwargs 不应有 data / json。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {})
|
|
|
+ client.post('/foo')
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'POST', 'http://example/dolphinscheduler/foo')
|
|
|
|
|
|
|
|
|
def test_post_2xx_non_json_raises_friendly(client):
|
|
|
- with patch.object(client.session, 'post') as mock_post:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.side_effect = ValueError()
|
|
|
- resp.text = 'NOT JSON'
|
|
|
- mock_post.return_value = resp
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, json_data=None, text='NOT JSON')
|
|
|
with pytest.raises(RuntimeError, match='POST.*返回非 JSON'):
|
|
|
client.post('/foo')
|
|
|
|
|
|
|
|
|
def test_post_non_2xx_raises(client):
|
|
|
- with patch.object(client.session, 'post') as mock_post:
|
|
|
- mock_post.return_value = MagicMock(status_code=403, text='Forbidden')
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(403, {}, text='Forbidden')
|
|
|
+ mock_req.return_value.status_code = 403
|
|
|
with pytest.raises(RuntimeError, match='POST .*failed rc=403'):
|
|
|
client.post('/foo')
|
|
|
|
|
|
|
|
|
-def test_post_form_data_uses_data_kwarg(client):
|
|
|
- """form_data 走 requests data= kwarg(form-encoded),不走 json="""
|
|
|
- with patch.object(client.session, 'post') as mock_post:
|
|
|
- resp = MagicMock(status_code=200)
|
|
|
- resp.json.return_value = {'ok': True}
|
|
|
- mock_post.return_value = resp
|
|
|
- client.post('/foo', form_data={'name': 'x', 'desc': 'hi'})
|
|
|
- mock_post.assert_called_once_with(
|
|
|
- 'http://example/dolphinscheduler/foo', data={'name': 'x', 'desc': 'hi'})
|
|
|
-
|
|
|
-
|
|
|
def test_post_json_and_form_mutex(client):
|
|
|
with pytest.raises(ValueError, match='互斥'):
|
|
|
client.post('/foo', json_body={'k': 'v'}, form_data={'k': 'v'})
|
|
|
+
|
|
|
+
|
|
|
+# --- PUT(与 POST 对称) ---
|
|
|
+
|
|
|
+def test_put_json_body(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'ok': True})
|
|
|
+ assert client.put('/foo/1', json_body={'k': 'v'}) == {'ok': True}
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'PUT', 'http://example/dolphinscheduler/foo/1', json={'k': 'v'})
|
|
|
+
|
|
|
+
|
|
|
+def test_put_form_data(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'ok': True})
|
|
|
+ client.put('/foo/1', form_data={'name': 'x'})
|
|
|
+ mock_req.assert_called_once_with(
|
|
|
+ 'PUT', 'http://example/dolphinscheduler/foo/1', data={'name': 'x'})
|
|
|
+
|
|
|
+
|
|
|
+def test_put_non_2xx_raises(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(400, {}, text='Bad Request')
|
|
|
+ mock_req.return_value.status_code = 400
|
|
|
+ with pytest.raises(RuntimeError, match='PUT .*failed rc=400'):
|
|
|
+ client.put('/foo/1')
|
|
|
+
|
|
|
+
|
|
|
+def test_put_2xx_non_json_raises(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, json_data=None, text='HTML')
|
|
|
+ with pytest.raises(RuntimeError, match='PUT.*返回非 JSON'):
|
|
|
+ client.put('/foo/1')
|
|
|
+
|
|
|
+
|
|
|
+def test_put_json_and_form_mutex(client):
|
|
|
+ with pytest.raises(ValueError, match='互斥'):
|
|
|
+ client.put('/foo/1', json_body={'k': 'v'}, form_data={'k': 'v'})
|
|
|
+
|
|
|
+
|
|
|
+# --- expect_business_ok:业务码 != 0 抛错 ---
|
|
|
+
|
|
|
+def test_business_ok_pass_when_code_zero(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'code': 0, 'data': {'id': 1}})
|
|
|
+ result = client.post('/foo', form_data={}, expect_business_ok=True)
|
|
|
+ assert result == {'code': 0, 'data': {'id': 1}}
|
|
|
+
|
|
|
+
|
|
|
+def test_business_ok_raises_when_code_nonzero(client):
|
|
|
+ """2xx + code != 0 抛 RuntimeError 含完整 response。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(
|
|
|
+ 200, {'code': 10105, 'msg': "Required request parameter 'name' is not present"})
|
|
|
+ with pytest.raises(RuntimeError) as exc:
|
|
|
+ client.post('/foo', form_data={}, expect_business_ok=True)
|
|
|
+ msg = str(exc.value)
|
|
|
+ assert 'POST /foo' in msg
|
|
|
+ assert '业务码 != 0' in msg
|
|
|
+ assert '10105' in msg
|
|
|
+
|
|
|
+
|
|
|
+def test_business_ok_off_default_passes_nonzero_code(client):
|
|
|
+ """expect_business_ok=False(默认)时 code != 0 不抛错,原样返。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'code': 50036, 'msg': 'oops'})
|
|
|
+ result = client.post('/foo', form_data={})
|
|
|
+ assert result == {'code': 50036, 'msg': 'oops'}
|
|
|
+
|
|
|
+
|
|
|
+def test_business_ok_works_for_get(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'code': 1, 'msg': 'fail'})
|
|
|
+ with pytest.raises(RuntimeError, match='GET .*业务码 != 0'):
|
|
|
+ client.get('/projects', expect_business_ok=True)
|
|
|
+
|
|
|
+
|
|
|
+def test_business_ok_works_for_put(client):
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, {'code': 7, 'msg': 'denied'})
|
|
|
+ with pytest.raises(RuntimeError, match='PUT .*业务码 != 0'):
|
|
|
+ client.put('/foo/1', form_data={}, expect_business_ok=True)
|
|
|
+
|
|
|
+
|
|
|
+def test_business_ok_skips_non_dict_response(client):
|
|
|
+ """响应不是 dict(罕见,如 list)时 business_ok 不应抛错。"""
|
|
|
+ with patch.object(client.session, 'request') as mock_req:
|
|
|
+ mock_req.return_value = _mock_resp(200, [1, 2, 3])
|
|
|
+ assert client.get('/p', expect_business_ok=True) == [1, 2, 3]
|