Organize uploader storage and restore transfer readiness

This commit is contained in:
2026-10-14 19:15:46 +09:00
parent 69e7449ba7
commit c3573b605c
24 changed files with 966 additions and 449 deletions

View File

@@ -2,11 +2,17 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
import { get } from 'node:http';
import { Readable } from 'node:stream';
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';
import {
existsSync,
readFileSync,
renameSync,
writeFileSync,
unlinkSync,
mkdirSync,
utimesSync,
} from 'node:fs';
import { resolve } from 'node:path';
import { and, eq, isNotNull } from 'drizzle-orm';
import { eq } from 'drizzle-orm';
import { fixture } from './helpers.js';
import * as s from '../src/db/schema.js';
@@ -76,265 +82,176 @@ async function file(f, a, b, name = '사진.JPG') {
function current(f, row) {
return f.service.db.select().from(s.files).where(eq(s.files.id, row.id)).get();
}
async function archived(f, row) {
// The HTTP response can finish before the file descriptor's asynchronous close.
for (let i = 0; i < 100 && !current(f, row).storageKey.startsWith('completed/'); i++)
await new Promise((r) => setTimeout(r, 5));
assert.equal(current(f, row).storageKey, `completed/${row.storageKey}`);
}
test('logged-in sender to anonymous recipient: extension kept, completed after download, seven-day expiry and re-download unchanged', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
await login(f, a);
const value = await file(f, a, b),
row = value.row;
assert.match(row.storageKey, /^[a-f0-9-]{36}\.jpg$/);
assert.equal((await f.call(a, '/vault')).json().items.length, 0);
const expiry = f.service.db
.select()
.from(s.items)
.where(eq(s.items.fileId, row.id))
.get().expiresAt;
assert.equal(existsSync(resolve(f.service.storage.originals, row.storageKey)), true);
const downloaded = await f.call(b, `/access/${value.item.accessId}/download`);
assert.equal(downloaded.statusCode, 200);
assert.deepEqual(downloaded.rawPayload, value.bytes);
await archived(f, row);
const stored = current(f, row);
assert.equal(stored.storageKey, `completed/${row.storageKey}`);
assert.equal(existsSync(resolve(f.service.storage.originals, row.storageKey)), false);
assert.deepEqual(readFileSync(f.service.storage.layout.path(stored)), value.bytes);
assert.equal(stored.checksum, row.checksum);
assert.equal(stored.updatedAt, row.updatedAt);
assert.equal(
f.service.db.select().from(s.items).where(eq(s.items.fileId, row.id)).get().expiresAt,
expiry,
);
assert.deepEqual(
(await f.call(b, `/access/${value.item.accessId}/download`)).rawPayload,
value.bytes,
);
} finally {
await f.close();
}
});
test('anonymous sender to signed-in recipient: received-account reference keeps the file in originals', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
await login(f, b);
const value = await file(f, a, b, 'received.pdf');
const downloaded = await f.call(b, `/access/${value.item.accessId}/download`);
assert.equal(downloaded.statusCode, 200);
await new Promise((r) => setImmediate(r));
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
assert(existsSync(f.service.storage.layout.path(value.row)));
assert.equal((await f.call(b, '/inbox')).json().items.length, 1);
} finally {
await f.close();
}
});
test('a sender read or unauthorized read does not classify a transfer as recipient-completed', async () => {
test('signed-in uploads use the account folder; download retains the key, original filename and seven-day expiry', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
c = await f.create();
userId = await login(f, a);
const value = await file(f, a, b);
assert.equal((await f.call(c, `/access/${value.item.accessId}/download`)).statusCode, 404);
const sent = (await f.call(a, '/device/items')).json().items[0];
assert.equal((await f.call(a, `/access/${sent.accessId}/download`)).statusCode, 200);
await new Promise((r) => setImmediate(r));
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
} finally {
await f.close();
}
});
test('relocation waits for all active readers and preserves concurrent download bytes', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b);
const guest = f.service.db
assert.match(value.row.storageKey, new RegExp(`^user-${userId}/[a-f0-9-]{36}\\.jpg$`));
assert.equal((await f.call(a, '/vault')).json().items.length, 0);
const expiry = f.service.db
.select()
.from(s.guests)
.where(eq(s.guests.id, f.service.deviceGuest(b.deviceId).guestId))
.get();
const first = await f.service.storage.openDownload(guest, value.item.accessId),
second = await f.service.storage.openDownload(guest, value.item.accessId);
f.service.storage.layout.downloaded(value.row.id);
.from(s.items)
.where(eq(s.items.fileId, value.row.id))
.get().expiresAt;
for (let i = 0; i < 2; i++) {
const response = await f.call(b, `/access/${value.item.accessId}/download`);
assert.equal(response.statusCode, 200);
assert.deepEqual(response.rawPayload, value.bytes);
assert.match(response.headers['content-disposition'], /filename\*=UTF-8/);
}
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
const firstClosed = once(first.stream, 'close');
first.stream.resume();
await firstClosed;
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
const chunks = [];
second.stream.on('data', (chunk) => chunks.push(chunk));
await once(second.stream, 'close');
assert.deepEqual(Buffer.concat(chunks), value.bytes);
assert.equal(current(f, value.row).storageKey, `completed/${value.row.storageKey}`);
} finally {
await f.close();
}
});
test('legacy extension-less files migrate without changing IDs, checksums, content or expiry', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b, 'old.png');
const legacy = randomUUID();
renameSync(
f.service.storage.layout.path(value.row),
resolve(f.service.storage.originals, legacy),
);
f.service.db
.update(s.files)
.set({ storageKey: legacy })
.where(eq(s.files.id, value.row.id))
.run();
f.service.storage.layout.prepare();
const stored = current(f, value.row);
assert.equal(stored.storageKey, legacy + '.png');
assert.equal(stored.id, value.row.id);
assert.deepEqual(readFileSync(f.service.storage.layout.path(stored)), value.bytes);
assert.equal(stored.checksum, value.row.checksum);
const before = stored.storageKey;
f.service.storage.layout.prepare();
assert.equal(current(f, value.row).storageKey, before);
} finally {
await f.close();
}
});
test('database relocation failure rolls back the filename and preserves the original bytes', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b);
f.sqlite.exec(
"CREATE TRIGGER reject_move BEFORE UPDATE OF storageKey ON files BEGIN SELECT RAISE(ABORT, 'test relocation failure'); END",
);
assert.throws(() =>
f.service.storage.layout.move(value.row, `completed/${value.row.storageKey}`),
);
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
assert.deepEqual(readFileSync(f.service.storage.layout.path(value.row)), value.bytes);
assert.equal(
existsSync(f.service.storage.layout.keyPath(`completed/${value.row.storageKey}`)),
false,
f.service.db.select().from(s.items).where(eq(s.items.fileId, value.row.id)).get().expiresAt,
expiry,
);
assert.equal(
existsSync(resolve(f.service.storage.layout.journal, value.row.id + '.json')),
false,
);
f.sqlite.exec('DROP TRIGGER reject_move');
f.service.storage.layout.downloaded(value.row.id);
assert.equal(current(f, value.row).storageKey, `completed/${value.row.storageKey}`);
assert.equal(existsSync(f.service.storage.layout.completed), false);
} finally {
await f.close();
}
});
test('account deletion undo remains protected; restoring an account reference returns an archive to originals', async () => {
test('anonymous upload uses its guest folder even when a signed-in recipient adds a received reference', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
await login(f, b);
const value = await file(f, a, b);
const reference = f.service.db
.select()
.from(s.access)
.where(and(eq(s.access.itemId, value.item.id), isNotNull(s.access.userId)))
.get();
assert(reference);
f.service.db
.update(s.access)
.set({ deletedAt: Date.now() })
.where(eq(s.access.id, reference.id))
.run();
f.service.storage.layout.downloaded(value.row.id);
const value = await file(f, a, b, 'received.mp4');
assert.equal(value.row.storageKey.split('/')[0], `guest-${value.row.ownerGuestId}`);
assert.match(value.row.storageKey, /\.mp4$/);
assert.equal((await f.call(b, '/inbox')).json().items.length, 1);
assert.equal((await f.call(b, `/access/${value.item.accessId}/download`)).statusCode, 200);
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
f.service.db
.update(s.access)
.set({ deletedAt: Date.now() - f.cfg.undoSeconds * 1000 - 1 })
.where(eq(s.access.id, reference.id))
.run();
f.service.storage.layout.downloaded(value.row.id);
assert.equal(current(f, value.row).storageKey, `completed/${value.row.storageKey}`);
f.service.db
.update(s.access)
.set({ deletedAt: null })
.where(eq(s.access.id, reference.id))
.run();
f.service.storage.layout.reconcile();
assert.equal(
(await f.call(await f.create(), `/access/${value.item.accessId}/download`)).statusCode,
404,
);
} finally {
await f.close();
}
});
test('the same account on different devices shares one upload folder with unique physical filenames', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
c = await f.create(),
d = await f.create();
const id = await login(f, a),
user = f.service.db.select().from(s.users).where(eq(s.users.id, id)).get();
const loginResponse = await f.call(c, '/auth/login', 'POST', {
email: user.email,
password: '12345678',
});
assert.equal(loginResponse.statusCode, 200);
c.cookie += '; ' + String(loginResponse.headers['set-cookie']).split(';')[0];
const one = await file(f, a, b),
two = await file(f, c, d);
assert.equal(one.row.storageKey.split('/')[0], `user-${id}`);
assert.equal(two.row.storageKey.split('/')[0], `user-${id}`);
assert.notEqual(one.row.storageKey, two.row.storageKey);
assert.deepEqual(readFileSync(f.service.storage.layout.path(one.row)), one.bytes);
assert.deepEqual(readFileSync(f.service.storage.layout.path(two.row)), two.bytes);
} finally {
await f.close();
}
});
test('old flat originals and completed files migrate to their recorded upload owner without changing IDs or expiry', async () => {
for (const completed of [false, true]) {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
value = await file(f, a, b, 'old.png'),
legacy = randomUUID();
if (completed) mkdirSync(f.service.storage.layout.completed, { recursive: true });
const key = completed ? `completed/${legacy}` : legacy;
renameSync(f.service.storage.layout.path(value.row), f.service.storage.layout.keyPath(key));
f.service.db
.update(s.files)
.set({ storageKey: key })
.where(eq(s.files.id, value.row.id))
.run();
f.service.storage.layout.prepare();
const stored = current(f, value.row);
assert.equal(stored.storageKey, `guest-${value.row.ownerGuestId}/${legacy}.png`);
assert.equal(stored.id, value.row.id);
assert.equal(stored.checksum, value.row.checksum);
assert.equal(stored.updatedAt, value.row.updatedAt);
assert.deepEqual(readFileSync(f.service.storage.layout.path(stored)), value.bytes);
assert.equal(existsSync(f.service.storage.layout.completed), false);
f.service.storage.layout.prepare();
assert.equal(current(f, value.row).storageKey, stored.storageKey);
assert.deepEqual(
(await f.call(b, `/access/${value.item.accessId}/download`)).rawPayload,
value.bytes,
);
} finally {
await f.close();
}
}
});
test('migration waits for all active readers before moving a legacy file', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
value = await file(f, a, b),
key = randomUUID();
renameSync(f.service.storage.layout.path(value.row), f.service.storage.layout.keyPath(key));
f.service.db.update(s.files).set({ storageKey: key }).where(eq(s.files.id, value.row.id)).run();
const guest = f.service.guest(a.cookie.split('=')[1]);
const sent = (await f.call(a, '/device/items')).json().items[0];
const first = await f.service.storage.openDownload(guest, sent.accessId),
second = await f.service.storage.openDownload(guest, sent.accessId);
f.service.storage.layout.prepare();
assert.equal(current(f, value.row).storageKey, key);
const closed = once(first.stream, 'close');
first.stream.resume();
await closed;
f.service.storage.layout.prepare();
assert.equal(current(f, value.row).storageKey, key);
const chunks = [];
second.stream.on('data', (chunk) => chunks.push(chunk));
await once(second.stream, 'close');
assert.deepEqual(Buffer.concat(chunks), value.bytes);
f.service.storage.layout.prepare();
assert.equal(current(f, value.row).storageKey, `guest-${value.row.ownerGuestId}/${key}.jpg`);
} finally {
await f.close();
}
});
test('database move failure rolls back the filename and leaves bytes and old key available', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
value = await file(f, a, b);
const destination = value.row.storageKey.split('/')[0] + '/' + randomUUID() + '.jpg';
f.sqlite.exec(
"CREATE TRIGGER reject_move BEFORE UPDATE OF storageKey ON files BEGIN SELECT RAISE(ABORT, 'test relocation failure'); END",
);
assert.throws(() => f.service.storage.layout.move(value.row, destination));
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
assert.deepEqual(readFileSync(f.service.storage.layout.path(value.row)), value.bytes);
} finally {
await f.close();
}
});
test('aborting an HTTP download before all bytes arrive leaves the file available in originals', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b);
const openDownload = f.service.storage.openDownload.bind(f.service.storage);
let slow;
f.service.storage.openDownload = async (...args) => {
const result = await openDownload(...args);
const closed = once(result.stream, 'close');
result.stream.destroy();
await closed;
slow = Readable.from(
(async function* () {
yield value.bytes.subarray(0, 1);
await new Promise((r) => setTimeout(r, 100));
yield value.bytes.subarray(1);
})(),
);
return { ...result, stream: slow };
};
await new Promise((resolve, reject) => {
const request = get(
`${f.url}/api/access/${value.item.accessId}/download`,
{ headers: { cookie: b.cookie, origin: 'http://hop.test' } },
(response) => {
assert.equal(response.statusCode, 200);
response.once('data', () => {
response.destroy();
request.destroy();
resolve();
});
},
);
request.once('error', reject);
});
if (!slow.closed) await once(slow, 'close');
assert.equal(current(f, value.row).storageKey, value.row.storageKey);
f.service.storage.openDownload = openDownload;
assert.deepEqual(
(await f.call(b, `/access/${value.item.accessId}/download`)).rawPayload,
value.bytes,
assert.equal(existsSync(f.service.storage.layout.keyPath(destination)), false);
assert.equal(
existsSync(resolve(f.service.storage.layout.journal, value.row.id + '.json')),
false,
);
await archived(f, value.row);
} finally {
await f.close();
}
});
test('journal recovers a restart between file rename and DB commit; unrelated files are not adopted', async () => {
test('move journal restores a rename interrupted before DB commit and never adopts unrelated replacement files', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b);
const key = `completed/${value.row.storageKey}`,
b = await f.create(),
value = await file(f, a, b);
const key = value.row.storageKey.split('/')[0] + '/' + randomUUID() + '.jpg',
target = f.service.storage.layout.keyPath(key);
renameSync(f.service.storage.layout.path(value.row), target);
writeFileSync(
@@ -355,39 +272,62 @@ test('journal recovers a restart between file rename and DB commit; unrelated fi
);
unlinkSync(target);
writeFileSync(
resolve(f.service.storage.originals, value.row.storageKey),
f.service.storage.layout.keyPath(value.row.storageKey),
Buffer.alloc(value.row.size, 120),
);
const missing = await f.call(b, `/access/${value.item.accessId}/download`);
assert.equal(missing.statusCode, 404, 'no journal: do not adopt an unrelated same-size file');
assert.equal((await f.call(b, `/access/${value.item.accessId}/download`)).statusCode, 404);
} finally {
await f.close();
}
});
test('completed manual deletion and natural expiry clear references and remove bytes in either directory', async () => {
for (const manual of [true, false]) {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
const value = await file(f, a, b);
await f.call(b, `/access/${value.item.accessId}/download`);
await archived(f, value.row);
const path = f.service.storage.layout.path(current(f, value.row));
if (manual) {
unlinkSync(path);
assert.equal((await f.call(b, `/access/${value.item.accessId}/download`)).statusCode, 404);
} else
f.service.db
.update(s.items)
.set({ expiresAt: Date.now() - 1 })
.where(eq(s.items.fileId, value.row.id))
.run();
await f.service.storage.cleanup();
assert.equal(existsSync(path), false);
assert.equal(current(f, value.row).status, 'failed');
} finally {
await f.close();
test('manual deletion and seven-day expiry remove personal and guest folder files and reconcile references', async () => {
for (const signedIn of [false, true])
for (const manual of [false, true]) {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
if (signedIn) await login(f, a);
const value = await file(f, a, b),
path = f.service.storage.layout.path(value.row);
if (manual) {
unlinkSync(path);
assert.equal(
(await f.call(b, `/access/${value.item.accessId}/download`)).statusCode,
404,
);
} else
f.service.db
.update(s.items)
.set({ expiresAt: Date.now() - 1 })
.where(eq(s.items.fileId, value.row.id))
.run();
await f.service.storage.cleanup();
assert.equal(existsSync(path), false);
assert.equal(current(f, value.row).status, 'failed');
} finally {
await f.close();
}
}
});
test('orphan cleanup scans nested owner folders and preserves valid old files', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create(),
value = await file(f, a, b);
const valid = f.service.storage.layout.path(value.row),
orphan = resolve(valid, '..', randomUUID() + '.txt'),
old = new Date(Date.now() - 25 * 3600000);
writeFileSync(orphan, 'orphan');
utimesSync(orphan, old, old);
utimesSync(valid, old, old);
await f.service.storage.cleanup();
assert.equal(existsSync(orphan), false);
assert.equal(existsSync(valid), true);
assert.throws(() => f.service.storage.layout.keyPath('../outside'));
assert.throws(() => f.service.storage.layout.keyPath('user-x/../../outside'));
} finally {
await f.close();
}
});

View File

@@ -0,0 +1,108 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { eq } from 'drizzle-orm';
import { fixture } from './helpers.js';
import { visitorRegion } from '../src/ownerInfo.js';
import { loadConfig } from '../src/config.js';
import * as s from '../src/db/schema.js';
test('basic country and city use only trusted Cloudflare headers and ignore precise coordinates', () => {
const cfg = { ...loadConfig(), clientIpSource: 'cloudflare', trustedProxies: ['127.0.0.1'] };
const req = {
raw: { socket: { remoteAddress: '127.0.0.1' } },
headers: {
'cf-connecting-ip': '8.8.8.8',
'cf-ipcountry': 'KR',
'cf-region': 'Daejeon',
'cf-ipcity': Buffer.from('대전', 'utf8').toString('latin1'),
'cf-iplatitude': '37.123456',
'cf-iplongitude': '127.123456',
},
};
assert.deepEqual(visitorRegion(req, cfg), {
ip: '8.8.8.8',
countryCode: 'KR',
region: 'Daejeon',
city: '대전',
source: 'cloudflare',
});
for (const change of [{ trustedProxies: [] }, { clientIpSource: 'forwarded' }])
assert.deepEqual(visitorRegion(req, { ...cfg, ...change }), {
ip: null,
countryCode: null,
region: null,
city: null,
source: 'unavailable',
});
assert.equal(
visitorRegion({ ...req, headers: { ...req.headers, 'cf-ipcountry': 'XX' } }, cfg).countryCode,
null,
);
});
test('region records are NAS-only, deduplicated by request, bounded and cleaned with owner files', async () => {
const f = await fixture();
try {
const a = await f.create(),
b = await f.create();
await f.space(a, b);
f.cfg.clientIpSource = 'cloudflare';
const key = randomUUID(),
input = {
targetDeviceId: b.deviceId,
kind: 'file',
byteSize: 4,
count: 1,
files: [{ name: 'test.mp4', size: 4, type: 'video/mp4' }],
},
headers = {
'cf-connecting-ip': '8.8.8.8',
'cf-ipcountry': 'KR',
'cf-region': 'Daejeon',
'cf-ipcity': 'Daejeon',
'cf-iplatitude': '37.123456',
};
const result = await f.call(a, '/transfer-requests', 'POST', input, key, headers);
assert.equal(result.statusCode, 200);
const id = result.json().requestId,
row = f.service.storage.fileRows(id)[0],
owner = f.service.storage.layout.ownerInfo.details(row.storageKey);
const json = () => JSON.parse(readFileSync(owner.path, 'utf8'));
assert.equal(json().uploader.id, row.ownerGuestId);
assert.equal(json().recentLocations[0].city, 'Daejeon');
const serialized = readFileSync(owner.path, 'utf8');
assert.equal(json().recentLocations[0].ip, '8.8.8.8');
for (const secret of ['37.123456', a.csrf, a.cookie])
assert.equal(serialized.includes(secret), false);
await f.call(a, '/transfer-requests', 'POST', input, key, headers);
assert.equal(json().recentLocations.length, 1);
for (let i = 0; i < 30; i++)
f.service.storage.layout.ownerInfo.record(row, randomUUID(), {
countryCode: 'CN',
region: null,
city: null,
source: 'cloudflare',
});
assert.equal(json().recentLocations.length, 20);
const response = await f.app.inject({
url: '/uploads/originals/' + row.storageKey.split('/')[0] + '/owner-info.json',
});
assert.equal(
response.body.includes('recentLocations'),
false,
'metadata is not publicly served',
);
const expired = json();
for (const event of expired.recentLocations)
event.recordedAt = new Date(Date.now() - 8 * 86400000).toISOString();
writeFileSync(owner.path, JSON.stringify(expired));
await f.service.storage.layout.ownerInfo.cleanup();
assert.equal(json().recentLocations.length, 0);
f.service.db.update(s.files).set({ status: 'failed' }).where(eq(s.files.id, row.id)).run();
await f.service.storage.layout.ownerInfo.cleanup();
assert.equal(existsSync(owner.path), false);
} finally {
await f.close();
}
});

View File

@@ -66,7 +66,7 @@ test('파일은 수락 전 업로드되지 않으며 실제 스트리밍 저장
{ name: '파일.txt', size: bytes.length, type: 'text/plain' },
]);
assert.equal((await upload(f, a, id, '파일.txt', bytes)).statusCode, 409);
assert.equal((await readdir(f.service.storage.originals)).length, 0);
assert.equal((await Array.fromAsync(f.service.storage.layout.entries())).length, 0);
assert.equal(
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
200,
@@ -267,7 +267,7 @@ test('실행 취소 기한과 자연 만료를 검사하며 마지막 권한 뒤
await f.call(b, '/access/delete-batch', 'POST', { ids: [receiver.accessId] })
).json();
await f.service.storage.cleanup();
assert.equal((await readdir(f.service.storage.originals)).length, 1);
assert.equal((await Array.fromAsync(f.service.storage.layout.entries())).length, 1);
f.service.db
.update(s.deletions)
.set({ undoUntil: Date.now() - 1 })
@@ -283,7 +283,7 @@ test('실행 취소 기한과 자연 만료를 검사하며 마지막 권한 뒤
.run();
f.service.cleanup();
await f.service.storage.cleanup();
assert.equal((await readdir(f.service.storage.originals)).length, 0);
assert.equal((await Array.fromAsync(f.service.storage.layout.entries())).length, 0);
} finally {
await f.close();
}

View File

@@ -2,6 +2,41 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { fixture } from './helpers.js';
import { Limiter, ApiError } from '../src/security.js';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
test('a known reconnecting peer returns a transient code without creating a request or charging quotas', async () => {
const f = await fixture(true);
try {
f.cfg.offlineGraceMs = 1000;
f.cfg.pairRate = 1;
const a = await f.create('8.8.8.8'),
b = await f.create('8.8.8.8');
const disconnected = once(f.service.io.sockets.sockets.get(b.socket.id), 'disconnect');
b.socket.disconnect();
await disconnected;
const key = randomUUID();
for (let i = 0; i < 3; i++) {
const response = await f.request(a, b, 'test', 'url', 1, key);
assert.equal(response.statusCode, 409);
assert.equal(response.json().code, 'TARGET_RECONNECTING');
assert.equal((await f.call(a, '/transfer-requests')).json().requests.length, 0);
}
await f.call(b, '/device/me', 'PATCH', { discoverable: false });
assert.equal((await f.request(a, b, 'test', 'url', 1, key)).json().code, 'TARGET_UNAVAILABLE');
await f.call(b, '/device/me', 'PATCH', { discoverable: true });
await f.connect(b);
const response = await f.request(a, b, 'test', 'url', 1, key);
assert.equal(response.statusCode, 200);
assert.equal(
(await f.request(a, b, 'test', 'url', 1, key)).json().requestId,
response.json().requestId,
);
assert.equal((await f.call(a, '/transfer-requests')).json().requests.length, 1);
} finally {
await f.close();
}
});
test('rate windows report their deadline; denied retries do not charge other quotas', (t) => {
let now = 1000;