diff --git a/server/package.json b/server/package.json index 6b1d037..8ba79da 100644 --- a/server/package.json +++ b/server/package.json @@ -6,7 +6,8 @@ "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", - "test": "tsx --env-file=.env.test tests/api.test.ts", + "test": "tsx --env-file=.env.test tests/run-tests.ts", + "test:only": "tsx --env-file=.env.test tests/api.test.ts", "test:get-token": "tsx --env-file=.env.test tests/get-token.ts" }, "dependencies": { diff --git a/server/tests/get-token.ts b/server/tests/get-token.ts index 1e12dbd..ecb231a 100644 --- a/server/tests/get-token.ts +++ b/server/tests/get-token.ts @@ -1,22 +1,21 @@ const AUTHENTIK_URL = process.env.AUTHENTIK_URL || 'http://localhost:9000'; const CLIENT_ID = process.env.CLIENT_ID || 'kaboot-spa'; -const APP_SLUG = process.env.APP_SLUG || 'kaboot'; -const USERNAME = process.env.TEST_USERNAME || 'kaboottest'; -const PASSWORD = process.env.TEST_PASSWORD || 'kaboottest'; +const CLIENT_SECRET = process.env.CLIENT_SECRET || ''; +const USERNAME = process.env.TEST_USERNAME || ''; +const PASSWORD = process.env.TEST_PASSWORD || ''; -async function getTokenViaPasswordGrant(): Promise { +async function getTokenWithClientSecret(): Promise { + if (!CLIENT_SECRET) throw new Error('CLIENT_SECRET not set'); + const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`; - const params = new URLSearchParams({ grant_type: 'client_credentials', client_id: CLIENT_ID, - username: USERNAME, - password: PASSWORD, + client_secret: CLIENT_SECRET, scope: 'openid profile email', }); - console.log(`Token URL: ${tokenUrl}`); - + console.log(` Trying client_credentials with client_secret...`); const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -25,7 +24,35 @@ async function getTokenViaPasswordGrant(): Promise { if (!response.ok) { const error = await response.text(); - throw new Error(`Password grant failed: ${response.status} - ${error}`); + throw new Error(`${response.status} - ${error}`); + } + + const data = await response.json(); + return data.access_token; +} + +async function getTokenWithServiceAccount(): Promise { + if (!USERNAME || !PASSWORD) throw new Error('USERNAME and PASSWORD not set'); + + const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`; + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: CLIENT_ID, + username: USERNAME, + password: PASSWORD, + scope: 'openid profile email', + }); + + console.log(` Trying client_credentials with username/password...`); + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`${response.status} - ${error}`); } const data = await response.json(); @@ -37,41 +64,57 @@ async function main() { console.log('==========================\n'); console.log(`Authentik URL: ${AUTHENTIK_URL}`); console.log(`Client ID: ${CLIENT_ID}`); - console.log(`Username: ${USERNAME}`); console.log(''); - try { - console.log('Attempting password/client_credentials grant...'); - const token = await getTokenViaPasswordGrant(); - console.log('\n✓ Token obtained successfully!\n'); - console.log('=== ACCESS TOKEN ==='); - console.log(token); - console.log('\n=== EXPORT COMMAND ==='); - console.log(`export TEST_TOKEN="${token}"`); - return; - } catch (error) { - console.log(`✗ ${error instanceof Error ? error.message : error}\n`); + let token: string | null = null; + + if (CLIENT_SECRET) { + try { + token = await getTokenWithClientSecret(); + console.log(' ✓ Success!\n'); + } catch (error) { + console.log(` ✗ Failed: ${error instanceof Error ? error.message : error}\n`); + } } - console.log('=== MANUAL TOKEN SETUP ===\n'); - console.log('Option 1: Create a Service Account in Authentik'); - console.log(' 1. Go to: Admin > Directory > Users'); - console.log(' 2. Click "Create Service Account"'); - console.log(' 3. Give it a name (e.g., "kaboot-test")'); - console.log(' 4. Copy the username and token generated'); - console.log(' 5. Run: TEST_USERNAME= TEST_PASSWORD= npm run test:get-token\n'); + if (!token && USERNAME && PASSWORD) { + try { + token = await getTokenWithServiceAccount(); + console.log(' ✓ Success!\n'); + } catch (error) { + console.log(` ✗ Failed: ${error instanceof Error ? error.message : error}\n`); + } + } - console.log('Option 2: Get token from browser'); - console.log(' 1. Log into Kaboot frontend with Authentik'); - console.log(' 2. Open browser DevTools > Application > Local Storage'); - console.log(' 3. Find the oidc.user entry'); - console.log(' 4. Copy the access_token value'); - console.log(' 5. Run: export TEST_TOKEN=""\n'); + if (token) { + console.log('=== ACCESS TOKEN ==='); + console.log(token); + console.log('\n=== FOR .env.test ==='); + console.log(`TEST_TOKEN=${token}`); + return; + } - console.log('Option 3: Use Authentik API directly'); - console.log(' 1. Go to: Admin > Directory > Tokens & App passwords'); - console.log(' 2. Create a new token for your user'); - console.log(' 3. Use that token for API testing\n'); + console.log('=== SETUP INSTRUCTIONS ===\n'); + console.log('Method 1: Client Secret (Recommended for testing)\n'); + console.log(' 1. Go to Authentik Admin: http://localhost:9000/if/admin/'); + console.log(' 2. Navigate to: Applications → Providers → Kaboot OAuth2'); + console.log(' 3. Change "Client type" from "Public" to "Confidential"'); + console.log(' 4. Copy the "Client Secret" value'); + console.log(' 5. Add to server/.env.test:'); + console.log(' CLIENT_SECRET=\n'); + + console.log('Method 2: Service Account + App Password\n'); + console.log(' 1. Go to Authentik Admin: http://localhost:9000/if/admin/'); + console.log(' 2. Navigate to: Directory → Users'); + console.log(' 3. Click "Create Service Account"'); + console.log(' 4. Name it (e.g., "kaboot-test")'); + console.log(' 5. After creation, click on the user → "App passwords" tab'); + console.log(' 6. Create a new app password, copy the token'); + console.log(' 7. Bind the service account to Kaboot app:'); + console.log(' Applications → Kaboot → Policy/Group/User Bindings → Bind existing user'); + console.log(' 8. Add to server/.env.test:'); + console.log(' TEST_USERNAME='); + console.log(' TEST_PASSWORD=\n'); process.exit(1); } diff --git a/server/tests/run-tests.ts b/server/tests/run-tests.ts new file mode 100644 index 0000000..5e33360 --- /dev/null +++ b/server/tests/run-tests.ts @@ -0,0 +1,83 @@ +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const AUTHENTIK_URL = process.env.AUTHENTIK_URL || 'http://localhost:9000'; +const CLIENT_ID = process.env.CLIENT_ID || 'kaboot-spa'; +const USERNAME = process.env.TEST_USERNAME || ''; +const PASSWORD = process.env.TEST_PASSWORD || ''; + +async function getToken(): Promise { + if (!USERNAME || !PASSWORD) { + throw new Error( + 'TEST_USERNAME and TEST_PASSWORD must be set in .env.test\n' + + 'See tests/README.md for setup instructions.' + ); + } + + const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`; + const params = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: CLIENT_ID, + username: USERNAME, + password: PASSWORD, + scope: 'openid profile email', + }); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get token: ${response.status} - ${error}`); + } + + const data = await response.json(); + return data.access_token; +} + +async function runTests(token: string): Promise { + return new Promise((resolve) => { + const __dirname = dirname(fileURLToPath(import.meta.url)); + const testFile = join(__dirname, 'api.test.ts'); + + const child = spawn(process.execPath, [ + '--import', 'tsx', + testFile + ], { + env: { + ...process.env, + TEST_TOKEN: token, + }, + stdio: 'inherit', + }); + + child.on('close', (code) => { + resolve(code ?? 1); + }); + }); +} + +async function main() { + console.log('Kaboot API Test Runner'); + console.log('======================\n'); + + console.log('Obtaining access token from Authentik...'); + let token: string; + try { + token = await getToken(); + console.log(' Token obtained successfully.\n'); + } catch (error) { + console.error(` Failed: ${error instanceof Error ? error.message : error}`); + process.exit(1); + } + + console.log('Running API tests...\n'); + const exitCode = await runTests(token); + process.exit(exitCode); +} + +main();