fix(migration): Ref 전용 아이콘을 업로드 API로 이관한다
전용 아이콘 바이트를 모두 검증한 뒤 sam-image 서명 API에 결정적 경로로 등록한다. API 반환 경로를 계정과 아이콘 소유 목록에 연결하고 Core에서 바뀐 현재 선택은 보존한다.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { Pool } from 'pg';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { legacyUserId } from '../src/identity.js';
|
||||
import { syncImportedUserIcons, type PreparedLegacyUserIcon } from '../src/legacyUserIcons.js';
|
||||
|
||||
const databaseUrl = process.env.LEGACY_ICON_TEST_DATABASE_URL;
|
||||
|
||||
const imported = (memberNo: number, sourcePicture: string, picture: string): PreparedLegacyUserIcon => ({
|
||||
memberNo,
|
||||
userId: legacyUserId(memberNo),
|
||||
sourcePicture,
|
||||
normalizedSourcePicture: sourcePicture.replace(/\?=[0-9]{8}$/u, ''),
|
||||
sourceImageServer: 1,
|
||||
picture,
|
||||
imageServer: 0,
|
||||
createdAt: new Date('2026-08-09T00:00:00.000Z'),
|
||||
source: 'legacy-file',
|
||||
sha256: 'a'.repeat(64),
|
||||
});
|
||||
|
||||
describe.skipIf(!databaseUrl)('legacy user icon PostgreSQL synchronization', () => {
|
||||
it('updates only an unchanged Ref selection and preserves newer Core state', async () => {
|
||||
const pool = new Pool({ connectionString: databaseUrl });
|
||||
const client = await pool.connect();
|
||||
const icons = [
|
||||
imported(700_001, 'first.png?=20260809', `users/core2026/${'1'.repeat(32)}.png`),
|
||||
imported(700_002, 'second.png?=20260809', `users/core2026/${'2'.repeat(32)}.png`),
|
||||
imported(700_003, 'third.png?=20260809', `users/core2026/${'3'.repeat(32)}.png`),
|
||||
];
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
for (const [index, icon] of icons.entries()) {
|
||||
const currentPicture =
|
||||
index === 0
|
||||
? icon.sourcePicture
|
||||
: index === 1
|
||||
? `users/core2026/${'8'.repeat(32)}.png`
|
||||
: 'default.jpg';
|
||||
await client.query(
|
||||
`INSERT INTO "app_user"
|
||||
("id", "login_id", "display_name", "password_hash", "password_salt",
|
||||
"updated_at", "picture", "image_server")
|
||||
VALUES ($1, $2, $3, 'hash', 'salt', CURRENT_TIMESTAMP, $4, $5)`,
|
||||
[icon.userId, `icon-test-${index}`, `아이콘테스트-${index}`, currentPicture, index === 0 ? 1 : 0]
|
||||
);
|
||||
}
|
||||
|
||||
await expect(syncImportedUserIcons(client, icons, new Date('2026-08-24T00:00:00Z'))).resolves.toEqual({
|
||||
currentLinked: 1,
|
||||
libraryInserted: 3,
|
||||
libraryRetired: 1,
|
||||
targetPreserved: 2,
|
||||
});
|
||||
const accounts = await client.query<{ id: string; picture: string; image_server: number }>(
|
||||
`SELECT "id", "picture", "image_server" FROM "app_user"
|
||||
WHERE "id" = ANY($1::text[]) ORDER BY "login_id"`,
|
||||
[icons.map((icon) => icon.userId)]
|
||||
);
|
||||
expect(accounts.rows.map(({ picture, image_server: imageServer }) => ({ picture, imageServer }))).toEqual([
|
||||
{ picture: icons[0]!.picture, imageServer: 0 },
|
||||
{ picture: `users/core2026/${'8'.repeat(32)}.png`, imageServer: 0 },
|
||||
{ picture: 'default.jpg', imageServer: 0 },
|
||||
]);
|
||||
const library = await client.query<{ picture: string; retired_at: Date | null }>(
|
||||
`SELECT "picture", "retired_at" FROM "user_icon"
|
||||
WHERE "user_id" = ANY($1::text[]) ORDER BY "picture"`,
|
||||
[icons.map((icon) => icon.userId)]
|
||||
);
|
||||
expect(library.rows).toHaveLength(3);
|
||||
expect(library.rows.filter((row) => row.retired_at !== null)).toHaveLength(1);
|
||||
} finally {
|
||||
await client.query('ROLLBACK');
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user