mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
724c88c201
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.
107 lines
4.7 KiB
TypeScript
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();
|
|
});
|
|
});
|