Skip to content

Commit f175dfa

Browse files
authored
Add comprehensive unit and integration tests for Keycloak functionality (#20)
* Add comprehensive unit and integration tests for Keycloak functionality - Implement unit tests for the Token class, covering constructor behavior, expiration checks, role verification, and edge cases. - Create integration tests for Keycloak interactions, including access token generation, role verification, token verification (both online and offline), and error handling. - Set up Docker-based integration testing environment with Keycloak and PostgreSQL. - Include a realm export configuration for testing, defining users, roles, and client settings. - Add a script to wait for Keycloak to be ready before running integration tests. - Document integration test setup and usage in README.md. * Update workflow to trigger on pull requests to both main and master branches * Update README.md
1 parent a8c0e30 commit f175dfa

20 files changed

+2696
-155
lines changed

.eslintignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
tests/
2+
dist/
3+
node_modules/
4+
coverage/

.github/workflows/tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main, master]
6+
workflow_dispatch:
7+
8+
jobs:
9+
tests:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: "18"
20+
21+
- name: Install dependencies
22+
run: npm install
23+
24+
- name: Build library
25+
run: npm run build
26+
27+
- name: Run unit tests
28+
run: npm run test:coverage

README.md

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
# keycloak-backend
2+
23
[![NPM version](https://badgen.net/npm/v/keycloak-backend)](https://www.npmjs.com/package/keycloak-backend)
34
[![NPM Total Downloads](https://badgen.net/npm/dt/keycloak-backend)](https://www.npmjs.com/package/keycloak-backend)
45
[![License](https://badgen.net/npm/license/keycloak-backend)](https://www.npmjs.com/package/keycloak-backend)
56
[![TypeScript support](https://badgen.net/npm/types/keycloak-backend)](https://www.npmjs.com/package/keycloak-backend)
67
[![Github stars](https://badgen.net/github/stars/BackendStack21/keycloak-backend?icon=github)](https://github.com/BackendStack21/keycloak-backend.git)
78

8-
<img src="logo.svg" width="400">
9+
<img src="logo.svg" width="400">
910

1011
Keycloak Node.js minimalist connector for backend services integration. It aims to serve as base for high performance authorization middlewares.
1112

1213
> In order to use this module, the used Keycloak client `Direct Access Grants Enabled` setting should be `ON`
1314
1415
## Keycloak Introduction
16+
1517
The awesome open-source Identity and Access Management solution develop by RedHat.
1618
Keycloak support those very nice features you are looking for:
19+
1720
- Single-Sign On
1821
- LDAP and Active Directory
1922
- Standard Protocols
@@ -29,72 +32,114 @@ Keycloak support those very nice features you are looking for:
2932

3033
More about Keycloak: http://www.keycloak.org/
3134

35+
### Compatibility
36+
37+
This library is tested against **Keycloak 26.0**. It supports modern Keycloak versions (18+) by default.
38+
For versions older than 18, set `is_legacy_endpoint: true`.
39+
3240
## Using the keycloak-backend module
41+
3342
### Configuration
43+
3444
```js
3545
const Keycloak = require('keycloak-backend').Keycloak
3646
const keycloak = new Keycloak({
3747
"realm": "realm-name",
3848
"keycloak_base_url": "https://keycloak.example.org",
3949
"client_id": "super-secure-client",
40-
"username": "user@example.org",
41-
"password": "passw0rd",
42-
"is_legacy_endpoint": false
50+
"client_secret": "super-secure-secret", // Optional: for client_credentials grant
51+
"username": "user@example.org", // Optional: for password grant
52+
"password": "passw0rd", // Optional: for password grant
53+
"is_legacy_endpoint": false, // Optional: true for Keycloak < 18
54+
"timeout": 10000, // Optional: HTTP request timeout in ms (default: 10000)
55+
"httpsAgent": new https.Agent({ ... }), // Optional: Custom HTTPS agent
56+
"onError": (err, ctx) => console.error(ctx, err) // Optional: Error handler hook
4357
})
4458
```
59+
4560
> The `is_legacy_endpoint` configuration property should be TRUE for older Keycloak versions (under 18)
4661
4762
For TypeScript:
63+
4864
```ts
49-
import { Keycloak } from "keycloak-backend"
65+
import { Keycloak } from "keycloak-backend";
5066
const keycloak = new Keycloak({
51-
"realm": "realm-name",
52-
"keycloak_base_url": "https://keycloak.example.org",
53-
"client_id": "super-secure-client",
54-
"username": "user@example.org",
55-
"password": "passw0rd",
56-
"is_legacy_endpoint": false
57-
})
67+
realm: "realm-name",
68+
keycloak_base_url: "https://keycloak.example.org",
69+
client_id: "super-secure-client",
70+
// ... other options
71+
});
5872
```
5973

6074
### Generating access tokens
75+
6176
```js
62-
const accessToken = await keycloak.accessToken.get()
77+
const accessToken = await keycloak.accessToken.get();
6378
```
79+
6480
Or:
81+
6582
```js
66-
request.get('http://service.example.org/api/endpoint', {
67-
'auth': {
68-
'bearer': await keycloak.accessToken.get()
69-
}
70-
})
83+
request.get("http://service.example.org/api/endpoint", {
84+
auth: {
85+
bearer: await keycloak.accessToken.get(),
86+
},
87+
});
7188
```
7289

7390
### Validating access tokens
91+
7492
#### Online validation
93+
7594
This method requires online connection to the Keycloak service to validate the access token. It is highly secure since it also check for possible token invalidation. The disadvantage is that a request to the Keycloak service happens on every validation:
95+
7696
```js
77-
const token = await keycloak.jwt.verify(accessToken)
97+
const token = await keycloak.jwt.verify(accessToken);
7898
//console.log(token.isExpired())
7999
//console.log(token.hasRealmRole('user'))
80100
//console.log(token.hasApplicationRole('app-client-name', 'some-role'))
81101
```
82102

83103
#### Offline validation
104+
84105
This method perform offline JWT verification against the access token using the Keycloak Realm public key. Performance is higher compared to the online method, as a disadvantage no access token invalidation on Keycloak server is checked:
106+
85107
```js
86-
const cert = fs.readFileSync('public_cert.pem')
87-
const token = await keycloak.jwt.verifyOffline(accessToken, cert)
108+
// Ensure your public key is in PEM format
109+
const cert = `-----BEGIN PUBLIC KEY-----
110+
...your public key...
111+
-----END PUBLIC KEY-----`;
112+
const token = await keycloak.jwt.verifyOffline(accessToken, cert);
88113
//console.log(token.isExpired())
89114
//console.log(token.hasRealmRole('user'))
90115
//console.log(token.hasApplicationRole('app-client-name', 'some-role'))
91116
```
92117

118+
## Testing
119+
120+
The project includes a comprehensive integration test suite that runs against a real Keycloak instance using Docker Compose.
121+
122+
To run the integration tests:
123+
124+
```bash
125+
npm run test:integration
126+
```
127+
128+
This will:
129+
130+
1. Spin up a Keycloak 26 container and a Postgres database.
131+
2. Import a test realm with pre-configured clients and users.
132+
3. Run the test suite to verify token generation, validation (online/offline), and role checks.
133+
4. Tear down the environment.
134+
93135
## Breaking changes
136+
94137
### v4
138+
95139
- Codebase migrated from JavaScript to TypeScript. Many thanks to @neferin12
96140

97141
### v3
142+
98143
- The `UserManager` class was dropped
99144
- The `auth-server-url` config property was changed to `keycloak_base_url`
100145
- Most recent Keycloak API is supported by default, old versions are still supported through the `is_legacy_endpoint` config property

docker-compose.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
version: '3.8'
2+
3+
services:
4+
postgres:
5+
image: postgres:15-alpine
6+
container_name: keycloak-postgres
7+
environment:
8+
POSTGRES_DB: keycloak
9+
POSTGRES_USER: keycloak
10+
POSTGRES_PASSWORD: keycloak
11+
ports:
12+
- "5432:5432"
13+
volumes:
14+
- postgres_data:/var/lib/postgresql/data
15+
healthcheck:
16+
test: ["CMD-SHELL", "pg_isready -U keycloak"]
17+
interval: 5s
18+
timeout: 5s
19+
retries: 5
20+
21+
keycloak:
22+
image: quay.io/keycloak/keycloak:26.0
23+
container_name: keycloak-test
24+
environment:
25+
KC_DB: postgres
26+
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
27+
KC_DB_USERNAME: keycloak
28+
KC_DB_PASSWORD: keycloak
29+
KEYCLOAK_ADMIN: admin
30+
KEYCLOAK_ADMIN_PASSWORD: admin
31+
KC_HEALTH_ENABLED: true
32+
KC_METRICS_ENABLED: true
33+
KC_HTTP_ENABLED: true
34+
command:
35+
- start-dev
36+
- --import-realm
37+
ports:
38+
- "8080:8080"
39+
volumes:
40+
- ./tests/integration/realm-export.json:/opt/keycloak/data/import/realm-export.json
41+
depends_on:
42+
postgres:
43+
condition: service_healthy
44+
45+
46+
volumes:
47+
postgres_data:

jest.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
preset: "ts-jest",
3+
testEnvironment: "node",
4+
roots: ["<rootDir>/libs", "<rootDir>/tests"],
5+
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
6+
testPathIgnorePatterns: ["/node_modules/", "/tests/integration/"],
7+
collectCoverageFrom: ["libs/**/*.ts", "!libs/**/*.d.ts", "!libs/index.ts"],
8+
coverageThreshold: {
9+
global: {
10+
branches: 90,
11+
functions: 100,
12+
lines: 100,
13+
statements: 100,
14+
},
15+
},
16+
coverageReporters: ["text", "lcov", "html"],
17+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
18+
};

jest.integration.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
preset: "ts-jest",
3+
testEnvironment: "node",
4+
roots: ["<rootDir>/tests/integration"],
5+
testMatch: ["**/integration.test.ts"],
6+
testTimeout: 30000,
7+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
8+
};

0 commit comments

Comments
 (0)