|
@@ -0,0 +1,130 @@
|
|
|
|
|
+"""The same clients and configuration keys must work in either deployment."""
|
|
|
|
|
+
|
|
|
|
|
+import copy
|
|
|
|
|
+import json
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+from tempfile import TemporaryDirectory
|
|
|
|
|
+import unittest
|
|
|
|
|
+
|
|
|
|
|
+from src.dms_schema import DEFAULT_SCHEMA, names
|
|
|
|
|
+from src.step3_question_answer.dms_company import DmsCompanyClient
|
|
|
|
|
+from src.step5_session_analysis.dms import DmsClient, DmsError, load_dms_config
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+MODES = (
|
|
|
|
|
+ ('http://example.invalid:8080', '', '', '/proxy_dms', 8080, 8080),
|
|
|
|
|
+ ('http://example.invalid', '10086', '10081', '/proxy_dms', 10086, 10081),
|
|
|
|
|
+ ('http://example.invalid:8080/proxy_dms', '10086', '', '/proxy_dms', 10086, 8080),
|
|
|
|
|
+ ('http://example.invalid:8080', '', '10081', '/proxy_dms', 8080, 10081),
|
|
|
|
|
+ ('http://example.invalid:8080/dms', '10086', '10081', '/dms', 10086, 10081),
|
|
|
|
|
+ ('http://example.invalid:8080/dms', '', '', '/dms', None, 8080),
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def config_for(base, auth, data, prefixed, company=False, token=''):
|
|
|
|
|
+ schema = copy.deepcopy(DEFAULT_SCHEMA)
|
|
|
|
|
+ # Deliberately vary every ID and actual field name; never depend on live data.
|
|
|
|
|
+ for i, spec in enumerate(schema['columns'].values()):
|
|
|
|
|
+ spec['id'] = 9000 + i if prefixed else 8000 + i
|
|
|
|
|
+ spec['fields'] = {key: ('c_' + key if prefixed else key) for key in spec['fields']}
|
|
|
|
|
+ with TemporaryDirectory() as directory:
|
|
|
|
|
+ path = Path(directory)
|
|
|
|
|
+ (path / 'schema.json').write_text(json.dumps(schema, ensure_ascii=False), encoding='utf-8')
|
|
|
|
|
+ (path / '.env').write_text(
|
|
|
|
|
+ f'DMS_BASE_URL={base}\nAUTH_PORT={auth}\nDMS_PORT={data}\n'
|
|
|
|
|
+ f'DMS_USERNAME=synthetic\nDMS_PASSWORD=synthetic\nDMS_TOKEN={token}\n', encoding='utf-8')
|
|
|
|
|
+ return load_dms_config(path / '.env', {}, schema_path=path / 'schema.json',
|
|
|
|
|
+ scope_names=names(schema, ('basic', 'honor')) if company else None)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class ConnectionModeTests(unittest.TestCase):
|
|
|
|
|
+ def test_same_config_keys_select_endpoints_and_both_field_layouts(self):
|
|
|
|
|
+ for base, auth, data, api_path, auth_port, data_port in MODES:
|
|
|
|
|
+ for prefixed in (False, True):
|
|
|
|
|
+ for company in (False, True):
|
|
|
|
|
+ with self.subTest(base=base, auth=auth, data=data, prefixed=prefixed, company=company):
|
|
|
|
|
+ config = config_for(base, auth, data, prefixed, company,
|
|
|
|
|
+ token='supplied' if auth_port is None else '')
|
|
|
|
|
+ calls = []
|
|
|
|
|
+ alias = 'basic' if company else 'sessions'
|
|
|
|
|
+ spec = config.schema['columns'][alias]
|
|
|
|
|
+ logical = 'c_name' if company else 'c_session_id'
|
|
|
|
|
+ actual = spec['fields'][logical]
|
|
|
|
|
+ def transport(url, form, headers):
|
|
|
|
|
+ calls.append((url, copy.deepcopy(form), dict(headers)))
|
|
|
|
|
+ if url.endswith('/user/login'):
|
|
|
|
|
+ return {'code': 200, 'message': 'renewed'}
|
|
|
|
|
+ self.assertEqual(headers['token'], 'supplied' if auth_port is None else 'renewed')
|
|
|
|
|
+ if url.endswith('/selectContentList'):
|
|
|
|
|
+ self.assertEqual(json.loads(form['search'])[0]['field'], actual)
|
|
|
|
|
+ return {'code': 200, 'content': {'count': 1, 'data': [
|
|
|
|
|
+ {'id': 'synthetic-row', actual: 'synthetic-value'}]}}
|
|
|
|
|
+ self.assertEqual(json.loads(form['content'])[actual], 'synthetic-value')
|
|
|
|
|
+ return {'code': 200, 'content': 'synthetic-row'}
|
|
|
|
|
+ client = (DmsCompanyClient if company else DmsClient)(config, transport)
|
|
|
|
|
+ client.columns[spec['id']] = {}
|
|
|
|
|
+ if auth_port is not None:
|
|
|
|
|
+ client._login()
|
|
|
|
|
+ self.assertEqual(calls[0][0], f'http://example.invalid:{auth_port}/proxy_oauth/user/login')
|
|
|
|
|
+ self.assertNotIn('token', calls[0][2])
|
|
|
|
|
+ rows = client.call('/content/selectContentList', {'columnId': spec['id'],
|
|
|
|
|
+ 'search': json.dumps([{'field': logical, 'searchType': 1,
|
|
|
|
|
+ 'content': {'value': 'synthetic-value'}}])})
|
|
|
|
|
+ self.assertEqual(rows['data'][0][logical], 'synthetic-value')
|
|
|
|
|
+ if not company:
|
|
|
|
|
+ for operation in ('addContent', 'updateContent'):
|
|
|
|
|
+ client.call('/content/' + operation, {'columnId': spec['id'],
|
|
|
|
|
+ 'content': json.dumps({logical: 'synthetic-value'})})
|
|
|
|
|
+ data_calls = [url for url, _, _ in calls if not url.endswith('/user/login')]
|
|
|
|
|
+ self.assertTrue(all(url.startswith(f'http://example.invalid:{data_port}{api_path}/')
|
|
|
|
|
+ for url in data_calls))
|
|
|
|
|
+
|
|
|
|
|
+ def test_both_empty_contracts_and_error_isolation_in_each_mode(self):
|
|
|
|
|
+ for base, auth, data, _, _, _ in MODES:
|
|
|
|
|
+ for company in (False, True):
|
|
|
|
|
+ config = config_for(base, auth, data, False, company, token='supplied')
|
|
|
|
|
+ identity = config.schema['columns']['basic' if company else 'sessions']['id']
|
|
|
|
|
+ for empty in ({'code': 200, 'content': {'count': 0, 'data': []}},
|
|
|
|
|
+ {'code': 202, 'message': '数据不存在', 'content': '数据不存在'}):
|
|
|
|
|
+ client = (DmsCompanyClient if company else DmsClient)(config, lambda *a: empty)
|
|
|
|
|
+ client.columns[identity] = {}
|
|
|
|
|
+ self.assertEqual(client.call('/content/selectContentList', {'columnId': identity}),
|
|
|
|
|
+ {'count': 0, 'data': []})
|
|
|
|
|
+ for invalid in ({'code': 202, 'message': 'different', 'content': '数据不存在'}, {'code': 201}):
|
|
|
|
|
+ client = (DmsCompanyClient if company else DmsClient)(config, lambda *a: invalid)
|
|
|
|
|
+ client.columns[identity] = {}
|
|
|
|
|
+ with self.assertRaises(DmsError):
|
|
|
|
|
+ client.call('/content/selectContentList', {'columnId': identity})
|
|
|
|
|
+
|
|
|
|
|
+ def test_token_refresh_uses_configured_auth_endpoint_and_is_bounded(self):
|
|
|
|
|
+ for base, auth, data, api_path, auth_port, data_port in MODES:
|
|
|
|
|
+ for failure in ('envelope', 'http401'):
|
|
|
|
|
+ config = config_for(base, auth, data, True, token='expired')
|
|
|
|
|
+ calls = []
|
|
|
|
|
+ def transport(url, form, headers):
|
|
|
|
|
+ calls.append(url)
|
|
|
|
|
+ if url.endswith('/user/login'):
|
|
|
|
|
+ return {'code': 200, 'message': 'renewed'}
|
|
|
|
|
+ if headers['token'] == 'expired':
|
|
|
|
|
+ if failure == 'http401':
|
|
|
|
|
+ raise DmsError('dms_token_invalid')
|
|
|
|
|
+ return {'code': 212}
|
|
|
|
|
+ return {'code': 200, 'content': 'ok'}
|
|
|
|
|
+ client = DmsClient(config, transport)
|
|
|
|
|
+ if auth_port is None:
|
|
|
|
|
+ with self.assertRaisesRegex(DmsError, 'dms_token_invalid'):
|
|
|
|
|
+ client.call('/column/getColumnById', {'id': config.sessions_id})
|
|
|
|
|
+ self.assertEqual(len(calls), 1)
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.assertEqual(client.call('/column/getColumnById', {'id': config.sessions_id}), 'ok')
|
|
|
|
|
+ self.assertEqual(len(calls), 3)
|
|
|
|
|
+ self.assertEqual(calls[1], f'http://example.invalid:{auth_port}/proxy_oauth/user/login')
|
|
|
|
|
+ self.assertEqual(calls[0], calls[2])
|
|
|
|
|
+
|
|
|
|
|
+ def test_direct_api_without_auth_port_never_guesses_a_login_address(self):
|
|
|
|
|
+ config = config_for('https://example.invalid/dms', '', '', False)
|
|
|
|
|
+ calls = []
|
|
|
|
|
+ client = DmsClient(config, lambda *args: calls.append(args))
|
|
|
|
|
+ with self.assertRaisesRegex(DmsError, 'dms_direct_api_requires_token'):
|
|
|
|
|
+ client.connect()
|
|
|
|
|
+ self.assertEqual(calls, [])
|