Skip to content

Commit a07353d

Browse files
committed
test: add unit tests for MailService, MailController, and MailUseCases
1 parent ecd534e commit a07353d

5 files changed

Lines changed: 411 additions & 8 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Test, type TestingModule } from '@nestjs/testing';
2+
import { createMock, type DeepMocked } from '@golevelup/ts-jest';
3+
import { ConflictException } from '@nestjs/common';
4+
import { ConfigService } from '@nestjs/config';
5+
import { MailService } from './mail.service';
6+
import { HttpClient } from '../http/http.service';
7+
8+
describe('MailService', () => {
9+
let mailService: MailService;
10+
let httpClient: DeepMocked<HttpClient>;
11+
let configService: DeepMocked<ConfigService>;
12+
13+
beforeEach(async () => {
14+
const moduleRef: TestingModule = await Test.createTestingModule({
15+
providers: [MailService],
16+
})
17+
.useMocker(() => createMock())
18+
.compile();
19+
20+
mailService = moduleRef.get(MailService);
21+
httpClient = moduleRef.get(HttpClient);
22+
configService = moduleRef.get(ConfigService);
23+
24+
configService.get.mockImplementation((key: string) => {
25+
const config = {
26+
'apis.mail.url': 'http://mail:3100',
27+
'secrets.gateway':
28+
'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT3dJQkFBSkJBTDlDTVRlZGEramdIcGJuTmtlSm51TlpnYzg5TGFvMGNQNkl6dlJrYTJ0MUVKbnh5ZTA1CndSWGZLMXFpbTFOMGU3cGhkd0RkRWYvNGJ1eFc5V2g1UWxzQ0F3RUFBUUpCQUpnRXljLzF2VDdGWFNyK3JpTWcKWFAxQ09LNTdaeCtCUFVyamZQTytHYSszWk1MRHhqaG44dGZmV1E4VUpKemJ5VkQ0Q0JqTmNra2xRN3phQ29BNwo1WWtDSVFEd0h2MXhVRkFVUkI2b3QwL0JMMWNxek5SNU80dFBMT0NjL2gyK0o4Y09WUUloQU12b0FrMm5IQWhSClpRNmhNZGFTdWtPVTE3MTYvRGxnNWNiSXNWYXh0bDN2QWlBTUdTT2YzL0lJODEyd0ZueFlPWEJrNGFrYTZwc2MKUkNDVkNHQ3JRZ25QZVFJZ2NTU2E2cFc0YzFFZTN5Qkl0RVNVZ0YxOTNKRDZsYWdUdDlxeXRHVkZ5UmNDSVFDYgp6dE85ampXcERmYTlnWTV2dVB4MFgyUkcxbjJQb0ZYVjVXT29RanNqbnc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=',
29+
isDevelopment: true,
30+
};
31+
return config[key];
32+
});
33+
});
34+
35+
it('should be defined', () => {
36+
expect(mailService).toBeDefined();
37+
});
38+
39+
describe('createAccount', () => {
40+
const payload = {
41+
userId: 'user-uuid',
42+
address: 'john@inxt.eu',
43+
domain: 'inxt.eu',
44+
displayName: 'John Doe',
45+
};
46+
47+
it('When mail gateway responds successfully, then it should return account data', async () => {
48+
httpClient.post.mockResolvedValueOnce({
49+
data: { address: 'john@inxt.eu', domain: 'inxt.eu' },
50+
} as any);
51+
52+
const result = await mailService.createAccount(payload);
53+
54+
expect(result).toEqual({ address: 'john@inxt.eu', domain: 'inxt.eu' });
55+
expect(httpClient.post).toHaveBeenCalledWith(
56+
'http://mail:3100/gateway/accounts',
57+
payload,
58+
expect.objectContaining({
59+
headers: expect.objectContaining({
60+
'Content-Type': 'application/json',
61+
Authorization: expect.stringMatching(/^Bearer /),
62+
}),
63+
}),
64+
);
65+
});
66+
67+
it('When mail gateway returns 409, then it should throw ConflictException', async () => {
68+
const axiosError = {
69+
response: {
70+
status: 409,
71+
data: { message: 'Address already in use' },
72+
},
73+
};
74+
httpClient.post.mockRejectedValueOnce(axiosError);
75+
76+
await expect(mailService.createAccount(payload)).rejects.toThrow(
77+
ConflictException,
78+
);
79+
});
80+
81+
it('When mail gateway returns 409 without message, then it should use default message', async () => {
82+
const axiosError = {
83+
response: { status: 409, data: {} },
84+
};
85+
httpClient.post.mockRejectedValueOnce(axiosError);
86+
87+
await expect(mailService.createAccount(payload)).rejects.toThrow(
88+
'Mail account already exists',
89+
);
90+
});
91+
92+
it('When mail gateway returns non-409 error, then it should rethrow', async () => {
93+
const axiosError = {
94+
response: { status: 500 },
95+
message: 'Internal Server Error',
96+
};
97+
httpClient.post.mockRejectedValueOnce(axiosError);
98+
99+
await expect(mailService.createAccount(payload)).rejects.toBe(axiosError);
100+
});
101+
});
102+
});

src/externals/mail/mail.service.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Inject, Injectable } from '@nestjs/common';
1+
import { ConflictException, Inject, Injectable } from '@nestjs/common';
22
import { sign } from 'jsonwebtoken';
33
import { ConfigService } from '@nestjs/config';
44
import { HttpClient } from '../http/http.service';
@@ -52,12 +52,21 @@ export class MailService {
5252
const baseUrl = this.configService.get('apis.mail.url');
5353
const headers = this.getAuthHeaders();
5454

55-
const res = await this.httpClient.post(
56-
`${baseUrl}/gateway/accounts`,
57-
payload,
58-
{ headers },
59-
);
55+
try {
56+
const res = await this.httpClient.post(
57+
`${baseUrl}/gateway/accounts`,
58+
payload,
59+
{ headers },
60+
);
6061

61-
return res.data;
62+
return res.data;
63+
} catch (error) {
64+
if (error?.response?.status === 409) {
65+
throw new ConflictException(
66+
error.response.data?.message || 'Mail account already exists',
67+
);
68+
}
69+
throw error;
70+
}
6271
}
6372
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Test, type TestingModule } from '@nestjs/testing';
2+
import { createMock, type DeepMocked } from '@golevelup/ts-jest';
3+
import { ConflictException } from '@nestjs/common';
4+
import { MailController } from './mail.controller';
5+
import { MailUseCases } from './mail.usecase';
6+
import { newUser } from '../../../test/fixtures';
7+
import { PaymentRequiredException } from '../feature-limit/exceptions/payment-required.exception';
8+
9+
describe('MailController', () => {
10+
let controller: MailController;
11+
let mailUseCases: DeepMocked<MailUseCases>;
12+
13+
beforeEach(async () => {
14+
const moduleRef: TestingModule = await Test.createTestingModule({
15+
controllers: [MailController],
16+
})
17+
.useMocker(() => createMock())
18+
.compile();
19+
20+
controller = moduleRef.get(MailController);
21+
mailUseCases = moduleRef.get(MailUseCases);
22+
});
23+
24+
describe('createMailAccount', () => {
25+
const dto = {
26+
address: 'john',
27+
domain: 'inxt.eu',
28+
displayName: 'John Doe',
29+
password: 'encrypted-password',
30+
};
31+
32+
it('When called with valid input, then it should return the usecase result', async () => {
33+
const user = newUser();
34+
const expected = {
35+
token: 'token',
36+
newToken: 'newToken',
37+
address: 'john@inxt.eu',
38+
};
39+
40+
mailUseCases.createMailAccount.mockResolvedValueOnce(expected);
41+
42+
const result = await controller.createMailAccount(user, dto);
43+
44+
expect(result).toEqual(expected);
45+
expect(mailUseCases.createMailAccount).toHaveBeenCalledWith(user, dto);
46+
});
47+
48+
it('When user has no mail access, then it should propagate PaymentRequiredException', async () => {
49+
const user = newUser();
50+
51+
mailUseCases.createMailAccount.mockRejectedValueOnce(
52+
new PaymentRequiredException('Mail access is not available'),
53+
);
54+
55+
await expect(controller.createMailAccount(user, dto)).rejects.toThrow(
56+
PaymentRequiredException,
57+
);
58+
});
59+
60+
it('When user already has a mail account, then it should propagate ConflictException', async () => {
61+
const user = newUser();
62+
63+
mailUseCases.createMailAccount.mockRejectedValueOnce(
64+
new ConflictException('User already has a mail account'),
65+
);
66+
67+
await expect(controller.createMailAccount(user, dto)).rejects.toThrow(
68+
ConflictException,
69+
);
70+
});
71+
});
72+
});

0 commit comments

Comments
 (0)