import { test } from 'node:test'; import assert from 'node:assert/strict'; import { xml } from '@xmpp/client'; import { checkSize, upload } from '../src/xmpp/upload.ts'; import { TAG_BYTES } from '../src/crypto/aesgcm.ts'; test('media rejection happens before any slot or PUT; revocation between slot and PUT also fails closed', async (t) => { let slots = 0, puts = 0; const xmpp: any = {iqCaller:{request:async () => {slots++; return xml('iq', null, xml('slot', {xmlns:'urn:xmpp:http:upload:0'}, xml('put', {url:'https://upload.example/put'}), xml('get', {url:'https://upload.example/get'})));}}}; class XHR { status = 201; upload = {}; onload?: () => void; onloadend?: () => void; onabort?: () => void; open() {} setRequestHeader() {} send() {puts++; this.onload?.(); this.onloadend?.();} abort() {this.onabort?.();this.onloadend?.();} } Object.defineProperty(globalThis, 'XMLHttpRequest', {value:XHR, configurable:true}); t.after(() => { delete (globalThis as any).XMLHttpRequest; }); const file = new Blob(['test'], {type:'text/plain'}), service = {jid:'upload.example'}; for (const reason of ['Encrypted media unsupported', 'Unknown capability', 'Offline']) { await assert.rejects(upload(xmpp,service,file,'test',undefined,undefined,async () => {throw Error(reason);}), new RegExp(reason)); } assert.equal(slots,0); assert.equal(puts,0); let checks = 0; await assert.rejects(upload(xmpp,service,file,'test',undefined,undefined,async () => {if (++checks === 2) throw Error('Capability changed');}), /Capability changed/); assert.equal(slots,1); assert.equal(puts,0); assert.equal(await upload(xmpp,service,file,'test',undefined,undefined,async () => {}), 'https://upload.example/get'); assert.equal(slots,2); assert.equal(puts,1); const abort = new AbortController(); abort.abort(); await assert.rejects(upload(xmpp,service,file,'test',undefined,abort.signal,async () => {}), /cancelled/); assert.equal(slots,2); assert.equal(puts,1); const mid = new AbortController(); await assert.rejects(upload(xmpp,service,file,'test',undefined,mid.signal,async () => {mid.abort();}), /cancelled/); assert.equal(slots,2); assert.equal(puts,1); }); test("the server limit is checked before anything is uploaded, tag included", () => { const service = {jid:'upload.example', maxSize:10485760}; // Prosody's own 10 MiB limit (blunix /etc/prosody/prosody.cfg.lua). A file // within 16 bytes of it only fails the PUT unless the GCM tag is counted. assert.doesNotThrow(() => checkSize(10485760, service)); assert.throws(() => checkSize(10485761, service), /File too large \(limit 10 MB\)/); // A plaintext file that fits on its own but not once the tag is added: // the encrypted path passes file.size + TAG_BYTES, so it is refused here // instead of by the PUT. const snug = 10485760 - TAG_BYTES + 1; assert.doesNotThrow(() => checkSize(snug, service)); assert.throws(() => checkSize(snug + TAG_BYTES, service), /File too large/); // No advertised limit: nothing to check against. assert.doesNotThrow(() => checkSize(Number.MAX_SAFE_INTEGER, {jid:'upload.example'})); });