From 19b544540ebc45010de74bceebfb51d46664b3ad Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Tue, 6 Feb 2024 07:46:57 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20724:=20API=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3535: API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3535) - タイピストグループ削除APIとテストを実装しました。 accountのテストがうまくいっていないようなので別途見直します。 ※タイピストグループ削除のテストはうまくいっています ## レビューポイント - エラーケースと出力されるコードは適切でしょうか? - テストケースは適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/common/error/code.ts | 3 + dictation_server/src/common/error/message.ts | 3 + dictation_server/src/common/test/modules.ts | 2 +- .../features/accounts/accounts.controller.ts | 2 +- .../accounts/accounts.service.spec.ts | 444 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 74 +++ .../accounts/test/accounts.service.mock.ts | 11 +- .../src/features/accounts/test/utility.ts | 7 + .../repositories/user_groups/errors/types.ts | 18 +- .../user_groups.repository.service.ts | 81 ++++ 10 files changed, 620 insertions(+), 25 deletions(-) diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 661c648..b6ad2af 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -76,4 +76,7 @@ export const ErrorCodes = [ 'E014007', // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) 'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった) 'E014009', // ユーザー削除エラー(削除しようとしたユーザーがタスクのルーティング(文字起こし候補)になっている場合) + 'E015001', // タイピストグループ削除エラー(削除しようとしたタイピストグループがすでに削除済みだった) + 'E015002', // タイピストグループ削除エラー(削除しようとしたタイピストグループがWorkflowのTypist候補として指定されていた) + 'E015003', // タイピストグループ削除エラー(削除しようとしたタイピストグループがチェックアウト可能なタスクが存在した) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index f885d26..20a176f 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -65,4 +65,7 @@ export const errors: Errors = { E014007: 'User delete failed Error: enabled license assigned', E014008: 'User delete failed Error: delete myself', E014009: 'User delete failed Error: user has checkout permissions.', + E015001: 'Typist Group delete failed Error: already deleted', + E015002: 'Typist Group delete failed Error: workflow assigned', + E015003: 'Typist Group delete failed Error: checkout permission existed', }; diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 7c51a50..8d50139 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -80,7 +80,7 @@ export const makeTestingModule = async ( WorktypesRepositoryModule, TermsRepositoryModule, RedisModule, - CacheModule.register({ isGlobal: true }), + CacheModule.register({ isGlobal: true, ttl: 86400 }), ], providers: [ AuthService, diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index c75d786..8e4c87f 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -834,7 +834,7 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 削除処理 + await this.accountService.deleteTypistGroup(context, userId, typistGroupId); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index fd8975f..f6f3096 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -22,6 +22,7 @@ import { getSortCriteria, getTypistGroup, getTypistGroupMember, + getTypistGroupMembers, getWorktypes, } from './test/utility'; import { DataSource } from 'typeorm'; @@ -46,6 +47,7 @@ import { LICENSE_ISSUE_STATUS, LICENSE_TYPE, OPTION_ITEM_VALUE_TYPE, + TASK_STATUS, TIERS, USER_ROLES, WORKTYPE_MAX_COUNT, @@ -76,9 +78,16 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; -import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflowTypists, + getWorkflows, +} from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; +import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; +import { createCheckoutPermissions } from '../tasks/test/utility'; describe('createAccount', () => { let source: DataSource | null = null; @@ -134,8 +143,8 @@ describe('createAccount', () => { }, }); - let _subject: string = ""; - let _url: string | undefined = ""; + let _subject: string = ''; + let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( context: Context, @@ -199,7 +208,9 @@ describe('createAccount', () => { // 想定通りのメールが送られているか確認 expect(_subject).toBe('User Registration Notification [U-102]'); - expect(_url?.startsWith('http://localhost:8081/mail-confirm?verify=')).toBeTruthy(); + expect( + _url?.startsWith('http://localhost:8081/mail-confirm?verify='), + ).toBeTruthy(); }); it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => { @@ -2281,6 +2292,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); @@ -2378,6 +2392,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); // 親と子アカウントを作成する @@ -2477,6 +2494,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const now = new Date(); // 親と子アカウントを作成する const { id: parentAccountId } = ( @@ -3611,6 +3631,319 @@ describe('updateTypistGroup', () => { }); }); +describe('deleteTypistGroup', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + }); + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + } + + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + //実行結果を確認 + { + const typistGroups = await getTypistGroup(source, account.id); + expect(typistGroups.length).toBe(0); + + const typistGroupUsers = await getTypistGroupMembers(source); + expect(typistGroupUsers.length).toBe(0); + } + }); + it('タイピストグループが存在しない場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + // 作成したアカウントにユーザーを追加する + const user = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [user.id], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015001')); + } else { + fail(); + } + } + }); + it('タイピストグループがルーティングルールに紐づいていた場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const workflow = await createWorkflow(source, account.id, authorUserId); + await createWorkflowTypist(source, workflow.id, undefined, group[0].id); + //作成したデータを確認 + { + const workflowTypists = await getWorkflowTypists(source, workflow.id); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(workflowTypists.length).toBe(1); + expect(workflowTypists[0].typist_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015002')); + } else { + fail(); + } + } + }); + it('タイピストグループがタスクのチェックアウト候補だった場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: authorId, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const { taskId } = await createTask( + source, + account.id, + authorUserId, + authorId, + 'worktypeId', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, taskId, undefined, group[0].id); + + //作成したデータを確認 + { + const checkoutPermission = await getCheckoutPermissions(source, taskId); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(checkoutPermission.length).toBe(1); + expect(checkoutPermission[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const typiptUserExternalId = 'typist-user-external-id'; + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(typistUserId); + } + + //DBアクセスに失敗するようにする + const typistGroupService = module.get( + UserGroupsRepositoryService, + ); + typistGroupService.deleteTypistGroup = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('getWorktypes', () => { let source: DataSource | null = null; beforeAll(async () => { @@ -5256,6 +5589,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5320,6 +5656,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5358,6 +5697,9 @@ describe('ライセンス発行キャンセル', () => { }); const poNumber = 'CANCEL_TEST'; const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5405,6 +5747,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5452,6 +5797,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5500,6 +5848,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5729,8 +6080,13 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ""; - let _url: string | undefined = ""; + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); + let _subject: string = ''; + let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( context: Context, @@ -5797,6 +6153,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5831,6 +6192,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5866,6 +6232,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5898,6 +6269,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5929,6 +6305,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -6000,6 +6381,9 @@ describe('getAccountInfo', () => { }); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const context = makeContext(admin.external_id, 'requestId'); const accountResponse = await service.getAccountInfo( @@ -6422,6 +6806,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); let _subject: string = ''; let _url: string | undefined = ''; overrideSendgridService(service, { @@ -6672,6 +7064,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6698,11 +7098,6 @@ describe('deleteAccountAndData', () => { deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()), }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6737,6 +7132,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除失敗 + overrideAdB2cService(service, { + deleteUsers: jest.fn().mockRejectedValue(new Error()), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6758,11 +7161,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除失敗 - overrideAdB2cService(service, { - deleteUsers: jest.fn().mockRejectedValue(new Error()), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6792,6 +7190,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); @@ -6814,11 +7220,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除失敗 overrideBlobstorageService(service, { deleteContainer: jest.fn().mockRejectedValue(new Error()), @@ -6989,6 +7390,7 @@ describe('getAccountInfoMinimalAccess', () => { } }); }); + describe('getCompanyName', () => { let source: DataSource | null = null; beforeAll(async () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 460d843..d200861 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -60,6 +60,8 @@ import { } from '../../repositories/licenses/errors/types'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -1348,6 +1350,78 @@ export class AccountsService { } } + /** + * タイピストグループを削除する + * @param context + * @param externalId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + externalId: string, + typistGroupId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTypistGroup.name + } | params: { ` + + `externalId: ${externalId}, ` + + `typistGroupId: ${typistGroupId}, `, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + // タイピストグループを削除する + await this.userGroupsRepository.deleteTypistGroup( + context, + account_id, + typistGroupId, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // タイピストグループ削除済み + case TypistGroupNotExistError: + throw new HttpException( + makeErrorResponse('E015001'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがルーティングルールに使用されている + case AssignedWorkflowDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015002'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがタスクの文字起こし候補に使用されている + case ExistsCheckoutPermissionDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTypistGroup.name}`, + ); + } + } + /** * ライセンス発行をキャンセルする * @param context diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index a031dae..58b1dd1 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -42,6 +42,7 @@ export type UserGroupsRepositoryMockValue = { export type AdB2cMockValue = { createUser: string | ConflictError | Error; getUsers: AdB2cUser[] | Error; + getUser: AdB2cUser | Error; }; export type SendGridMockValue = { sendMail: undefined | Error; @@ -206,7 +207,7 @@ export const makeUserGroupsRepositoryMock = ( }; }; export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { - const { createUser, getUsers } = value; + const { createUser, getUsers, getUser } = value; return { createUser: @@ -219,6 +220,10 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { getUsers instanceof Error ? jest.fn, []>().mockRejectedValue(getUsers) : jest.fn, []>().mockResolvedValue(getUsers), + getUser: + getUser instanceof Error + ? jest.fn, []>().mockRejectedValue(getUser) + : jest.fn, []>().mockResolvedValue(getUser), }; }; export const makeConfigMock = (value: ConfigMockValue) => { @@ -437,6 +442,10 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { displayName: 'Typist3', }, ], + getUser: { + id: 'typist1', + displayName: 'Typist1', + }, }; }; export const makeDefaultSendGridlValue = (): SendGridMockValue => { diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index e658d9b..b68b5fd 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -123,6 +123,13 @@ export const getTypistGroupMember = async ( }); }; +// タイピストグループメンバー一覧を取得する +export const getTypistGroupMembers = async ( + datasource: DataSource, +): Promise => { + return await datasource.getRepository(UserGroupMember).find(); +}; + // Worktypeを作成する export const createWorktype = async ( datasource: DataSource, diff --git a/dictation_server/src/repositories/user_groups/errors/types.ts b/dictation_server/src/repositories/user_groups/errors/types.ts index 4a215a7..a1ebabe 100644 --- a/dictation_server/src/repositories/user_groups/errors/types.ts +++ b/dictation_server/src/repositories/user_groups/errors/types.ts @@ -12,10 +12,26 @@ export class TypistIdInvalidError extends Error { this.name = 'TypistIdInvalidError'; } } + +// 削除対象グループがWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowDeleteFailedError'; + } +} + +// 削除対象グループがチェックアウト権限を持っている事が原因の削除失敗エラー +export class ExistsCheckoutPermissionDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsCheckoutPermissionDeleteFailedError'; + } +} // 同名のタイピストグループが存在する場合のエラー export class TypistGroupNameAlreadyExistError extends Error { constructor(message: string) { super(message); this.name = 'TypistGroupNameAlreadyExistError'; } -} \ No newline at end of file +} diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index a900569..8fbbc25 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -3,7 +3,10 @@ import { DataSource, In, IsNull, Not } from 'typeorm'; import { UserGroup } from './entity/user_group.entity'; import { UserGroupMember } from './entity/user_group_member.entity'; import { User } from '../users/entity/user.entity'; +import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -16,6 +19,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; @Injectable() export class UserGroupsRepositoryService { @@ -285,4 +289,81 @@ export class UserGroupsRepositoryService { return typistGroup; }); } + + /** + * 指定したIDのタイピストグループを削除します + * @param context + * @param accountId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + accountId: number, + typistGroupId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userGroupRepo = entityManager.getRepository(UserGroup); + // GroupIdが自アカウント内に存在するか確認する + const typistGroup = await userGroupRepo.findOne({ + relations: { userGroupMembers: true }, + where: { + id: typistGroupId, + account_id: accountId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!typistGroup) { + throw new TypistGroupNotExistError( + `TypistGroup not exists Error. accountId: ${accountId}; typistGroupId: ${typistGroupId}`, + ); + } + + // ルーティングルールに紐づくタイピストグループは削除できない + const workflowTypistRepo = entityManager.getRepository(WorkflowTypist); + const workflowTypist = await workflowTypistRepo.findOne({ + where: { typist_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (workflowTypist) { + throw new AssignedWorkflowDeleteFailedError( + `Typist Group is used in routing rule. typistGroupId: ${typistGroupId}`, + ); + } + + // タスクのチェックアウト候補のタイピストグループは削除できない + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + + const checkoutPermission = await checkoutPermissionRepo.findOne({ + where: { user_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (checkoutPermission) { + throw new ExistsCheckoutPermissionDeleteFailedError( + `Typist Group is used in task checkout permission. typistGroupId: ${typistGroupId}`, + ); + } + + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + // 対象のタイピストグループのユーザーを削除する + await deleteEntity( + userGroupMemberRepo, + { user_group_id: typistGroupId }, + this.isCommentOut, + context, + ); + + // 対象のタイピストグループを削除する + await deleteEntity( + userGroupRepo, + { id: typistGroupId }, + this.isCommentOut, + context, + ); + }); + } }