Make admin quotas editable and simplify uploader storage
This commit is contained in:
366
apps/server/test/admin-capacity.test.js
Normal file
366
apps/server/test/admin-capacity.test.js
Normal file
@@ -0,0 +1,366 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { statfsSync } from 'node:fs';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import Database from 'better-sqlite3';
|
||||
import { fixture } from './helpers.js';
|
||||
import * as s from '../src/db/schema.js';
|
||||
import { Administration } from '../src/administration.js';
|
||||
import { CHUNK_BYTES } from '../src/resumable.js';
|
||||
|
||||
async function signed(f, email) {
|
||||
const client = await f.create(),
|
||||
id = randomUUID();
|
||||
f.service.db
|
||||
.insert(s.users)
|
||||
.values({
|
||||
id,
|
||||
email,
|
||||
displayName: email,
|
||||
passwordHash: await f.service.auth.password('12345678'),
|
||||
status: 'active',
|
||||
verifiedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
.run();
|
||||
const response = await f.call(client, '/auth/login', 'POST', { email, password: '12345678' });
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
client.cookie += '; ' + String(response.headers['set-cookie']).split(';')[0];
|
||||
return client;
|
||||
}
|
||||
async function patch(f, admin, values) {
|
||||
const response = await f.call(admin, '/admin/settings', 'PATCH', {
|
||||
revision: f.service.admin.state.revision,
|
||||
...values,
|
||||
});
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
return response.json();
|
||||
}
|
||||
async function request(f, sender, receiver, bytes = 8) {
|
||||
return f.call(
|
||||
sender,
|
||||
'/transfer-requests',
|
||||
'POST',
|
||||
{
|
||||
targetDeviceId: receiver.deviceId,
|
||||
kind: 'file',
|
||||
count: 1,
|
||||
byteSize: bytes,
|
||||
files: [{ name: 'quota.txt', size: bytes, type: 'text/plain' }],
|
||||
},
|
||||
randomUUID(),
|
||||
);
|
||||
}
|
||||
async function accept(f, receiver, response) {
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
const id = response.json().requestId;
|
||||
const result = await f.call(
|
||||
receiver,
|
||||
`/transfer-requests/${id}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
);
|
||||
assert.equal(result.statusCode, 200, result.body);
|
||||
return id;
|
||||
}
|
||||
const own = (f, sender, bytes = 8) =>
|
||||
f.call(
|
||||
sender,
|
||||
'/vault',
|
||||
'POST',
|
||||
{ kind: 'file', files: [{ name: 'quota.txt', size: bytes, type: 'text/plain' }] },
|
||||
randomUUID(),
|
||||
);
|
||||
async function upload(f, sender, id, bytes) {
|
||||
return f.call(
|
||||
sender,
|
||||
`/uploads?requestId=${id}&fileIndex=0`,
|
||||
'POST',
|
||||
Buffer.concat([
|
||||
Buffer.from(
|
||||
'--quota\r\nContent-Disposition: form-data; name="file"; filename="quota.txt"\r\nContent-Type: text/plain\r\n\r\n',
|
||||
),
|
||||
bytes,
|
||||
Buffer.from('\r\n--quota--\r\n'),
|
||||
]),
|
||||
randomUUID(),
|
||||
{ 'content-type': 'multipart/form-data; boundary=quota' },
|
||||
);
|
||||
}
|
||||
|
||||
test('administrator limits persist, report to clients, reject invalid updates and enforce single-file/account quotas immediately', async () => {
|
||||
const f = await fixture(false, undefined, { maxFileBytes: 4 });
|
||||
try {
|
||||
const admin = await signed(f, 'hello@noname.company'),
|
||||
user = await signed(f, 'quota@hop.test'),
|
||||
a = await f.create(),
|
||||
b = await f.create();
|
||||
await f.space(a, b);
|
||||
assert.equal((await request(f, a, b)).json().code, 'TRANSFER_LIMIT');
|
||||
assert.equal((await own(f, user)).json().code, 'TRANSFER_LIMIT');
|
||||
const updated = await patch(f, admin, {
|
||||
perFileBytes: 16,
|
||||
accountBytes: 12,
|
||||
deviceBytes: 24,
|
||||
devicesTotalBytes: 32,
|
||||
minDiskFreeBytes: 0,
|
||||
});
|
||||
const restored = new Administration(f.service);
|
||||
for (const key of [
|
||||
'perFileBytes',
|
||||
'accountBytes',
|
||||
'deviceBytes',
|
||||
'devicesTotalBytes',
|
||||
'minDiskFreeBytes',
|
||||
])
|
||||
assert.equal(restored.state[key], updated[key]);
|
||||
assert.equal((await f.call(user, '/account/storage')).json().limitBytes, 12);
|
||||
const id = await accept(f, b, await request(f, a, b));
|
||||
// Runtime increase must also override the original multipart plugin's 4-byte limit.
|
||||
assert.equal((await upload(f, a, id, Buffer.from('12345678'))).statusCode, 200);
|
||||
const saved = await own(f, user);
|
||||
assert.equal(saved.statusCode, 200, saved.body);
|
||||
assert.equal(
|
||||
(await own(f, user)).json().code,
|
||||
'ACCOUNT_QUOTA',
|
||||
'pending reservations count toward the account limit',
|
||||
);
|
||||
await patch(f, admin, { accountBytes: 24 });
|
||||
assert.equal((await own(f, user)).statusCode, 200);
|
||||
assert.equal(
|
||||
(await upload(f, user, saved.json().requestId, Buffer.from('12345678'))).statusCode,
|
||||
200,
|
||||
);
|
||||
const list = (await f.call(admin, '/admin/files')).json().items;
|
||||
const accountFile = list.find((file) => file.uploader?.email === 'quota@hop.test');
|
||||
assert.ok(accountFile);
|
||||
assert.equal(accountFile.path.split('/')[0], accountFile.uploader.id);
|
||||
assert.equal(accountFile.uploader.kind, 'user');
|
||||
assert.equal('name' in accountFile.uploader, false);
|
||||
const guestFile = list.find((file) => file.uploader?.kind === 'guest');
|
||||
assert.equal(guestFile.uploader.email, null);
|
||||
assert.equal(guestFile.path.split('/')[0], guestFile.uploader.id);
|
||||
const before = f.service.admin.state.revision;
|
||||
for (const invalid of [
|
||||
{ perFileBytes: 0 },
|
||||
{ accountBytes: -1 },
|
||||
{ deviceBytes: 1.2 },
|
||||
{ devicesTotalBytes: 1e12 + 1 },
|
||||
{ minDiskFreeBytes: -1 },
|
||||
]) {
|
||||
assert.equal(
|
||||
(await f.call(admin, '/admin/settings', 'PATCH', { revision: before, ...invalid }))
|
||||
.statusCode,
|
||||
400,
|
||||
);
|
||||
assert.equal(f.service.admin.state.revision, before);
|
||||
}
|
||||
const audits = (await f.call(admin, '/admin/audit')).json().events;
|
||||
assert.ok(
|
||||
audits.some((event) =>
|
||||
event.details.changes?.some(
|
||||
(change) => change.field === 'perFileBytes' && change.before === 4 && change.after === 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('administrator device, combined device and minimum disk limits govern new reservations without changing existing files', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
const admin = await signed(f, 'hello@noname.company'),
|
||||
a = await f.create(),
|
||||
b = await f.create();
|
||||
await f.space(a, b);
|
||||
await patch(f, admin, { deviceBytes: 4 });
|
||||
let pending = await request(f, a, b);
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
b,
|
||||
`/transfer-requests/${pending.json().requestId}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
)
|
||||
).json().code,
|
||||
'GUEST_QUOTA',
|
||||
);
|
||||
await patch(f, admin, { deviceBytes: 16, devicesTotalBytes: 4 });
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
b,
|
||||
`/transfer-requests/${pending.json().requestId}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
)
|
||||
).json().code,
|
||||
'TOTAL_QUOTA',
|
||||
);
|
||||
await patch(f, admin, { devicesTotalBytes: 24 });
|
||||
const disk = statfsSync(f.cfg.uploadDir),
|
||||
free = disk.bavail * disk.bsize;
|
||||
if (free + 1 <= 1e12) {
|
||||
await patch(f, admin, { minDiskFreeBytes: free + 1 });
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
b,
|
||||
`/transfer-requests/${pending.json().requestId}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
)
|
||||
).json().code,
|
||||
'DISK_FULL',
|
||||
);
|
||||
}
|
||||
await patch(f, admin, { minDiskFreeBytes: 0 });
|
||||
const id = await accept(f, b, pending);
|
||||
await patch(f, admin, { deviceBytes: 1, devicesTotalBytes: 1, perFileBytes: 1 });
|
||||
assert.equal(
|
||||
(await upload(f, a, id, Buffer.from('12345678'))).statusCode,
|
||||
200,
|
||||
'accepted reservation survives reduced limits',
|
||||
);
|
||||
const item = (await f.call(b, '/device/items'))
|
||||
.json()
|
||||
.items.find((item) => item.file?.name === 'quota.txt');
|
||||
assert.deepEqual(
|
||||
(await f.call(b, `/access/${item.accessId}/download`)).rawPayload,
|
||||
Buffer.from('12345678'),
|
||||
);
|
||||
assert.equal(f.service.admin.summary().storedFiles, 1);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('resumable offsets and completion honor the accepted manifest after reducing the file and account limits', async () => {
|
||||
const f = await fixture(false, undefined, { maxFileBytes: 4 });
|
||||
try {
|
||||
const admin = await signed(f, 'hello@noname.company'),
|
||||
user = await signed(f, 'resume@hop.test');
|
||||
await patch(f, admin, { perFileBytes: CHUNK_BYTES + 7, accountBytes: CHUNK_BYTES + 7 });
|
||||
const response = await own(f, user, CHUNK_BYTES + 7),
|
||||
bytes = Buffer.alloc(CHUNK_BYTES + 7, 120);
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
const requestId = response.json().requestId,
|
||||
hash = (value) => createHash('sha256').update(value).digest(),
|
||||
fingerprint = hash(
|
||||
Buffer.concat([hash(bytes.subarray(0, CHUNK_BYTES)), hash(bytes.subarray(CHUNK_BYTES))]),
|
||||
).toString('hex');
|
||||
const started = await f.call(user, '/uploads/resumable', 'POST', {
|
||||
requestId,
|
||||
fileIndex: 0,
|
||||
fingerprint,
|
||||
});
|
||||
assert.equal(started.statusCode, 200, started.body);
|
||||
const id = started.json().uploadId;
|
||||
const put = (offset, body) =>
|
||||
f.call(user, `/uploads/resumable/${id}`, 'PUT', body, undefined, {
|
||||
'content-type': 'application/octet-stream',
|
||||
'upload-offset': String(offset),
|
||||
});
|
||||
assert.equal((await put(0, bytes.subarray(0, CHUNK_BYTES))).statusCode, 200);
|
||||
await patch(f, admin, { perFileBytes: 1, accountBytes: 1 });
|
||||
assert.equal(
|
||||
(await put(CHUNK_BYTES, bytes.subarray(CHUNK_BYTES))).statusCode,
|
||||
200,
|
||||
'offset is checked against manifest, not a stale startup limit',
|
||||
);
|
||||
const completed = await f.call(user, `/uploads/resumable/${id}/complete`, 'POST', {});
|
||||
assert.equal(completed.statusCode, 200, completed.body);
|
||||
assert.equal((await f.call(user, '/account/storage')).json().usedBytes, bytes.length);
|
||||
assert.equal((await own(f, user)).json().code, 'TRANSFER_LIMIT');
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('offline transfer reset removes only transfer/guest data and keeps accounts, authentication, settings and user information', async () => {
|
||||
const f = await fixture(),
|
||||
folder = await mkdtemp(resolve(tmpdir(), 'hop-reset-test-'));
|
||||
let database;
|
||||
try {
|
||||
const admin = await signed(f, 'hello@noname.company'),
|
||||
user = await signed(f, 'keep@hop.test');
|
||||
const saved = await own(f, user);
|
||||
assert.equal(
|
||||
(await upload(f, user, saved.json().requestId, Buffer.from('12345678'))).statusCode,
|
||||
200,
|
||||
);
|
||||
await patch(f, admin, { accountBytes: 3e9 });
|
||||
const path = resolve(folder, 'snapshot.sqlite');
|
||||
await f.sqlite.backup(path);
|
||||
database = new Database(path);
|
||||
const kept = [
|
||||
'users',
|
||||
'account_sessions',
|
||||
'account_tokens',
|
||||
'user_rules',
|
||||
'user_activity',
|
||||
'admin_settings',
|
||||
'admin_audit',
|
||||
'__drizzle_migrations',
|
||||
];
|
||||
const before = kept.map((table) =>
|
||||
JSON.stringify(database.prepare(`SELECT * FROM "${table}"`).all()),
|
||||
);
|
||||
const script = fileURLToPath(
|
||||
new URL('../../../scripts/reset-transfer-records.mjs', import.meta.url),
|
||||
);
|
||||
const denied = spawnSync(process.execPath, [script], {
|
||||
env: { ...process.env, DATABASE_PATH: path },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.notEqual(denied.status, 0);
|
||||
assert.equal(database.prepare('SELECT count(*) AS count FROM files').get().count, 1);
|
||||
const cleared = spawnSync(process.execPath, [script, '--confirm-reset-transfer-records'], {
|
||||
env: { ...process.env, DATABASE_PATH: path },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(cleared.status, 0, cleared.stderr);
|
||||
for (const table of [
|
||||
'files',
|
||||
'items',
|
||||
'item_access',
|
||||
'transfer_requests',
|
||||
'transfer_batches',
|
||||
'quota_reservations',
|
||||
'file_upload_contexts',
|
||||
'guest_sessions',
|
||||
'browser_devices',
|
||||
'idempotency_records',
|
||||
])
|
||||
assert.equal(
|
||||
database.prepare(`SELECT count(*) AS count FROM "${table}"`).get().count,
|
||||
0,
|
||||
table,
|
||||
);
|
||||
kept.forEach((table, index) =>
|
||||
assert.equal(
|
||||
JSON.stringify(database.prepare(`SELECT * FROM "${table}"`).all()),
|
||||
before[index],
|
||||
table,
|
||||
),
|
||||
);
|
||||
assert.deepEqual(database.pragma('foreign_key_check'), []);
|
||||
assert.equal(f.service.admin.summary().storedFiles, 1, 'only isolated DB copy is reset');
|
||||
} finally {
|
||||
database?.close();
|
||||
await f.close();
|
||||
await rm(folder, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -89,7 +89,7 @@ test('signed-in uploads use the account folder; download retains the key, origin
|
||||
b = await f.create(),
|
||||
userId = await login(f, a);
|
||||
const value = await file(f, a, b);
|
||||
assert.match(value.row.storageKey, new RegExp(`^user-${userId}/[a-f0-9-]{36}\\.jpg$`));
|
||||
assert.match(value.row.storageKey, new RegExp(`^${userId}/[a-f0-9-]{36}\\.jpg$`));
|
||||
assert.equal((await f.call(a, '/vault')).json().items.length, 0);
|
||||
const expiry = f.service.db
|
||||
.select()
|
||||
@@ -119,7 +119,7 @@ test('anonymous upload uses its guest folder even when a signed-in recipient add
|
||||
b = await f.create();
|
||||
await login(f, b);
|
||||
const value = await file(f, a, b, 'received.mp4');
|
||||
assert.equal(value.row.storageKey.split('/')[0], `guest-${value.row.ownerGuestId}`);
|
||||
assert.equal(value.row.storageKey.split('/')[0], 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);
|
||||
@@ -149,8 +149,8 @@ test('the same account on different devices shares one upload folder with unique
|
||||
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.equal(one.row.storageKey.split('/')[0], id);
|
||||
assert.equal(two.row.storageKey.split('/')[0], 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);
|
||||
@@ -176,7 +176,7 @@ test('old flat originals and completed files migrate to their recorded upload ow
|
||||
.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.storageKey, `${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);
|
||||
@@ -218,7 +218,7 @@ test('migration waits for all active readers before moving a legacy file', async
|
||||
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`);
|
||||
assert.equal(current(f, value.row).storageKey, `${value.row.ownerGuestId}/${key}.jpg`);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ export function urlFor(value: string) {
|
||||
}
|
||||
const origin = 'http://hop.test';
|
||||
type Client = { cookie: string; csrf: string; deviceId: string; socket?: Socket; ip: string };
|
||||
export async function fixture(discovery = false, mailer?: import('../src/auth.js').Mailer) {
|
||||
export async function fixture(
|
||||
discovery = false,
|
||||
mailer?: import('../src/auth.js').Mailer,
|
||||
overrides: Partial<import('../src/config.js').Config> = {},
|
||||
) {
|
||||
const folder = await mkdtemp(join(tmpdir(), 'hop-api-'));
|
||||
const cfg = {
|
||||
...loadConfig({
|
||||
@@ -33,6 +37,7 @@ export async function fixture(discovery = false, mailer?: import('../src/auth.js
|
||||
minFreeBytes: 0,
|
||||
trustedProxies: ['127.0.0.1', '::1'],
|
||||
offlineGraceMs: 20,
|
||||
...overrides,
|
||||
};
|
||||
const runtime = await createApp(cfg, { mailer });
|
||||
await runtime.app.listen({ port: 0, host: '127.0.0.1' });
|
||||
|
||||
@@ -485,3 +485,83 @@ test(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('IPv4 trailing wildcards normalize to CIDR for both lists, preserve range boundaries and reject invalid patterns or operator lockout', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
const admin = await login(f),
|
||||
guest = await f.create();
|
||||
f.cfg.clientIpSource = 'cloudflare';
|
||||
for (const [value, expected] of [
|
||||
['203.0.113.*', '203.0.113.0/24'],
|
||||
['203.0.*.*', '203.0.0.0/16'],
|
||||
['203.*.*.*', '203.0.0.0/8'],
|
||||
['*.*.*.*', '0.0.0.0/0'],
|
||||
]) {
|
||||
const response = await f.call(
|
||||
admin,
|
||||
'/admin/network-rules/validate',
|
||||
'POST',
|
||||
{ kind: 'ip', value },
|
||||
undefined,
|
||||
actorHeaders,
|
||||
);
|
||||
assert.equal(response.statusCode, 200, response.body);
|
||||
assert.equal(response.json().value, expected);
|
||||
}
|
||||
for (const value of [
|
||||
'203.*.113.*',
|
||||
'203.0.113.1*',
|
||||
'203.0.113.* /24',
|
||||
'256.0.113.*',
|
||||
'203.0.*',
|
||||
'203.00.113.*',
|
||||
'2001:db8::*',
|
||||
])
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
admin,
|
||||
'/admin/network-rules/validate',
|
||||
'POST',
|
||||
{ kind: 'ip', value },
|
||||
undefined,
|
||||
actorHeaders,
|
||||
)
|
||||
).statusCode,
|
||||
400,
|
||||
value,
|
||||
);
|
||||
await patch(f, admin, {
|
||||
blockedIps: ['1.1.1.*', '1.1.1.0/24'],
|
||||
allowedIps: ['8.8.4.*', '1.1.*.*'],
|
||||
});
|
||||
assert.deepEqual(f.service.admin.state.blockedIps, ['1.1.1.0/24']);
|
||||
assert.deepEqual(new Administration(f.service).state.allowedIps, ['1.1.0.0/16', '8.8.4.0/24']);
|
||||
for (const [ip, status] of [
|
||||
['1.1.1.0', 403],
|
||||
['1.1.1.255', 403],
|
||||
['1.1.2.0', 200],
|
||||
['1.2.0.0', 403],
|
||||
['8.8.4.255', 200],
|
||||
['8.8.5.0', 403],
|
||||
])
|
||||
assert.equal(
|
||||
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers(ip))).statusCode,
|
||||
status,
|
||||
ip,
|
||||
);
|
||||
const lockout = await f.call(
|
||||
admin,
|
||||
'/admin/settings',
|
||||
'PATCH',
|
||||
{ revision: f.service.admin.state.revision, blockedIps: ['8.8.4.*'] },
|
||||
undefined,
|
||||
actorHeaders,
|
||||
);
|
||||
assert.equal(lockout.json().code, 'ADMIN_SELF_BLOCK');
|
||||
assert.deepEqual(f.service.admin.state.blockedIps, ['1.1.1.0/24']);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -130,7 +130,7 @@ test('부분 성공은 유지하고 실패 파일만 재시도', async () => {
|
||||
test('예약을 원자적으로 적용해 전체 익명 상한을 우회하지 못한다', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
f.cfg.totalGuestQuota = 4;
|
||||
f.service.admin.state.devicesTotalBytes = 4;
|
||||
const a = await f.create(),
|
||||
b = await f.create(),
|
||||
c = await f.create();
|
||||
@@ -162,7 +162,7 @@ test('예약을 원자적으로 적용해 전체 익명 상한을 우회하지
|
||||
test('개별 익명 용량, 파일당 제한과 취소 후 예약 회수', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
f.cfg.guestQuota = 3;
|
||||
f.service.admin.state.deviceBytes = 3;
|
||||
const a = await f.create(),
|
||||
b = await f.create();
|
||||
await f.space(a, b);
|
||||
@@ -173,7 +173,7 @@ test('개별 익명 용량, 파일당 제한과 취소 후 예약 회수', async
|
||||
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
||||
413,
|
||||
);
|
||||
f.cfg.guestQuota = 4;
|
||||
f.service.admin.state.deviceBytes = 4;
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
||||
200,
|
||||
|
||||
@@ -395,7 +395,7 @@ test('기존 이메일 자료: 계정 파일은 수락 후 저장되고 익명
|
||||
try {
|
||||
const a = await register(f, 'alice@example.com'),
|
||||
b = await register(f, 'bob@example.com');
|
||||
f.cfg.totalGuestQuota = 1;
|
||||
f.service.admin.state.devicesTotalBytes = 1;
|
||||
const bytes = Buffer.from('account file');
|
||||
const response = await legacyRequest(f, a, {
|
||||
email: 'bob@example.com',
|
||||
@@ -407,12 +407,12 @@ test('기존 이메일 자료: 계정 파일은 수락 후 저장되고 익명
|
||||
assert.equal(response.statusCode, 200);
|
||||
const id = response.json().requestId;
|
||||
assert.equal((await fileUpload(f, a, id, bytes)).statusCode, 409);
|
||||
f.cfg.userQuota = bytes.length - 1;
|
||||
f.service.admin.state.accountBytes = bytes.length - 1;
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
||||
413,
|
||||
);
|
||||
f.cfg.userQuota = bytes.length;
|
||||
f.service.admin.state.accountBytes = bytes.length;
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
||||
200,
|
||||
|
||||
@@ -49,7 +49,7 @@ test('개인 보관함은 임시 저장·받은 파일을 합산하고 초과
|
||||
const f = await fixture();
|
||||
try {
|
||||
limits(f);
|
||||
f.cfg.userQuota = 7;
|
||||
f.service.admin.state.accountBytes = 7;
|
||||
const sender = await f.create(),
|
||||
receiver = await f.create(),
|
||||
outsider = await f.create();
|
||||
|
||||
Reference in New Issue
Block a user