diff --git a/dictation_server/src/common/jwt/jwt.ts b/dictation_server/src/common/jwt/jwt.ts index f50f01d..2e97368 100644 --- a/dictation_server/src/common/jwt/jwt.ts +++ b/dictation_server/src/common/jwt/jwt.ts @@ -132,7 +132,7 @@ export const getPrivateKey = (configService: ConfigService): string => { return ( // 開発環境用に改行コードを置換する // 本番環境では\\nが含まれないため、置換が行われない想定 - configService.get('JWT_PRIVATE_KEY')?.replace(/\\n/g, '\n') ?? '' + configService.getOrThrow('JWT_PRIVATE_KEY').replace(/\\n/g, '\n') ); }; @@ -140,6 +140,6 @@ export const getPublicKey = (configService: ConfigService): string => { return ( // 開発環境用に改行コードを置換する // 本番環境では\\nが含まれないため、置換が行われない想定 - configService.get('JWT_PUBLIC_KEY')?.replace(/\\n/g, '\n') ?? '' + configService.getOrThrow('JWT_PUBLIC_KEY').replace(/\\n/g, '\n') ); }; diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index dbc8a6f..2efce63 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -140,6 +140,9 @@ export const makeDefaultUsersRepositoryMockValue = notification: true, encryption: false, prompt: false, + encryption_password: null, + license: null, + userGroupMembers: null, account: { id: 2, parent_account_id: 2, @@ -172,6 +175,14 @@ export const makeDefaultTasksRepositoryMockValue = status: 'Uploaded', priority: '01', created_at: new Date(), + finished_at: null, + started_at: null, + template_file_id: null, + typist_user_id: null, + file: null, + option_items: null, + template_file: null, + typist_user: null, }, getTasksFromAccountId: { tasks: [], diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index b2c860e..40b7de0 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, Headers, + HttpException, HttpStatus, Param, ParseIntPipe, @@ -32,6 +33,8 @@ import { TasksResponse, } from './types/types'; import { + SortDirection, + TaskListSortableAttribute, isSortDirection, isTaskListSortableAttribute, } from '../../common/types/sort'; @@ -43,6 +46,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES, USER_ROLES } from '../../constants'; import { Roles } from '../../common/types/role'; import { makeContext } from '../../common/log'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('tasks') @Controller('tasks') @@ -80,22 +84,39 @@ export class TasksController { @Req() req, @Query() body: TasksRequest, ): Promise { - const accessToken = retrieveAuthorizationToken(req); - const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, role } = decodedAccessToken as AccessToken; + // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う + const roles = role.split(' ') as Roles[]; - const context = makeContext(decodedToken.userId); + const context = makeContext(userId); const { limit, offset, status } = body; - const paramName = isTaskListSortableAttribute(body.paramName) - ? body.paramName + const paramName = isTaskListSortableAttribute(body.paramName ?? '') + ? (body.paramName as TaskListSortableAttribute) : undefined; - const direction = isSortDirection(body.direction) - ? body.direction + const direction = isSortDirection(body.direction ?? '') + ? (body.direction as SortDirection) : undefined; const { tasks, total } = await this.taskService.getTasks( context, - decodedToken, + userId, + roles, offset, limit, // statusが指定されていない場合は全てのステータスを取得する @@ -183,10 +204,22 @@ export class TasksController { @Param() param: ChangeStatusRequest, ): Promise { // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない - const accessToken = retrieveAuthorizationToken(req); - const { role, userId } = jwt.decode(accessToken, { - json: true, - }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; @@ -241,10 +274,22 @@ export class TasksController { ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない - const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { - json: true, - }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, role } = decodedAccessToken as AccessToken; const context = makeContext(userId); @@ -296,10 +341,22 @@ export class TasksController { ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない - const accessToken = retrieveAuthorizationToken(req); - const { userId, role } = jwt.decode(accessToken, { - json: true, - }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; @@ -353,10 +410,22 @@ export class TasksController { ): Promise { const { audioFileId } = params; // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない - const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { - json: true, - }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); @@ -491,11 +560,22 @@ export class TasksController { @Body() body: PostCheckoutPermissionRequest, ): Promise { const { assignees } = body; - const accessToken = retrieveAuthorizationToken(req); - - const { role, userId } = jwt.decode(accessToken, { - json: true, - }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, role } = decodedAccessToken as AccessToken; // RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う const roles = role.split(' ') as Roles[]; diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 6b11396..f203563 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -16,11 +16,12 @@ import { createUserGroup, getCheckoutPermissions, getTask, - makeTaskTestingModule, + makeTaskTestingModuleWithNotificaiton, } from './test/utility'; import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeContext } from '../../common/log'; import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility'; +import { ADMIN_ROLES, USER_ROLES } from '../../constants'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -39,7 +40,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'admin', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -48,7 +49,8 @@ describe('TasksService', () => { expect( await service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -102,7 +104,6 @@ describe('TasksService', () => { const userGroupsRepositoryMockValue = makeDefaultUserGroupsRepositoryMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); - const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); usersRepositoryMockValue.findUserByExternalId = new Error('DB failed'); @@ -114,7 +115,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'admin', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -123,7 +124,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -155,7 +157,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'admin', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -164,7 +166,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -192,7 +195,13 @@ describe('TasksService', () => { status: 'Uploaded', priority: '00', created_at: new Date('2023-01-01T01:01:01.000'), - option_items: undefined, // 存在しない場合でも空配列であるはずのものがundefined + option_items: null, // 存在しない場合でも空配列であるはずのものがnullになっているため形式不正 + finished_at: null, + started_at: null, + typist_user_id: null, + template_file_id: null, + typist_user: null, + template_file: null, file: { id: 1, account_id: 1, @@ -229,7 +238,7 @@ describe('TasksService', () => { adb2cServiceMockValue, notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'admin', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -238,7 +247,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -273,7 +283,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'author', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -281,7 +291,8 @@ describe('TasksService', () => { const direction = 'ASC'; const result = await service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [USER_ROLES.AUTHOR], offset, limit, status, @@ -354,7 +365,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'author', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -363,7 +374,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [USER_ROLES.AUTHOR], offset, limit, status, @@ -399,7 +411,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'typist', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -407,7 +419,8 @@ describe('TasksService', () => { const direction = 'ASC'; const result = await service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [USER_ROLES.TYPIST], offset, limit, status, @@ -480,7 +493,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'typist', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -489,47 +502,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, - offset, - limit, - status, - paramName, - direction, - ), - ).rejects.toEqual( - new HttpException( - makeErrorResponse('E009999'), - HttpStatus.INTERNAL_SERVER_ERROR, - ), - ); - }); - - it('想定外のRoleの場合、エラーを返却する', async () => { - const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue(); - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - const userGroupsRepositoryMockValue = - makeDefaultUserGroupsRepositoryMockValue(); - const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const service = await makeTasksServiceMock( - tasksRepositoryMockValue, - usersRepositoryMockValue, - userGroupsRepositoryMockValue, - adb2cServiceMockValue, - notificationhubServiceMockValue, - ); - - const accessToken = { userId: 'userId', role: 'XXX', tier: 5 }; - const offset = 0; - const limit = 20; - const status = ['Uploaded', 'Backup']; - const paramName = 'JOB_NUMBER'; - const direction = 'ASC'; - await expect( - service.tasksService.getTasks( - makeContext('trackingId'), - accessToken, + userId, + [USER_ROLES.TYPIST], offset, limit, status, @@ -561,7 +535,7 @@ describe('TasksService', () => { notificationhubServiceMockValue, ); - const accessToken = { userId: 'userId', role: 'admin', tier: 5 }; + const userId = 'userId'; const offset = 0; const limit = 20; const status = ['Uploaded,Backup']; @@ -570,7 +544,8 @@ describe('TasksService', () => { await expect( service.tasksService.getTasks( makeContext('trackingId'), - accessToken, + userId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -586,7 +561,7 @@ describe('TasksService', () => { }); describe('DBテスト', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -599,6 +574,7 @@ describe('TasksService', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); @@ -606,10 +582,12 @@ describe('TasksService', () => { it('[Admin] Taskが0件であっても実行できる', async () => { const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + if (!source) fail(); + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: externalId } = await makeTestUser(source, { account_id: accountId, @@ -618,7 +596,6 @@ describe('TasksService', () => { }); const service = module.get(TasksService); - const accessToken = { userId: externalId, role: 'admin', tier: 5 }; const offset = 0; const limit = 20; const status = ['Uploaded,Backup']; @@ -627,7 +604,8 @@ describe('TasksService', () => { const { tasks, total } = await service.getTasks( makeContext('trackingId'), - accessToken, + externalId, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], offset, limit, status, @@ -637,15 +615,18 @@ describe('TasksService', () => { expect(tasks).toEqual([]); expect(total).toEqual(0); }); + it('[Author] Authorは自分が作成者のTask一覧を取得できる', async () => { const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + if (!source) fail(); + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { id: userId } = await makeTestUser(source, { + const { id: userId, external_id } = await makeTestUser(source, { account_id: accountId, external_id: 'userId', role: 'author', @@ -673,7 +654,6 @@ describe('TasksService', () => { ); const service = module.get(TasksService); - const accessToken = { userId: 'userId', role: 'author', tier: 5 }; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -682,7 +662,8 @@ describe('TasksService', () => { const { tasks, total } = await service.getTasks( makeContext('trackingId'), - accessToken, + external_id, + [USER_ROLES.AUTHOR], offset, limit, status, @@ -703,12 +684,14 @@ describe('TasksService', () => { it('[Author] Authorは同一アカウントであっても自分以外のAuhtorのTaskは取得できない', async () => { const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + if (!source) fail(); + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { id: userId_1 } = await makeTestUser(source, { + const { id: userId_1, external_id } = await makeTestUser(source, { account_id: accountId, external_id: 'userId_1', role: 'author', @@ -743,7 +726,6 @@ describe('TasksService', () => { ); const service = module.get(TasksService); - const accessToken = { userId: 'userId_1', role: 'author', tier: 5 }; const offset = 0; const limit = 20; const status = ['Uploaded', 'Backup']; @@ -752,7 +734,8 @@ describe('TasksService', () => { const { tasks, total } = await service.getTasks( makeContext('trackingId'), - accessToken, + external_id, + [USER_ROLES.AUTHOR], offset, limit, status, @@ -769,7 +752,7 @@ describe('TasksService', () => { }); describe('changeCheckoutPermission', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -782,17 +765,20 @@ describe('changeCheckoutPermission', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('タスクのチェックアウト権限を変更できる。(個人指定)', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -847,12 +833,14 @@ describe('changeCheckoutPermission', () => { }); it('タスクのチェックアウト権限を変更できる。(グループ指定)', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -913,12 +901,14 @@ describe('changeCheckoutPermission', () => { }); it('タスクのチェックアウト権限を変更できる。(チェックアウト権限を外す)', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -962,12 +952,14 @@ describe('changeCheckoutPermission', () => { }); it('ユーザーが存在しない場合、タスクのチェックアウト権限を変更できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -1014,12 +1006,14 @@ describe('changeCheckoutPermission', () => { }); it('ユーザーグループが存在しない場合、タスクのチェックアウト権限を変更できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -1066,12 +1060,14 @@ describe('changeCheckoutPermission', () => { }); it('タスクが存在しない場合、タスクのチェックアウト権限を変更できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1100,12 +1096,14 @@ describe('changeCheckoutPermission', () => { }); it('タスクのステータスがUploadedでない場合、タスクのチェックアウト権限を変更できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1144,12 +1142,14 @@ describe('changeCheckoutPermission', () => { }); it('ユーザーのRoleがAuthorでタスクのAuthorIDと自身のAuthorIDが一致しない場合、タスクのチェックアウト権限を変更できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1188,13 +1188,15 @@ describe('changeCheckoutPermission', () => { }); it('通知に失敗した場合、エラーとなる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); notificationhubServiceMockValue.notify = new Error('Notify Error.'); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId_1 } = await makeTestUser(source, { account_id: accountId, @@ -1250,7 +1252,7 @@ describe('changeCheckoutPermission', () => { }); describe('checkout', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1263,17 +1265,20 @@ describe('checkout', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('ユーザーのRoleがTypistで、タスクのチェックアウト権限が個人指定である時、タスクをチェックアウトできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1315,15 +1320,12 @@ describe('checkout', () => { ['typist'], 'typist-user-external-id', ); - const { status, typist_user_id, started_at } = await getTask( - source, - taskId, - ); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('InProgress'); - expect(typist_user_id).toEqual(typistUserId); - expect(started_at).not.toEqual(initTask.started_at); + expect(resultTask?.status).toEqual('InProgress'); + expect(resultTask?.typist_user_id).toEqual(typistUserId); + expect(resultTask?.started_at).not.toEqual(initTask?.started_at); expect(permisions.length).toEqual(1); expect(permisions[0]).toEqual({ id: 3, @@ -1334,12 +1336,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがTypistで、タスクのチェックアウト権限がグループ指定である時、タスクをチェックアウトできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1381,15 +1385,12 @@ describe('checkout', () => { ['typist'], 'typist-user-external-id', ); - const { status, typist_user_id, started_at } = await getTask( - source, - taskId, - ); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('InProgress'); - expect(typist_user_id).toEqual(typistUserId); - expect(started_at).not.toEqual(initTask.started_at); + expect(resultTask?.status).toEqual('InProgress'); + expect(resultTask?.typist_user_id).toEqual(typistUserId); + expect(resultTask?.started_at).not.toEqual(initTask?.started_at); expect(permisions.length).toEqual(1); expect(permisions[0]).toEqual({ id: 3, @@ -1400,12 +1401,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがTypistで、タスクのステータスがPendingである時、タスクをチェックアウトできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1440,16 +1443,13 @@ describe('checkout', () => { ['typist'], 'typist-user-external-id', ); - const { status, typist_user_id, started_at } = await getTask( - source, - taskId, - ); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('InProgress'); - expect(typist_user_id).toEqual(typistUserId); + expect(resultTask?.status).toEqual('InProgress'); + expect(resultTask?.typist_user_id).toEqual(typistUserId); //タスクの元々のステータスがPending,Inprogressの場合、文字起こし開始時刻は更新されない - expect(started_at).toEqual(initTask.started_at); + expect(resultTask?.started_at).toEqual(initTask?.started_at); expect(permisions.length).toEqual(1); expect(permisions[0]).toEqual({ id: 2, @@ -1460,12 +1460,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1504,12 +1506,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1548,12 +1552,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Uploaded)', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1584,12 +1590,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Finished)', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1620,12 +1628,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクが存在しない場合、タスクをチェックアウトできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1648,12 +1658,14 @@ describe('checkout', () => { }); it('ユーザーのRoleがAuthorで、音声ファイルに紐づいたタスクでユーザーと一致するAuthorIDでない場合、タスクをチェックアウトできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { account_id: accountId, @@ -1686,12 +1698,14 @@ describe('checkout', () => { }); it('ユーザーのRoleに[Typist,author]が設定されていない時、タスクをチェックアウトできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1715,7 +1729,7 @@ describe('checkout', () => { }); describe('checkin', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1728,17 +1742,20 @@ describe('checkin', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('API実行者が文字起こし実行中のタスクである場合、タスクをチェックインできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1773,19 +1790,21 @@ describe('checkin', () => { 1, 'typist-user-external-id', ); - const { status, finished_at } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); - expect(status).toEqual('Finished'); - expect(finished_at).not.toEqual(initTask.finished_at); + expect(resultTask?.status).toEqual('Finished'); + expect(resultTask?.finished_at).not.toEqual(initTask?.finished_at); }); it('タスクのステータスがInprogressでない時、タスクをチェックインできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1820,12 +1839,14 @@ describe('checkin', () => { }); it('API実行者が文字起こし実行中のタスクでない場合、タスクをチェックインできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1866,12 +1887,14 @@ describe('checkin', () => { }); it('タスクがない時、タスクをチェックインできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -1897,7 +1920,7 @@ describe('checkin', () => { }); describe('suspend', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1910,17 +1933,20 @@ describe('suspend', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('API実行者が文字起こし実行中のタスクである場合、タスクを中断できる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1952,18 +1978,20 @@ describe('suspend', () => { 1, 'typist-user-external-id', ); - const { status } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); - expect(status).toEqual('Pending'); + expect(resultTask?.status).toEqual('Pending'); }); it('タスクのステータスがInprogressでない時、タスクを中断できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -1998,12 +2026,14 @@ describe('suspend', () => { }); it('API実行者が文字起こし実行中のタスクでない場合、タスクを中断できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -2044,12 +2074,14 @@ describe('suspend', () => { }); it('タスクがない時、タスクを中断できない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -2075,7 +2107,7 @@ describe('suspend', () => { }); describe('cancel', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -2088,17 +2120,20 @@ describe('cancel', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -2132,21 +2167,23 @@ describe('cancel', () => { 'typist-user-external-id', ['typist', 'standard'], ); - const { status, typist_user_id } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('Uploaded'); - expect(typist_user_id).toEqual(null); + expect(resultTask?.status).toEqual('Uploaded'); + expect(resultTask?.typist_user_id).toEqual(null); expect(permisions.length).toEqual(0); }); it('API実行者のRoleがTypistの場合、自身が文字起こし中断しているタスクをキャンセルできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -2179,22 +2216,24 @@ describe('cancel', () => { 'typist-user-external-id', ['typist', 'standard'], ); - const { status, typist_user_id } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('Uploaded'); - expect(typist_user_id).toEqual(null); + expect(resultTask?.status).toEqual('Uploaded'); + expect(resultTask?.typist_user_id).toEqual(null); expect(permisions.length).toEqual(0); }); it('API実行者のRoleがAdminの場合、文字起こし実行中のタスクをキャンセルできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -2229,21 +2268,23 @@ describe('cancel', () => { 'typist-user-external-id', ['admin', 'author'], ); - const { status, typist_user_id } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('Uploaded'); - expect(typist_user_id).toEqual(null); + expect(resultTask?.status).toEqual('Uploaded'); + expect(resultTask?.typist_user_id).toEqual(null); expect(permisions.length).toEqual(0); }); it('API実行者のRoleがAdminの場合、文字起こし中断しているタスクをキャンセルできる', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -2278,21 +2319,23 @@ describe('cancel', () => { 'typist-user-external-id', ['admin', 'author'], ); - const { status, typist_user_id } = await getTask(source, taskId); + const resultTask = await getTask(source, taskId); const permisions = await getCheckoutPermissions(source, taskId); - expect(status).toEqual('Uploaded'); - expect(typist_user_id).toEqual(null); + expect(resultTask?.status).toEqual('Uploaded'); + expect(resultTask?.typist_user_id).toEqual(null); expect(permisions.length).toEqual(0); }); it('タスクのステータスが[Inprogress,Pending]でない時、タスクをキャンセルできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { account_id: accountId, @@ -2330,12 +2373,14 @@ describe('cancel', () => { }); it('API実行者のRoleがTypistの場合、他人が文字起こし実行中のタスクをキャンセルできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, @@ -2379,12 +2424,14 @@ describe('cancel', () => { }); it('タスクがない時、タスクをキャンセルできない', async () => { + if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModule( + const module = await makeTaskTestingModuleWithNotificaiton( source, notificationhubServiceMockValue, ); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { account_id: accountId, diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 23659b7..902dfc1 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -45,10 +45,10 @@ export class TasksService { private readonly notificationhubService: NotificationhubService, ) {} - // TODO [Task2244] 引数にAccessTokenがあるのは不適切なのでController側で分解したい async getTasks( context: Context, - accessToken: AccessToken, + userId: string, + roles: Roles[], offset: number, limit: number, status?: string[], @@ -59,10 +59,6 @@ export class TasksService { `[IN] [${context.trackingId}] ${this.getTasks.name} | params: { offset: ${offset}, limit: ${limit}, status: ${status}, paramName: ${paramName}, direction: ${direction} };`, ); - const { role, userId } = accessToken; - // TODO [Task2244] Roleに型で定義されている値が入っているかをチェックして異常値を弾く実装に修正する - const roles = role.split(' '); - // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER'; const defaultDirection: SortDirection = 'ASC'; @@ -95,6 +91,10 @@ export class TasksService { return { tasks: tasks, total: result.count }; } if (roles.includes(USER_ROLES.AUTHOR)) { + // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする + if (!author_id) { + throw new Error('AuthorID not found'); + } const result = await this.taskRepository.getTasksFromAuthorIdAndAccountId( author_id, @@ -179,6 +179,10 @@ export class TasksService { await this.usersRepository.findUserByExternalId(externalId); if (roles.includes(USER_ROLES.AUTHOR)) { + // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする + if (!author_id) { + throw new Error('AuthorID not found'); + } await this.taskRepository.getTaskFromAudioFileId( audioFileId, account_id, @@ -407,30 +411,21 @@ export class TasksService { permissions: CheckoutPermission[], ): Promise { // 割り当て候補の外部IDを列挙 - const assigneesExternalIds = permissions.map((x) => { - if (x.user) { - return x.user.external_id; - } - }); + const assigneesExternalIds = permissions.flatMap((permission) => + permission.user ? [permission.user.external_id] : [], + ); // 割り当てられているタイピストの外部IDを列挙 - const typistExternalIds = tasks.flatMap((x) => { - if (x.typist_user) { - return x.typist_user.external_id; - } - }); + const typistExternalIds = tasks.flatMap((task) => + task.typist_user ? [task.typist_user.external_id] : [], + ); //重複をなくす const distinctedExternalIds = [ ...new Set(assigneesExternalIds.concat(typistExternalIds)), ]; - // undefinedがあった場合、取り除く - const filteredExternalIds: string[] = distinctedExternalIds.filter( - (x): x is string => x !== undefined, - ); - // B2Cからユーザー名を取得する - return await this.adB2cService.getUsers(context, filteredExternalIds); + return await this.adB2cService.getUsers(context, distinctedExternalIds); } /** * 文字起こし候補を変更する @@ -451,10 +446,14 @@ export class TasksService { ); const { author_id, account_id } = await this.usersRepository.findUserByExternalId(externalId); + // RoleがAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする + if (role.includes(USER_ROLES.AUTHOR) && !author_id) { + throw new Error('AuthorID not found'); + } await this.taskRepository.changeCheckoutPermission( audioFileId, - author_id, + author_id ?? undefined, account_id, role, assignees, @@ -462,11 +461,16 @@ export class TasksService { // すべての割り当て候補ユーザーを取得する const assigneesGroupIds = assignees - .filter((x) => x.typistGroupId) - .map((x) => x.typistGroupId); + .filter((assignee) => assignee.typistGroupId) + .flatMap((assignee) => + assignee.typistGroupId ? [assignee.typistGroupId] : [], + ); + const assigneesUserIds = assignees - .filter((x) => x.typistUserId) - .map((x) => x.typistUserId); + .filter((assignee) => assignee.typistUserId) + .flatMap((assignee) => + assignee.typistUserId ? [assignee.typistUserId] : [], + ); const groupMembers = await this.userGroupsRepositoryService.getGroupMembersFromGroupIds( diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index c09742f..e94a728 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -307,7 +307,7 @@ export const makeDefaultUsersRepositoryMockValue = user1.auto_renew = false; user1.license_alert = false; user1.notification = false; - user1.deleted_at = undefined; + user1.deleted_at = null; user1.created_by = 'test'; user1.created_at = new Date(); user1.author_id = 'abcdef'; @@ -331,6 +331,12 @@ const defaultTasksRepositoryMockValue: { status: 'Uploaded', priority: '00', created_at: new Date('2023-01-01T01:01:01.000Z'), + finished_at: null, + started_at: null, + typist_user_id: null, + template_file_id: null, + typist_user: null, + template_file: null, option_items: [ { id: 1, @@ -435,6 +441,12 @@ const defaultTasksRepositoryMockValue: { created_at: new Date(), updated_by: 'test', updated_at: new Date(), + account: null, + author_id: null, + deleted_at: null, + encryption_password: null, + license: null, + userGroupMembers: null, }, }, ], diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 8c49279..8b8d1c4 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -39,10 +39,10 @@ import { makeNotificationhubServiceMock, } from './tasks.service.mock'; -export const makeTaskTestingModule = async ( +export const makeTaskTestingModuleWithNotificaiton = async ( datasource: DataSource, notificationhubServiceMockValue: NotificationhubServiceMockValue, -): Promise => { +): Promise => { try { const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -205,7 +205,7 @@ export const createUserGroup = async ( export const getTask = async ( datasource: DataSource, task_id: number, -): Promise => { +): Promise => { const task = await datasource.getRepository(Task).findOne({ where: { id: task_id, diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index bdb96f6..5353542 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -45,7 +45,7 @@ const createTask = ( const assignees = createAssignees(permissions, b2cUserInfo); // RepositoryDTO => ControllerDTOに変換 - const typist: Typist = + const typist: Typist | undefined = typist_user != null ? convertUserToTypist(typist_user, b2cUserInfo) : undefined; @@ -113,7 +113,10 @@ const convertUserToAssignee = ( ): Assignee => { const typistName = b2cUserInfo.find( (x) => x.id === user.external_id, - ).displayName; + )?.displayName; + if (!typistName) { + throw new Error('typistName not found.'); + } return { typistUserId: user.id, typistName, @@ -135,7 +138,10 @@ const convertUserToTypist = ( ): Typist => { const typistName = b2cUserInfo.find( (x) => x.id === user.external_id, - ).displayName; + )?.displayName; + if (!typistName) { + throw new Error('typistName not found.'); + } return { id: user.id, name: typistName, diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 036f759..375669a 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -64,7 +64,7 @@ export type ConfigMockValue = { export const makeUsersServiceMock = async ( usersRepositoryMockValue: UsersRepositoryMockValue, - licensesRepositoryMockValue: LicensesRepositoryMockValue, + licensesRepositoryMockValue: LicensesRepositoryMockValue | null, adB2cMockValue: AdB2cMockValue, sendGridMockValue: SendGridMockValue, configMockValue: ConfigMockValue, @@ -368,7 +368,7 @@ export const makeDefaultUsersRepositoryMockValue = user1.created_by = 'test'; user1.created_at = new Date(); user1.updated_by = null; - user1.updated_at = null; + user1.updated_at = new Date(); const user2 = new User(); user2.id = 3; @@ -388,7 +388,7 @@ export const makeDefaultUsersRepositoryMockValue = user2.created_by = 'test'; user2.created_at = new Date(); user2.updated_by = null; - user2.updated_at = null; + user2.updated_at = new Date(); return { updateUserVerified: undefined, diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index 7f66b68..ab4dfe2 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -107,7 +107,7 @@ export const createLicense = async ( export const makeTestingModuleWithAdb2c = async ( datasource: DataSource, adB2cMockValue: AdB2cMockValue, -): Promise => { +): Promise => { try { const module: TestingModule = await Test.createTestingModule({ imports: [ diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 9ee6370..d36f77e 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -130,10 +130,24 @@ export class UsersController { @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) @Get() async getUsers(@Req() req: Request): Promise { - const accessToken = retrieveAuthorizationToken(req); - const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; - const users = await this.usersService.getUsers(decodedToken.userId); + const users = await this.usersService.getUsers(userId); return { users }; } @@ -179,15 +193,29 @@ export class UsersController { prompt, } = body; - const accessToken = retrieveAuthorizationToken(req); - const payload = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; - const context = makeContext(payload.userId); + const context = makeContext(userId); //ユーザ作成処理 await this.usersService.createUser( context, - payload, + userId, name, role as UserRoles, email, @@ -225,8 +253,22 @@ export class UsersController { @UseGuards(AuthGuard) @Get('relations') async getRelations(@Req() req: Request): Promise { - const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); @@ -265,8 +307,22 @@ export class UsersController { @Req() req: Request, ): Promise { const { direction, paramName } = body; - const accessToken = retrieveAuthorizationToken(req); - const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; //型チェック if ( @@ -278,11 +334,7 @@ export class UsersController { HttpStatus.BAD_REQUEST, ); } - await this.usersService.updateSortCriteria( - paramName, - direction, - decodedToken, - ); + await this.usersService.updateSortCriteria(paramName, direction, userId); return {}; } @@ -313,11 +365,25 @@ export class UsersController { @Req() req: Request, ): Promise { const {} = query; - const accessToken = retrieveAuthorizationToken(req); - const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const { direction, paramName } = await this.usersService.getSortCriteria( - decodedToken, + userId, ); return { direction, paramName }; } @@ -366,8 +432,22 @@ export class UsersController { prompt, } = body; + // TODO strictNullChecks対応 const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); @@ -421,8 +501,22 @@ export class UsersController { @Body() body: AllocateLicenseRequest, @Req() req: Request, ): Promise { - const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); await this.usersService.allocateLicense( @@ -467,8 +561,22 @@ export class UsersController { @Body() body: DeallocateLicenseRequest, @Req() req: Request, ): Promise { - const accessToken = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + // TODO strictNullChecks対応 + const accessToken = retrieveAuthorizationToken(req) as string; + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; const context = makeContext(userId); diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 79114d1..f4c42aa 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -46,7 +46,7 @@ import { import { v4 as uuidv4 } from 'uuid'; describe('UsersService.confirmUser', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -57,12 +57,15 @@ describe('UsersService.confirmUser', () => { return source.initialize(); }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになり、トライアルライセンスが100件作成される', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = (await makeTestAccount(source)).account; const { id: userId } = await makeTestUser(source, { account_id: accountId, @@ -127,7 +130,9 @@ describe('UsersService.confirmUser', () => { }); it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const token = 'invalid.id.token'; const service = module.get(UsersService); await expect(service.confirmUser(token)).rejects.toEqual( @@ -136,7 +141,9 @@ describe('UsersService.confirmUser', () => { }); it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = (await makeTestAccount(source)).account; await makeTestUser(source, { account_id: accountId, @@ -168,7 +175,9 @@ describe('UsersService.confirmUser', () => { ); }); it('ユーザーが存在しない場合は、想定外のエラーとなる', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -201,6 +210,12 @@ describe('UsersService.confirmUserAndInitPassword', () => { notification: true, encryption: false, prompt: false, + account: null, + author_id: null, + deleted_at: null, + encryption_password: null, + license: null, + userGroupMembers: null, }; const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); @@ -246,6 +261,12 @@ describe('UsersService.confirmUserAndInitPassword', () => { notification: true, encryption: false, prompt: false, + account: null, + author_id: null, + deleted_at: null, + encryption_password: null, + license: null, + userGroupMembers: null, }; const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); @@ -287,6 +308,12 @@ describe('UsersService.confirmUserAndInitPassword', () => { notification: true, encryption: false, prompt: false, + account: null, + author_id: null, + deleted_at: null, + encryption_password: null, + license: null, + userGroupMembers: null, }; const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); @@ -332,6 +359,12 @@ describe('UsersService.confirmUserAndInitPassword', () => { notification: true, encryption: false, prompt: false, + account: null, + author_id: null, + deleted_at: null, + encryption_password: null, + license: null, + userGroupMembers: null, }; const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); @@ -362,7 +395,7 @@ describe('UsersService.confirmUserAndInitPassword', () => { }); describe('UsersService.createUser', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -375,12 +408,15 @@ describe('UsersService.createUser', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; @@ -433,7 +469,7 @@ describe('UsersService.createUser', () => { expect( await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -446,16 +482,16 @@ describe('UsersService.createUser', () => { // 追加されたユーザーが正しくDBに登録されていることを確認 const user = await getUserFromExternalId(source, externalId); expect(user).not.toBeNull(); - expect(user.account_id).toEqual(accountId); - expect(user.role).toEqual(role); - expect(user.author_id).toEqual(null); - expect(user.email_verified).toEqual(false); - expect(user.auto_renew).toEqual(autoRenew); - expect(user.license_alert).toEqual(licenseAlert); - expect(user.notification).toEqual(notification); - expect(user.encryption).toEqual(false); - expect(user.encryption_password).toEqual(null); - expect(user.prompt).toEqual(false); + expect(user?.account_id).toEqual(accountId); + expect(user?.role).toEqual(role); + expect(user?.author_id).toEqual(null); + expect(user?.email_verified).toEqual(false); + expect(user?.auto_renew).toEqual(autoRenew); + expect(user?.license_alert).toEqual(licenseAlert); + expect(user?.notification).toEqual(notification); + expect(user?.encryption).toEqual(false); + expect(user?.encryption_password).toEqual(null); + expect(user?.prompt).toEqual(false); // 他にユーザーが登録されていないことを確認 const users = await getUsers(source); @@ -463,7 +499,9 @@ describe('UsersService.createUser', () => { }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -519,7 +557,7 @@ describe('UsersService.createUser', () => { expect( await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -536,16 +574,16 @@ describe('UsersService.createUser', () => { // 追加されたユーザーが正しくDBに登録されていることを確認 const user = await getUserFromExternalId(source, externalId); expect(user).not.toBeNull(); - expect(user.account_id).toEqual(accountId); - expect(user.role).toEqual(role); - expect(user.author_id).toEqual(authorId); - expect(user.email_verified).toEqual(false); - expect(user.auto_renew).toEqual(autoRenew); - expect(user.license_alert).toEqual(licenseAlert); - expect(user.notification).toEqual(notification); - expect(user.encryption).toEqual(encryption); - expect(user.encryption_password).toEqual(encryptionPassword); - expect(user.prompt).toEqual(prompt); + expect(user?.account_id).toEqual(accountId); + expect(user?.role).toEqual(role); + expect(user?.author_id).toEqual(authorId); + expect(user?.email_verified).toEqual(false); + expect(user?.auto_renew).toEqual(autoRenew); + expect(user?.license_alert).toEqual(licenseAlert); + expect(user?.notification).toEqual(notification); + expect(user?.encryption).toEqual(encryption); + expect(user?.encryption_password).toEqual(encryptionPassword); + expect(user?.prompt).toEqual(prompt); // 他にユーザーが登録されていないことを確認 const users = await getUsers(source); @@ -553,7 +591,9 @@ describe('UsersService.createUser', () => { }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化無し)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -608,7 +648,7 @@ describe('UsersService.createUser', () => { expect( await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -625,16 +665,16 @@ describe('UsersService.createUser', () => { // 追加されたユーザーが正しくDBに登録されていることを確認 const user = await getUserFromExternalId(source, externalId); expect(user).not.toBeNull(); - expect(user.account_id).toEqual(accountId); - expect(user.role).toEqual(role); - expect(user.author_id).toEqual(authorId); - expect(user.email_verified).toEqual(false); - expect(user.auto_renew).toEqual(autoRenew); - expect(user.license_alert).toEqual(licenseAlert); - expect(user.notification).toEqual(notification); - expect(user.encryption).toEqual(encryption); - expect(user.encryption_password).toBeNull(); - expect(user.prompt).toEqual(prompt); + expect(user?.account_id).toEqual(accountId); + expect(user?.role).toEqual(role); + expect(user?.author_id).toEqual(authorId); + expect(user?.email_verified).toEqual(false); + expect(user?.auto_renew).toEqual(autoRenew); + expect(user?.license_alert).toEqual(licenseAlert); + expect(user?.notification).toEqual(notification); + expect(user?.encryption).toEqual(encryption); + expect(user?.encryption_password).toBeNull(); + expect(user?.prompt).toEqual(prompt); // 他にユーザーが登録されていないことを確認 const users = await getUsers(source); @@ -642,7 +682,9 @@ describe('UsersService.createUser', () => { }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Transcriptioninst)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -694,7 +736,7 @@ describe('UsersService.createUser', () => { expect( await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -707,16 +749,16 @@ describe('UsersService.createUser', () => { // 追加されたユーザーが正しくDBに登録されていることを確認 const user = await getUserFromExternalId(source, externalId); expect(user).not.toBeNull(); - expect(user.account_id).toEqual(accountId); - expect(user.role).toEqual(role); - expect(user.author_id).toBeNull(); - expect(user.email_verified).toEqual(false); - expect(user.auto_renew).toEqual(autoRenew); - expect(user.license_alert).toEqual(licenseAlert); - expect(user.notification).toEqual(notification); - expect(user.encryption).toEqual(false); - expect(user.encryption_password).toBeNull(); - expect(user.prompt).toEqual(false); + expect(user?.account_id).toEqual(accountId); + expect(user?.role).toEqual(role); + expect(user?.author_id).toBeNull(); + expect(user?.email_verified).toEqual(false); + expect(user?.auto_renew).toEqual(autoRenew); + expect(user?.license_alert).toEqual(licenseAlert); + expect(user?.notification).toEqual(notification); + expect(user?.encryption).toEqual(false); + expect(user?.encryption_password).toBeNull(); + expect(user?.prompt).toEqual(false); // 他にユーザーが登録されていないことを確認 const users = await getUsers(source); @@ -724,7 +766,9 @@ describe('UsersService.createUser', () => { }); it('DBネットワークエラーとなる場合、リカバリ処理を実施し、ADB2Cに作成したユーザーを削除する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const b2cService = module.get(AdB2cService); const adminExternalId = 'ADMIN0001'; @@ -785,7 +829,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -809,7 +853,9 @@ describe('UsersService.createUser', () => { }); it('DBネットワークエラーとなる場合、リカバリ処理を実施されるが、そのリカバリ処理に失敗した場合、ADB2Cのユーザーは削除されない', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const b2cService = module.get(AdB2cService); const adminExternalId = 'ADMIN0001'; @@ -870,7 +916,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -899,7 +945,9 @@ describe('UsersService.createUser', () => { }); it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -949,7 +997,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -972,7 +1020,9 @@ describe('UsersService.createUser', () => { }); it('Azure AD B2C内でメールアドレスが重複している場合、エラーとなる。', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -1026,7 +1076,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -1049,7 +1099,9 @@ describe('UsersService.createUser', () => { }); it('AuthorIDが重複している場合、エラーとなる。(AuthorID重複チェックでエラー)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const adminExternalId = 'ADMIN0001'; const { account, admin } = await makeTestAccount( @@ -1105,7 +1157,7 @@ describe('UsersService.createUser', () => { expect( await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email_1, @@ -1146,7 +1198,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email_2, @@ -1175,7 +1227,9 @@ describe('UsersService.createUser', () => { }); it('AuthorIDが重複している場合、エラー(insert失敗)となり、リカバリ処理が実行され、ADB2Cに追加したユーザーが削除される', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const b2cService = module.get(AdB2cService); const adminExternalId = 'ADMIN0001'; @@ -1240,7 +1294,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -1272,7 +1326,9 @@ describe('UsersService.createUser', () => { }); it('メール送信に失敗した場合、リカバリ処理が実行され、ADB2C,DBのユーザーが削除される', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const b2cService = module.get(AdB2cService); @@ -1327,7 +1383,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -1357,7 +1413,9 @@ describe('UsersService.createUser', () => { }); it('メール送信に失敗した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、ADB2C,DBのユーザーが削除されない', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const service = module.get(UsersService); const b2cService = module.get(AdB2cService); @@ -1417,7 +1475,7 @@ describe('UsersService.createUser', () => { try { await service.createUser( makeContext('trackingId'), - token, + adminExternalId, name, role, email, @@ -1446,7 +1504,7 @@ describe('UsersService.createUser', () => { }); describe('UsersService.getUsers', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1459,13 +1517,16 @@ describe('UsersService.getUsers', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('ユーザーの一覧を取得できる(ライセンス未割当)', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); + if (!source) fail(); const module = await makeTestingModuleWithAdb2c(source, adb2cParam); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: externalId_author, id: authorUserId } = @@ -1565,7 +1626,9 @@ describe('UsersService.getUsers', () => { it('ユーザーの一覧を取得できること(ライセンス割当済み)', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); + if (!source) fail(); const module = await makeTestingModuleWithAdb2c(source, adb2cParam); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: user1, external_id: external_id1 } = await makeTestUser( @@ -1679,7 +1742,9 @@ describe('UsersService.getUsers', () => { it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); + if (!source) fail(); const module = await makeTestingModuleWithAdb2c(source, adb2cParam); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { @@ -1697,12 +1762,14 @@ describe('UsersService.getUsers', () => { await expect(service.getUsers('externalId_failed')).rejects.toEqual( new HttpException(makeErrorResponse('E009999'), HttpStatus.NOT_FOUND), ); - }); + },60000000); it('ADB2Cからのユーザーの取得に失敗した場合、エラーとなる', async () => { const adb2cParam = makeDefaultAdB2cMockValue(); adb2cParam.getUsers = new Error('ADB2C error'); + if (!source) fail(); const module = await makeTestingModuleWithAdb2c(source, adb2cParam); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: externalId_author } = await makeTestUser(source, { @@ -1742,11 +1809,7 @@ describe('UsersService.updateSortCriteria', () => { ); expect( - await service.updateSortCriteria('AUTHOR_ID', 'ASC', { - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), + await service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'), ).toEqual(undefined); }); @@ -1771,11 +1834,7 @@ describe('UsersService.updateSortCriteria', () => { ); await expect( - service.updateSortCriteria('AUTHOR_ID', 'ASC', { - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), + service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'), ).rejects.toEqual( new HttpException( makeErrorResponse('E009999'), @@ -1806,11 +1865,7 @@ describe('UsersService.updateSortCriteria', () => { ); await expect( - service.updateSortCriteria('AUTHOR_ID', 'ASC', { - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), + service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'), ).rejects.toEqual( new HttpException( makeErrorResponse('E009999'), @@ -1838,13 +1893,10 @@ describe('UsersService.getSortCriteria', () => { sortCriteriaRepositoryMockValue, ); - expect( - await service.getSortCriteria({ - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), - ).toEqual({ direction: 'ASC', paramName: 'JOB_NUMBER' }); + expect(await service.getSortCriteria('external_id')).toEqual({ + direction: 'ASC', + paramName: 'JOB_NUMBER', + }); }); it('ソート条件が存在せず、ソート条件を取得できない', async () => { @@ -1869,13 +1921,7 @@ describe('UsersService.getSortCriteria', () => { sortCriteriaRepositoryMockValue, ); - await expect( - service.getSortCriteria({ - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), - ).rejects.toEqual( + await expect(service.getSortCriteria('external_id')).rejects.toEqual( new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, @@ -1907,13 +1953,7 @@ describe('UsersService.getSortCriteria', () => { sortCriteriaRepositoryMockValue, ); - await expect( - service.getSortCriteria({ - role: 'none admin', - userId: 'xxxxxxxxxxxx', - tier: 5, - }), - ).rejects.toEqual( + await expect(service.getSortCriteria('external_id')).rejects.toEqual( new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, @@ -1923,7 +1963,7 @@ describe('UsersService.getSortCriteria', () => { }); describe('UsersService.updateUser', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -1936,12 +1976,15 @@ describe('UsersService.updateUser', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('ユーザー情報を更新できる(None)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -1997,7 +2040,9 @@ describe('UsersService.updateUser', () => { }); it('ユーザー情報を更新できる(Typist)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2053,7 +2098,9 @@ describe('UsersService.updateUser', () => { }); it('ユーザー情報を更新できる(Author)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2109,7 +2156,9 @@ describe('UsersService.updateUser', () => { }); it('ユーザーのRoleを更新できる(None⇒Typist)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2165,7 +2214,9 @@ describe('UsersService.updateUser', () => { }); it('ユーザーのRoleを更新できる(None⇒Author)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2221,7 +2272,9 @@ describe('UsersService.updateUser', () => { }); it('None以外からRoleを変更した場合、エラーとなる(Typist⇒None)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2267,7 +2320,9 @@ describe('UsersService.updateUser', () => { }); it('Authorがパスワードundefinedで渡されたとき、元のパスワードを維持する(Encryptionがtrue)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2323,7 +2378,9 @@ describe('UsersService.updateUser', () => { }); it('Authorが暗号化なしで更新した場合、パスワードをNULLにする(Encryptionがfalse)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2379,7 +2436,9 @@ describe('UsersService.updateUser', () => { }); it('AuthorのDBにパスワードが設定されていない場合、パスワードundefinedでわたすとエラーとなる(Encryptionがtrue)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2425,7 +2484,9 @@ describe('UsersService.updateUser', () => { }); it('AuthorIdが既存のユーザーと重複した場合、エラーとなる', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: external_id } = await makeTestUser(source, { @@ -2483,7 +2544,7 @@ describe('UsersService.updateUser', () => { }); describe('UsersService.updateAcceptedVersion', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -2496,12 +2557,15 @@ describe('UsersService.updateAcceptedVersion', () => { }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('同意済み利用規約バージョンを更新できる(第五)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { admin } = await makeTestAccount(source, { tier: 5, }); @@ -2515,7 +2579,9 @@ describe('UsersService.updateAcceptedVersion', () => { }); it('同意済み利用規約バージョンを更新できる(第一~第四)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { admin } = await makeTestAccount(source, { tier: 4, }); @@ -2534,23 +2600,10 @@ describe('UsersService.updateAcceptedVersion', () => { expect(user.accepted_dpa_version).toBe('v3.0'); }); - it('パラメータが不在のときエラーとなる(第五)', async () => { - const module = await makeTestingModule(source); - const { admin } = await makeTestAccount(source, { - tier: 5, - }); - const context = makeContext(uuidv4()); - - const service = module.get(UsersService); - await expect( - service.updateAcceptedVersion(context, admin.external_id, undefined), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST), - ); - }); - it('パラメータが不在のときエラーとなる(第一~第四)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); const { admin } = await makeTestAccount(source, { tier: 4, }); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 142c165..8df69b4 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -48,9 +48,13 @@ import { LicenseExpiredError, LicenseUnavailableError, } from '../../repositories/licenses/errors/types'; +import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; @Injectable() export class UsersService { + private readonly logger = new Logger(UsersService.name); + private readonly mailFrom: string; + private readonly appDomain: string; constructor( private readonly usersRepository: UsersRepositoryService, private readonly licensesRepository: LicensesRepositoryService, @@ -58,8 +62,10 @@ export class UsersService { private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, private readonly sendgridService: SendGridService, - ) {} - private readonly logger = new Logger(UsersService.name); + ) { + this.mailFrom = this.configService.getOrThrow('MAIL_FROM'); + this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); + } /** * Confirms user @@ -128,7 +134,7 @@ export class UsersService { */ async createUser( context: Context, - accessToken: AccessToken, + externalId: string, name: string, role: UserRoles, email: string, @@ -145,9 +151,7 @@ export class UsersService { //DBよりアクセス者の所属するアカウントIDを取得する let adminUser: EntityUser; try { - adminUser = await this.usersRepository.findUserByExternalId( - accessToken.userId, - ); + adminUser = await this.usersRepository.findUserByExternalId(externalId); } catch (e) { this.logger.error(`error=${e}`); throw new HttpException( @@ -254,9 +258,6 @@ export class UsersService { //Email送信用のコンテンツを作成する try { - // メールの送信元を取得 - const from = this.configService.get('MAIL_FROM') ?? ''; - // メールの内容を構成 const { subject, text, html } = await this.sendgridService.createMailContentFromEmailConfirmForNormalUser( @@ -269,7 +270,7 @@ export class UsersService { await this.sendgridService.sendMail( context, email, - from, + this.mailFrom, subject, text, html, @@ -343,6 +344,12 @@ export class UsersService { license_alert: licenseAlert, notification, role, + accepted_dpa_version: null, + accepted_eula_version: null, + encryption: false, + encryption_password: null, + prompt: false, + author_id: null, }; case USER_ROLES.AUTHOR: return { @@ -352,10 +359,12 @@ export class UsersService { license_alert: licenseAlert, notification, role, - author_id: authorId, - encryption, - encryption_password: encryptionPassword, - prompt, + author_id: authorId ?? null, + encryption: encryption ?? false, + encryption_password: encryptionPassword ?? null, + prompt: prompt ?? false, + accepted_dpa_version: null, + accepted_eula_version: null, }; default: //不正なroleが指定された場合はログを出力してエラーを返す @@ -405,19 +414,16 @@ export class UsersService { await this.adB2cService.changePassword(extarnalId, ramdomPassword); // ユーザを認証済みにする await this.usersRepository.updateUserVerified(userId); - // メールの送信元を取得 - const from = this.configService.get('MAIL_FROM') ?? ''; // TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする const subject = 'A temporary password has been issued.'; const text = 'temporary password: ' + ramdomPassword; - const domains = this.configService.get('APP_DOMAIN'); - const html = `

OMDS TOP PAGE URL.

${domains}"
temporary password: ${ramdomPassword}`; + const html = `

OMDS TOP PAGE URL.

${this.appDomain}"
temporary password: ${ramdomPassword}`; // メールを送信 await this.sendgridService.sendMail( context, email, - from, + this.mailFrom, subject, text, html, @@ -464,17 +470,29 @@ export class UsersService { ); // DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出 - const users = dbUsers.map((x) => { + const users = dbUsers.map((dbUser): User => { // ユーザーの所属グループ名を取得する - const groupNames = - x.userGroupMembers?.map((group) => group.userGroup?.name) ?? []; + const userGroupMembers = + dbUser.userGroupMembers !== null ? dbUser.userGroupMembers : []; - const adb2cUser = adb2cUsers.find((user) => user.id === x.external_id); + //所属グループ名の配列にする + const groupNames = userGroupMembers.flatMap((userGroupMember) => + userGroupMember.userGroup ? [userGroupMember.userGroup.name] : [], + ); + + const adb2cUser = adb2cUsers.find( + (user) => user.id === dbUser.external_id, + ); // メールアドレスを取得する - const mail = adb2cUser.identities.find( + const mail = adb2cUser?.identities?.find( (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS, - ).issuerAssignedId; + )?.issuerAssignedId; + + //メールアドレスが取得できない場合はエラー + if (!mail) { + throw new Error('mail not found.'); + } let status = USER_LICENSE_STATUS.NORMAL; @@ -483,9 +501,9 @@ export class UsersService { let expiration: string | undefined = undefined; let remaining: number | undefined = undefined; - if (x.license) { + if (dbUser.license) { // 有効期限日付 YYYY/MM/DD - const expiry_date = x.license.expiry_date; + const expiry_date = dbUser.license.expiry_date; expiration = `${expiry_date.getFullYear()}/${ expiry_date.getMonth() + 1 }/${expiry_date.getDate()}`; @@ -497,7 +515,7 @@ export class UsersService { (1000 * 60 * 60 * 24), ); if (remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS) { - status = x.auto_renew + status = dbUser.auto_renew ? USER_LICENSE_STATUS.RENEW : USER_LICENSE_STATUS.ALERT; } @@ -506,18 +524,18 @@ export class UsersService { } return { - id: x.id, + id: dbUser.id, name: adb2cUser.displayName, - role: x.role, - authorId: x.author_id ?? undefined, + role: dbUser.role, + authorId: dbUser.author_id ?? undefined, typistGroupName: groupNames, email: mail, - emailVerified: x.email_verified, - autoRenew: x.auto_renew, - licenseAlert: x.license_alert, - notification: x.notification, - encryption: x.encryption, - prompt: x.prompt, + emailVerified: dbUser.email_verified, + autoRenew: dbUser.auto_renew, + licenseAlert: dbUser.license_alert, + notification: dbUser.notification, + encryption: dbUser.encryption, + prompt: dbUser.prompt, expiration: expiration, remaining: remaining, licenseStatus: status, @@ -545,14 +563,13 @@ export class UsersService { async updateSortCriteria( paramName: TaskListSortableAttribute, direction: SortDirection, - token: AccessToken, + externalId: string, ): Promise { this.logger.log(`[IN] ${this.updateSortCriteria.name}`); let user: EntityUser; try { // ユーザー情報を取得 - const sub = token.userId; - user = await this.usersRepository.findUserByExternalId(sub); + user = await this.usersRepository.findUserByExternalId(externalId); } catch (e) { this.logger.error(`error=${e}`); @@ -584,7 +601,7 @@ export class UsersService { * @param token * @returns sort criteria */ - async getSortCriteria(token: AccessToken): Promise<{ + async getSortCriteria(externalId: string): Promise<{ paramName: TaskListSortableAttribute; direction: SortDirection; }> { @@ -592,8 +609,7 @@ export class UsersService { let user: EntityUser; try { // ユーザー情報を取得 - const sub = token.userId; - user = await this.usersRepository.findUserByExternalId(sub); + user = await this.usersRepository.findUserByExternalId(externalId); } catch (e) { this.logger.error(`error=${e}`); @@ -643,8 +659,8 @@ export class UsersService { // TODO: PBI2105 本実装時に修正すること return { - authorId: user.author_id, - authorIdList: [user.author_id, 'XXX'], + authorId: user.author_id ?? '', + authorIdList: [user.author_id ?? '', 'XXX'], isEncrypted: true, encryptionPassword: 'abcd@123?dcba', audioFormat: 'DS2(QP)', @@ -1004,6 +1020,11 @@ export class UsersService { makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); case UpdateTermsVersionNotSetError: throw new HttpException( makeErrorResponse('E010001'), diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 047e870..3d4a65d 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -2062,7 +2062,7 @@ describe('updateWorkflow', () => { }); describe('deleteWorkflows', () => { - let source: DataSource = null; + let source: DataSource | null = null; beforeEach(async () => { source = new DataSource({ type: 'sqlite', @@ -2074,12 +2074,15 @@ describe('deleteWorkflows', () => { return source.initialize(); }); afterEach(async () => { + if (!source) return; await source.destroy(); source = null; }); it('アカウント内のWorkflowを削除できる', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -2126,7 +2129,9 @@ describe('deleteWorkflows', () => { }); it('アカウント内のWorkflowを削除できる(複数ワークフローがある場合)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId1 } = await makeTestUser(source, { @@ -2177,7 +2182,9 @@ describe('deleteWorkflows', () => { }); it('指定されたワークフローが存在しない場合、400エラーを返却する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -2226,7 +2233,9 @@ describe('deleteWorkflows', () => { }); it('指定されたワークフローが存在しない場合、400エラーを返却する(ログインユーザーのアカウント外)', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { @@ -2303,7 +2312,9 @@ describe('deleteWorkflows', () => { }); it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); const module = await makeTestingModule(source); + if (!module) fail(); // 第五階層のアカウント作成 const { account, admin } = await makeTestAccount(source, { tier: 5 }); const { id: authorId } = await makeTestUser(source, { diff --git a/dictation_server/src/features/workflows/workflows.service.ts b/dictation_server/src/features/workflows/workflows.service.ts index b8be4e6..6f98c95 100644 --- a/dictation_server/src/features/workflows/workflows.service.ts +++ b/dictation_server/src/features/workflows/workflows.service.ts @@ -57,8 +57,8 @@ export class WorkflowsService { return workflowTypists; }); // externalIdsからundefinedを除外 - const filteredExternalIds = externalIds.filter( - (externalId): externalId is string => externalId !== undefined, + const filteredExternalIds = externalIds.flatMap((externalId) => + externalId ? [externalId] : [], ); // externalIdsから重複を除外 const distinctedExternalIds = [...new Set(filteredExternalIds)]; diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 1b29705..6f894ff 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -8,7 +8,13 @@ import { Context } from '../../common/log'; @Injectable() export class SendGridService { private readonly logger = new Logger(SendGridService.name); + private readonly emailConfirmLifetime: number; + readonly appDomain: string; constructor(private readonly configService: ConfigService) { + this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); + this.emailConfirmLifetime = this.configService.getOrThrow( + 'EMAIL_CONFIRM_LIFETIME', + ); const key = this.configService.getOrThrow('SENDGRID_API_KEY'); sendgrid.setApiKey(key); } @@ -30,8 +36,6 @@ export class SendGridService { `[IN] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`, ); - const lifetime = - this.configService.get('EMAIL_CONFIRM_LIFETIME') ?? 0; const privateKey = getPrivateKey(this.configService); const token = sign<{ accountId: number; userId: number; email: string }>( { @@ -39,10 +43,9 @@ export class SendGridService { userId, email, }, - lifetime, + this.emailConfirmLifetime, privateKey, ); - const domains = this.configService.get('APP_DOMAIN'); const path = 'mail-confirm/'; this.logger.log( @@ -50,8 +53,8 @@ export class SendGridService { ); return { subject: 'Verify your new account', - text: `The verification URL. ${domains}${path}?verify=${token}`, - html: `

The verification URL.

${domains}${path}?verify=${token}"`, + text: `The verification URL. ${this.appDomain}${path}?verify=${token}`, + html: `

The verification URL.

${this.appDomain}${path}?verify=${token}"`, }; } @@ -68,9 +71,6 @@ export class SendGridService { userId: number, email: string, ): Promise<{ subject: string; text: string; html: string }> { - const lifetime = - this.configService.get('EMAIL_CONFIRM_LIFETIME') ?? 0; - const privateKey = getPrivateKey(this.configService); const token = sign<{ accountId: number; userId: number; email: string }>( @@ -79,16 +79,15 @@ export class SendGridService { userId, email, }, - lifetime, + this.emailConfirmLifetime, privateKey, ); - const domains = this.configService.get('APP_DOMAIN'); const path = 'mail-confirm/user/'; return { subject: 'Verify your new account', - text: `The verification URL. ${domains}${path}?verify=${token}`, - html: `

The verification URL.

${domains}${path}?verify=${token}"`, + text: `The verification URL. ${this.appDomain}${path}?verify=${token}`, + html: `

The verification URL.

${this.appDomain}${path}?verify=${token}"`, }; } diff --git a/dictation_server/src/repositories/tasks/entity/task.entity.ts b/dictation_server/src/repositories/tasks/entity/task.entity.ts index 1252620..13d5537 100644 --- a/dictation_server/src/repositories/tasks/entity/task.entity.ts +++ b/dictation_server/src/repositories/tasks/entity/task.entity.ts @@ -21,32 +21,32 @@ export class Task { @Column() account_id: number; @Column({ nullable: true }) - is_job_number_enabled?: boolean; + is_job_number_enabled: boolean | null; @Column() audio_file_id: number; @Column() status: string; @Column({ nullable: true }) - typist_user_id?: number; + typist_user_id: number | null; @Column() priority: string; @Column({ nullable: true }) - template_file_id?: number; + template_file_id: number | null; @Column({ nullable: true }) - started_at?: Date; + started_at: Date | null; @Column({ nullable: true }) - finished_at?: Date; + finished_at: Date | null; @Column({}) created_at: Date; @OneToOne(() => AudioFile, (audiofile) => audiofile.task) @JoinColumn({ name: 'audio_file_id' }) - file?: AudioFile; + file: AudioFile | null; @OneToMany(() => AudioOptionItem, (option) => option.task) - option_items?: AudioOptionItem[]; + option_items: AudioOptionItem[] | null; @OneToOne(() => User, (user) => user.id) @JoinColumn({ name: 'typist_user_id' }) - typist_user?: User; + typist_user: User | null; @ManyToOne(() => TemplateFile, (templateFile) => templateFile.id) @JoinColumn({ name: 'template_file_id' }) - template_file?: TemplateFile; + template_file: TemplateFile | null; } diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 3436d1c..0f88525 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -757,7 +757,7 @@ export class TasksRepositoryService { */ async changeCheckoutPermission( audio_file_id: number, - author_id: string, + author_id: string | undefined, account_id: number, roles: Roles[], assignees: Assignee[], diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index 387a668..6b2a8d3 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -29,13 +29,13 @@ export class User { role: string; @Column({ nullable: true }) - author_id?: string; + author_id: string | null; @Column({ nullable: true }) - accepted_eula_version?: string; + accepted_eula_version: string | null; @Column({ nullable: true }) - accepted_dpa_version?: string; + accepted_dpa_version: string | null; @Column({ default: false }) email_verified: boolean; @@ -50,16 +50,16 @@ export class User { notification: boolean; @Column({ default: false }) - encryption?: boolean; + encryption: boolean; @Column({ nullable: true }) - encryption_password?: string; + encryption_password: string | null; @Column({ default: false }) - prompt?: boolean; + prompt: boolean; @Column({ nullable: true }) - deleted_at?: Date; + deleted_at: Date | null; @Column({ nullable: true }) created_by: string; @@ -68,7 +68,7 @@ export class User { created_at: Date; @Column({ nullable: true }) - updated_by?: string; + updated_by: string | null; @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; @@ -77,13 +77,13 @@ export class User { createForeignKeyConstraints: false, }) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 @JoinColumn({ name: 'account_id' }) - account?: Account; + account: Account | null; @OneToOne(() => License, (license) => license.user) - license?: License; + license: License | null; @OneToMany(() => UserGroupMember, (userGroupMember) => userGroupMember.user) - userGroupMembers?: UserGroupMember[]; + userGroupMembers: UserGroupMember[] | null; } @Entity({ name: 'users_archive' }) @@ -101,13 +101,13 @@ export class UserArchive { role: string; @Column({ nullable: true }) - author_id?: string; + author_id: string | null; @Column({ nullable: true }) - accepted_eula_version?: string; + accepted_eula_version: string | null; @Column({ nullable: true }) - accepted_dpa_version?: string; + accepted_dpa_version: string | null; @Column() email_verified: boolean; @@ -128,7 +128,7 @@ export class UserArchive { prompt: boolean; @Column({ nullable: true }) - deleted_at?: Date; + deleted_at: Date | null; @Column({ nullable: true }) created_by: string; @@ -137,7 +137,7 @@ export class UserArchive { created_at: Date; @Column({ nullable: true }) - updated_by?: string; + updated_by: string | null; @Column() updated_at: Date; diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 4bd4d02..16555c2 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -27,6 +27,7 @@ import { License } from '../licenses/entity/license.entity'; import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types'; import { Term } from '../terms/entity/term.entity'; import { TermsCheckInfo } from '../../features/auth/types/types'; +import { AccountNotFoundError } from '../accounts/errors/types'; @Injectable() export class UsersRepositoryService { @@ -120,7 +121,7 @@ export class UsersRepositoryService { return user; } - async findUserById(id: number): Promise { + async findUserById(id: number): Promise { const user = await this.dataSource.getRepository(User).findOne({ where: { id: id, @@ -128,7 +129,7 @@ export class UsersRepositoryService { }); if (!user) { - return undefined; + throw new UserNotFoundError(); } return user; } @@ -138,10 +139,7 @@ export class UsersRepositoryService { * @param user * @returns 存在する:true 存在しない:false */ - async existsAuthorId( - accountId: number, - authorId: string, - ): Promise { + async existsAuthorId(accountId: number, authorId: string): Promise { const user = await this.dataSource.getRepository(User).findOne({ where: [ { @@ -219,9 +217,9 @@ export class UsersRepositoryService { } // Author用項目を更新 - targetUser.author_id = authorId; - targetUser.encryption = encryption; - targetUser.prompt = prompt; + targetUser.author_id = authorId ?? null; + targetUser.encryption = encryption ?? false; + targetUser.prompt = prompt ?? false; } else { // ユーザーのロールがAuthor以外の場合はAuthor用項目はundefinedにする targetUser.author_id = null; @@ -315,7 +313,7 @@ export class UsersRepositoryService { for (let i = 0; i < TRIAL_LICENSE_ISSUE_NUM; i++) { const license = new License(); license.expiry_date = expiryDate; - license.account_id = targetUser.account.id; + license.account_id = targetUser.account_id; license.type = LICENSE_TYPE.TRIAL; license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED; licenses.push(license); @@ -339,8 +337,11 @@ export class UsersRepositoryService { return await this.dataSource.transaction(async (entityManager) => { const repo = entityManager.getRepository(User); - const accountId = (await repo.findOne({ where: { external_id } })) - .account_id; + const accountId = (await repo.findOne({ where: { external_id } }))?.account_id; + + if (!accountId) { + throw new AccountNotFoundError('Account is Not Found.'); + } const dbUsers = await this.dataSource.getRepository(User).find({ relations: { @@ -371,6 +372,11 @@ export class UsersRepositoryService { }, }); + // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!user) { + throw new UserNotFoundError(); + } + const typists = await repo.find({ where: { account_id: user.account_id, @@ -442,6 +448,9 @@ export class UsersRepositoryService { if (!user) { throw new UserNotFoundError(); } + if (!user.account) { + throw new AccountNotFoundError('Account is Not Found.'); + } const termRepo = entityManager.getRepository(Term); const latestEulaInfo = await termRepo.findOne({ @@ -506,6 +515,10 @@ export class UsersRepositoryService { ); } + if (!user.account) { + throw new AccountNotFoundError('Account is Not Found.'); + } + // パラメータが不在の場合はエラーを返却 if (!eulaVersion) { throw new UpdateTermsVersionNotSetError(`EULA version param not set.`); diff --git a/dictation_server/src/repositories/workflows/entity/workflow_typists.entity.ts b/dictation_server/src/repositories/workflows/entity/workflow_typists.entity.ts index b3d7139..b44b906 100644 --- a/dictation_server/src/repositories/workflows/entity/workflow_typists.entity.ts +++ b/dictation_server/src/repositories/workflows/entity/workflow_typists.entity.ts @@ -20,10 +20,10 @@ export class WorkflowTypist { workflow_id: number; @Column({ nullable: true }) - typist_id?: number; + typist_id: number | null; @Column({ nullable: true }) - typist_group_id?: number; + typist_group_id: number | null; @Column({ nullable: true }) created_by: string; @@ -32,20 +32,20 @@ export class WorkflowTypist { created_at: Date; @Column({ nullable: true }) - updated_by?: string; + updated_by: string | null; @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; @ManyToOne(() => Workflow, (workflow) => workflow.id) @JoinColumn({ name: 'workflow_id' }) - workflow?: Workflow; + workflow: Workflow | null; @ManyToOne(() => User, (user) => user.id) @JoinColumn({ name: 'typist_id' }) - typist?: User; + typist: User | null; @ManyToOne(() => UserGroup, (userGroup) => userGroup.id) @JoinColumn({ name: 'typist_group_id' }) - typistGroup?: UserGroup; + typistGroup: UserGroup | null; } diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index 6ec186d..8a4ccf3 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -337,8 +337,8 @@ export class WorkflowsRepositoryService { private makeWorkflow( accountId: number, authorId: number, - worktypeId?: number | undefined, - templateId?: number | undefined, + worktypeId?: number, + templateId?: number, ): Workflow { const workflow = new Workflow(); workflow.account_id = accountId; @@ -363,8 +363,8 @@ export class WorkflowsRepositoryService { ): DbWorkflowTypist { const workflowTypist = new DbWorkflowTypist(); workflowTypist.workflow_id = workflowId; - workflowTypist.typist_id = typistId; - workflowTypist.typist_group_id = typistGroupId; + workflowTypist.typist_id = typistId ?? null; + workflowTypist.typist_group_id = typistGroupId ?? null; return workflowTypist; }