Skip to content

Commit 7210d49

Browse files
committed
fix: Correcting keycloak test config setup
1 parent cde369c commit 7210d49

6 files changed

Lines changed: 416 additions & 116 deletions

File tree

.bin/start-server.mjs

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import tar from 'tar-fs'
1414
const DIR_NAME = path.dirname(fileURLToPath(import.meta.url))
1515
const SERVER_DIR = path.resolve(DIR_NAME, '../tmp/server')
1616
const SCRIPT_EXTENSION = process.platform === 'win32' ? '.bat' : '.sh'
17+
const KC_BASE = 'http://localhost:8080'
18+
const ADMIN_USER = 'admin'
19+
const ADMIN_PASS = 'admin'
1720

1821
// TODO: Once support for Node.js 14 has been dropped this can be replaced with an import from 'node:stream/promises'.
1922
// More information: https://nodejs.org/api/stream.html#streams-promises-api
@@ -24,23 +27,125 @@ await startServer()
2427
async function startServer () {
2528
await downloadServer()
2629

30+
// Wipe data so each start gets a clean realm state (binary is preserved)
31+
const dataDir = path.join(SERVER_DIR, 'data')
32+
if (fs.existsSync(dataDir)) {
33+
fs.rmSync(dataDir, { recursive: true })
34+
console.info('Cleared server data directory for clean start.')
35+
}
36+
2737
console.info('Starting server …')
2838

29-
const args = process.argv.slice(2)
3039
const child = spawn(
3140
path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`),
32-
['start-dev', ...args],
41+
['start-dev'],
3342
{
3443
env: {
35-
KC_BOOTSTRAP_ADMIN_USERNAME: 'master-admin',
36-
KC_BOOTSTRAP_ADMIN_PASSWORD: 'admin',
44+
KC_BOOTSTRAP_ADMIN_USERNAME: ADMIN_USER,
45+
KC_BOOTSTRAP_ADMIN_PASSWORD: ADMIN_PASS,
3746
...process.env
3847
}
3948
}
4049
)
4150

4251
child.stdout.pipe(process.stdout)
4352
child.stderr.pipe(process.stderr)
53+
54+
await waitForReady()
55+
await provisionKeycloakReporter()
56+
}
57+
58+
async function waitForReady () {
59+
const url = `${KC_BASE}/realms/master/.well-known/openid-configuration`
60+
console.info('Waiting for Keycloak to be ready…')
61+
for (let i = 0; i < 30; i++) {
62+
try {
63+
const res = await fetch(url)
64+
if (res.ok) {
65+
console.info('Keycloak is ready.')
66+
return
67+
}
68+
} catch {
69+
// not ready yet
70+
}
71+
await new Promise(r => setTimeout(r, 5000))
72+
}
73+
throw new Error('Keycloak failed to start within timeout.')
74+
}
75+
76+
async function provisionKeycloakReporter () {
77+
const tokenRes = await fetch(
78+
`${KC_BASE}/realms/master/protocol/openid-connect/token`,
79+
{
80+
method: 'POST',
81+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
82+
body: new URLSearchParams({
83+
grant_type: 'password',
84+
client_id: 'admin-cli',
85+
username: ADMIN_USER,
86+
password: ADMIN_PASS,
87+
}),
88+
}
89+
)
90+
const { access_token } = /** @type {{ access_token: string }} */ (await tokenRes.json())
91+
const headers = {
92+
'Content-Type': 'application/json',
93+
Authorization: `Bearer ${access_token}`,
94+
}
95+
96+
// Create the keycloak-reporter client (409 = already exists, safe to ignore)
97+
const clientRes = await fetch(`${KC_BASE}/admin/realms/master/clients`, {
98+
method: 'POST',
99+
headers,
100+
body: JSON.stringify({
101+
clientId: 'keycloak-reporter',
102+
secret: '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
103+
serviceAccountsEnabled: true,
104+
directAccessGrantsEnabled: true,
105+
publicClient: false,
106+
enabled: true,
107+
fullScopeAllowed: true,
108+
}),
109+
})
110+
if (!clientRes.ok && clientRes.status !== 409) {
111+
throw new Error(`Failed to create client: ${clientRes.status}`)
112+
}
113+
114+
// Resolve the service account user Keycloak auto-creates
115+
const saRes = await fetch(
116+
`${KC_BASE}/admin/realms/master/users?username=service-account-keycloak-reporter`,
117+
{ headers }
118+
)
119+
const [serviceAccount] = /** @type {{ id: string }[]} */ (await saRes.json())
120+
121+
// Fetch the admin realm role and assign it to the service account
122+
const roleRes = await fetch(`${KC_BASE}/admin/realms/master/roles/admin`, { headers })
123+
const adminRole = /** @type {{ id: string, name: string }} */ (await roleRes.json())
124+
125+
await fetch(
126+
`${KC_BASE}/admin/realms/master/users/${serviceAccount.id}/role-mappings/realm`,
127+
{
128+
method: 'POST',
129+
headers,
130+
body: JSON.stringify([{ id: adminRole.id, name: adminRole.name }]),
131+
}
132+
)
133+
134+
// Create a test user so e2e user-listing tests have predictable data
135+
await fetch(`${KC_BASE}/admin/realms/master/users`, {
136+
method: 'POST',
137+
headers,
138+
body: JSON.stringify({
139+
username: 'kermit',
140+
firstName: 'Kermit',
141+
lastName: 'the Frog',
142+
email: 'kermit@example.com',
143+
enabled: true,
144+
credentials: [{ type: 'password', value: 'kermit', temporary: false }],
145+
}),
146+
})
147+
148+
console.info('keycloak-reporter client provisioned successfully.')
44149
}
45150

46151
async function downloadServer () {
@@ -55,7 +160,13 @@ async function downloadServer () {
55160

56161
const nightlyAsset = await getNightlyAsset()
57162
//console.log(nightlyAsset)
163+
if (!nightlyAsset) {
164+
throw new Error('Could not find nightly release asset.')
165+
}
58166
const assetStream = await getAssetAsStream(nightlyAsset)
167+
if (!assetStream) {
168+
throw new Error('Asset stream is empty.')
169+
}
59170

60171
await extractTarball(assetStream, SERVER_DIR, { strip: 1 })
61172
}
@@ -78,16 +189,22 @@ async function getNightlyAsset () {
78189
)
79190
}
80191

192+
/** @param {{ browser_download_url: string }} asset */
81193
async function getAssetAsStream (asset) {
82194
const response = await fetch(asset.browser_download_url)
83195

84196
if (!response.ok) {
85197
throw new Error('Something went wrong requesting the nightly release.')
86198
}
87199

88-
return response.body
200+
return /** @type {import('node:stream').Readable} */ (response.body)
89201
}
90202

91-
function extractTarball (stream, path, options) {
92-
return pipelineAsync(stream, gunzip(), tar.extract(path, options))
93-
}
203+
/**
204+
* @param {import('node:stream').Readable} stream
205+
* @param {string} destPath
206+
* @param {object} options
207+
*/
208+
function extractTarball (stream, destPath, options) {
209+
return pipelineAsync(stream, gunzip(), tar.extract(destPath, options))
210+
}

0 commit comments

Comments
 (0)