Merged PR 268: Typist割り当てAPIに通知処理を追加

## 概要
[Task2220: Typist割り当てAPIに通知処理を追加](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2220)

- タスクの割り当て候補変更APIに通知処理を追加しました。
  - 割り当てられたTypist全員を対象に通知を送るように実装しています。

## レビューポイント
- 対象となるユーザーは認識通りか
- 通知送信部分に問題はないか
  - tagは20個までのようなので分割して送信するようにしています。

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
  - テストが通ること
  - 割り当て変更が問題ないこと
This commit is contained in:
makabe.t 2023-07-31 01:50:49 +00:00
parent dbb61274a1
commit c8ecc32b80
12 changed files with 731 additions and 40 deletions

View File

@ -0,0 +1,13 @@
/*
M+6
- 1~2...
- 3~4...
- 5~6
ex)
E00XXXX : 正常系メッセージ
E01XXXX : 以上系メッセージ
EXX00XX : 全般
EXX01XX : タスク関連
*/
export const NotifyMessageCodes = ['M000101'] as const;

View File

@ -0,0 +1,7 @@
import { notifyMessages } from './message';
import { NotifyMessageCodeType } from './types/types';
export const makeNotifyMessage = (code: NotifyMessageCodeType): string => {
const msg = notifyMessages[code];
return msg;
};

View File

@ -0,0 +1,6 @@
import { NotifyMessages } from './types/types';
// エラーコードとメッセージ対応表
export const notifyMessages: NotifyMessages = {
M000101: 'You are assigned to Typist.',
};

View File

@ -0,0 +1,7 @@
import { NotifyMessageCodes } from '../code';
export type NotifyMessageCodeType = (typeof NotifyMessageCodes)[number];
export type NotifyMessages = {
[P in NotifyMessageCodeType]: string;
};

View File

@ -174,6 +174,13 @@ export const TASK_LIST_SORTABLE_ATTRIBUTES = [
*/
export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const;
/**
*
* NotificationHubの仕様上タグ式のOR条件で使えるタグは20個まで
* https://learn.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-tags-segment-push-message#tag-expressions
*/
export const TAG_MAX_COUNT = 20;
/**
*
*/

View File

@ -4,9 +4,17 @@ import { TasksController } from './tasks.controller';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
@Module({
imports: [UsersRepositoryModule, TasksRepositoryModule, AdB2cModule],
imports: [
UsersRepositoryModule,
UserGroupsRepositoryModule,
TasksRepositoryModule,
AdB2cModule,
NotificationhubModule,
],
providers: [TasksService],
controllers: [TasksController],
})

View File

@ -1,6 +1,8 @@
import {
makeDefaultAdb2cServiceMockValue,
makeDefaultNotificationhubServiceMockValue,
makeDefaultTasksRepositoryMockValue,
makeDefaultUserGroupsRepositoryMockValue,
makeDefaultUsersRepositoryMockValue,
makeTasksServiceMock,
} from './test/tasks.service.mock';
@ -16,19 +18,25 @@ import {
createUserGroup,
getCheckoutPermissions,
getTask,
makeTaskTestingModule,
} from './test/utility';
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
import { makeTestingModule } from '../../common/test/modules';
describe('TasksService', () => {
it('タスク一覧を取得できるadmin', 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: 'admin', tier: 5 };
@ -90,12 +98,19 @@ describe('TasksService', () => {
it('アカウント情報の取得に失敗した場合、エラーを返却する', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('DB failed');
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
@ -124,12 +139,18 @@ describe('TasksService', () => {
it('タスク一覧の取得に失敗した場合、エラーを返却するadmin', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed');
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
@ -193,11 +214,17 @@ describe('TasksService', () => {
count: 1,
};
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: 'admin', tier: 5 };
const offset = 0;
@ -225,7 +252,11 @@ describe('TasksService', () => {
it('タスク一覧を取得できるauthor', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return;
}
@ -233,7 +264,9 @@ describe('TasksService', () => {
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
@ -300,14 +333,20 @@ describe('TasksService', () => {
it('タスク一覧の取得に失敗した場合、エラーを返却するauthor', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error(
'DB failed',
);
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
@ -336,7 +375,11 @@ describe('TasksService', () => {
it('タスク一覧を取得できるtypist', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return;
}
@ -345,7 +388,9 @@ describe('TasksService', () => {
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'typist', tier: 5 };
@ -412,14 +457,20 @@ describe('TasksService', () => {
it('タスク一覧の取得に失敗した場合、エラーを返却するtypist', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
tasksRepositoryMockValue.getTasksFromTypistRelations = new Error(
'DB failed',
);
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'typist', tier: 5 };
@ -448,11 +499,17 @@ describe('TasksService', () => {
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 };
@ -481,12 +538,18 @@ describe('TasksService', () => {
it('AdB2Cのリクエスト上限超過時、専用のエラーを返却する', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const userGroupsRepositoryMockValue =
makeDefaultUserGroupsRepositoryMockValue();
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
);
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
@ -531,7 +594,12 @@ describe('TasksService', () => {
});
it('[Admin] Taskが0件であっても実行できる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
@ -560,7 +628,12 @@ describe('TasksService', () => {
expect(total).toEqual(0);
});
it('[Author] Authorは自分が作成者のTask一覧を取得できる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId } = await createUser(
source,
@ -618,7 +691,12 @@ describe('TasksService', () => {
}
});
it('[Author] Authorは同一アカウントであっても自分以外のAuhtorのTaskは取得できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: userId_1 } = await createUser(
source,
@ -700,7 +778,12 @@ describe('changeCheckoutPermission', () => {
});
it('タスクのチェックアウト権限を変更できる。(個人指定)', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
@ -757,7 +840,12 @@ describe('changeCheckoutPermission', () => {
});
it('タスクのチェックアウト権限を変更できる。(グループ指定)', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
@ -820,7 +908,12 @@ describe('changeCheckoutPermission', () => {
});
it('タスクのチェックアウト権限を変更できる。(チェックアウト権限を外す)', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
@ -862,7 +955,12 @@ describe('changeCheckoutPermission', () => {
});
it('ユーザーが存在しない場合、タスクのチェックアウト権限を変更できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
@ -910,7 +1008,12 @@ describe('changeCheckoutPermission', () => {
});
it('ユーザーグループが存在しない場合、タスクのチェックアウト権限を変更できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
@ -958,7 +1061,12 @@ describe('changeCheckoutPermission', () => {
});
it('タスクが存在しない場合、タスクのチェックアウト権限を変更できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -988,7 +1096,12 @@ describe('changeCheckoutPermission', () => {
});
it('タスクのステータスがUploadedでない場合、タスクのチェックアウト権限を変更できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1028,7 +1141,12 @@ describe('changeCheckoutPermission', () => {
});
it('ユーザーのRoleがAuthorでタスクのAuthorIDと自身のAuthorIDが一致しない場合、タスクのチェックアウト権限を変更できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1066,6 +1184,69 @@ describe('changeCheckoutPermission', () => {
new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST),
);
});
it('通知に失敗した場合、エラーとなる', async () => {
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
notificationhubServiceMockValue.notify = new Error('Notify Error.');
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId_1 } = await createUser(
source,
accountId,
'typist-user-external-id',
'typist',
);
const { userId: typistUserId_2 } = await createUser(
source,
accountId,
'typist-user-2-external-id',
'typist',
);
const { userId: authorUserId } = await createUser(
source,
accountId,
'author-user-external-id',
'author',
'MY_AUTHOR_ID',
);
const { taskId } = await createTask(
source,
accountId,
authorUserId,
'MY_AUTHOR_ID',
'',
'01',
'00000001',
'Uploaded',
);
const { userGroupId } = await createUserGroup(
source,
accountId,
'USER_GROUP_A',
[typistUserId_1],
);
await createCheckoutPermissions(source, taskId, typistUserId_1);
await createCheckoutPermissions(source, taskId, undefined, userGroupId);
const service = module.get<TasksService>(TasksService);
await expect(
service.changeCheckoutPermission(
1,
[{ typistName: 'typist-user-2', typistUserId: typistUserId_2 }],
'author-user-external-id',
['admin'],
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});
describe('checkout', () => {
@ -1087,7 +1268,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがTypistで、タスクのチェックアウト権限が個人指定である時、タスクをチェックアウトできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1145,7 +1331,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがTypistで、タスクのチェックアウト権限がグループ指定である時、タスクをチェックアウトできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1203,7 +1394,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがTypistで、タスクのステータスがPendingである時、タスクをチェックアウトできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1255,7 +1451,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(
source,
@ -1291,7 +1492,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(
source,
@ -1327,7 +1533,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Uploaded)', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: authorUserId } = await createUser(
source,
@ -1354,7 +1565,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Finished)', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: authorUserId } = await createUser(
source,
@ -1381,7 +1597,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクが存在しない場合、タスクをチェックアウトできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(
source,
@ -1400,7 +1621,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleがAuthorで、音声ファイルに紐づいたタスクでユーザーと一致するAuthorIDでない場合、タスクをチェックアウトできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: authorUserId } = await createUser(
source,
@ -1429,7 +1655,12 @@ describe('checkout', () => {
});
it('ユーザーのRoleに[Typist,author]が設定されていない時、タスクをチェックアウトできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(
source,
@ -1467,7 +1698,12 @@ describe('checkin', () => {
});
it('API実行者が文字起こし実行中のタスクである場合、タスクをチェックインできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1507,7 +1743,12 @@ describe('checkin', () => {
});
it('タスクのステータスがInprogressでない時、タスクをチェックインできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1542,7 +1783,12 @@ describe('checkin', () => {
});
it('API実行者が文字起こし実行中のタスクでない場合、タスクをチェックインできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');
const { userId: anotherTypistUserId } = await createUser(
@ -1579,7 +1825,12 @@ describe('checkin', () => {
});
it('タスクがない時、タスクをチェックインできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');
@ -1618,7 +1869,12 @@ describe('suspend', () => {
});
it('API実行者が文字起こし実行中のタスクである場合、タスクを中断できる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1654,7 +1910,12 @@ describe('suspend', () => {
});
it('タスクのステータスがInprogressでない時、タスクを中断できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1689,7 +1950,12 @@ describe('suspend', () => {
});
it('API実行者が文字起こし実行中のタスクでない場合、タスクを中断できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');
const { userId: anotherTypistUserId } = await createUser(
@ -1726,7 +1992,12 @@ describe('suspend', () => {
});
it('タスクがない時、タスクを中断できない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');
@ -1765,7 +2036,12 @@ describe('cancel', () => {
});
it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1805,7 +2081,12 @@ describe('cancel', () => {
});
it('API実行者のRoleがTypistの場合、自身が文字起こし中断しているタスクをキャンセルできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1845,7 +2126,12 @@ describe('cancel', () => {
});
it('API実行者のRoleがAdminの場合、文字起こし実行中のタスクをキャンセルできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1886,7 +2172,12 @@ describe('cancel', () => {
});
it('API実行者のRoleがAdminの場合、文字起こし中断しているタスクをキャンセルできる', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1927,7 +2218,12 @@ describe('cancel', () => {
});
it('タスクのステータスが[Inprogress,Pending]でない時、タスクをキャンセルできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
const { userId: typistUserId } = await createUser(
source,
@ -1964,7 +2260,12 @@ describe('cancel', () => {
});
it('API実行者のRoleがTypistの場合、他人が文字起こし実行中のタスクをキャンセルできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');
const { userId: anotherTypistUserId } = await createUser(
@ -2003,7 +2304,12 @@ describe('cancel', () => {
});
it('タスクがない時、タスクをキャンセルできない', async () => {
const module = await makeTestingModule(source);
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
const module = await makeTaskTestingModule(
source,
notificationhubServiceMockValue,
);
const { accountId } = await createAccount(source);
await createUser(source, accountId, 'typist-user-external-id', 'typist');

View File

@ -29,6 +29,9 @@ import {
} from '../../repositories/tasks/errors/types';
import { Roles } from '../../common/types/role';
import { InvalidRoleError } from './errors/types';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
@Injectable()
export class TasksService {
@ -36,7 +39,9 @@ export class TasksService {
constructor(
private readonly taskRepository: TasksRepositoryService,
private readonly usersRepository: UsersRepositoryService,
private readonly userGroupsRepositoryService: UserGroupsRepositoryService,
private readonly adB2cService: AdB2cService,
private readonly notificationhubService: NotificationhubService,
) {}
// TODO [Task2244] 引数にAccessTokenがあるのは不適切なのでController側で分解したい
@ -405,6 +410,43 @@ export class TasksService {
role,
assignees,
);
// すべての割り当て候補ユーザーを取得する
const assigneesGroupIds = assignees
.filter((x) => x.typistGroupId)
.map((x) => x.typistGroupId);
const assigneesUserIds = assignees
.filter((x) => x.typistUserId)
.map((x) => x.typistUserId);
const groupMembers =
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
assigneesGroupIds,
);
// 重複のない割り当て候補ユーザーID一覧を取得する
const distinctUserIds = [
...new Set([
...assigneesUserIds,
...groupMembers.map((x) => x.user_id),
]),
];
// 割り当てられたユーザーがいない場合は通知不要
if (distinctUserIds.length === 0) {
this.logger.log('No user assigned.');
return;
}
// タグを生成
const tags = distinctUserIds.map((x) => `user_${x}`);
this.logger.log(`tags: ${tags}`);
// タグ対象に通知送信
await this.notificationhubService.notify(
tags,
makeNotifyMessage('M000101'),
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {

View File

@ -12,6 +12,9 @@ import {
import { AdB2cService } from '../../../gateways/adb2c/adb2c.service';
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
import { Assignee } from '../types/types';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service';
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
export type TasksRepositoryMockValue = {
getTasksFromAccountId:
@ -46,10 +49,20 @@ export type UsersRepositoryMockValue = {
findUserByExternalId: User | Error;
};
export type UserGroupsRepositoryMockValue = {
getGroupMembersFromGroupIds: UserGroupMember[] | Error;
};
export type NotificationhubServiceMockValue = {
notify: undefined | Error;
};
export const makeTasksServiceMock = async (
tasksRepositoryMockValue: TasksRepositoryMockValue,
usersRepositoryMockValue: UsersRepositoryMockValue,
userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue,
adB2CServiceMockValue: AdB2CServiceMockValue,
notificationhubServiceMockValue: NotificationhubServiceMockValue,
): Promise<{
tasksService: TasksService;
taskRepoService: TasksRepositoryService;
@ -63,8 +76,14 @@ export const makeTasksServiceMock = async (
return makeTasksRepositoryMock(tasksRepositoryMockValue);
case UsersRepositoryService:
return makeUsersRepositoryMock(usersRepositoryMockValue);
case UserGroupsRepositoryService:
return makeUserGroupsRepositoryMock(userGroupsRepositoryMockValue);
case AdB2cService:
return makeAdb2cServiceMock(adB2CServiceMockValue);
case NotificationhubService:
return makeNotificationhubServiceMock(
notificationhubServiceMockValue,
);
}
})
.compile();
@ -197,6 +216,86 @@ export const makeDefaultAdb2cServiceMockValue = (): AdB2CServiceMockValue => {
};
};
export const makeNotificationhubServiceMock = (
value: NotificationhubServiceMockValue,
) => {
const { notify } = value;
return {
notify:
notify instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(notify)
: jest.fn<Promise<void>, []>().mockResolvedValue(notify),
};
};
export const makeDefaultNotificationhubServiceMockValue =
(): NotificationhubServiceMockValue => {
return {
notify: undefined,
};
};
export const makeUserGroupsRepositoryMock = (
value: UserGroupsRepositoryMockValue,
) => {
const { getGroupMembersFromGroupIds } = value;
return {
getGroupMembersFromGroupIds:
getGroupMembersFromGroupIds instanceof Error
? jest
.fn<Promise<void>, []>()
.mockRejectedValue(getGroupMembersFromGroupIds)
: jest
.fn<Promise<UserGroupMember[]>, []>()
.mockResolvedValue(getGroupMembersFromGroupIds),
};
};
export const makeDefaultUserGroupsRepositoryMockValue =
(): UserGroupsRepositoryMockValue => {
return {
getGroupMembersFromGroupIds: [
{
id: 1,
user_group_id: 1,
user_id: 1,
created_by: 'test',
updated_by: 'test',
},
{
id: 2,
user_group_id: 1,
user_id: 2,
created_by: 'test',
updated_by: 'test',
},
{
id: 3,
user_group_id: 2,
user_id: 1,
created_by: 'test',
updated_by: 'test',
},
{
id: 4,
user_group_id: 3,
user_id: 1,
created_by: 'test',
updated_by: 'test',
},
{
id: 5,
user_group_id: 3,
user_id: 3,
created_by: 'test',
updated_by: 'test',
},
],
};
};
export const makeDefaultUsersRepositoryMockValue =
(): UsersRepositoryMockValue => {
const user1 = new User();

View File

@ -1,4 +1,6 @@
import { DataSource } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { Task } from '../../../repositories/tasks/entity/task.entity';
@ -6,6 +8,99 @@ import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.e
import { CheckoutPermission } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity';
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { UserGroupsRepositoryModule } from '../../../repositories/user_groups/user_groups.repository.module';
import { TasksRepositoryModule } from '../../../repositories/tasks/tasks.repository.module';
import { AuthModule } from '../../../features/auth/auth.module';
import { AdB2cModule } from '../../../gateways/adb2c/adb2c.module';
import { AccountsModule } from '../../../features/accounts/accounts.module';
import { UsersModule } from '../../../features/users/users.module';
import { FilesModule } from '../../../features/files/files.module';
import { TasksModule } from '../../../features/tasks/tasks.module';
import { SendGridModule } from '../../../features/../gateways/sendgrid/sendgrid.module';
import { LicensesModule } from '../../../features/licenses/licenses.module';
import { AccountsRepositoryModule } from '../../../repositories/accounts/accounts.repository.module';
import { UsersRepositoryModule } from '../../../repositories/users/users.repository.module';
import { LicensesRepositoryModule } from '../../../repositories/licenses/licenses.repository.module';
import { AudioFilesRepositoryModule } from '../../../repositories/audio_files/audio_files.repository.module';
import { AudioOptionItemsRepositoryModule } from '../../../repositories/audio_option_items/audio_option_items.repository.module';
import { CheckoutPermissionsRepositoryModule } from '../../../repositories/checkout_permissions/checkout_permissions.repository.module';
import { NotificationModule } from '../../../features//notification/notification.module';
import { NotificationhubModule } from '../../../gateways/notificationhub/notificationhub.module';
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.module';
import { AuthService } from '../../../features/auth/auth.service';
import { AccountsService } from '../../../features/accounts/accounts.service';
import { UsersService } from '../../../features/users/users.service';
import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service';
import { FilesService } from '../../../features/files/files.service';
import { LicensesService } from '../../../features/licenses/licenses.service';
import { TasksService } from '../../../features/tasks/tasks.service';
import {
NotificationhubServiceMockValue,
makeNotificationhubServiceMock,
} from './tasks.service.mock';
export const makeTaskTestingModule = async (
datasource: DataSource,
notificationhubServiceMockValue: NotificationhubServiceMockValue,
): Promise<TestingModule> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
AuthModule,
AdB2cModule,
AccountsModule,
UsersModule,
FilesModule,
TasksModule,
UsersModule,
SendGridModule,
LicensesModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
AudioFilesRepositoryModule,
AudioOptionItemsRepositoryModule,
TasksRepositoryModule,
CheckoutPermissionsRepositoryModule,
UserGroupsRepositoryModule,
UserGroupsRepositoryModule,
NotificationModule,
NotificationhubModule,
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
],
providers: [
AuthService,
AccountsService,
UsersService,
NotificationhubService,
FilesService,
TasksService,
LicensesService,
],
})
.useMocker(async (token) => {
switch (token) {
case DataSource:
return datasource;
}
})
.overrideProvider(NotificationhubService)
.useValue(makeNotificationhubServiceMock(notificationhubServiceMockValue))
.compile();
return module;
} catch (e) {
console.log(e);
}
};
export const createAccount = async (
datasource: DataSource,

View File

@ -2,10 +2,17 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
NotificationHubsClient,
createAppleNotificationBody,
createAppleNotification,
createTagExpression,
createFcmLegacyNotification,
createWindowsToastNotification,
createFirebaseLegacyNotificationBody,
createWindowsInstallation,
createAppleInstallation,
createFcmLegacyInstallation,
createWindowsInstallation,
} from '@azure/notification-hubs';
import { TAG_MAX_COUNT } from '../../constants';
import { PNS } from '../../constants';
@Injectable()
export class NotificationhubService {
@ -70,4 +77,72 @@ export class NotificationhubService {
this.logger.log(`[OUT] ${this.register.name}`);
}
}
/**
*
* @param tags
* @param message
* @returns notify
*/
async notify(tags: string[], message: string): Promise<void> {
this.logger.log(`[IN] ${this.notify.name}`);
try {
// OR条件によるtag指定は20個までなので分割して送信する
const chunkTags = splitArrayInChunks(tags, TAG_MAX_COUNT);
for (let index = 0; index < chunkTags.length; index++) {
const currentTags = chunkTags[index];
const tagExpression = createTagExpression(currentTags);
// Windows
try {
const body = `<toast><visual><binding template="ToastText01"><text id="1">${message}</text></binding></visual></toast>`;
const notification = createWindowsToastNotification({ body });
const result = await this.client.sendNotification(notification, {
tagExpression,
});
this.logger.log(result);
} catch (e) {
this.logger.error(`error=${e}`);
}
// Apple
try {
const body = createAppleNotificationBody({ aps: { alert: message } });
const notification = createAppleNotification({ body });
const result = await this.client.sendNotification(notification, {
tagExpression,
});
this.logger.log(result);
} catch (e) {
this.logger.error(`error=${e}`);
}
// Android
try {
const body = createFirebaseLegacyNotificationBody({
data: { message: message },
});
const notification = createFcmLegacyNotification({ body });
const result = await this.client.sendNotification(notification, {
tagExpression,
});
this.logger.log(result);
} catch (e) {
this.logger.error(`error=${e}`);
}
}
} catch (e) {
throw e;
} finally {
this.logger.log(`[OUT] ${this.notify.name}`);
}
}
}
const splitArrayInChunks = (arr: string[], size: number): string[][] => {
const result: string[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};

View File

@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
import { DataSource, In, IsNull } from 'typeorm';
import { UserGroup } from './entity/user_group.entity';
import { Assignee } from '../../features/tasks/types/types';
import { UserGroupMember } from './entity/user_group_member.entity';
import { User } from '../users/entity/user.entity';
@Injectable()
export class UserGroupsRepositoryService {
@ -19,4 +22,27 @@ export class UserGroupsRepositoryService {
});
return value;
}
/**
* IDからユーザー所属一覧を取得する
* @param groupIds
* @returns users from groups
*/
async getGroupMembersFromGroupIds(
groupIds: number[],
): Promise<UserGroupMember[]> {
return await this.dataSource.transaction(async (entityManager) => {
const groupMemberRepo = entityManager.getRepository(UserGroupMember);
const groupMembers = await groupMemberRepo.find({
relations: {
user: true,
},
where: {
user_group_id: In(groupIds),
},
});
return groupMembers;
});
}
}