Skip to content

Commit 3990ea6

Browse files
author
hersveit
authored
Merge pull request #27 from fullstack-development/25-e2e-tests
e2e tests
2 parents f93f998 + ce71432 commit 3990ea6

File tree

13 files changed

+338
-30
lines changed

13 files changed

+338
-30
lines changed

api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@nestjs/passport": "^9.0.0",
3333
"@nestjs/platform-express": "^9.0.0",
3434
"@nestjs/schedule": "^2.1.0",
35-
"@prisma/client": "^3.14.0",
35+
"@prisma/client": "3.14.0",
3636
"body-parser": "^1.19.0",
3737
"class-transformer": "^0.3.1",
3838
"class-validator": "^0.12.2",
@@ -43,8 +43,8 @@
4343
"express": "^4.17.2",
4444
"node-fetch": "^2.6.7",
4545
"passport": "^0.4.1",
46+
"prisma": "3.14.0",
4647
"passport-jwt": "^4.0.0",
47-
"prisma": "^3.14.0",
4848
"ramda": "^0.27.1",
4949
"reflect-metadata": "^0.1.13",
5050
"rimraf": "^3.0.2",

api/src/controllers/auth/__tests__/auth.controller.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getUserStub } from '../../../__mocks__/user.stub';
1919
import { DatabaseServiceProvider } from '../../../services/database/database.service';
2020
import { ModuleRef } from '@nestjs/core';
2121
import { TokenServiceProvider } from '../../../services/token/token.service';
22+
import { v4 } from 'uuid';
2223

2324
describe('AuthController', () => {
2425
const appWrap = {} as AppWrap;
@@ -187,12 +188,13 @@ describe('AuthController', () => {
187188
refreshCookie: '2',
188189
refreshToken: '2',
189190
});
191+
const confirmUuid = v4();
190192
const response = await request(appWrap.app.getHttpServer())
191193
.post('/api/auth/confirm-email')
192194
.set('Content-Type', 'application/x-www-form-urlencoded')
193-
.send({ confirmUuid: '123' });
195+
.send({ confirmUuid });
194196

195-
expect(authService.confirmEmail).toBeCalledWith('123');
197+
expect(authService.confirmEmail).toBeCalledWith(confirmUuid);
196198
expect(response.statusCode).toEqual(200);
197199
expect(response.body).toEqual({
198200
success: true,

api/src/controllers/auth/auth.model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
1+
import { IsEmail, IsString, IsUUID, MaxLength, MinLength } from 'class-validator';
22

33
export class SignUpInput {
44
@IsEmail()
@@ -21,6 +21,6 @@ export class SignInInput {
2121
}
2222

2323
export class ConfirmEmailInput {
24-
@IsString() //TODO: replace validate string for UUID
24+
@IsUUID('4')
2525
confirmUuid: string;
2626
}

api/src/controllers/user/__tests__/user.controller.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe('UserController', () => {
117117

118118
expect(response.statusCode).toEqual(200);
119119
expect(omit(['created'], response.body.data)).toEqual(
120-
omit(['refreshToken', 'created'], user),
120+
omit(['refreshToken', 'created', 'emailConfirm', 'hash'], user),
121121
);
122122
});
123123
});

api/src/controllers/user/user.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class UserControllerProvider {
1313
async me(@User() { id }: RequestUser) {
1414
return mapResponse(await this.users.findUser({ id }))((user) => {
1515
return ControllerResponse.Success({
16-
body: R.omit(['refreshToken'], user),
16+
body: R.omit(['refreshToken', 'emailConfirm', 'hash'], user),
1717
});
1818
});
1919
}

api/yarn.lock

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -797,22 +797,22 @@
797797
consola "^2.15.0"
798798
node-fetch "^2.6.1"
799799

800-
"@prisma/client@^3.14.0":
801-
version "3.15.2"
802-
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.2.tgz#2181398147afc79bfe0d83c03a88dc45b49bd365"
803-
integrity sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==
800+
"@prisma/client@3.14.0":
801+
version "3.14.0"
802+
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48"
803+
integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==
804804
dependencies:
805-
"@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
805+
"@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
806806

807-
"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
808-
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
809-
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53"
810-
integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==
807+
"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a":
808+
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
809+
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9"
810+
integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ==
811811

812-
"@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
813-
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
814-
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570"
815-
integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==
812+
"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a":
813+
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
814+
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf"
815+
integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg==
816816

817817
"@sinclair/typebox@^0.24.1":
818818
version "0.24.51"
@@ -4072,12 +4072,12 @@ pretty-format@^28.0.0, pretty-format@^28.1.3:
40724072
ansi-styles "^5.0.0"
40734073
react-is "^18.0.0"
40744074

4075-
prisma@^3.14.0:
4076-
version "3.15.2"
4077-
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.2.tgz#4ebe32fb284da3ac60c49fbc16c75e56ecf32067"
4078-
integrity sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==
4075+
prisma@3.14.0:
4076+
version "3.14.0"
4077+
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81"
4078+
integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw==
40794079
dependencies:
4080-
"@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
4080+
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
40814081

40824082
process-nextick-args@~2.0.0:
40834083
version "2.0.1"
Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,151 @@
1+
import { PrismaClient } from '@prisma/client';
2+
import { isJWT } from 'class-validator';
3+
import { v4 } from 'uuid';
4+
import { confirmEmail, refresh, signIn, signUp } from './endpoints/auth.controller.endpoints';
5+
16
describe('Auth Controller', () => {
2-
it('no test', () => {
3-
expect(true).toBeTruthy();
4-
})
5-
});
7+
let prisma: PrismaClient;
8+
9+
beforeAll(async () => {
10+
prisma = new PrismaClient();
11+
await prisma.$connect();
12+
});
13+
14+
afterAll(async () => {
15+
await prisma.$disconnect();
16+
});
17+
18+
describe('POST /api/auth/sign-up', () => {
19+
it('should return 400 on invalid body', async () => {
20+
await signUp.spec().withBody({}).expectStatus(400).toss();
21+
});
22+
23+
it('should return 400 on invalid email', async () => {
24+
await signUp
25+
.spec()
26+
.withBody({ password: '12345678', email: 'invalid' })
27+
.expectStatus(400)
28+
.toss();
29+
});
30+
31+
it('should success sign up user', async () => {
32+
await signUp.send({ email: 'test@example.com', password: '12345678' });
33+
34+
expect(
35+
(await prisma.user.findFirst({ where: { email: 'test@example.com' } }))
36+
?.emailConfirmed,
37+
).toBeFalsy();
38+
});
39+
40+
it('should return error if user already exist', async () => {
41+
await signUp
42+
.spec()
43+
.withBody({ email: 'test@example.com', password: '12345678' })
44+
.expectStatus(200)
45+
.expectJsonLike({ data: { error: 'userAlreadyExist' } });
46+
});
47+
});
48+
49+
describe('POST /api/auth/confirm-email', () => {
50+
it('should return 400 on invalid body', async () => {
51+
await confirmEmail.spec().withBody({}).expectStatus(400).toss();
52+
});
53+
54+
it('should return 400 on invalid uuid', async () => {
55+
await confirmEmail.spec().withBody({ confirmUuid: 'invalid' }).expectStatus(400).toss();
56+
});
57+
58+
it('should return error if email confirm not found', async () => {
59+
await confirmEmail
60+
.spec()
61+
.withBody({ confirmUuid: v4() })
62+
.expectStatus(200)
63+
.expectJsonLike({ data: { error: 'cannotFindEmailConfirm' } });
64+
});
65+
66+
it('should success confirm email', async () => {
67+
const user = await prisma.user.findFirst({
68+
where: { email: 'test@example.com' },
69+
include: { emailConfirm: true },
70+
});
71+
72+
expect(
73+
(await prisma.user.findFirst({ where: { email: 'test@example.com' } }))
74+
?.emailConfirmed,
75+
).toBeFalsy();
76+
77+
await confirmEmail.send(user?.emailConfirm?.confirmUuid as string);
78+
79+
expect(
80+
(await prisma.user.findFirst({ where: { email: 'test@example.com' } }))
81+
?.emailConfirmed,
82+
).toBeTruthy();
83+
});
84+
85+
it('should return error if user already confirmed email', async () => {
86+
const user = await prisma.user.findFirst({
87+
where: { email: 'test@example.com' },
88+
include: { emailConfirm: true },
89+
});
90+
91+
await confirmEmail
92+
.spec()
93+
.withBody({ confirmUuid: user?.emailConfirm?.confirmUuid })
94+
.expectStatus(200)
95+
.expectJsonLike({ data: { error: 'emailAlreadyConfirmed' } });
96+
});
97+
});
98+
99+
describe('POST /api/auth/sign-in', () => {
100+
it('should return 400 on invalid body', async () => {
101+
await signIn.spec().withBody({}).expectStatus(400).toss();
102+
await signIn.spec().withBody({ email: 'test@example.com' }).expectStatus(400).toss();
103+
await signIn
104+
.spec()
105+
.withBody({ email: 'invalid', password: '12345678' })
106+
.expectStatus(400)
107+
.toss();
108+
});
109+
110+
it('should return error if user not confirm email', async () => {
111+
await signUp.send({ email: 'test1@example.com', password: '12345678' });
112+
113+
await signIn
114+
.spec()
115+
.withBody({ email: 'test1@example.com', password: '12345678' })
116+
.expectStatus(200)
117+
.expectJsonLike({ data: { error: 'emailNotConfirmed' } });
118+
});
119+
120+
it('should return error if email or password incorrect', async () => {
121+
await signIn
122+
.spec()
123+
.withBody({ email: 'test@example.com', password: '123456789' })
124+
.expectStatus(200)
125+
.expectJsonLike({ data: { error: 'emailOrPasswordIncorrect' } });
126+
await signIn
127+
.spec()
128+
.withBody({ email: 'test2@example.com', password: '12345678' })
129+
.expectStatus(200)
130+
.expectJsonLike({ data: { error: 'emailOrPasswordIncorrect' } });
131+
});
132+
133+
it('should success return tokens', async () => {
134+
await signIn.send({ email: 'test@example.com', password: '12345678' });
135+
});
136+
});
137+
138+
describe('GET /api/auth/refresh', () => {
139+
it('should return 401', async () => {
140+
await refresh.spec().expectStatus(401).toss();
141+
});
142+
143+
it('should return new access token and refresh token in cookie', async () => {
144+
const tokens = await signIn.send({ email: 'test@example.com', password: '12345678' });
145+
146+
const result = await refresh.send(tokens.refreshToken);
147+
expect(isJWT(result.accessToken)).toEqual(true);
148+
expect(isJWT(result.refreshToken)).toEqual(true);
149+
});
150+
});
151+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Spec from 'pactum/src/models/Spec';
2+
import { plainToClass } from 'class-transformer';
3+
import { IsJWT, isJWT, validate } from 'class-validator';
4+
import * as cookieParser from 'cookie';
5+
import { getSpec, postSpec } from '../../../pactum';
6+
import { IsTrue } from '../../../utils/validation.utils';
7+
8+
type SignInput = {
9+
email: string;
10+
password: string;
11+
};
12+
13+
const signIn = postSpec.api<SignInput, { accessToken: string; refreshToken: string }>(
14+
'auth/sign-in',
15+
(spec: Spec) => async (data: SignInput) => {
16+
const response = await spec
17+
.withBody({
18+
email: data.email,
19+
password: data.password,
20+
})
21+
.expectStatus(200)
22+
.toss();
23+
24+
return extractAuthData(response);
25+
},
26+
);
27+
28+
const signUp = postSpec.api<SignInput, true>(
29+
'auth/sign-up',
30+
(spec: Spec) => async (data: SignInput) => {
31+
const response = await spec
32+
.withBody({
33+
email: data.email,
34+
password: data.password,
35+
})
36+
.expectStatus(200)
37+
.toss();
38+
39+
const body = plainToClass(SignUpResponse, response.json);
40+
41+
const errors = await validate(body);
42+
expect(errors.length).toEqual(0);
43+
44+
return body.success;
45+
},
46+
);
47+
48+
const refresh = getSpec.api<string, { accessToken: string; refreshToken: string }>(
49+
'auth/refresh',
50+
(spec) => async (refreshToken) => {
51+
const response = await spec
52+
.withCookies({
53+
Refresh: refreshToken,
54+
})
55+
.expectStatus(200)
56+
.toss();
57+
58+
return extractAuthData(response);
59+
},
60+
);
61+
62+
const confirmEmail = postSpec.api<string, { accessToken: string; refreshToken: string }>(
63+
'auth/confirm-email',
64+
(spec) => async (confirmUuid) => {
65+
const response = await spec.withBody({ confirmUuid }).expectStatus(200).toss();
66+
67+
return extractAuthData(response);
68+
},
69+
);
70+
71+
async function extractAuthData(response: any) {
72+
const refreshToken = (cookieParser.parse(response.headers['set-cookie'][0]) || {})['Refresh'];
73+
expect(isJWT(refreshToken)).toEqual(true);
74+
75+
const bodyErrors = await validate(plainToClass(AuthBody, response.json));
76+
expect(bodyErrors.length).toEqual(0);
77+
78+
return {
79+
accessToken: response.json.data as string,
80+
refreshToken: refreshToken,
81+
};
82+
}
83+
84+
class AuthBody {
85+
@IsJWT()
86+
data: string;
87+
}
88+
89+
class SignUpResponse {
90+
@IsTrue()
91+
success: true;
92+
}
93+
94+
export { signIn, refresh, confirmEmail, signUp };

0 commit comments

Comments
 (0)