Files
core2026/tools/legacy-db-migration/test/legacyUserIcons.postgres.test.ts
T
Hide_D cd0f50f89d fix(migration): 검증 실패 아이콘을 명시적으로 제외한다
검토한 회원 번호의 이미지가 계속 유효성 조건을 어길 때만 업로드에서 제외한다. 기존 Ref 경로가 현재 선택인 경우 기본 아이콘으로 정리하고 최신 Core 선택은 보존한다.
2026-08-24 08:45:47 +00:00

110 lines
5.1 KiB
TypeScript

import { Pool } from 'pg';
import { describe, expect, it } from 'vitest';
import { legacyUserId } from '../src/identity.js';
import { syncImportedUserIcons, syncRejectedUserIcons, 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);
const rejectedUserId = legacyUserId(700_004);
await client.query(
`INSERT INTO "app_user"
("id", "login_id", "display_name", "password_hash", "password_salt",
"updated_at", "picture", "image_server")
VALUES ($1, 'icon-test-rejected', '아이콘테스트-제외', 'hash', 'salt',
CURRENT_TIMESTAMP, 'invalid.gif?=20260809', 1)`,
[rejectedUserId]
);
await expect(
syncRejectedUserIcons(
client,
[
{
memberNo: 700_004,
userId: rejectedUserId,
sourcePicture: 'invalid.gif?=20260809',
normalizedSourcePicture: 'invalid.gif',
sourceImageServer: 1,
reason: 'not square',
},
],
new Date('2026-08-24T00:00:00Z')
)
).resolves.toEqual({ currentReset: 1, targetPreserved: 0 });
const rejectedAccount = await client.query<{ picture: string; image_server: number }>(
`SELECT "picture", "image_server" FROM "app_user" WHERE "id" = $1`,
[rejectedUserId]
);
expect(rejectedAccount.rows[0]).toEqual({ picture: 'default.jpg', image_server: 0 });
} finally {
await client.query('ROLLBACK');
client.release();
await pool.end();
}
});
});