@@ -14,6 +14,9 @@ import tar from 'tar-fs'
1414const DIR_NAME = path . dirname ( fileURLToPath ( import . meta. url ) )
1515const SERVER_DIR = path . resolve ( DIR_NAME , '../tmp/server' )
1616const 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()
2427async 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
46151async 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 */
81193async 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