Files
TREK/server/tests/unit/nest/storage/storage-stats.service.test.ts
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

107 lines
4.7 KiB
TypeScript

import { db as testDb } from '../../../../src/db/database';
import { AppSettings } from '../../../../src/db/entities/AppSettings.entity';
import { StorageEventsService } from '../../../../src/nest/storage/storage-events.service';
import { StorageRegistryService } from '../../../../src/nest/storage/storage-registry.service';
import { StatsBusyError, StorageStatsService } from '../../../../src/nest/storage/storage-stats.service';
import { StorageService } from '../../../../src/nest/storage/storage.service';
import { deleteRows } from '../../../helpers/factories/rows';
import { readAppSetting, setAppSetting } from '../../../helpers/factories/settings';
import { createTestUnitOfWork, createTestAppSettingsRepo, sharedTestOrm } from '../../../helpers/test-uow';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
vi.mock('../../../../src/db/database', async () => {
const { createSnapshotTestDb } = await import('../../../helpers/db-mock');
const db = createSnapshotTestDb();
return { db, closeDb: () => {}, reinitialize: () => {} };
});
const tmpDirs: string[] = [];
function makeTmpDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-stats-'));
tmpDirs.push(dir);
return dir;
}
async function setSetting(key: string, value: string): Promise<void> {
await setAppSetting(await sharedTestOrm(testDb), key, value);
}
beforeEach(async () => {
await deleteRows(await sharedTestOrm(testDb), AppSettings, { key: { $like: 'storage.%' } });
});
afterEach(() => {
vi.restoreAllMocks();
while (tmpDirs.length) fs.rmSync(tmpDirs.pop()!, { recursive: true, force: true });
});
async function makeWorld() {
const uploadsRoot = makeTmpDir();
const backupsRoot = makeTmpDir();
await setSetting(
'storage.backends',
JSON.stringify([
{ name: 'uploads-local', type: 'local', options: { root: uploadsRoot } },
{ name: 'backups-local', type: 'local', options: { root: backupsRoot } },
]),
);
const env = { placePhotoDir: undefined };
const appSettings = await createTestAppSettingsRepo(testDb);
const registry = new StorageRegistryService(
appSettings,
env,
new StorageEventsService(),
await createTestUnitOfWork(testDb),
(await sharedTestOrm(testDb)).orm,
);
await registry.onModuleInit();
const storage = new StorageService(registry);
const stats = new StorageStatsService(storage, appSettings);
return { storage, stats, uploadsRoot };
}
describe('StorageStatsService', () => {
it('STATS-001 sums objects and bytes per category, legacy photos separate, and persists with computedAt', async () => {
const { storage, stats } = await makeWorld();
await storage.put('files', 'a.pdf', Readable.from('12345')); // 5 bytes
await storage.put('files', 'b.pdf', Readable.from('123')); // 3 bytes
await storage.put('covers', 'c.jpg', Readable.from('1234567')); // 7 bytes
await storage.put('photos', 'legacy.jpg', Readable.from('12')); // legacy dir
const usage = await stats.scan();
expect(usage.categories.files).toEqual({ objects: 2, bytes: 8 });
expect(usage.categories.covers).toEqual({ objects: 1, bytes: 7 });
expect(usage.categories.backups).toEqual({ objects: 0, bytes: 0 });
expect(usage.legacyPhotos).toEqual({ objects: 1, bytes: 2 });
expect(usage.computedAt).toBeGreaterThan(0);
// Persisted round-trip:
expect(await stats.readUsage()).toEqual(usage);
const raw = await readAppSetting(await sharedTestOrm(testDb), 'storage.usage');
expect(JSON.parse(raw ?? 'null')).toEqual(usage);
});
it('STATS-002 photos-google/photos-trek nested content is NOT double-counted into legacy photos', async () => {
const { storage, stats } = await makeWorld();
await storage.put('photos-google', 'g.jpg', Readable.from('gggg'));
const usage = await stats.scan();
expect(usage.categories['photos-google']).toEqual({ objects: 1, bytes: 4 });
expect(usage.legacyPhotos).toEqual({ objects: 0, bytes: 0 });
});
it('STATS-003 concurrent scans throw StatsBusyError', async () => {
const { storage, stats } = await makeWorld();
for (let i = 0; i < 30; i++) await storage.put('files', `f${i}.bin`, Readable.from('x'.repeat(500)));
const first = stats.scan();
await expect(stats.scan()).rejects.toThrow(StatsBusyError);
await first;
});
it('STATS-004 readUsage returns null on absent or unparseable rows', async () => {
const { stats } = await makeWorld();
expect(await stats.readUsage()).toBeNull();
await setSetting('storage.usage', 'not json');
expect(await stats.readUsage()).toBeNull();
});
});