managarten/services/mana-core-auth/test/e2e/b2c-journey.e2e-spec.ts
Wuesteon 9c47119535 Fix wrong type
import, make auth and chat work
2025-12-04 23:25:25 +01:00

503 lines
13 KiB
TypeScript

/**
* B2C User Journey E2E Tests
*
* Complete end-to-end test for B2C user lifecycle:
* 1. Register account
* 2. Login and get tokens
* 3. Use credits for various apps
* 4. Check balance and history
* 5. Refresh token
* 6. Logout
*/
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('B2C User Journey (E2E)', () => {
let app: INestApplication;
let accessToken: string;
let refreshToken: string;
let userId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('Complete B2C Journey', () => {
const uniqueEmail = `b2c-e2e-${Date.now()}@example.com`;
const password = 'SecurePassword123!';
it('Step 1: Register new B2C user', async () => {
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
email: uniqueEmail,
password,
name: 'B2C E2E User',
})
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
email: uniqueEmail,
name: 'B2C E2E User',
});
userId = response.body.id;
});
it('Step 2: Login and receive JWT tokens', async () => {
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password,
})
.expect(200);
expect(response.body).toMatchObject({
user: {
id: userId,
email: uniqueEmail,
},
accessToken: expect.any(String),
refreshToken: expect.any(String),
tokenType: 'Bearer',
expiresIn: 900,
});
accessToken = response.body.accessToken;
refreshToken = response.body.refreshToken;
});
it('Step 3: Get initial credit balance', async () => {
const response = await request(app.getHttpServer())
.get('/credits/balance')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toMatchObject({
balance: 0,
freeCreditsRemaining: 150, // Signup bonus
dailyFreeCredits: 5,
totalSpent: 0,
});
});
it('Step 4: Use credits for audio transcription (Memoro)', async () => {
const response = await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 25,
appId: 'memoro',
description: 'Audio transcription',
metadata: {
fileId: 'audio-123',
duration: 120,
},
})
.expect(200);
expect(response.body).toMatchObject({
success: true,
newBalance: {
balance: 0,
freeCreditsRemaining: 125, // 150 - 25
totalSpent: 25,
},
});
});
it('Step 5: Use credits for image generation (Picture)', async () => {
const response = await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 30,
appId: 'picture',
description: 'AI image generation',
metadata: {
prompt: 'Beautiful sunset',
model: 'dall-e-3',
},
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.newBalance.freeCreditsRemaining).toBe(95); // 125 - 30
});
it('Step 6: Use credits for chat conversation', async () => {
const response = await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 15,
appId: 'chat',
description: 'AI chat conversation',
})
.expect(200);
expect(response.body.newBalance.freeCreditsRemaining).toBe(80); // 95 - 15
expect(response.body.newBalance.totalSpent).toBe(70); // 25 + 30 + 15
});
it('Step 7: Check updated balance', async () => {
const response = await request(app.getHttpServer())
.get('/credits/balance')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toMatchObject({
balance: 0,
freeCreditsRemaining: 80,
totalSpent: 70,
});
});
it('Step 8: Get transaction history', async () => {
const response = await request(app.getHttpServer())
.get('/credits/transactions')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThanOrEqual(4); // signup + 3 usage
// Verify transactions are in descending order
const transactions = response.body;
expect(transactions[0].appId).toBe('chat'); // Most recent
});
it('Step 9: Refresh access token', async () => {
const response = await request(app.getHttpServer())
.post('/auth/refresh')
.send({
refreshToken,
})
.expect(200);
expect(response.body).toMatchObject({
user: {
id: userId,
},
accessToken: expect.any(String),
refreshToken: expect.any(String),
});
// Update tokens
const newAccessToken = response.body.accessToken;
const newRefreshToken = response.body.refreshToken;
expect(newAccessToken).not.toBe(accessToken);
expect(newRefreshToken).not.toBe(refreshToken);
accessToken = newAccessToken;
refreshToken = newRefreshToken;
});
it('Step 10: Verify new access token works', async () => {
const response = await request(app.getHttpServer())
.get('/credits/balance')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body.freeCreditsRemaining).toBe(80);
});
it('Step 11: Attempt to use more credits than available', async () => {
await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 200, // More than available
appId: 'wisekeep',
description: 'Video analysis',
})
.expect(400);
});
it('Step 12: Test idempotency with duplicate request', async () => {
const idempotencyKey = `idempotent-${Date.now()}`;
// First request
const response1 = await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 5,
appId: 'test',
description: 'Idempotency test',
idempotencyKey,
})
.expect(200);
const balanceAfterFirst = response1.body.newBalance.freeCreditsRemaining;
// Second request with same idempotency key
const response2 = await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${accessToken}`)
.send({
amount: 5,
appId: 'test',
description: 'Idempotency test',
idempotencyKey,
})
.expect(200);
expect(response2.body.message).toBe('Transaction already processed');
// Verify balance unchanged
const balanceCheck = await request(app.getHttpServer())
.get('/credits/balance')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(balanceCheck.body.freeCreditsRemaining).toBe(balanceAfterFirst);
});
it('Step 13: Get credit packages', async () => {
const response = await request(app.getHttpServer())
.get('/credits/packages')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
if (response.body.length > 0) {
expect(response.body[0]).toMatchObject({
id: expect.any(String),
name: expect.any(String),
credits: expect.any(Number),
priceEuroCents: expect.any(Number),
});
}
});
it('Step 14: Logout and revoke session', async () => {
const response = await request(app.getHttpServer())
.post('/auth/logout')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toMatchObject({
message: 'Logged out successfully',
});
});
it('Step 15: Verify access token no longer works after logout', async () => {
await request(app.getHttpServer())
.get('/credits/balance')
.set('Authorization', `Bearer ${accessToken}`)
.expect(401);
});
it('Step 16: Verify refresh token no longer works after logout', async () => {
await request(app.getHttpServer())
.post('/auth/refresh')
.send({
refreshToken,
})
.expect(401);
});
});
describe('Edge Cases and Error Handling', () => {
it('should reject registration with invalid email', async () => {
await request(app.getHttpServer())
.post('/auth/register')
.send({
email: 'invalid-email',
password: 'SecurePassword123!',
name: 'Test User',
})
.expect(400);
});
it('should reject registration with weak password', async () => {
await request(app.getHttpServer())
.post('/auth/register')
.send({
email: `test-weak-${Date.now()}@example.com`,
password: '123', // Too weak
name: 'Test User',
})
.expect(400);
});
it('should reject credit usage without authentication', async () => {
await request(app.getHttpServer())
.post('/credits/use')
.send({
amount: 10,
appId: 'test',
description: 'Unauthorized attempt',
})
.expect(401);
});
it('should reject credit usage with invalid token', async () => {
await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', 'Bearer invalid-token-12345')
.send({
amount: 10,
appId: 'test',
description: 'Invalid token attempt',
})
.expect(401);
});
it('should reject negative credit amounts', async () => {
// First, register and login
const uniqueEmail = `negative-test-${Date.now()}@example.com`;
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Negative Test',
})
.expect(201);
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password: 'SecurePassword123!',
})
.expect(200);
const token = loginResponse.body.accessToken;
// Attempt to use negative credits
await request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${token}`)
.send({
amount: -10, // Negative amount
appId: 'test',
description: 'Negative credits',
})
.expect(400);
});
it('should handle concurrent requests safely', async () => {
const uniqueEmail = `concurrent-e2e-${Date.now()}@example.com`;
// Register and login
await request(app.getHttpServer())
.post('/auth/register')
.send({
email: uniqueEmail,
password: 'SecurePassword123!',
name: 'Concurrent User',
})
.expect(201);
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: uniqueEmail,
password: 'SecurePassword123!',
})
.expect(200);
const token = loginResponse.body.accessToken;
// Send multiple concurrent requests
const requests = [];
for (let i = 0; i < 5; i++) {
requests.push(
request(app.getHttpServer())
.post('/credits/use')
.set('Authorization', `Bearer ${token}`)
.send({
amount: 5,
appId: 'test',
description: `Concurrent request ${i}`,
})
);
}
const responses = await Promise.all(requests);
// All should succeed
responses.forEach((response) => {
expect([200, 409]).toContain(response.status); // 200 success or 409 conflict
});
});
});
describe('Security Tests', () => {
it('should not expose sensitive data in error messages', async () => {
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'SomePassword123!',
})
.expect(401);
// Error should not reveal whether user exists
expect(response.body.message).toBe('Invalid credentials');
expect(response.body).not.toHaveProperty('userId');
});
it('should enforce rate limiting on login attempts', async () => {
// Note: This test assumes rate limiting is configured
// Make multiple failed login attempts
const promises = [];
for (let i = 0; i < 20; i++) {
promises.push(
request(app.getHttpServer())
.post('/auth/login')
.send({
email: `brute-force-${Date.now()}@example.com`,
password: 'wrong-password',
})
);
}
const responses = await Promise.all(promises);
// Eventually should get rate limited (429)
const rateLimited = responses.some((r) => r.status === 429);
// If rate limiting is implemented, this should be true
// If not implemented yet, this test will fail (which is good feedback)
if (rateLimited) {
expect(rateLimited).toBe(true);
}
});
it('should reject SQL injection attempts in email field', async () => {
const sqlInjectionPayloads = ["admin'--", "' OR '1'='1", "'; DROP TABLE users; --"];
for (const payload of sqlInjectionPayloads) {
const response = await request(app.getHttpServer()).post('/auth/login').send({
email: payload,
password: 'SomePassword123!',
});
// Should fail safely without SQL injection
expect([400, 401]).toContain(response.status);
}
});
});
});