import time import pytest from app.config import settings from app.db import Job,claim,sessions from app.storage import storage from app.worker import process from app.cleanup import cleanup from test_platform import client,KEY from pcb_fixture import pcb ROUTES=['/api/v1/schematics/render','/api/v1/comparisons','/api/v1/pcbs/render','/api/v1/pcbs/comparisons'] def create(client,route,raw,expires=None): suffix='PcbDoc' if '/pcbs/' in route else 'SchDoc' files={f'{side}_file':(side+'.'+suffix,raw) for side in ('old','new')} if route.endswith('comparisons') else {'file':('input.'+suffix,raw)} return client.post(route,headers={'X-API-Key':KEY},files=files,data={} if expires is None else {'expires_in':str(expires)}) @pytest.mark.parametrize('route',ROUTES) @pytest.mark.parametrize('backend',['local','s3']) def test_unlimited_retention_survives_claim_read_and_cleanup(client,ascii_schematic,monkeypatch,route,backend): fake=None if backend=='s3': import boto3 from test_storage_s3 import MemoryS3 fake=MemoryS3();monkeypatch.setattr(boto3,'client',lambda *a,**kw:fake) monkeypatch.setenv('STORAGE_BACKEND','s3');monkeypatch.setenv('S3_BUCKET','test-private');monkeypatch.setenv('S3_PREFIX','unlimited') settings.cache_clear();storage.cache_clear() raw=pcb() if '/pcbs/' in route else ascii_schematic created=create(client,route,raw,-1);assert created.status_code==202,created.text body=created.json();assert body['expires_at']==-1 with sessions()() as db:assert db.get(Job,body['id']).expires_at==-1 # Advance the application clock far beyond every configured finite TTL. future=time.time()+100*365*86400;monkeypatch.setattr(time,'time',lambda:future) claimed=claim();assert claimed is not None and claimed['id']==body['id'] process(claimed) headers={'X-Viewer-Token':body['viewer_token']} response=client.get(body['status_url'],headers=headers);assert response.status_code==200 status=response.json();assert status['status']=='completed',status assert status['expires_at']==-1 base=body['status_url'].removesuffix('/status') assert client.get(base+('/svg/old' if route.endswith('comparisons') else '/svg'),headers=headers).status_code==200 assert client.get(body['status_url']).status_code==404 assert cleanup()==0 with sessions()() as db:assert db.get(Job,body['id']) is not None if fake:assert fake.objects and not (settings().storage_dir/'objects').exists() else:assert list((settings().storage_dir/'objects'/body['id']).rglob('*')) @pytest.mark.parametrize('route',ROUTES) @pytest.mark.parametrize('expires',[0,-2,999999999]) def test_invalid_expiry_rejected_for_every_operation(client,ascii_schematic,route,expires): response=create(client,route,pcb() if '/pcbs/' in route else ascii_schematic,expires) assert response.status_code==422,response.text assert claim() is None @pytest.mark.parametrize('route',ROUTES) def test_unlimited_jobs_still_count_toward_queue_limit(client,ascii_schematic,monkeypatch,route): monkeypatch.setenv('MAX_PENDING_JOBS','1');settings.cache_clear() raw=pcb() if '/pcbs/' in route else ascii_schematic assert create(client,route,raw,-1).status_code==202 assert create(client,route,raw,-1).status_code==503 @pytest.mark.parametrize('route',ROUTES) def test_default_and_positive_expiry_remain_finite(client,ascii_schematic,route): raw=pcb() if '/pcbs/' in route else ascii_schematic now=time.time() for expires,ttl in [(None,settings().link_ttl),(60,60)]: response=create(client,route,raw,expires);assert response.status_code==202 assert now+ttl<=response.json()['expires_at']<=time.time()+ttl