59 خطوط
3.6 KiB
Python
59 خطوط
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Real HTTP smoke: private PCB render + comparison, without printing read capabilities."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import secrets
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('old',type=Path);parser.add_argument('new',type=Path,nargs='?')
|
|
parser.add_argument('--url',default='http://127.0.0.1:8000')
|
|
parser.add_argument('--timeout',type=int,default=180)
|
|
args=parser.parse_args();base=args.url.rstrip('/');key=os.environ.get('API_KEY','')
|
|
def request(path,headers=None,data=None):
|
|
req=urllib.request.Request(base+path,data=data,headers=headers or {})
|
|
with urllib.request.urlopen(req,timeout=30) as response:return response.status,response.headers,response.read()
|
|
def upload(path,files):
|
|
boundary='pcb-'+secrets.token_hex(16);parts=[]
|
|
for field,file in files.items():
|
|
parts.extend([f'--{boundary}\r\nContent-Disposition: form-data; name="{field}"; filename="board.PcbDoc"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode(),file.read_bytes(),b'\r\n'])
|
|
parts.extend([f'--{boundary}\r\nContent-Disposition: form-data; name="access"\r\n\r\nprivate\r\n--{boundary}--\r\n'.encode()])
|
|
status,headers,body=request(path,{'X-API-Key':key,'Content-Type':f'multipart/form-data; boundary={boundary}'},b''.join(parts))
|
|
assert status==202;created=json.loads(body);assert '#token=' in created['viewer_url']
|
|
return created
|
|
for comparison in (False,True):
|
|
path='/api/v1/pcbs/comparisons' if comparison else '/api/v1/pcbs/render'
|
|
files={'old_file':args.old,'new_file':args.new or args.old} if comparison else {'file':args.old}
|
|
created=upload(path,files);identifier=created['id'];headers={'X-Viewer-Token':created['viewer_token']}
|
|
resource=f'/api/v1/pcbs/comparisons/{identifier}' if comparison else f'/api/v1/pcbs/{identifier}'
|
|
status_path=resource if comparison else resource+'/status'
|
|
try:request(status_path)
|
|
except urllib.error.HTTPError as exc:assert exc.code==404
|
|
else:raise AssertionError('Private PCB accessible without a capability')
|
|
deadline=time.monotonic()+args.timeout
|
|
while True:
|
|
_,_,body=request(status_path,headers);result=json.loads(body)
|
|
if result['status']=='failed':raise RuntimeError(result['error'])
|
|
if result['status']=='completed':break
|
|
if time.monotonic()>deadline:raise TimeoutError('PCB worker did not finish')
|
|
time.sleep(.5)
|
|
for side in (('old','new') if comparison else ('',)):
|
|
suffix='/svg/'+side if comparison else '/svg'
|
|
status,_,svg=request(resource+suffix,headers);assert status==200 and b'<svg' in svg and b'data-layer-id' in svg
|
|
view=f'/compare/pcb/{identifier}' if comparison else f'/view/pcb/{identifier}'
|
|
_,response_headers,html=request(view)
|
|
csp=response_headers.get_all('Content-Security-Policy')
|
|
assert len(csp)==1 and "frame-ancestors 'self' https://git.sadentel.net" in csp[0]
|
|
assert not response_headers.get_all('X-Frame-Options') and b'/assets/pcb.js' in html
|
|
if comparison and (args.new is None or args.old.read_bytes()==args.new.read_bytes()):assert result['total_changed']==0
|
|
print(json.dumps(dict(kind='pcb_comparison' if comparison else 'pcb_render',id=identifier,status=result['status'],
|
|
summary=result.get('summary'),primitive_count=result.get('primitive_count'))))
|
|
|
|
if __name__=='__main__':main()
|