feat(twenty-server): add trusted domain - backend crud (#10290)

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Antoine Moreaux
2025-02-21 17:02:48 +01:00
committed by GitHub
parent 22203bfd3c
commit bf92860d19
49 changed files with 1812 additions and 147 deletions

View File

@ -0,0 +1,45 @@
import { ObjectType } from '@nestjs/graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
Unique,
} from 'typeorm';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'approvedAccessDomain', schema: 'core' })
@ObjectType()
@Unique('IndexOnDomainAndWorkspaceId', ['domain', 'workspaceId'])
export class ApprovedAccessDomain {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ type: 'varchar', nullable: false })
domain: string;
@Column({ type: 'boolean', default: false, nullable: false })
isValidated: boolean;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.approvedAccessDomains, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
}

View File

@ -0,0 +1,15 @@
import { CustomException } from 'src/utils/custom-exception';
export class ApprovedAccessDomainException extends CustomException {
constructor(message: string, code: ApprovedAccessDomainExceptionCode) {
super(message, code);
}
}
export enum ApprovedAccessDomainExceptionCode {
APPROVED_ACCESS_DOMAIN_NOT_FOUND = 'APPROVED_ACCESS_DOMAIN_NOT_FOUND',
APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED = 'APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED',
APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL = 'APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL',
APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID = 'APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID',
APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED = 'APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED',
}

View File

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { ApprovedAccessDomainResolver } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.resolver';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
@Module({
imports: [
DomainManagerModule,
NestjsQueryTypeOrmModule.forFeature([ApprovedAccessDomain], 'core'),
],
exports: [ApprovedAccessDomainService],
providers: [ApprovedAccessDomainService, ApprovedAccessDomainResolver],
})
export class ApprovedAccessDomainModule {}

View File

@ -0,0 +1,71 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver, Query } from '@nestjs/graphql';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { User } from 'src/engine/core-modules/user/user.entity';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/dtos/approved-access-domain.dto';
import { CreateApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/create-approved-access.domain.input';
import { DeleteApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/delete-approved-access-domain.input';
import { ValidateApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/validate-approved-access-domain.input';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
export class ApprovedAccessDomainResolver {
constructor(
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
) {}
@Mutation(() => ApprovedAccessDomain)
async createApprovedAccessDomain(
@Args('input') { domain, email }: CreateApprovedAccessDomainInput,
@AuthWorkspace() currentWorkspace: Workspace,
@AuthUser() currentUser: User,
): Promise<ApprovedAccessDomain> {
return this.approvedAccessDomainService.createApprovedAccessDomain(
domain,
currentWorkspace,
currentUser,
email,
);
}
@Mutation(() => Boolean)
async deleteApprovedAccessDomain(
@Args('input') { id }: DeleteApprovedAccessDomainInput,
@AuthWorkspace() currentWorkspace: Workspace,
): Promise<boolean> {
await this.approvedAccessDomainService.deleteApprovedAccessDomain(
currentWorkspace,
id,
);
return true;
}
@Mutation(() => ApprovedAccessDomain)
async validateApprovedAccessDomain(
@Args('input')
{
validationToken,
approvedAccessDomainId,
}: ValidateApprovedAccessDomainInput,
): Promise<ApprovedAccessDomain> {
return await this.approvedAccessDomainService.validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId,
});
}
@Query(() => [ApprovedAccessDomain])
async getApprovedAccessDomains(
@AuthWorkspace() currentWorkspace: Workspace,
): Promise<Array<ApprovedAccessDomain>> {
return await this.approvedAccessDomainService.getApprovedAccessDomains(
currentWorkspace,
);
}
}

View File

@ -0,0 +1,26 @@
import { isDefined } from 'twenty-shared';
import { CustomException } from 'src/utils/custom-exception';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import {
ApprovedAccessDomainException,
ApprovedAccessDomainExceptionCode,
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
const assertIsDefinedOrThrow = (
approvedAccessDomain: ApprovedAccessDomain | undefined | null,
exceptionToThrow: CustomException = new ApprovedAccessDomainException(
'Approved access domain not found',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_NOT_FOUND,
),
): asserts approvedAccessDomain is ApprovedAccessDomain => {
if (!isDefined(approvedAccessDomain)) {
throw exceptionToThrow;
}
};
export const approvedAccessDomainValidator: {
assertIsDefinedOrThrow: typeof assertIsDefinedOrThrow;
} = {
assertIsDefinedOrThrow,
};

View File

@ -0,0 +1,20 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('ApprovedAccessDomain')
export class ApprovedAccessDomain {
@IDField(() => UUIDScalarType)
id: string;
@Field({ nullable: false })
domain: string;
@Field({ nullable: false })
isValidated: boolean;
@Field()
createdAt: Date;
}

View File

@ -0,0 +1,16 @@
import { InputType, Field } from '@nestjs/graphql';
import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
@InputType()
export class CreateApprovedAccessDomainInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
domain: string;
@Field(() => String)
@IsEmail()
@IsNotEmpty()
email: string;
}

View File

@ -0,0 +1,10 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString } from 'class-validator';
@InputType()
export class DeleteApprovedAccessDomainInput {
@Field()
@IsString()
id: string;
}

View File

@ -0,0 +1,16 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsNotEmpty } from 'class-validator';
@InputType()
export class ValidateApprovedAccessDomainInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
validationToken: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
approvedAccessDomainId: string;
}

View File

@ -0,0 +1,184 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { render } from '@react-email/render';
import { Repository } from 'typeorm';
import { APP_LOCALES } from 'twenty-shared';
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ApprovedAccessDomain as ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate';
import {
ApprovedAccessDomainException,
ApprovedAccessDomainExceptionCode,
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
export class ApprovedAccessDomainService {
constructor(
@InjectRepository(ApprovedAccessDomainEntity, 'core')
private readonly approvedAccessDomainRepository: Repository<ApprovedAccessDomainEntity>,
private readonly emailService: EmailService,
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
) {}
async sendApprovedAccessDomainValidationEmail(
sender: User,
to: string,
workspace: Workspace,
approvedAccessDomain: ApprovedAccessDomainEntity,
) {
if (approvedAccessDomain.isValidated) {
throw new ApprovedAccessDomainException(
'Approved access domain has already been validated',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED,
);
}
if (to.split('@')[1] !== approvedAccessDomain.domain) {
throw new ApprovedAccessDomainException(
'Approved access domain does not match email domain',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL,
);
}
const link = this.domainManagerService.buildWorkspaceURL({
workspace,
pathname: `settings/security`,
searchParams: {
wtdId: approvedAccessDomain.id,
validationToken: this.generateUniqueHash(approvedAccessDomain),
},
});
const emailTemplate = SendApprovedAccessDomainValidation({
link: link.toString(),
workspace: { name: workspace.displayName, logo: workspace.logo },
domain: approvedAccessDomain.domain,
sender: {
email: sender.email,
firstName: sender.firstName,
lastName: sender.lastName,
},
serverUrl: this.environmentService.get('SERVER_URL'),
locale: 'en' as keyof typeof APP_LOCALES,
});
const html = render(emailTemplate);
const text = render(emailTemplate, {
plainText: true,
});
await this.emailService.send({
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
to,
subject: 'Approve your access domain',
text,
html,
});
}
private generateUniqueHash(approvedAccessDomain: ApprovedAccessDomainEntity) {
return crypto
.createHash('sha256')
.update(
JSON.stringify({
id: approvedAccessDomain.id,
domain: approvedAccessDomain.domain,
key: this.environmentService.get('APP_SECRET'),
}),
)
.digest('hex');
}
async validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId,
}: {
validationToken: string;
approvedAccessDomainId: string;
}) {
const approvedAccessDomain =
await this.approvedAccessDomainRepository.findOneBy({
id: approvedAccessDomainId,
});
approvedAccessDomainValidator.assertIsDefinedOrThrow(approvedAccessDomain);
if (approvedAccessDomain.isValidated) {
throw new ApprovedAccessDomainException(
'Approved access domain has already been validated',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED,
);
}
const isHashValid =
this.generateUniqueHash(approvedAccessDomain) === validationToken;
if (!isHashValid) {
throw new ApprovedAccessDomainException(
'Invalid approved access domain validation token',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID,
);
}
return await this.approvedAccessDomainRepository.save({
...approvedAccessDomain,
isValidated: true,
});
}
async createApprovedAccessDomain(
domain: string,
inWorkspace: Workspace,
fromUser: User,
emailToValidateDomain: string,
): Promise<ApprovedAccessDomainEntity> {
const approvedAccessDomain = await this.approvedAccessDomainRepository.save(
{
workspaceId: inWorkspace.id,
domain,
},
);
await this.sendApprovedAccessDomainValidationEmail(
fromUser,
emailToValidateDomain,
inWorkspace,
approvedAccessDomain,
);
return approvedAccessDomain;
}
async deleteApprovedAccessDomain(
workspace: Workspace,
approvedAccessDomainId: string,
) {
const approvedAccessDomain =
await this.approvedAccessDomainRepository.findOneBy({
id: approvedAccessDomainId,
workspaceId: workspace.id,
});
approvedAccessDomainValidator.assertIsDefinedOrThrow(approvedAccessDomain);
await this.approvedAccessDomainRepository.delete(approvedAccessDomain);
}
async getApprovedAccessDomains(workspace: Workspace) {
return await this.approvedAccessDomainRepository.find({
where: {
workspaceId: workspace.id,
},
});
}
}

View File

@ -0,0 +1,390 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DeleteResult, Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import {
ApprovedAccessDomainException,
ApprovedAccessDomainExceptionCode,
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
import { ApprovedAccessDomainService } from './approved-access-domain.service';
describe('ApprovedAccessDomainService', () => {
let service: ApprovedAccessDomainService;
let approvedAccessDomainRepository: Repository<ApprovedAccessDomain>;
let emailService: EmailService;
let environmentService: EnvironmentService;
let domainManagerService: DomainManagerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApprovedAccessDomainService,
{
provide: getRepositoryToken(ApprovedAccessDomain, 'core'),
useValue: {
delete: jest.fn(),
findOneBy: jest.fn(),
find: jest.fn(),
save: jest.fn(),
},
},
{
provide: EmailService,
useValue: {
send: jest.fn(),
},
},
{
provide: EnvironmentService,
useValue: {
get: jest.fn(),
},
},
{
provide: DomainManagerService,
useValue: {
buildWorkspaceURL: jest.fn(),
},
},
],
}).compile();
service = module.get<ApprovedAccessDomainService>(
ApprovedAccessDomainService,
);
approvedAccessDomainRepository = module.get(
getRepositoryToken(ApprovedAccessDomain, 'core'),
);
emailService = module.get<EmailService>(EmailService);
environmentService = module.get<EnvironmentService>(EnvironmentService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
});
describe('createApprovedAccessDomain', () => {
it('should successfully create an approved access domain', async () => {
const domain = 'custom-domain.com';
const inWorkspace = {
id: 'workspace-id',
customDomain: null,
isCustomDomainEnabled: false,
} as Workspace;
const fromUser = {
email: 'user@custom-domain.com',
isEmailVerified: true,
} as User;
const expectedApprovedAccessDomain = {
workspaceId: 'workspace-id',
domain,
isValidated: true,
};
jest
.spyOn(approvedAccessDomainRepository, 'save')
.mockResolvedValue(
expectedApprovedAccessDomain as unknown as ApprovedAccessDomain,
);
jest
.spyOn(service, 'sendApprovedAccessDomainValidationEmail')
.mockResolvedValue();
const result = await service.createApprovedAccessDomain(
domain,
inWorkspace,
fromUser,
'validator@custom-domain.com',
);
expect(approvedAccessDomainRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId: 'workspace-id',
domain,
}),
);
expect(result).toEqual(expectedApprovedAccessDomain);
});
});
describe('deleteApprovedAccessDomain', () => {
it('should delete an approved access domain successfully', async () => {
const workspace: Workspace = { id: 'workspace-id' } as Workspace;
const approvedAccessDomainId = 'approved-access-domain-id';
const approvedAccessDomainEntity = {
id: approvedAccessDomainId,
workspaceId: workspace.id,
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomainEntity);
jest
.spyOn(approvedAccessDomainRepository, 'delete')
.mockResolvedValue({} as unknown as DeleteResult);
await service.deleteApprovedAccessDomain(
workspace,
approvedAccessDomainId,
);
expect(approvedAccessDomainRepository.findOneBy).toHaveBeenCalledWith({
id: approvedAccessDomainId,
workspaceId: workspace.id,
});
expect(approvedAccessDomainRepository.delete).toHaveBeenCalledWith(
approvedAccessDomainEntity,
);
});
it('should throw an error if the approved access domain does not exist', async () => {
const workspace: Workspace = { id: 'workspace-id' } as Workspace;
const approvedAccessDomainId = 'approved-access-domain-id';
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(null);
await expect(
service.deleteApprovedAccessDomain(workspace, approvedAccessDomainId),
).rejects.toThrow();
expect(approvedAccessDomainRepository.findOneBy).toHaveBeenCalledWith({
id: approvedAccessDomainId,
workspaceId: workspace.id,
});
expect(approvedAccessDomainRepository.delete).not.toHaveBeenCalled();
});
});
describe('sendApprovedAccessDomainValidationEmail', () => {
it('should throw an exception if the approved access domain is already validated', async () => {
const approvedAccessDomainId = 'approved-access-domain-id';
const sender = {} as User;
const workspace = {} as Workspace;
const email = 'validator@example.com';
const approvedAccessDomain = {
id: approvedAccessDomainId,
isValidated: true,
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
await expect(
service.sendApprovedAccessDomainValidationEmail(
sender,
email,
workspace,
approvedAccessDomain,
),
).rejects.toThrowError(
new ApprovedAccessDomainException(
'Approved access domain has already been validated',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED,
),
);
});
it('should throw an exception if the email does not match the approved access domain', async () => {
const approvedAccessDomainId = 'approved-access-domain-id';
const sender = {} as User;
const workspace = {} as Workspace;
const email = 'validator@different.com';
const approvedAccessDomain = {
id: approvedAccessDomainId,
isValidated: false,
domain: 'example.com',
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
await expect(
service.sendApprovedAccessDomainValidationEmail(
sender,
email,
workspace,
approvedAccessDomain,
),
).rejects.toThrowError(
new ApprovedAccessDomainException(
'Approved access domain does not match email domain',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL,
),
);
});
it('should send a validation email if all conditions are met', async () => {
const sender = {
email: 'sender@example.com',
firstName: 'John',
lastName: 'Doe',
} as User;
const workspace = {
displayName: 'Test Workspace',
logo: '/logo.png',
} as Workspace;
const email = 'validator@custom-domain.com';
const approvedAccessDomain = {
isValidated: false,
domain: 'custom-domain.com',
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
jest
.spyOn(domainManagerService, 'buildWorkspaceURL')
.mockReturnValue(new URL('https://sub.twenty.com'));
jest
.spyOn(environmentService, 'get')
.mockImplementation((key: string) => {
if (key === 'EMAIL_FROM_ADDRESS') return 'no-reply@example.com';
if (key === 'SERVER_URL') return 'https://api.example.com';
});
await service.sendApprovedAccessDomainValidationEmail(
sender,
email,
workspace,
approvedAccessDomain,
);
expect(domainManagerService.buildWorkspaceURL).toHaveBeenCalledWith({
workspace: workspace,
pathname: 'settings/security',
searchParams: { validationToken: expect.any(String) },
});
expect(emailService.send).toHaveBeenCalledWith({
from: 'John Doe (via Twenty) <no-reply@example.com>',
to: email,
subject: 'Approve your access domain',
text: expect.any(String),
html: expect.any(String),
});
});
});
describe('validateApprovedAccessDomain', () => {
it('should validate the approved access domain successfully with a correct token', async () => {
const approvedAccessDomainId = 'domain-id';
const validationToken = 'valid-token';
const approvedAccessDomain = {
id: approvedAccessDomainId,
domain: 'example.com',
isValidated: false,
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
jest
.spyOn(service as any, 'generateUniqueHash')
.mockReturnValue(validationToken);
const saveSpy = jest.spyOn(approvedAccessDomainRepository, 'save');
await service.validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId: approvedAccessDomainId,
});
expect(approvedAccessDomainRepository.findOneBy).toHaveBeenCalledWith({
id: approvedAccessDomainId,
});
expect(saveSpy).toHaveBeenCalledWith(
expect.objectContaining({ isValidated: true }),
);
});
it('should throw an error if the approved access domain does not exist', async () => {
const approvedAccessDomainId = 'invalid-domain-id';
const validationToken = 'valid-token';
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(null);
await expect(
service.validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId: approvedAccessDomainId,
}),
).rejects.toThrowError(
new ApprovedAccessDomainException(
'Approved access domain not found',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_NOT_FOUND,
),
);
});
it('should throw an error if the validation token is invalid', async () => {
const approvedAccessDomainId = 'domain-id';
const validationToken = 'invalid-token';
const approvedAccessDomain = {
id: approvedAccessDomainId,
domain: 'example.com',
isValidated: false,
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
jest
.spyOn(service as any, 'generateUniqueHash')
.mockReturnValue('valid-token');
await expect(
service.validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId: approvedAccessDomainId,
}),
).rejects.toThrowError(
new ApprovedAccessDomainException(
'Invalid approved access domain validation token',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID,
),
);
});
it('should throw an error if the approved access domain is already validated', async () => {
const approvedAccessDomainId = 'domain-id';
const validationToken = 'valid-token';
const approvedAccessDomain = {
id: approvedAccessDomainId,
domain: 'example.com',
isValidated: true,
} as ApprovedAccessDomain;
jest
.spyOn(approvedAccessDomainRepository, 'findOneBy')
.mockResolvedValue(approvedAccessDomain);
await expect(
service.validateApprovedAccessDomain({
validationToken,
approvedAccessDomainId: approvedAccessDomainId,
}),
).rejects.toThrowError(
new ApprovedAccessDomainException(
'Approved access domain has already been validated',
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED,
),
);
});
});
});