From ff4cd35ed3ad9240a97b031da0f80d9e58d20888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 11 Mar 2024 02:08:29 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20819:=20[3848]=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E5=89=8A=E9=99=A4=E5=87=A6=E7=90=86?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3847: [3848]アカウント削除処理修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3847) - AccountArchiveエンティティを追加 - アカウント削除時、Accountをアーカイブする処理を追加 - アーカイブ関連テストを追加 ## レビューポイント - 実装の修正内容は問題なさそうか - テストケースの修正内容は問題なさそうか - クエリの変更内容の確認方法は問題なさそうか to 斎藤さん ## レビュー対象外 - テスト用ロガーに以下の比較用前処理を追加するべきだが、別タスクを作って対応予定 - 下記クエリの変更点にて、CommentOut判定に環境変数STAGEを使用している部分にRequestIdが表示されていない部分が存在するが、テスト用環境変数の変更は上記と同じく別タスクを作って対応予定 [タスク 3889: クエリ比較用ログ出力の仕組みを改良](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/3889) ## クエリの変更 - ロガーを有効にした状態でテストを実行し、ログのUUIDと日付を処理して比較できるよう加工した - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%AF%E3%82%A8%E3%83%AA/3847?csf=1&web=1&e=xlK011 ## 動作確認状況 - npm run testで確認 - 行った修正がデグレを発生させていないことを確認できるか - アカウント削除テストで発行されるクエリを比較し、AccountArchiveする対象を特定するためのAccountのSELECT、AccountArchiveのINSERTとSELECTのみが追加されている事が確認できたので、デグレはないと判断 --- dictation_server/src/common/test/logger.ts | 65 ++++++++++++++++++ dictation_server/src/common/test/utility.ts | 7 ++ .../accounts/accounts.service.spec.ts | 34 ++++++++++ .../accounts/accounts.repository.service.ts | 19 +++++- .../accounts/entity/account_archive.entity.ts | 66 +++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 dictation_server/src/common/test/logger.ts create mode 100644 dictation_server/src/repositories/accounts/entity/account_archive.entity.ts diff --git a/dictation_server/src/common/test/logger.ts b/dictation_server/src/common/test/logger.ts new file mode 100644 index 0000000..a6f3ba1 --- /dev/null +++ b/dictation_server/src/common/test/logger.ts @@ -0,0 +1,65 @@ +import { Logger, QueryRunner } from 'typeorm'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class FileLogger implements Logger { + private logPath = path.join(__dirname, 'logs'); + + constructor() { + if (!fs.existsSync(this.logPath)) { + fs.mkdirSync(this.logPath, { recursive: true }); + } + } + + private writeToFile(message: string): void { + const logFile = path.join( + this.logPath, + `${new Date().toISOString().split('T')[0]}.log`, + ); + fs.appendFileSync(logFile, `${message}\n`); + } + + logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { + this.writeToFile( + `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`, + ); + } + + logQueryError( + error: string, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.writeToFile( + `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logQuerySlow( + time: number, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.writeToFile( + `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logSchemaBuild(message: string, queryRunner?: QueryRunner) { + this.writeToFile(`Schema Build: ${message}`); + } + + logMigration(message: string, queryRunner?: QueryRunner) { + this.writeToFile(`Migration: ${message}`); + } + + log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) { + this.writeToFile(`${level.toUpperCase()}: ${message}`); + } +} diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 6fb99ab..28c8b0c 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -8,6 +8,7 @@ import { USER_ROLES, } from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; +import { AccountArchive } from '../../repositories/accounts/entity/account_archive.entity'; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -398,6 +399,12 @@ export const getUsers = async (dataSource: DataSource): Promise => { * @param dataSource データソース * @returns ユーザー退避テーブルの内容 */ +export const getAccountArchive = async ( + dataSource: DataSource, +): Promise => { + return await dataSource.getRepository(AccountArchive).find(); +}; + export const getUserArchive = async ( dataSource: DataSource, ): Promise => { diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 47a1188..6eb5918 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -39,6 +39,7 @@ import { getUser, getLicenses, getUserArchive, + getAccountArchive, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; @@ -7158,6 +7159,11 @@ describe('deleteAccountAndData', () => { ); expect(LicenseAllocationHistoryRecordB.length).not.toBe(0); + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5AccountsA.account.id); + const UserArchive = await getUserArchive(source); expect(UserArchive.length).toBe(2); @@ -7238,6 +7244,12 @@ describe('deleteAccountAndData', () => { expect(accountRecord?.id).not.toBeNull(); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord?.id).not.toBeNull(); + + // アーカイブが作成されていないことを確認 + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(0); + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(0); }); it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => { if (!source) fail(); @@ -7296,6 +7308,17 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => { if (!source) fail(); @@ -7355,6 +7378,17 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); }); describe('getAccountInfoMinimalAccess', () => { diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index a2d4ad5..f15a428 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -64,6 +64,7 @@ import { PartnerInfoFromDb, PartnerLicenseInfoForRepository, } from '../../features/accounts/types/types'; +import { AccountArchive } from './entity/account_archive.entity'; @Injectable() export class AccountsRepositoryService { @@ -1156,6 +1157,23 @@ export class AccountsRepositoryService { accountId: number, ): Promise { return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のアカウントを退避テーブルに退避 + const accountRepo = entityManager.getRepository(Account); + const account = await accountRepo.find({ + where: { + id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const accountArchiveRepo = entityManager.getRepository(AccountArchive); + await insertEntities( + AccountArchive, + accountArchiveRepo, + account, + this.isCommentOut, + context, + ); + // 削除対象のユーザーを退避テーブルに退避 const users = await this.dataSource.getRepository(User).find({ where: { @@ -1209,7 +1227,6 @@ export class AccountsRepositoryService { ); // アカウントを削除 - const accountRepo = entityManager.getRepository(Account); await deleteEntity( accountRepo, { id: accountId }, diff --git a/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts new file mode 100644 index 0000000..d78c0ba --- /dev/null +++ b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts @@ -0,0 +1,66 @@ +import { bigintTransformer } from '../../../common/entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'accounts_archive' }) +export class AccountArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + parent_account_id: number | null; + + @Column() + tier: number; + + @Column() + country: string; + + @Column({ default: false }) + delegation_permission: boolean; + + @Column({ default: false }) + locked: boolean; + + @Column({ default: false }) + verified: boolean; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + primary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + secondary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + active_worktype_id: number | null; + + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: 0 }) + file_retention_days: number; + + @Column({ nullable: true, type: 'datetime' }) + deleted_at: Date | null; + + @Column({ nullable: true, type: 'datetime' }) + created_by: string | null; + + @CreateDateColumn({ + type: 'datetime', + }) + created_at: Date; + + @Column({ nullable: true, type: 'datetime' }) + updated_by: string | null; + + @UpdateDateColumn({ + type: 'datetime', + }) + updated_at: Date; +}