92 lines
5.5 KiB
Python
92 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
||
"""通过真实 HTTP 验证应用接入,不打印或记录密码、Cookie 与 Token。"""
|
||
import copy
|
||
import http.cookiejar
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import urllib.error
|
||
import urllib.request
|
||
import uuid
|
||
|
||
base = os.environ['ORBIT_E2E_URL'].rstrip('/')
|
||
user = os.environ['ORBIT_E2E_USER']
|
||
password = Path(os.environ['ORBIT_E2E_PASSWORD_FILE']).read_text().strip()
|
||
jar = http.cookiejar.CookieJar()
|
||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
||
|
||
|
||
def call(path, body=None, token=None, headers=None, expected=200, session=True):
|
||
hdr = {'Accept': 'application/json', **(headers or {})}
|
||
if body is not None:
|
||
hdr['Content-Type'] = 'application/json'
|
||
if path.startswith('/console/applications'):
|
||
hdr.setdefault('X-Orbit-Console', '1')
|
||
hdr.setdefault('Origin', base)
|
||
if token:
|
||
hdr['Authorization'] = 'Bearer ' + token
|
||
req = urllib.request.Request(base + path, data=None if body is None else json.dumps(body).encode(), headers=hdr)
|
||
try:
|
||
response = (opener.open if session else urllib.request.urlopen)(req, timeout=15)
|
||
except urllib.error.HTTPError as error:
|
||
response = error
|
||
with response:
|
||
status = response.status
|
||
value = json.load(response)
|
||
assert status == expected, f'{path}: HTTP {status}, expected {expected}; code={value.get("error", {}).get("code")}'
|
||
return value.get('data', value)
|
||
|
||
|
||
call('/console/applications', expected=401, session=False)
|
||
call('/console/auth/login', {'username': user, 'password': password})
|
||
assert call('/console/auth/session')['authenticated']
|
||
call('/console/applications/prepare', {}, headers={'Origin': 'https://other.invalid'}, expected=403)
|
||
name = 'access-demo-' + uuid.uuid4().hex[:8]
|
||
request = {'kind': 'CREATE', 'application': name, 'namespace': 'default', 'environment': 'test', 'queue': 'integration', 'capabilities': ['worker', 'events', 'schedules', 'workflows'], 'valid_days': 30, 'expected_revision': '0', 'reason': 'application onboarding acceptance'}
|
||
op = call('/console/applications/prepare', request)
|
||
# 固定操作在提交前写入测试私密目录,以便提交结果不明时核对。
|
||
private = os.environ.get('ORBIT_E2E_APPLICATION_DIR')
|
||
if private:
|
||
os.umask(0o077)
|
||
Path(private).mkdir(parents=True, exist_ok=True)
|
||
Path(private, 'create.operation.json').write_text(json.dumps(op))
|
||
first = call('/console/applications/apply', op)
|
||
assert not first['deduplicated']
|
||
assert call('/console/applications/apply', op)['deduplicated']
|
||
changed = copy.deepcopy(op)
|
||
changed['input']['reason'] = 'changed'
|
||
call('/console/applications/apply', changed, expected=409)
|
||
page = call('/console/applications')
|
||
assert any(x['namespace_id'] == op['namespace_id'] for x in page['items'])
|
||
detail = call('/console/applications/' + op['namespace_id'])
|
||
assert op['token'] not in json.dumps(detail)
|
||
queue = detail['queues'][0]['id']
|
||
assert queue == op['queue_id']
|
||
call('/console/applications', token=op['token'], expected=401, session=False)
|
||
call('/v1/queues/' + queue, token=op['token'], session=False)
|
||
sub = call('/v1/subscriptions', {'name': 'result-example', 'queue_id': queue}, token=op['token'], headers={'Idempotency-Key': 'onboarding-subscription'}, expected=201, session=False)
|
||
subscription_id = sub['subscription']['id']
|
||
task = call('/v1/tasks', {'queue_id': queue, 'handler': 'onboarding.noop', 'handler_version': 'v1', 'payload_version': 'v1', 'payload': {}}, token=op['token'], headers={'Idempotency-Key': 'onboarding-task'}, expected=201, session=False)
|
||
task_id = task['task']['id']
|
||
call('/v1/tasks/' + task_id + ':cancel', {'expected_revision': task['task']['revision'], 'reason': 'acceptance complete'}, token=op['token'], headers={'Idempotency-Key': 'onboarding-cancel'}, session=False)
|
||
rotate = call('/console/applications/prepare', {'kind': 'ROTATE', 'namespace_id': op['namespace_id'], 'credential_id': op['credential_id'], 'expected_revision': '1', 'valid_days': 30, 'reason': 'rotation acceptance'})
|
||
if private:
|
||
Path(private, 'rotate.operation.json').write_text(json.dumps(rotate))
|
||
assert call('/console/applications/apply', rotate)['revision'] == '2'
|
||
assert call('/console/applications/apply', rotate)['deduplicated']
|
||
call('/v1/queues/' + queue, token=op['token'], expected=401, session=False)
|
||
call('/v1/queues/' + queue, token=rotate['token'], session=False)
|
||
current_task = call('/v1/tasks/' + task_id, token=rotate['token'], session=False)
|
||
assert current_task['state'] == 'CANCELLED'
|
||
current_sub = call('/v1/subscriptions/' + subscription_id, token=rotate['token'], session=False)
|
||
assert current_sub['id'] == subscription_id
|
||
# 不让联调示例遗留启用的订阅,历史和稳定凭据身份保持不变。
|
||
call('/v1/subscriptions/' + subscription_id + ':configure', {'expected_revision': current_sub['revision'], 'enabled': False, 'reason': 'acceptance complete'}, token=rotate['token'], headers={'Idempotency-Key': 'onboarding-pause-subscription'}, session=False)
|
||
if private:
|
||
Path(private, 'host.env').write_text(f"ORBIT_URL={base}\nORBIT_TOKEN={rotate['token']}\nORBIT_NAMESPACE_ID={op['namespace_id']}\nORBIT_QUEUE_ID={queue}\n")
|
||
Path(private, 'result.json').write_text(json.dumps({'application': name, 'namespace_id': op['namespace_id'], 'queue_id': queue, 'credential_id': op['credential_id'], 'task_id': task_id, 'subscription_id': subscription_id}))
|
||
call('/console/auth/logout', {})
|
||
call('/console/applications', expected=401)
|
||
print('PASS: application create/replay, CSRF/session isolation, token rotation, original task/subscription preserved, logout')
|
||
print('Test application: ' + name)
|