Skip to content

Commit 5cc6946

Browse files
committed
test(user): add E2E tests for user registration flow
1 parent 941d79c commit 5cc6946

3 files changed

Lines changed: 460 additions & 1 deletion

File tree

src/modules/user/user.e2e-spec.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { HttpStatus } from '@nestjs/common';
2+
import { NestExpressApplication } from '@nestjs/platform-express';
3+
import { getModelToken } from '@nestjs/sequelize';
4+
import request from 'supertest';
5+
6+
import { createTestApp } from '../../../test/helpers/test-app.helper';
7+
import {
8+
generateValidRegistrationData,
9+
RegisterUserDto,
10+
} from '../../../test/helpers/register.helper';
11+
import { UserModel } from './user.model';
12+
import { FolderModel } from '../folder/folder.model';
13+
import { FileModel } from '../file/file.model';
14+
import { KeyServerModel } from '../keyserver/key-server.model';
15+
import { CryptoService } from '../../externals/crypto/crypto.service';
16+
17+
describe('User Registration E2E', () => {
18+
let app: NestExpressApplication;
19+
let userModel: typeof UserModel;
20+
let folderModel: typeof FolderModel;
21+
let fileModel: typeof FileModel;
22+
let keyServerModel: typeof KeyServerModel;
23+
let cryptoService: CryptoService;
24+
let createdUserIds: number[] = [];
25+
26+
const encryptRegistrationData = (data: RegisterUserDto): RegisterUserDto => {
27+
return {
28+
...data,
29+
password: cryptoService.encryptText(data.password),
30+
salt: cryptoService.encryptText(data.salt),
31+
};
32+
};
33+
34+
beforeAll(async () => {
35+
app = await createTestApp();
36+
userModel = app.get(getModelToken(UserModel));
37+
folderModel = app.get(getModelToken(FolderModel));
38+
fileModel = app.get(getModelToken(FileModel));
39+
keyServerModel = app.get(getModelToken(KeyServerModel));
40+
cryptoService = app.get(CryptoService);
41+
});
42+
43+
afterEach(async () => {
44+
for (const userId of createdUserIds) {
45+
await userModel.update({ rootFolderId: null }, { where: { id: userId } });
46+
await keyServerModel.destroy({ where: { userId } });
47+
await fileModel.destroy({ where: { userId } });
48+
await folderModel.destroy({ where: { userId } });
49+
await userModel.destroy({ where: { id: userId } });
50+
}
51+
createdUserIds = [];
52+
});
53+
54+
afterAll(async () => {
55+
await app.close();
56+
});
57+
58+
const registerUser = (data: RegisterUserDto) => {
59+
return request(app.getHttpServer()).post('/users').send(data);
60+
};
61+
62+
describe('POST /users - User Registration', () => {
63+
describe('Users can register successfully with valid data', () => {
64+
it('When valid registration data is provided, then user is created successfully', async () => {
65+
const registrationData = generateValidRegistrationData();
66+
const encryptedData = encryptRegistrationData(registrationData);
67+
68+
const response = await registerUser(encryptedData).expect(
69+
HttpStatus.CREATED,
70+
);
71+
72+
expect(response.body).toHaveProperty('user');
73+
expect(response.body).toHaveProperty('token');
74+
expect(response.body).toHaveProperty('uuid');
75+
expect(response.body.user.email).toBe(registrationData.email);
76+
expect(response.body.user.name).toBe(registrationData.name);
77+
expect(response.body.user.lastname).toBe(registrationData.lastname);
78+
79+
const createdUser = await userModel.findOne({
80+
where: { email: registrationData.email },
81+
});
82+
expect(createdUser).not.toBeNull();
83+
createdUserIds.push(createdUser.id);
84+
});
85+
86+
it('When registration is successful, then tokens are returned', async () => {
87+
const registrationData = generateValidRegistrationData();
88+
const encryptedData = encryptRegistrationData(registrationData);
89+
90+
const response = await registerUser(encryptedData).expect(
91+
HttpStatus.CREATED,
92+
);
93+
94+
expect(response.body.token).toBeDefined();
95+
expect(response.body.newToken).toBeDefined();
96+
expect(typeof response.body.token).toBe('string');
97+
expect(typeof response.body.newToken).toBe('string');
98+
99+
const createdUser = await userModel.findOne({
100+
where: { email: registrationData.email },
101+
});
102+
createdUserIds.push(createdUser.id);
103+
});
104+
});
105+
106+
describe('The system properly rejects invalid registration attempts', () => {
107+
it('When email is missing, then registration fails with 400', async () => {
108+
const registrationData = generateValidRegistrationData();
109+
delete (registrationData as any).email;
110+
111+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
112+
});
113+
114+
it('When name is missing, then registration fails with 400', async () => {
115+
const registrationData = generateValidRegistrationData();
116+
delete (registrationData as any).name;
117+
118+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
119+
});
120+
121+
it('When password is missing, then registration fails with 400', async () => {
122+
const registrationData = generateValidRegistrationData();
123+
delete (registrationData as any).password;
124+
125+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
126+
});
127+
128+
it('When mnemonic is missing, then registration fails with 400', async () => {
129+
const registrationData = generateValidRegistrationData();
130+
delete (registrationData as any).mnemonic;
131+
132+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
133+
});
134+
135+
it('When email is invalid format, then registration fails with 400', async () => {
136+
const registrationData = generateValidRegistrationData({
137+
email: 'invalid-email',
138+
});
139+
140+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
141+
});
142+
143+
it('When email is already registered, then registration fails with 409', async () => {
144+
const registrationData = generateValidRegistrationData();
145+
const encryptedData = encryptRegistrationData(registrationData);
146+
147+
await registerUser(encryptedData).expect(HttpStatus.CREATED);
148+
149+
const createdUser = await userModel.findOne({
150+
where: { email: registrationData.email },
151+
});
152+
createdUserIds.push(createdUser.id);
153+
154+
const duplicateData = generateValidRegistrationData({
155+
email: registrationData.email,
156+
});
157+
const encryptedDuplicate = encryptRegistrationData(duplicateData);
158+
159+
await registerUser(encryptedDuplicate).expect(HttpStatus.CONFLICT);
160+
});
161+
162+
it('When name exceeds 100 characters, then registration fails with 400', async () => {
163+
const registrationData = generateValidRegistrationData({
164+
name: 'a'.repeat(101),
165+
});
166+
167+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
168+
});
169+
170+
it('When lastname exceeds 100 characters, then registration fails with 400', async () => {
171+
const registrationData = generateValidRegistrationData({
172+
lastname: 'a'.repeat(101),
173+
});
174+
175+
await registerUser(registrationData).expect(HttpStatus.BAD_REQUEST);
176+
});
177+
});
178+
179+
describe('All user resources are created correctly after registration', () => {
180+
it('When registration is successful, then root folder is created', async () => {
181+
const registrationData = generateValidRegistrationData();
182+
const encryptedData = encryptRegistrationData(registrationData);
183+
184+
const response = await registerUser(encryptedData).expect(
185+
HttpStatus.CREATED,
186+
);
187+
188+
expect(response.body.user.root_folder_id).toBeDefined();
189+
190+
const createdUser = await userModel.findOne({
191+
where: { email: registrationData.email },
192+
});
193+
createdUserIds.push(createdUser.id);
194+
195+
const rootFolder = await folderModel.findOne({
196+
where: { userId: createdUser.id, parentId: null },
197+
});
198+
expect(rootFolder).not.toBeNull();
199+
// Note: plainName is not set by the registration endpoint, only name (encrypted)
200+
expect(rootFolder.name).toBeDefined();
201+
});
202+
203+
it('When registration includes ECC keys, then keys are stored', async () => {
204+
const registrationData = generateValidRegistrationData();
205+
const encryptedData = encryptRegistrationData(registrationData);
206+
207+
const response = await registerUser(encryptedData).expect(
208+
HttpStatus.CREATED,
209+
);
210+
211+
expect(response.body.user.keys).toBeDefined();
212+
expect(response.body.user.keys.ecc).toBeDefined();
213+
expect(response.body.user.keys.ecc.publicKey).toBeDefined();
214+
expect(response.body.user.keys.ecc.privateKey).toBeDefined();
215+
216+
const createdUser = await userModel.findOne({
217+
where: { email: registrationData.email },
218+
});
219+
createdUserIds.push(createdUser.id);
220+
221+
const storedKeys = await keyServerModel.findOne({
222+
where: { userId: createdUser.id },
223+
});
224+
expect(storedKeys).not.toBeNull();
225+
expect(storedKeys.publicKey).toBeDefined();
226+
expect(storedKeys.privateKey).toBeDefined();
227+
});
228+
229+
it('When registration is successful, then user UUID is generated', async () => {
230+
const registrationData = generateValidRegistrationData();
231+
const encryptedData = encryptRegistrationData(registrationData);
232+
233+
const response = await registerUser(encryptedData).expect(
234+
HttpStatus.CREATED,
235+
);
236+
237+
expect(response.body.uuid).toBeDefined();
238+
expect(response.body.uuid).toMatch(
239+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
240+
);
241+
242+
const createdUser = await userModel.findOne({
243+
where: { email: registrationData.email },
244+
});
245+
createdUserIds.push(createdUser.id);
246+
expect(createdUser.uuid).toBe(response.body.uuid);
247+
});
248+
});
249+
});
250+
});

test/helpers/register.helper.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { v4 } from 'uuid';
2+
import { generateMnemonic } from 'bip39';
3+
4+
export interface RegisterUserDto {
5+
name: string;
6+
lastname: string;
7+
email: string;
8+
password: string;
9+
mnemonic: string;
10+
salt: string;
11+
keys?: {
12+
ecc?: {
13+
publicKey: string;
14+
privateKey: string;
15+
revocationKey?: string;
16+
};
17+
kyber?: {
18+
publicKey: string;
19+
privateKey: string;
20+
};
21+
};
22+
referrer?: string;
23+
}
24+
25+
export function generateValidRegistrationData(
26+
overrides: Partial<RegisterUserDto> = {},
27+
): RegisterUserDto {
28+
const timestamp = Date.now();
29+
const randomSuffix = v4().substring(0, 8);
30+
const uniqueId = `${timestamp}-${randomSuffix}`;
31+
32+
return {
33+
name: 'Test',
34+
lastname: 'User',
35+
email: overrides.email || `test-${uniqueId}@test.com`,
36+
password: generateHashedPassword(),
37+
mnemonic: generateMnemonic(256),
38+
salt: generateSalt(),
39+
keys: {
40+
ecc: {
41+
publicKey: `ecc-public-key-test-${uniqueId}`,
42+
privateKey: `ecc-private-key-test-${uniqueId}`,
43+
revocationKey: `ecc-revocation-key-test-${uniqueId}`,
44+
},
45+
kyber: {
46+
publicKey: `kyber-public-key-test-${uniqueId}`,
47+
privateKey: `kyber-private-key-test-${uniqueId}`,
48+
},
49+
},
50+
...overrides,
51+
};
52+
}
53+
54+
export function generateHashedPassword(): string {
55+
return '$2a$08$' + v4().replace(/-/g, '').substring(0, 53);
56+
}
57+
58+
export function generateSalt(): string {
59+
return v4().replace(/-/g, '').substring(0, 32);
60+
}

0 commit comments

Comments
 (0)