Phase 2 + 3 complete
This commit is contained in:
parent
9a3fc97a34
commit
6d24f3c112
25 changed files with 3275 additions and 98 deletions
68
server/tests/README.md
Normal file
68
server/tests/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Kaboot Backend API Tests
|
||||
|
||||
## Getting a Test Token
|
||||
|
||||
Since Authentik uses OAuth2 flows that require browser interaction, you need to obtain a token manually.
|
||||
|
||||
### Method 1: Browser DevTools (Easiest)
|
||||
|
||||
1. Start the Kaboot frontend: `npm run dev` (in root directory)
|
||||
2. Open `http://localhost:5173`
|
||||
3. Click "Sign In" and log in with Authentik
|
||||
4. Open browser DevTools (F12)
|
||||
5. Go to **Application** > **Local Storage** > `http://localhost:5173`
|
||||
6. Find the key starting with `oidc.user:`
|
||||
7. Click on it and find `"access_token"` in the JSON value
|
||||
8. Copy the token value (without quotes)
|
||||
|
||||
### Method 2: Service Account
|
||||
|
||||
1. Go to Authentik Admin: `http://localhost:9000/if/admin/`
|
||||
2. Navigate to **Directory** > **Users**
|
||||
3. Click **Create Service Account**
|
||||
4. Enter a name (e.g., `kaboot-test-service`)
|
||||
5. Note the generated username and token
|
||||
6. Use these credentials:
|
||||
```bash
|
||||
TEST_USERNAME=<service-account-username> \
|
||||
TEST_PASSWORD=<generated-token> \
|
||||
npm run test:get-token
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
|
||||
# Set the token you obtained
|
||||
export TEST_TOKEN="your-access-token-here"
|
||||
|
||||
# Run tests
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
- **Health Check**: Basic server availability
|
||||
- **Authentication**: 401 without token, 401 with invalid token
|
||||
- **User API**: GET /api/users/me
|
||||
- **Quiz CRUD**:
|
||||
- GET /api/quizzes (list)
|
||||
- POST /api/quizzes (create)
|
||||
- GET /api/quizzes/:id (read)
|
||||
- PUT /api/quizzes/:id (update)
|
||||
- DELETE /api/quizzes/:id (delete)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `API_URL` | `http://localhost:3001` | Backend API URL |
|
||||
| `TEST_TOKEN` | (required) | JWT access token from Authentik |
|
||||
| `AUTHENTIK_URL` | `http://localhost:9000` | Authentik server URL |
|
||||
| `CLIENT_ID` | `kaboot-spa` | OAuth2 client ID |
|
||||
| `TEST_USERNAME` | `kaboottest` | Username for token request |
|
||||
| `TEST_PASSWORD` | `kaboottest` | Password for token request |
|
||||
191
server/tests/api.test.ts
Normal file
191
server/tests/api.test.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
||||
const TOKEN = process.env.TEST_TOKEN;
|
||||
|
||||
if (!TOKEN) {
|
||||
console.error('ERROR: TEST_TOKEN environment variable is required');
|
||||
console.log('Run: npm run test:get-token');
|
||||
console.log('Then: export TEST_TOKEN="<token>"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const results: TestResult[] = [];
|
||||
|
||||
async function request(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
expectStatus = 200
|
||||
): Promise<{ status: number; data: unknown }> {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TOKEN}`,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const data = response.headers.get('content-type')?.includes('application/json')
|
||||
? await response.json()
|
||||
: null;
|
||||
|
||||
if (response.status !== expectStatus) {
|
||||
throw new Error(`Expected ${expectStatus}, got ${response.status}: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
async function test(name: string, fn: () => Promise<void>) {
|
||||
try {
|
||||
await fn();
|
||||
results.push({ name, passed: true });
|
||||
console.log(` ✓ ${name}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
results.push({ name, passed: false, error: message });
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n=== Kaboot API Tests ===\n');
|
||||
console.log(`API: ${API_URL}`);
|
||||
console.log('');
|
||||
|
||||
let createdQuizId: string | null = null;
|
||||
|
||||
console.log('Health Check:');
|
||||
await test('GET /health returns ok', async () => {
|
||||
const res = await fetch(`${API_URL}/health`);
|
||||
const data = await res.json();
|
||||
if (data.status !== 'ok') throw new Error('Health check failed');
|
||||
});
|
||||
|
||||
console.log('\nAuth Tests:');
|
||||
await test('GET /api/quizzes without token returns 401', async () => {
|
||||
const res = await fetch(`${API_URL}/api/quizzes`);
|
||||
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
|
||||
});
|
||||
|
||||
await test('GET /api/quizzes with invalid token returns 401', async () => {
|
||||
const res = await fetch(`${API_URL}/api/quizzes`, {
|
||||
headers: { Authorization: 'Bearer invalid-token' },
|
||||
});
|
||||
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
|
||||
});
|
||||
|
||||
console.log('\nUser Tests:');
|
||||
await test('GET /api/users/me returns user info', async () => {
|
||||
const { data } = await request('GET', '/api/users/me');
|
||||
const user = data as Record<string, unknown>;
|
||||
if (!user.id) throw new Error('Missing user id');
|
||||
if (!user.username) throw new Error('Missing username');
|
||||
});
|
||||
|
||||
console.log('\nQuiz CRUD Tests:');
|
||||
await test('GET /api/quizzes returns array', async () => {
|
||||
const { data } = await request('GET', '/api/quizzes');
|
||||
if (!Array.isArray(data)) throw new Error('Expected array');
|
||||
});
|
||||
|
||||
await test('POST /api/quizzes creates quiz', async () => {
|
||||
const quiz = {
|
||||
title: 'Test Quiz',
|
||||
source: 'manual',
|
||||
questions: [
|
||||
{
|
||||
text: 'What is 2 + 2?',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: '3', isCorrect: false, shape: 'triangle', color: 'red' },
|
||||
{ text: '4', isCorrect: true, shape: 'diamond', color: 'blue' },
|
||||
{ text: '5', isCorrect: false, shape: 'circle', color: 'yellow' },
|
||||
{ text: '6', isCorrect: false, shape: 'square', color: 'green' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
||||
const result = data as { id: string };
|
||||
if (!result.id) throw new Error('Missing quiz id');
|
||||
createdQuizId = result.id;
|
||||
});
|
||||
|
||||
await test('GET /api/quizzes/:id returns full quiz', async () => {
|
||||
if (!createdQuizId) throw new Error('No quiz created');
|
||||
const { data } = await request('GET', `/api/quizzes/${createdQuizId}`);
|
||||
const quiz = data as Record<string, unknown>;
|
||||
if (quiz.title !== 'Test Quiz') throw new Error('Wrong title');
|
||||
if (!Array.isArray(quiz.questions)) throw new Error('Missing questions');
|
||||
const questions = quiz.questions as Record<string, unknown>[];
|
||||
if (questions.length !== 1) throw new Error('Wrong question count');
|
||||
const q = questions[0];
|
||||
if (!Array.isArray(q.options)) throw new Error('Missing options');
|
||||
if ((q.options as unknown[]).length !== 4) throw new Error('Wrong option count');
|
||||
});
|
||||
|
||||
await test('PUT /api/quizzes/:id updates quiz', async () => {
|
||||
if (!createdQuizId) throw new Error('No quiz created');
|
||||
const updatedQuiz = {
|
||||
title: 'Updated Test Quiz',
|
||||
questions: [
|
||||
{
|
||||
text: 'Updated question?',
|
||||
timeLimit: 30,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
{ text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' },
|
||||
{ text: 'D', isCorrect: false, shape: 'square', color: 'green' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await request('PUT', `/api/quizzes/${createdQuizId}`, updatedQuiz);
|
||||
|
||||
const { data } = await request('GET', `/api/quizzes/${createdQuizId}`);
|
||||
const quiz = data as Record<string, unknown>;
|
||||
if (quiz.title !== 'Updated Test Quiz') throw new Error('Title not updated');
|
||||
});
|
||||
|
||||
await test('DELETE /api/quizzes/:id deletes quiz', async () => {
|
||||
if (!createdQuizId) throw new Error('No quiz created');
|
||||
await request('DELETE', `/api/quizzes/${createdQuizId}`, undefined, 204);
|
||||
});
|
||||
|
||||
await test('GET /api/quizzes/:id returns 404 for deleted quiz', async () => {
|
||||
if (!createdQuizId) throw new Error('No quiz created');
|
||||
await request('GET', `/api/quizzes/${createdQuizId}`, undefined, 404);
|
||||
});
|
||||
|
||||
console.log('\n=== Results ===');
|
||||
const passed = results.filter((r) => r.passed).length;
|
||||
const failed = results.filter((r) => !r.passed).length;
|
||||
console.log(`Passed: ${passed}/${results.length}`);
|
||||
console.log(`Failed: ${failed}/${results.length}`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\nFailed tests:');
|
||||
results
|
||||
.filter((r) => !r.passed)
|
||||
.forEach((r) => console.log(` - ${r.name}: ${r.error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nAll tests passed!');
|
||||
}
|
||||
|
||||
runTests().catch((err) => {
|
||||
console.error('Test runner error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
79
server/tests/get-token.ts
Normal file
79
server/tests/get-token.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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';
|
||||
|
||||
async function getTokenViaPasswordGrant(): Promise<string> {
|
||||
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(`Token URL: ${tokenUrl}`);
|
||||
|
||||
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(`Password grant failed: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Kaboot API Token Generator');
|
||||
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`);
|
||||
}
|
||||
|
||||
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=<username> TEST_PASSWORD=<token> npm run test:get-token\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="<token>"\n');
|
||||
|
||||
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');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue