Files
core2026/app/gateway-api/test/accountIconInternalRoute.test.ts
T

104 lines
3.9 KiB
TypeScript

import { createHmac, randomUUID } from 'node:crypto';
import fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { registerAccountIconInternalRoute } from '../src/auth/accountIconInternalRoute.js';
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
const secret = 'gateway-test-secret';
const token = createHmac('sha256', secret).update('sammo:account-icon-source:v1').digest('hex');
describe('account icon internal route', () => {
const app = fastify();
const users = createInMemoryUserRepository();
let userId = '';
beforeEach(async () => {
if (!app.hasRoute({ method: 'GET', url: '/internal/account-icons/:userId' })) {
registerAccountIconInternalRoute(app, { users, secret });
}
const user = await users.createUser({
username: `internal-${randomUUID()}`,
password: 'password',
displayName: `내부-${randomUUID()}`,
});
userId = user.id;
await users.updateIcon(userId, 'latest.png', 1, new Date('2026-07-31T09:00:00.000Z'));
});
afterEach(async () => {
if (userId) {
await users.deleteUser(userId);
}
});
it('requires the purpose-derived token and exposes only the projection', async () => {
const unauthorized = await app.inject({
method: 'GET',
url: `/internal/account-icons/${userId}`,
headers: { 'x-sammo-internal-token': secret },
});
expect(unauthorized.statusCode).toBe(401);
const response = await app.inject({
method: 'GET',
url: `/internal/account-icons/${userId}`,
headers: { 'x-sammo-internal-token': token },
});
expect(response.statusCode).toBe(200);
expect(response.headers['cache-control']).toBe('no-store');
expect(response.json()).toEqual({
revision: '2026-07-31T09:00:00.000Z',
picture: 'latest.png',
imageServer: 1,
});
expect(Object.keys(response.json()).sort()).toEqual(['imageServer', 'picture', 'revision']);
});
it('returns 404 for a missing account without leaking account fields', async () => {
const response = await app.inject({
method: 'GET',
url: `/internal/account-icons/${randomUUID()}`,
headers: { 'x-sammo-internal-token': token },
});
expect(response.statusCode).toBe(404);
expect(response.json()).toEqual({ ok: false, error: 'not_found' });
});
it('returns the durable reset marker even after a newer ordinary icon change', async () => {
const resetRevision = await users.resetProfileIcon(userId, new Date('2099-07-31T09:00:00.001Z'));
await users.updateIcon(userId, 'newer.png', 1, new Date('2099-07-31T09:00:00.002Z'));
const response = await app.inject({
method: 'POST',
url: '/internal/account-icon-resets',
headers: { 'x-sammo-internal-token': token },
payload: { userIds: [userId, randomUUID()] },
});
expect(response.statusCode).toBe(200);
expect(response.headers['cache-control']).toBe('no-store');
expect(response.json()).toEqual({
resets: [
{
userId,
resetRevision,
current: {
revision: '2099-07-31T09:00:00.002Z',
picture: 'newer.png',
imageServer: 1,
},
},
],
});
const invalid = await app.inject({
method: 'POST',
url: '/internal/account-icon-resets',
headers: { 'x-sammo-internal-token': token },
payload: { userIds: [userId], extra: true },
});
expect(invalid.statusCode).toBe(400);
});
});