Flesh out payment stuff

This commit is contained in:
Joey Yakimowich-Payne 2026-01-22 12:21:12 -07:00
commit acfed861ab
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
27 changed files with 938 additions and 173 deletions

View file

@ -2159,6 +2159,126 @@ console.log('\n=== Game Session Tests ===');
}
});
await test('GET /api/payments/status returns expected generation limit for access type', async () => {
const { data } = await request('GET', '/api/payments/status');
const status = data as Record<string, unknown>;
const accessType = status.accessType as string;
const limit = status.generationLimit as number | null;
if (accessType === 'group') {
if (limit !== null) throw new Error('Expected null generationLimit for group access');
return;
}
if (accessType === 'subscription') {
if (limit !== 250) throw new Error(`Expected generationLimit 250, got ${limit}`);
return;
}
if (accessType === 'none') {
if (limit !== 5) throw new Error(`Expected free tier generationLimit 5, got ${limit}`);
return;
}
throw new Error(`Unexpected accessType: ${accessType}`);
});
await test('POST /api/generate with two documents blocks free tier users', async () => {
const statusRes = await request('GET', '/api/payments/status');
const status = statusRes.data as Record<string, unknown>;
const accessType = status.accessType as string;
const res = await fetch(`${API_URL}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({
topic: 'Document content',
documents: [
{ type: 'text', content: 'Doc A' },
{ type: 'text', content: 'Doc B' }
],
questionCount: 2
}),
});
if (accessType === 'none') {
if (res.status !== 403) throw new Error(`Expected 403 for free tier, got ${res.status}`);
const data = await res.json();
if (!data.error || !String(data.error).toLowerCase().includes('document')) {
throw new Error('Expected document limit error message');
}
return;
}
const VALID_STATUSES = [200, 503];
if (!VALID_STATUSES.includes(res.status)) {
throw new Error(`Expected one of ${VALID_STATUSES.join('/')}, got ${res.status}`);
}
});
await test('POST /api/upload with OCR blocks free tier users', async () => {
const statusRes = await request('GET', '/api/payments/status');
const status = statusRes.data as Record<string, unknown>;
const accessType = status.accessType as string;
const formData = new FormData();
const blob = new Blob(['test content'], { type: 'text/plain' });
formData.append('document', blob, 'test.txt');
formData.append('useOcr', 'true');
const res = await fetch(`${API_URL}/api/upload`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
},
body: formData,
});
if (accessType === 'none') {
if (res.status !== 403) throw new Error(`Expected 403 for free tier OCR, got ${res.status}`);
const data = await res.json();
if (!data.error || !String(data.error).toLowerCase().includes('ocr')) {
throw new Error('Expected OCR access error message');
}
return;
}
const VALID_STATUSES = [200, 400];
if (!VALID_STATUSES.includes(res.status)) {
throw new Error(`Expected one of ${VALID_STATUSES.join('/')}, got ${res.status}`);
}
});
console.log('\nPayments Refund Tests:');
await test('POST /api/payments/refund without token returns 401', async () => {
const res = await fetch(`${API_URL}/api/payments/refund`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentIntentId: 'pi_test' }),
});
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
});
await test('POST /api/payments/refund with non-admin returns 403 or 503', async () => {
const res = await fetch(`${API_URL}/api/payments/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({ paymentIntentId: 'pi_test' }),
});
const VALID_STATUSES = [403, 503];
if (!VALID_STATUSES.includes(res.status)) {
throw new Error(`Expected one of ${VALID_STATUSES.join('/')}, got ${res.status}`);
}
});
console.log('\n=== Quiz Sharing Tests ===');
let shareTestQuizId: string | null = null;

View file

@ -36,14 +36,14 @@ async function getTokenWithServiceAccount(): Promise<string> {
const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`;
const params = new URLSearchParams({
grant_type: 'client_credentials',
grant_type: 'password',
client_id: CLIENT_ID,
username: USERNAME,
password: PASSWORD,
scope: 'openid profile email',
});
console.log(` Trying client_credentials with username/password...`);
console.log(` Trying password grant with username/password...`);
const response = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },

View file

@ -4,10 +4,34 @@ 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 CLIENT_SECRET = process.env.CLIENT_SECRET || '';
const USERNAME = process.env.TEST_USERNAME || '';
const PASSWORD = process.env.TEST_PASSWORD || '';
const TEST_TOKEN = process.env.TEST_TOKEN || '';
async function getToken(): Promise<string> {
const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`;
if (CLIENT_SECRET) {
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
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 data = await response.json();
return data.access_token;
}
}
if (!USERNAME || !PASSWORD) {
throw new Error(
'TEST_USERNAME and TEST_PASSWORD must be set in .env.test\n' +
@ -15,9 +39,8 @@ async function getToken(): Promise<string> {
);
}
const tokenUrl = `${AUTHENTIK_URL}/application/o/token/`;
const params = new URLSearchParams({
grant_type: 'client_credentials',
grant_type: 'password',
client_id: CLIENT_ID,
username: USERNAME,
password: PASSWORD,
@ -65,14 +88,19 @@ 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);
if (TEST_TOKEN) {
console.log('Using TEST_TOKEN from environment.\n');
token = TEST_TOKEN;
} else {
console.log('Obtaining access token from Authentik...');
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');