feat: synchronize account icons across game profiles
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveAccountIconProjection } from '../src/auth/accountIconProjection.js';
|
||||
|
||||
describe('account icon projection', () => {
|
||||
it('resolves the current account icon from its durable revision', () => {
|
||||
expect(
|
||||
resolveAccountIconProjection({
|
||||
createdAt: '2026-07-01T00:00:00.000Z',
|
||||
iconUpdatedAt: '2026-07-31T09:00:00.000Z',
|
||||
picture: 'account.png',
|
||||
imageServer: 1,
|
||||
})
|
||||
).toEqual({
|
||||
revision: '2026-07-31T09:00:00.000Z',
|
||||
picture: 'account.png',
|
||||
imageServer: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('lets an administrator reset win at the same or a later revision', () => {
|
||||
expect(
|
||||
resolveAccountIconProjection({
|
||||
createdAt: '2026-07-01T00:00:00.000Z',
|
||||
iconUpdatedAt: '2026-07-31T09:00:00.000Z',
|
||||
profileIconResetAt: '2026-07-31T09:00:00.000Z',
|
||||
picture: 'account.png',
|
||||
imageServer: 1,
|
||||
})
|
||||
).toEqual({
|
||||
revision: '2026-07-31T09:00:00.000Z',
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ createdAt: 'not-a-date', picture: 'account.png', imageServer: 1 },
|
||||
{ createdAt: '2026-07-31T09:00:00.000Z', picture: '', imageServer: 1 },
|
||||
{ createdAt: '2026-07-31T09:00:00.000Z', picture: 'account.png', imageServer: -1 },
|
||||
])('rejects invalid durable account icon state: %j', (value) => {
|
||||
expect(() => resolveAccountIconProjection(value)).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
decryptGameSessionToken,
|
||||
encryptGameSessionToken,
|
||||
parseGameSessionTokenPayload,
|
||||
type GameSessionTokenPayload,
|
||||
} from '../src/auth/gameToken.js';
|
||||
|
||||
const buildPayload = (): GameSessionTokenPayload => ({
|
||||
version: 1,
|
||||
profile: 'che:default',
|
||||
issuedAt: '2026-07-31T09:00:00.000Z',
|
||||
expiresAt: '2026-07-31T09:10:00.000Z',
|
||||
sessionId: 'session-1',
|
||||
user: {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '테스트',
|
||||
roles: ['user'],
|
||||
picture: 'account-icon.png',
|
||||
imageServer: 1,
|
||||
iconUpdatedAt: '2026-07-31T08:59:00.000Z',
|
||||
},
|
||||
sanctions: {},
|
||||
});
|
||||
|
||||
describe('game session token account icon revision', () => {
|
||||
it('round-trips the canonical account icon revision', () => {
|
||||
const payload = buildPayload();
|
||||
const token = encryptGameSessionToken(payload, 'test-only-secret');
|
||||
|
||||
expect(decryptGameSessionToken(token, 'test-only-secret')).toEqual(payload);
|
||||
});
|
||||
|
||||
it.each([1, {}, '2026-07-31', '2026-07-31T08:59:00Z', 'not-a-date'])(
|
||||
'rejects a non-canonical icon revision: %j',
|
||||
(iconUpdatedAt) => {
|
||||
const payload = buildPayload() as unknown as {
|
||||
user: { iconUpdatedAt: unknown };
|
||||
};
|
||||
payload.user.iconUpdatedAt = iconUpdatedAt;
|
||||
|
||||
expect(parseGameSessionTokenPayload(payload)).toBeNull();
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user