Skip to content

Commit 5c4aa29

Browse files
committed
test(kratos-client-wrapper): create E2E test suite
1 parent 1c8f2b2 commit 5c4aa29

File tree

10 files changed

+318
-11
lines changed

10 files changed

+318
-11
lines changed

packages/base-client-wrapper/src/lib/ory-base.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class OryBaseService implements OnModuleInit {
2222
const shouldRetry =
2323
typeof config.retryCondition === 'function'
2424
? config.retryCondition(error)
25-
: true;
25+
: false;
2626
if (config?.retries && shouldRetry) {
2727
const retryDelay =
2828
typeof config.retryDelay === 'function'

packages/keto-client-wrapper/test/docker-compose.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ services:
88
- '44670:4467' # admin
99
command: serve -c /home/ory/keto.yaml
1010
restart: on-failure
11+
healthcheck:
12+
test:
13+
[
14+
'CMD-SHELL',
15+
'wget -nv --spider -t1 http://localhost:4466/health/ready || exit 1',
16+
]
17+
interval: 3s
18+
timeout: 3s
19+
retries: 3
20+
start_period: 2s
1121
volumes:
1222
- type: bind
1323
source: ./keto.yaml

packages/kratos-client-wrapper/src/lib/ory-authentication.guard.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
CanActivate,
33
ExecutionContext,
44
Injectable,
5+
Logger,
56
mixin,
67
} from '@nestjs/common';
78
import { OryFrontendService } from './ory-frontend';
@@ -34,6 +35,8 @@ export const OryAuthenticationGuard = (
3435
) => {
3536
@Injectable()
3637
class AuthenticationGuard implements CanActivate {
38+
readonly logger = new Logger(AuthenticationGuard.name);
39+
3740
constructor(readonly oryService: OryFrontendService) {}
3841

3942
async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -47,19 +50,13 @@ export const OryAuthenticationGuard = (
4750
...options,
4851
};
4952

50-
const cookie = cookieResolver(context);
51-
const xSessionToken = sessionTokenResolver(context);
52-
console.warn({
53-
cookie,
54-
xSessionToken,
55-
});
5653
try {
54+
const cookie = cookieResolver(context);
55+
const xSessionToken = sessionTokenResolver(context);
5756
const { data: session } = await this.oryService.toSession({
5857
cookie,
5958
xSessionToken,
6059
});
61-
console.warn('session', session, isValidSession(session));
62-
6360
if (!isValidSession(session)) {
6461
return false;
6562
}
@@ -68,7 +65,7 @@ export const OryAuthenticationGuard = (
6865
}
6966
return true;
7067
} catch (error) {
71-
console.error(error);
68+
this.logger.error(error);
7269
return false;
7370
}
7471
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Controller, Get, UseGuards } from '@nestjs/common';
2+
3+
import { ExampleService } from './app.service.mock';
4+
import { OryAuthenticationGuard } from '../src/lib/ory-authentication.guard';
5+
6+
@Controller('Example')
7+
export class ExampleController {
8+
constructor(private readonly exampleService: ExampleService) {}
9+
10+
@UseGuards(
11+
OryAuthenticationGuard({
12+
postValidationHook: (ctx, session) => {
13+
const req = ctx.switchToHttp().getRequest();
14+
req.session = session;
15+
req.user = session.identity;
16+
},
17+
isValidSession(session): boolean {
18+
return !!session.active;
19+
},
20+
sessionTokenResolver: (ctx) =>
21+
ctx
22+
.switchToHttp()
23+
.getRequest()
24+
?.headers?.authorization?.replace('Bearer ', ''),
25+
})
26+
)
27+
@Get()
28+
getExample() {
29+
return this.exampleService.getExample();
30+
}
31+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class ExampleService {
5+
getExample() {
6+
return {
7+
message: 'OK',
8+
};
9+
}
10+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
version: '2.7'
2+
3+
networks:
4+
ory:
5+
driver: bridge
6+
7+
services:
8+
kratos:
9+
image: oryd/kratos:v1.0.0
10+
ports:
11+
- '44330:4433' # public
12+
- '44340:4434' # admin
13+
networks:
14+
- ory
15+
restart: unless-stopped
16+
command: serve -c /etc/config/kratos/kratos.yaml --dev --watch-courier
17+
healthcheck:
18+
test:
19+
[
20+
'CMD-SHELL',
21+
'wget -nv --spider -t1 http://localhost:4433/health/ready || exit 1',
22+
]
23+
interval: 2s
24+
timeout: 3s
25+
retries: 3
26+
start_period: 1s
27+
volumes:
28+
- type: bind
29+
source: ./kratos.yaml
30+
target: /etc/config/kratos/kratos.yaml
31+
- type: bind
32+
source: ./identity.schema.json
33+
target: /etc/config/kratos/identity.schema.json
34+
# for docker on linux
35+
# extra_hosts:
36+
# - "host.docker.internal:host-gateway"
37+
38+
mailslurper:
39+
image: oryd/mailslurper:latest-smtps
40+
ports:
41+
- '4436:4436'
42+
- '4437:4437'
43+
networks:
44+
- ory
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
3+
"$schema": "http://json-schema.org/draft-07/schema#",
4+
"title": "Person",
5+
"type": "object",
6+
"properties": {
7+
"traits": {
8+
"type": "object",
9+
"properties": {
10+
"email": {
11+
"type": "string",
12+
"format": "email",
13+
"title": "E-Mail",
14+
"minLength": 4,
15+
"ory.sh/kratos": {
16+
"credentials": {
17+
"password": {
18+
"identifier": true
19+
}
20+
}
21+
}
22+
}
23+
},
24+
"required": ["email"],
25+
"additionalProperties": false
26+
}
27+
}
28+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test, TestingModule } from '@nestjs/testing';
3+
import type { Identity } from '@ory/client';
4+
import { execSync } from 'node:child_process';
5+
import { randomBytes } from 'node:crypto';
6+
import { join, resolve } from 'node:path';
7+
import request from 'supertest';
8+
9+
import { OryFrontendModule, OryFrontendService } from '../src/lib/ory-frontend';
10+
import {
11+
OryIdentitiesModule,
12+
OryIdentitiesService,
13+
} from '../src/lib/ory-identities';
14+
import { ExampleController } from './app.controller.mock';
15+
import { ExampleService } from './app.service.mock';
16+
17+
describe('Kratos client wrapper E2E', () => {
18+
let app: INestApplication;
19+
let oryFrontendService: OryFrontendService;
20+
let oryIdentityService: OryIdentitiesService;
21+
const dockerComposeFile = resolve(join(__dirname, 'docker-compose.yaml'));
22+
const route = '/Example';
23+
24+
const register = async (
25+
email: string,
26+
password: string
27+
): Promise<Identity> => {
28+
const { data: registrationFlow } =
29+
await oryFrontendService.createNativeRegistrationFlow();
30+
31+
const { data } = await oryFrontendService.updateRegistrationFlow({
32+
flow: registrationFlow.id,
33+
updateRegistrationFlowBody: {
34+
traits: { email },
35+
password,
36+
method: 'password',
37+
},
38+
});
39+
return data.identity;
40+
};
41+
42+
const login = async (email: string, password: string): Promise<string> => {
43+
const { data: loginFlow } =
44+
await oryFrontendService.createNativeLoginFlow();
45+
46+
const { data } = await oryFrontendService.updateLoginFlow({
47+
flow: loginFlow.id,
48+
updateLoginFlowBody: {
49+
password,
50+
identifier: email,
51+
method: 'password',
52+
},
53+
});
54+
return data.session_token as string;
55+
};
56+
57+
const createOryUser = async (
58+
email: string,
59+
password: string
60+
): Promise<{ identity: Identity; sessionToken: string }> => {
61+
const identity = await register(email, password);
62+
const response = await oryIdentityService.getIdentity({
63+
id: identity.id,
64+
});
65+
expect(response.data.id).toEqual(identity.id);
66+
const sessionToken = await login(email, password);
67+
return { identity, sessionToken };
68+
};
69+
70+
beforeAll(() => {
71+
execSync(`docker-compose -f ${dockerComposeFile} up -d --wait`, {
72+
stdio: 'ignore',
73+
});
74+
});
75+
76+
afterAll(() => {
77+
execSync(`docker-compose -f ${dockerComposeFile} down`, {
78+
stdio: 'ignore',
79+
});
80+
});
81+
82+
beforeEach(async () => {
83+
const module: TestingModule = await Test.createTestingModule({
84+
imports: [
85+
OryFrontendModule.forRoot({
86+
basePath: 'http://localhost:44330',
87+
}),
88+
OryIdentitiesModule.forRootAsync({
89+
useFactory: () => ({
90+
basePath: 'http://localhost:44340',
91+
accessToken: '',
92+
}),
93+
}),
94+
],
95+
providers: [ExampleService],
96+
controllers: [ExampleController],
97+
}).compile();
98+
99+
oryFrontendService = module.get(OryFrontendService);
100+
oryIdentityService = module.get(OryIdentitiesService);
101+
102+
app = module.createNestApplication();
103+
await app.init();
104+
});
105+
106+
afterEach(() => {
107+
return app?.close();
108+
});
109+
110+
it('should successfully authenticate user when it is registered in Ory Kratos', async () => {
111+
const password = randomBytes(8).toString('hex');
112+
const email = `${randomBytes(8).toString('hex')}@example.com`;
113+
const { sessionToken } = await createOryUser(email, password);
114+
115+
const { body } = await request(app.getHttpServer())
116+
.get(route)
117+
.set({
118+
Authorization: `Bearer ${sessionToken}`,
119+
});
120+
121+
expect(body).toEqual({ message: 'OK' });
122+
});
123+
124+
it('should fail to authenticate user when it is not registered in Ory Kratos', async () => {
125+
const { body } = await request(app.getHttpServer())
126+
.get(route)
127+
.set({
128+
Authorization: `Bearer ory_st_${randomBytes(8).toString('hex')}`,
129+
});
130+
expect(body).toEqual({
131+
error: 'Forbidden',
132+
message: 'Forbidden resource',
133+
statusCode: 403,
134+
});
135+
});
136+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
courier:
2+
message_retries: 5
3+
delivery_strategy: smtp
4+
smtp:
5+
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
6+
dsn: memory
7+
identity:
8+
default_schema_id: default
9+
schemas:
10+
- id: default
11+
url: file:///etc/config/kratos/identity.schema.json
12+
secrets:
13+
cipher:
14+
- 32-LONG-SECRET-NOT-SECURE-AT-ALL
15+
cookie:
16+
- cookie_secret_not_good_not_secure
17+
selfservice:
18+
default_browser_return_url: http://127.0.0.1:8080/
19+
methods:
20+
code:
21+
enabled: true
22+
link:
23+
config:
24+
base_url: ''
25+
lifespan: 15m
26+
enabled: true
27+
lookup_secret:
28+
enabled: true
29+
password:
30+
config:
31+
haveibeenpwned_enabled: true
32+
identifier_similarity_check_enabled: true
33+
ignore_network_errors: true
34+
max_breaches: 1
35+
min_password_length: 8
36+
enabled: true
37+
profile:
38+
enabled: true
39+
totp:
40+
config:
41+
issuer: Ticketing
42+
enabled: true
43+
serve:
44+
admin:
45+
base_url: http://kratos:4434/
46+
public:
47+
base_url: http://127.0.0.1:4433/
48+
49+
version: v1.0.0

packages/kratos-client-wrapper/tsconfig.spec.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"jest.config.ts",
1010
"src/**/*.test.ts",
1111
"src/**/*.spec.ts",
12-
"src/**/*.d.ts"
12+
"src/**/*.d.ts",
13+
"test/**/*.mock.ts",
14+
"test/**/*.spec.ts"
1315
]
1416
}

0 commit comments

Comments
 (0)