145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
STORE = ROOT / "frontend" / "search-reply-draft-store.js"
|
|
ATTACHMENT = ROOT / "frontend" / "issue-attachment.js"
|
|
INDEX = ROOT / "frontend" / "index.html"
|
|
DASHBOARD = ROOT / "frontend" / "dashboard.js"
|
|
REGISTRY = ROOT / "frontend" / "private-data-registry.js"
|
|
WORKER = ROOT / "frontend" / "service-worker.js"
|
|
|
|
|
|
def run_node(script: str) -> str:
|
|
return subprocess.run(
|
|
["node", "-e", script], check=True, capture_output=True, text=True
|
|
).stdout
|
|
|
|
|
|
def test_search_reply_photo_drafts_restore_only_for_the_confirmed_account_and_exact_result():
|
|
script = f"""
|
|
const createStore = require({json.dumps(str(STORE))});
|
|
const records = new Map();
|
|
let login = 'timmy';
|
|
const transaction = async (operation, key, value) => {{
|
|
if (operation === 'put') records.set(key, structuredClone(value));
|
|
if (operation === 'get') return records.has(key) ? structuredClone(records.get(key)) : null;
|
|
if (operation === 'delete') records.delete(key);
|
|
}};
|
|
const store = createStore({{transaction, getOwnerLogin:()=>login}});
|
|
const target = {{kind:'issue', repository:'stackchain/dashboard', number:957}};
|
|
const other = {{kind:'issue', repository:'stackchain/dashboard', number:958}};
|
|
const photo = new Blob(['field-evidence'], {{type:'image/webp'}});
|
|
(async()=>{{
|
|
await store.save(target, [{{
|
|
filename:'camera.webp', contentType:'image/webp', blob:photo,
|
|
note:'rack label', operationId:'upload-stable-1',
|
|
confirmed:{{markdown:''}},
|
|
}}]);
|
|
const restored = await store.load(target);
|
|
const wrongTarget = await store.load(other);
|
|
login = 'alexander';
|
|
const wrongAccount = await store.load(target);
|
|
process.stdout.write(JSON.stringify({{
|
|
restored:{{...restored[0], blobText:await restored[0].blob.text(), blob:undefined}},
|
|
wrongTarget, wrongAccount, recordCount:records.size,
|
|
}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output == {
|
|
"restored": {
|
|
"filename": "camera.webp",
|
|
"contentType": "image/webp",
|
|
"note": "rack label",
|
|
"operationId": "upload-stable-1",
|
|
"confirmed": {"markdown": ""},
|
|
"blobText": "field-evidence",
|
|
},
|
|
"wrongTarget": None,
|
|
"wrongAccount": None,
|
|
"recordCount": 1,
|
|
}
|
|
|
|
|
|
def test_search_store_lists_only_confirmed_accounts_photo_drafts_as_metadata():
|
|
script = f"""
|
|
const createStore = require({json.dumps(str(STORE))});
|
|
const records = [
|
|
{{id:'mine',version:1,ownerLogin:'timmy',kind:'pull',repository:'stackchain/dashboard',number:19,updatedAt:220,attachments:[{{filename:'proof.jpg',blob:new Blob(['secret'])}}]}},
|
|
{{id:'other',version:1,ownerLogin:'alexander',kind:'issue',repository:'stackchain/private',number:3,updatedAt:300,attachments:[{{filename:'private.jpg',blob:new Blob(['private'])}}]}},
|
|
{{id:'empty',version:1,ownerLogin:'timmy',kind:'issue',repository:'stackchain/dashboard',number:20,updatedAt:400,attachments:[]}},
|
|
];
|
|
const transaction=async operation=>operation === 'list' ? records.map(value=>structuredClone(value)) : null;
|
|
const store=createStore({{transaction,getOwnerLogin:()=> 'timmy'}});
|
|
(async()=>process.stdout.write(JSON.stringify(await store.list())))().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
assert json.loads(run_node(script)) == [{
|
|
"id": "mine",
|
|
"kind": "photo-reply",
|
|
"label": "Search PR photos",
|
|
"repository": "stackchain/dashboard",
|
|
"number": 19,
|
|
"title": "stackchain/dashboard#19",
|
|
"photo_count": 1,
|
|
"updated_at": 220,
|
|
"route": {"kind": "search", "target_kind": "pull", "repository": "stackchain/dashboard", "number": 19},
|
|
"photo_store": "search",
|
|
}]
|
|
|
|
|
|
def test_photo_bundle_checkpoint_restores_stable_uploads_without_repeating_them():
|
|
script = f"""
|
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
|
const photo = new Blob(['proof'], {{type:'image/png'}}); photo.name = 'proof.png';
|
|
const uploads = [];
|
|
const first = attachment.create({{
|
|
createOperationId:()=> 'stable-upload-957',
|
|
upload:async payload=>{{uploads.push(payload.operation_id);return {{markdown:''}};}},
|
|
}});
|
|
first.select(photo);
|
|
(async()=>{{
|
|
await first.prepareComment({{repository:'stackchain/dashboard',number:957}}, 'Ready');
|
|
const checkpoint = await first.serialize();
|
|
const restored = attachment.create({{
|
|
createOperationId:()=> 'must-not-replace-operation',
|
|
upload:async payload=>{{uploads.push(payload.operation_id);throw new Error('must not upload twice');}},
|
|
}});
|
|
restored.restore(checkpoint);
|
|
const comment = await restored.prepareComment({{repository:'stackchain/dashboard',number:957}}, 'Ready');
|
|
process.stdout.write(JSON.stringify({{checkpoint:{{...checkpoint,blob:undefined}},comment,uploads}}));
|
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
"""
|
|
output = json.loads(run_node(script))
|
|
|
|
assert output == {
|
|
"checkpoint": {
|
|
"filename": "proof.png",
|
|
"contentType": "image/png",
|
|
"operationId": "stable-upload-957",
|
|
"confirmed": {"markdown": ""},
|
|
},
|
|
"comment": "Ready\n\n",
|
|
"uploads": ["stable-upload-957"],
|
|
}
|
|
|
|
|
|
def test_dashboard_checkpoints_restores_and_clears_search_photo_drafts_at_user_boundaries():
|
|
html = INDEX.read_text()
|
|
dashboard = DASHBOARD.read_text()
|
|
registry = REGISTRY.read_text()
|
|
worker = WORKER.read_text()
|
|
|
|
assert '<script src="static/search-reply-draft-store.js"></script>' in html
|
|
assert html.index('static/search-reply-draft-store.js') < html.index('static/dashboard.js')
|
|
assert "'stackchain-search-reply-drafts-v1'" in registry
|
|
assert "BASE + 'static/search-reply-draft-store.js'" in worker
|
|
assert "createSearchReplyDraftStore({" in dashboard
|
|
assert "getOwnerLogin:() => confirmedOwnerLogin" in dashboard
|
|
assert "onCheckpoint:() => persistSearchReplyPhotos()" in dashboard
|
|
assert "searchReplyDraftStore.load(target)" in dashboard
|
|
assert "searchReplyDraftStore.remove(item)" in dashboard
|