Skip to content

Commit dd0c29d

Browse files
authored
Merge pull request #70 from rcbj/feature/issue-67
Create an automated test suite to validate debugger functionality. #67
2 parents 7c3026a + b81529e commit dd0c29d

8 files changed

+568
-0
lines changed

.github/workflows/tests.yml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Selenium Tests
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
id: checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Start & configure Keycloak and debugger
18+
id: configure
19+
run: |
20+
# Start Docker containers
21+
CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up -d --build
22+
sleep 30
23+
24+
# Configure client credentials flow
25+
KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token')
26+
curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}'
27+
curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"name": "client-credentials-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}'
28+
curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"clientId": "client-credentials", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}'
29+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].id')
30+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].clientId')
31+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].secret')
32+
KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .id')
33+
KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .name')
34+
curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID/optional-client-scopes/$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN"
35+
36+
# Share variables to next steps
37+
echo "CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT=http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" >> $GITHUB_OUTPUT
38+
echo "CLIENT_CREDENTIALS_CLIENT_ID=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID)" >> $GITHUB_OUTPUT
39+
echo "CLIENT_CREDENTIALS_CLIENT_SECRET=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET)" >> $GITHUB_OUTPUT
40+
echo "CLIENT_CREDENTIALS_SCOPE=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME)" >> $GITHUB_OUTPUT
41+
42+
- name: Test client credentials flow
43+
id: test_client_credentials
44+
run: |
45+
# Install dependencies
46+
cd tests && npm install
47+
48+
# Test client credentials flow
49+
DISCOVERY_ENDPOINT=${{ steps.configure.outputs.CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT }} \
50+
CLIENT_ID=${{ steps.configure.outputs.CLIENT_CREDENTIALS_CLIENT_ID }} \
51+
CLIENT_SECRET=${{ steps.configure.outputs.CLIENT_CREDENTIALS_CLIENT_SECRET }} \
52+
SCOPE=${{ steps.configure.outputs.CLIENT_CREDENTIALS_SCOPE }} \
53+
node oauth2_client_credentials.js

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
api/node_modules
22
client/node_modules
3+
node_modules

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ For the other grants and flows, similar steps to the above are used.
142142

143143
See the blog [posts](https://medium.com/@robert.broeckelmann/red-hat-sso-and-3scale-series-d904f2127702) for more information.
144144

145+
## Running tests
146+
* sudo CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up -d --build
147+
148+
* KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token')
149+
* curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}'
150+
* curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"name": "client-credentials-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}'
151+
* curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"clientId": "client-credentials", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}'
152+
* KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].id')
153+
* KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].clientId')
154+
* KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].secret')
155+
* KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .id')
156+
* KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .name')
157+
* curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID/optional-client-scopes/$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN"
158+
159+
160+
* cd tests && npm install
161+
* DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" \\\
162+
CLIENT_ID=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID \\\
163+
CLIENT_SECRET=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET \\\
164+
SCOPE=$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME \\\
165+
node oauth2_client_credentials.js
166+
145167
## Prerequisites
146168

147169
To run this project you will need to install docker.

docker-compose-with-keycloak.yml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
version: '3'
2+
services:
3+
postgres:
4+
image: postgres:15
5+
network_mode: "host"
6+
volumes:
7+
- postgres_data:/var/lib/postgresql/data
8+
environment:
9+
POSTGRES_USER: keycloak
10+
POSTGRES_PASSWORD: keycloak
11+
POSTGRES_DB: keycloak
12+
13+
keycloak:
14+
image: quay.io/keycloak/keycloak:26.1.4
15+
command: ["start-dev"]
16+
network_mode: "host"
17+
environment:
18+
KC_DB: postgres
19+
KC_DB_URL: jdbc:postgresql://localhost:5432/keycloak
20+
KC_DB_USERNAME: keycloak
21+
KC_DB_PASSWORD: keycloak
22+
KEYCLOAK_ADMIN: keycloak
23+
KEYCLOAK_ADMIN_PASSWORD: keycloak
24+
depends_on:
25+
- postgres
26+
27+
api:
28+
container_name: api
29+
image: rcbj/api
30+
environment:
31+
- HOST=0.0.0.0
32+
- PORT=4000
33+
- LOG_LEVEL=debug
34+
build:
35+
context: api
36+
dockerfile: Dockerfile
37+
network_mode: "host"
38+
depends_on:
39+
- keycloak
40+
41+
client:
42+
container_name: client
43+
image: rcbj/client
44+
environment:
45+
- CONFIG_FILE=./env/local.js
46+
build:
47+
context: client
48+
dockerfile: Dockerfile
49+
args:
50+
CONFIG_FILE: ${CONFIG_FILE}
51+
network_mode: "host"
52+
depends_on:
53+
- api
54+
55+
volumes:
56+
postgres_data:

run-tests.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
# Start Docker containers
4+
CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up -d --build
5+
sleep 30
6+
7+
# Configure client credentials flow
8+
KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token')
9+
curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}'
10+
curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"name": "client-credentials-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}'
11+
curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"clientId": "client-credentials", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}'
12+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].id')
13+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].clientId')
14+
KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].secret')
15+
KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .id')
16+
KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .name')
17+
curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID/optional-client-scopes/$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN"
18+
19+
# Install dependencies
20+
cd tests && npm install
21+
22+
# Test client credentials flow
23+
DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" \
24+
CLIENT_ID=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID \
25+
CLIENT_SECRET=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET \
26+
SCOPE=$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME \
27+
node oauth2_client_credentials.js

tests/oauth2_client_credentials.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const { Builder, By, until } = require("selenium-webdriver");
2+
const { Select } = require('selenium-webdriver/lib/select');
3+
const chrome = require("selenium-webdriver/chrome");
4+
const jwt = require("jsonwebtoken");
5+
const assert = require("assert");
6+
7+
async function populateMetadata(driver, discovery_endpoint) {
8+
oidc_discovery_endpoint = By.id("oidc_discovery_endpoint");
9+
btn_oidc_discovery_endpoint = By.className("btn_oidc_discovery_endpoint");
10+
btn_oidc_populate_meta_data = By.className("btn_oidc_populate_meta_data");
11+
12+
// Wait until page is loaded
13+
await driver.wait(until.elementLocated(oidc_discovery_endpoint), 10000);
14+
await driver.wait(until.elementIsVisible(driver.findElement(oidc_discovery_endpoint)), 10000);
15+
16+
// Enter discovery endpoint
17+
await driver.findElement(oidc_discovery_endpoint).clear();
18+
await driver.findElement(oidc_discovery_endpoint).sendKeys(discovery_endpoint);
19+
await driver.findElement(btn_oidc_discovery_endpoint).click();
20+
21+
// Populate metadata
22+
await driver.wait(until.elementLocated(btn_oidc_populate_meta_data), 10000);
23+
await driver.wait(until.elementIsVisible(driver.findElement(btn_oidc_populate_meta_data)), 10000);
24+
await driver.executeScript("arguments[0].scrollIntoView({ behavior: 'smooth', block: 'center' });", await driver.findElement(btn_oidc_populate_meta_data));
25+
await driver.findElement(btn_oidc_populate_meta_data).click();
26+
}
27+
28+
async function getAccessToken(driver, client_id, client_secret, scope) {
29+
authorization_grant_type = By.id("authorization_grant_type");
30+
token_client_id = By.id("token_client_id");
31+
token_client_secret = By.id("token_client_secret");
32+
token_scope = By.id("token_scope");
33+
btn1 = By.className("btn1");
34+
token_access_token = By.id("token_access_token");
35+
display_token_error_form_textarea1 = By.id("display_token_error_form_textarea1");
36+
37+
// Select client credential login type
38+
await new Select(await driver.findElement(authorization_grant_type)).selectByVisibleText('OAuth2 Client Credential');
39+
await driver.wait(until.elementLocated(token_client_id), 10000);
40+
await driver.wait(until.elementIsVisible(driver.findElement(token_client_id)), 10000);
41+
42+
// Submit credentials
43+
await driver.findElement(token_client_id).clear();
44+
await driver.findElement(token_client_id).sendKeys(client_id);
45+
await driver.findElement(token_client_secret).clear();
46+
await driver.findElement(token_client_secret).sendKeys(client_secret);
47+
await driver.findElement(token_scope).clear();
48+
await driver.findElement(token_scope).sendKeys(scope);
49+
await driver.findElement(btn1).click();
50+
51+
// Get access token result
52+
async function waitForVisibility(element) {
53+
await driver.wait(until.elementLocated(element), 10000);
54+
await driver.wait(until.elementIsVisible(driver.findElement(element)), 10000);
55+
return element;
56+
}
57+
58+
let visibleAccessTokenElement = await Promise.any([
59+
waitForVisibility(token_access_token),
60+
waitForVisibility(display_token_error_form_textarea1)
61+
]);
62+
63+
return await driver.findElement(visibleAccessTokenElement).getAttribute("value");
64+
}
65+
66+
async function verifyAccessToken(access_token, client_id, scope) {
67+
async function compareScopes(scope1, scope2) {
68+
scope1 = scope1.split(" ");
69+
scope2 = scope2.split(" ");
70+
71+
return scope2.every(element => scope1.includes(element));
72+
}
73+
74+
let decoded_access_token = jwt.decode(access_token, { complete: true });
75+
let response_text = access_token.match(/responseText: (.*)/);
76+
77+
assert.notStrictEqual(decoded_access_token, null, "Cannot decode access token. Request result: " + (response_text ? response_text[1] : "no response text"));
78+
assert.strictEqual(decoded_access_token.payload.client_id, client_id, "Access token client ID does not match client ID.");
79+
assert.strictEqual(await compareScopes(decoded_access_token.payload.scope, scope), true, "Access token scope does not match scope.");
80+
}
81+
82+
async function test() {
83+
const options = new chrome.Options();
84+
options.addArguments("--headless");
85+
options.addArguments("--no-sandbox");
86+
const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build();
87+
88+
try {
89+
const discovery_endpoint = process.env.DISCOVERY_ENDPOINT;
90+
const client_id = process.env.CLIENT_ID;
91+
const client_secret = process.env.CLIENT_SECRET;
92+
const scope = process.env.SCOPE;
93+
94+
assert(discovery_endpoint, "DISCOVERY_ENDPOINT environment variable is not set.");
95+
assert(client_id, "CLIENT_ID environment variable is not set.");
96+
assert(client_secret, "CLIENT_SECRET environment variable is not set.");
97+
assert(scope, "SCOPE environment variable is not set.");
98+
99+
await driver.get("http://localhost:3000");
100+
await populateMetadata(driver, discovery_endpoint);
101+
let access_token = await getAccessToken(driver, client_id, client_secret, scope);
102+
await verifyAccessToken(access_token, client_id, scope);
103+
console.log("Test completed successfully.")
104+
} catch (error) {
105+
console.log(error.message);
106+
process.exit(1);
107+
} finally {
108+
await driver.quit();
109+
}
110+
}
111+
112+
test();

0 commit comments

Comments
 (0)