diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts new file mode 100644 index 0000000..054573e --- /dev/null +++ b/dictation_server/src/common/test/overrides.ts @@ -0,0 +1,63 @@ +import { ConflictError } from '../../gateways/adb2c/adb2c.service'; + +/** + * adB2cServiceのモックを作成して、TServiceが依存するサービス(adB2cService)の参照を上書きする + * ※ serviceに指定するオブジェクトは`adB2cService: AdB2cService`メンバ変数を持つ必要がある + * @param service 上書きしたいTService + * @param overrides adB2cServiceの各種メソッドのモックが返す値(省略した場合は既定のダミーの値) + */ +export const overrideAdB2cService = ( + service: TService, + overrides: { + createUser?: ( + email: string, + password: string, + username: string, + ) => Promise<{ sub: string } | ConflictError>; + }, +): void => { + const { createUser } = overrides; + Object.defineProperty(service, 'adB2cService', { + value: { + createUser: createUser ?? jest.fn().mockResolvedValue({ sub: 'dummy' }), + }, + }); +}; + +/** + * sendgridServiceのモックを作成して、TServiceが依存するサービス(sendgridService)の参照を上書きする + * ※ serviceに指定するオブジェクトは`sendgridService: SendgridService`メンバ変数を持つ必要がある + * @param service 上書きしたいTService + * @param overrides sendgridServiceの各種メソッドのモックが返す値(省略した場合は既定のダミーの値) + */ +export const overrideSendgridService = ( + service: TService, + overrides: { + createMailContentFromEmailConfirm?: ( + accountId: number, + userId: number, + email: string, + ) => Promise<{ subject: string; text: string; html: string }>; + sendMail?: ( + to: string, + from: string, + subject: string, + text: string, + html: string, + ) => Promise; + }, +): void => { + const { createMailContentFromEmailConfirm, sendMail } = overrides; + Object.defineProperty(service, 'sendgridService', { + value: { + createMailContentFromEmailConfirm: + createMailContentFromEmailConfirm ?? + jest.fn().mockResolvedValue({ + subject: 'dummySubject', + text: 'dummyText', + html: 'dummyHtml', + }), + sendMail: sendMail ?? jest.fn().mockResolvedValue(undefined), + }, + }); +}; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 4c523e8..f068766 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -16,6 +16,10 @@ import { createLicenseOrder, createUser, createLicenseSetExpiryDateAndStatus, + getAccount, + getUser, + getAccounts, + getUsers, } from './test/utility'; import { DataSource } from 'typeorm'; import { makeTestingModule } from '../../common/test/modules'; @@ -23,6 +27,176 @@ import { AccountsService } from './accounts.service'; import { makeContext } from '../../common/log'; import { TIERS } from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; +import { + overrideAdB2cService, + overrideSendgridService, +} from '../../common/test/overrides'; + +describe('createAccount', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + + it('アカウントを作成できる', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + + const externalId = 'test_external_id'; + const companyName = 'test_company_name'; + const country = 'US'; + const dealerAccountId = 1; + const email = 'dummy@dummy.dummy'; + const password = 'dummy_password'; + const username = 'dummy_username'; + const role = 'admin none'; + const acceptedTermsVersion = '1.0.0'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: externalId }; + }, + }); + overrideSendgridService(service, {}); + + const { accountId, externalUserId, userId } = await service.createAccount( + companyName, + country, + dealerAccountId, + email, + password, + username, + role, + acceptedTermsVersion, + ); + // 作成したアカウントのIDが返ってくるか確認 + expect(accountId).toBe(1); + expect(externalUserId).toBe(externalId); + expect(userId).toBe(1); + + // DB内が想定通りになっているか確認 + const account = await getAccount(source, accountId); + const user = await getUser(source, externalUserId); + expect(account.company_name).toBe(companyName); + expect(account.country).toBe(country); + expect(account.parent_account_id).toBe(dealerAccountId); + expect(account.tier).toBe(TIERS.TIER5); + expect(account.primary_admin_user_id).toBe(user.id); + expect(account.secondary_admin_user_id).toBe(null); + expect(user.accepted_terms_version).toBe(acceptedTermsVersion); + expect(user.account_id).toBe(accountId); + expect(user.role).toBe(role); + }); + + it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + + overrideAdB2cService(service, { + createUser: async () => { + throw new Error(); + }, + }); + + overrideSendgridService(service, {}); + + const companyName = 'test_company_name'; + const country = 'US'; + const dealerAccountId = 1; + const email = 'dummy@dummy.dummy'; + const password = 'dummy_password'; + const username = 'dummy_username'; + const role = 'admin none'; + const acceptedTermsVersion = '1.0.0'; + + try { + await service.createAccount( + companyName, + country, + dealerAccountId, + email, + password, + username, + role, + acceptedTermsVersion, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + expect(true).toBe(false); // ここには来てはいけない + } + } + + // DB内が想定通りになっているか確認 + const accounts = await getAccounts(source); + expect(accounts.length).toBe(0); + const users = await getUsers(source); + expect(users.length).toBe(0); + }); + + it('アカウントを作成がメールアドレス重複によって失敗すると400エラーが発生する', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + + overrideAdB2cService(service, { + createUser: async () => { + // EmailのConflictエラーを返す + return { reason: 'email', message: 'dummy' }; + }, + }); + + overrideSendgridService(service, {}); + + const companyName = 'test_company_name'; + const country = 'US'; + const dealerAccountId = 1; + const email = 'dummy@dummy.dummy'; + const password = 'dummy_password'; + const username = 'dummy_username'; + const role = 'admin none'; + const acceptedTermsVersion = '1.0.0'; + + try { + await service.createAccount( + companyName, + country, + dealerAccountId, + email, + password, + username, + role, + acceptedTermsVersion, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010301')); + } else { + expect(true).toBe(false); // ここには来てはいけない + } + } + + // DB内が想定通りになっているか確認 + const accounts = await getAccounts(source); + expect(accounts.length).toBe(0); + const users = await getUsers(source); + expect(users.length).toBe(0); + }); +}); describe('AccountsService', () => { it('アカウントに紐づくライセンス情報を取得する', async () => { @@ -505,7 +679,7 @@ describe('getPartnerAccount', () => { ); // 有効期限が14日後のライセンスを追加(5ライセンス) - let expiryDate = new Date(); + const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 14); expiryDate.setHours(23, 59, 59, 999); for (let i = 0; i < 5; i++) { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 5d08225..bbe0e39 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -10,11 +10,7 @@ import { } from '../../gateways/adb2c/adb2c.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; import { User } from '../../repositories/users/entity/user.entity'; -import { - LICENSE_EXPIRATION_THRESHOLD_DAYS, - TIERS, - USER_ROLES, -} from '../../constants'; +import { TIERS, USER_ROLES } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TypistGroup, diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index 31f4237..e66bff0 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -30,6 +30,50 @@ export const createAccount = async ( return { accountId: account.id }; }; +/** + * テスト ユーティリティ: 指定IDのアカウントを取得する + * @param dataSource データソース + * @param id アカウントID + * @returns 該当アカウント + */ +export const getAccount = async (dataSource: DataSource, id: number) => { + return await dataSource.getRepository(Account).findOne({ + where: { id: id }, + }); +}; + +/** + * テスト ユーティリティ: すべてのアカウントを取得する + * @param dataSource データソース + * @returns 該当アカウント一覧 + */ +export const getAccounts = async ( + dataSource: DataSource, +): Promise => { + return await dataSource.getRepository(Account).find(); +}; + +/** + * テスト ユーティリティ: 指定ExternalIdのアカウントを取得する + * @param dataSource データソース + * @param externalId 外部ID + * @returns 該当ユーザー + */ +export const getUser = async (dataSource: DataSource, externalId: string) => { + return await dataSource.getRepository(User).findOne({ + where: { external_id: externalId }, + }); +}; + +/** + * テスト ユーティリティ: すべてのユーザーを取得する + * @param dataSource データソース + * @returns 該当ユーザー一覧 + */ +export const getUsers = async (dataSource: DataSource): Promise => { + return await dataSource.getRepository(User).find(); +}; + export const createLicense = async ( datasource: DataSource, accountId: number, diff --git a/dictation_server/src/repositories/accounts/entity/account.entity.ts b/dictation_server/src/repositories/accounts/entity/account.entity.ts index 82d57ea..ad65437 100644 --- a/dictation_server/src/repositories/accounts/entity/account.entity.ts +++ b/dictation_server/src/repositories/accounts/entity/account.entity.ts @@ -22,16 +22,16 @@ export class Account { @Column() country: string; - @Column() + @Column({ default: false }) delegation_permission: boolean; - @Column() + @Column({ default: false }) locked: boolean; @Column() company_name: string; - @Column() + @Column({ default: false }) verified: boolean; @Column({ nullable: true }) @@ -43,16 +43,16 @@ export class Account { @Column({ nullable: true }) deleted_at?: Date; - @Column() - created_by: string; + @Column({ nullable: true }) + created_by?: string; - @CreateDateColumn() + @CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 created_at: Date; - @Column() - updated_by: string; + @Column({ nullable: true }) + updated_by?: string; - @UpdateDateColumn() + @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; @OneToMany(() => User, (user) => user.id) diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index 1e01643..7ed3f88 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -33,16 +33,16 @@ export class User { @Column({ nullable: true }) accepted_terms_version?: string; - @Column() + @Column({ default: false }) email_verified: boolean; - @Column() + @Column({ default: true }) auto_renew: boolean; - @Column() + @Column({ default: true }) license_alert: boolean; - @Column() + @Column({ default: true }) notification: boolean; @Column({ nullable: true }) @@ -57,16 +57,16 @@ export class User { @Column({ nullable: true }) deleted_at?: Date; - @Column() + @Column({ nullable: true }) created_by: string; - @CreateDateColumn() + @CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 created_at: Date; - @Column() - updated_by: string; + @Column({ nullable: true }) + updated_by?: string; - @UpdateDateColumn() + @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; @ManyToOne(() => Account, (account) => account.user)