refactor(auth): spool avatar upload through storage

This commit is contained in:
jubnl
2026-08-16 09:46:27 +02:00
parent 8b0c768ba0
commit c75d431e4c
4 changed files with 64 additions and 23 deletions
+26
View File
@@ -108,6 +108,32 @@ describe('Avatar', () => {
expect(typeof res.body.avatar_url).toBe('string');
});
it('PROFILE-P01 — avatar bytes land at uploads/avatars under a bare uuid name', async () => {
const fsMod = require('fs') as typeof import('fs');
const pathMod = require('path') as typeof import('path');
const avatarsDir = pathMod.join(__dirname, '../../uploads/avatars');
const { user } = createUser(testDb);
const res = await request(app)
.post('/api/auth/avatar')
.set('Cookie', authCookie(user.id))
.attach('avatar', FIXTURE_JPEG);
expect(res.status).toBe(200);
const first = pathMod.basename(res.body.avatar_url);
expect(first).toMatch(/^[0-9a-f-]{36}\.jpg$/);
expect(fsMod.existsSync(pathMod.join(avatarsDir, first))).toBe(true);
// Re-upload removes the previous file (saveAvatar's cleanup branch).
const second = await request(app)
.post('/api/auth/avatar')
.set('Cookie', authCookie(user.id))
.attach('avatar', FIXTURE_JPEG);
expect(second.status).toBe(200);
const secondName = pathMod.basename(second.body.avatar_url);
expect(fsMod.existsSync(pathMod.join(avatarsDir, secondName))).toBe(true);
expect(fsMod.existsSync(pathMod.join(avatarsDir, first))).toBe(false);
});
it('PROFILE-003 — uploading non-image (PDF) is rejected', async () => {
const { user } = createUser(testDb);
const res = await request(app)
@@ -35,8 +35,9 @@ const audit = { writeAudit } as unknown as AuditService;
const apc = (a: AuthService, limiter: RateLimitService) => new AuthPublicController(a, limiter, audit);
// Tokens moved to TokenService; the controller takes it second. Stubbed via a
// third, optional argument so every non-token call site stays as it was.
const storageStub = { put: vi.fn().mockResolvedValue(undefined) } as unknown as import('../../../src/nest/storage/storage.service').StorageService;
const ac = (a: AuthService, limiter: RateLimitService, t: Partial<TokenService> = {}, pr: Partial<UserProfileService> = {}) =>
new AuthController(a, pr as UserProfileService, t as TokenService, limiter, audit, new RuntimeEnvService());
new AuthController(a, pr as UserProfileService, t as TokenService, limiter, audit, new RuntimeEnvService(), storageStub);
function thrown(fn: () => unknown): { status: number; body: unknown } {
try { fn(); } catch (err) {