Files
chimera-Public/test/server/ch-0301-import-upload-service.test.js

74 lines
5.0 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Readable } = require('stream');
const test = require('node:test');
const { createImportUploadService, sanitizeRelativePath } = require('../../src/services/import-upload-service');
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
test('folder upload sanitizes paths, resumes exact offsets and survives service restart', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-upload-'));
try {
const service = createImportUploadService({ root, limits: { maxChunkBytes: 8 } });
const created = service.create({ owner: 'alice', files: [{ relativePath: 'Game/base.pkg', size: 6, sha256: digest('abcdef') }] });
assert.equal(Object.hasOwn(created, 'owner'), false);
assert.equal(JSON.stringify(created).includes(root), false);
const file = created.files[0];
const first = await service.writeChunk({ id: created.id, fileId: file.id, owner: 'alice', offset: 0, length: 3, chunkSha256: digest('abc'), stream: Readable.from('abc') });
assert.equal(first.files[0].offset, 3);
const restarted = createImportUploadService({ root, limits: { maxChunkBytes: 8 } });
assert.equal(restarted.get(created.id, 'alice').files[0].offset, 3);
await assert.rejects(
restarted.writeChunk({ id: created.id, fileId: file.id, owner: 'alice', offset: 2, length: 3, chunkSha256: digest('def'), stream: Readable.from('def') }),
error => error.code === 'UPLOAD_OFFSET' && error.expectedOffset === 3,
);
const completed = await restarted.writeChunk({ id: created.id, fileId: file.id, owner: 'alice', offset: 3, length: 3, chunkSha256: digest('def'), stream: Readable.from('def') });
assert.equal(completed.state, 'uploaded');
assert.equal(completed.bytesReceived, 6);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test('hash failures roll back, quotas are enforced and hostile paths are rejected', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-upload-policy-'));
try {
const service = createImportUploadService({ root, limits: { maxFiles: 2, maxFileBytes: 8, maxSessionBytes: 10, maxUserBytes: 12, maxGlobalBytes: 14, maxChunkBytes: 8 } });
for (const value of ['../escape.pkg', '/absolute.pkg', 'C:\\escape.pkg', 'folder//file.pkg', 'folder/CON']) assert.throws(() => sanitizeRelativePath(value));
const created = service.create({ owner: 'alice', files: [{ relativePath: 'safe/file.pkg', size: 4 }] });
const file = created.files[0];
await assert.rejects(
service.writeChunk({ id: created.id, fileId: file.id, owner: 'alice', offset: 0, length: 4, chunkSha256: digest('nope'), stream: Readable.from('data') }),
error => error.code === 'UPLOAD_HASH',
);
assert.equal(service.get(created.id, 'alice').files[0].offset, 0);
assert.throws(() => service.create({ owner: 'alice', files: [{ relativePath: 'too-large.pkg', size: 9 }] }), error => error.code === 'UPLOAD_QUOTA');
assert.throws(() => service.create({ owner: 'alice', files: [{ relativePath: 'a.pkg', size: 5 }, { relativePath: 'A.pkg', size: 5 }] }), error => error.code === 'UPLOAD_COLLISION');
service.create({ owner: 'alice', files: [{ relativePath: 'second.pkg', size: 8 }] });
assert.throws(() => service.create({ owner: 'alice', files: [{ relativePath: 'over-user.pkg', size: 1 }] }), error => error.code === 'UPLOAD_QUOTA');
assert.throws(() => service.get(created.id, 'bob'), error => error.code === 'UPLOAD_NOT_FOUND');
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test('cancel and expiry remove partial content without exposing or mutating a library', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-upload-lifecycle-'));
let clock = 1000;
const events = [];
try {
const service = createImportUploadService({ root, now: () => clock, emit: (channel, payload) => events.push({ channel, payload }), limits: { ttlMs: 10, maxChunkBytes: 8 } });
const cancelled = service.create({ owner: 'alice', files: [{ relativePath: 'cancel.pkg', size: 4 }] });
await service.writeChunk({ id: cancelled.id, fileId: cancelled.files[0].id, owner: 'alice', offset: 0, length: 2, chunkSha256: digest('ab'), stream: Readable.from('ab') });
assert.equal(service.cancel(cancelled.id, 'alice').state, 'cancelled');
const expiring = service.create({ owner: 'alice', files: [{ relativePath: 'expire.pkg', size: 2 }] });
clock = 1011;
assert.equal(service.expire(), 1);
assert.equal(service.get(expiring.id, 'alice').state, 'expired');
assert.equal(fs.readdirSync(root).some(name => name === 'library'), false);
assert.deepEqual(events.map(event => event.channel), ['import:upload:created', 'import:upload:progress', 'import:upload:cancelled', 'import:upload:created', 'import:upload:expired']);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});