- Fix DI issues: circular MCP module dependency, EventBus type import, SearchModule provider, CacheService metric counters placement - Fix Express 5 readonly req.query in SanitizeInputMiddleware - Fix Typesense client lazy initialization (getter instead of constructor) - Fix MinIO bucket init error handling (non-fatal on 403) - Fix missing class-validator decorators on bigint DTO fields (priceVND, amountVND) - Fix subscription plan 404 (was returning 500 for invalid tier) - Disable CSRF and raise rate limits in test environment - Update E2E tests to match actual API response shapes - Update CI workflow with Redis, Typesense, MinIO services and env vars All 101 API E2E tests now pass against Docker dev environment. Co-Authored-By: Paperclip <noreply@paperclip.ing>
205 lines
6.9 KiB
TypeScript
205 lines
6.9 KiB
TypeScript
import { test, expect, registerUser } from '../fixtures';
|
|
|
|
test.describe('Subscriptions API', () => {
|
|
let accessToken: string;
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
const { accessToken: token } = await registerUser(request);
|
|
accessToken = token;
|
|
});
|
|
|
|
test.describe('GET /subscriptions/plans — List plans', () => {
|
|
test('returns all available subscription plans', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/plans');
|
|
|
|
expect(res.status()).toBe(200);
|
|
const body = await res.json();
|
|
expect(Array.isArray(body)).toBeTruthy();
|
|
expect(body.length).toBeGreaterThan(0);
|
|
|
|
// Verify plan structure
|
|
const plan = body[0];
|
|
expect(plan).toHaveProperty('tier');
|
|
expect(plan).toHaveProperty('name');
|
|
expect(plan).toHaveProperty('priceMonthlyVND');
|
|
});
|
|
|
|
test('includes FREE tier in plans', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/plans');
|
|
const body = await res.json();
|
|
|
|
const freePlan = body.find((p: { tier: string }) => p.tier === 'FREE');
|
|
expect(freePlan).toBeDefined();
|
|
});
|
|
});
|
|
|
|
test.describe('GET /subscriptions/plans/:tier — Get specific plan', () => {
|
|
test('returns plan details for FREE tier', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/plans/FREE');
|
|
|
|
expect(res.status()).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.tier).toBe('FREE');
|
|
expect(body).toHaveProperty('name');
|
|
expect(body).toHaveProperty('maxListings');
|
|
});
|
|
|
|
test('returns plan details for AGENT_PRO tier', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/plans/AGENT_PRO');
|
|
|
|
expect(res.status()).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.tier).toBe('AGENT_PRO');
|
|
});
|
|
|
|
test('returns 404 for non-existent tier', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/plans/NONEXISTENT');
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect([404, 400]).toContain(res.status());
|
|
});
|
|
});
|
|
|
|
test.describe('POST /subscriptions — Create subscription', () => {
|
|
test('creates a FREE subscription', async ({ request }) => {
|
|
const res = await request.post('/subscriptions', {
|
|
data: { planTier: 'FREE', billingCycle: 'monthly' },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
// May succeed or conflict if user already has subscription
|
|
expect([201, 409]).toContain(res.status());
|
|
if (res.status() === 201) {
|
|
const body = await res.json();
|
|
expect(body).toHaveProperty('subscriptionId');
|
|
expect(body).toHaveProperty('planTier');
|
|
}
|
|
});
|
|
|
|
test('rejects subscription with invalid plan tier', async ({ request }) => {
|
|
const res = await request.post('/subscriptions', {
|
|
data: { planTier: 'INVALID_TIER', billingCycle: 'monthly' },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects subscription with invalid billing cycle', async ({ request }) => {
|
|
const res = await request.post('/subscriptions', {
|
|
data: { planTier: 'FREE', billingCycle: 'weekly' },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('rejects unauthenticated subscription creation', async ({ request }) => {
|
|
const res = await request.post('/subscriptions', {
|
|
data: { planTier: 'FREE', billingCycle: 'monthly' },
|
|
});
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe('GET /subscriptions/quota/:metric — Check quota', () => {
|
|
test('returns quota for listings metric', async ({ request }) => {
|
|
// Ensure user has a subscription first
|
|
await request.post('/subscriptions', {
|
|
data: { planTier: 'FREE', billingCycle: 'monthly' },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
const res = await request.get('/subscriptions/quota/listings', {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
// 200 if subscription exists, 404 if no subscription
|
|
if (res.status() === 200) {
|
|
const body = await res.json();
|
|
expect(body).toHaveProperty('limit');
|
|
expect(body).toHaveProperty('used');
|
|
expect(body).toHaveProperty('remaining');
|
|
}
|
|
});
|
|
|
|
test('rejects unauthenticated quota check', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/quota/listings');
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe('POST /subscriptions/usage — Meter usage', () => {
|
|
test('meters usage for authenticated user', async ({ request }) => {
|
|
const res = await request.post('/subscriptions/usage', {
|
|
data: { metric: 'listings', count: 1 },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
// 200/201 if subscription exists, 404 if not
|
|
expect([200, 201, 404]).toContain(res.status());
|
|
});
|
|
|
|
test('rejects usage with invalid count', async ({ request }) => {
|
|
const res = await request.post('/subscriptions/usage', {
|
|
data: { metric: 'listings', count: -1 },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
expect(res.ok()).toBeFalsy();
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
});
|
|
|
|
test.describe('GET /subscriptions/billing — Billing history', () => {
|
|
test('returns billing history for authenticated user', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/billing', {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
expect(res.status()).toBe(200);
|
|
const body = await res.json();
|
|
// Response contains subscription, payments, and total
|
|
expect(body).toHaveProperty('payments');
|
|
expect(Array.isArray(body.payments)).toBeTruthy();
|
|
});
|
|
|
|
test('supports pagination', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/billing', {
|
|
params: { limit: 5, offset: 0 },
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
|
|
test('rejects unauthenticated billing request', async ({ request }) => {
|
|
const res = await request.get('/subscriptions/billing');
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe('PUT /subscriptions/upgrade — Upgrade subscription', () => {
|
|
test('rejects unauthenticated upgrade', async ({ request }) => {
|
|
const res = await request.put('/subscriptions/upgrade', {
|
|
data: { newPlanTier: 'AGENT_PRO' },
|
|
});
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe('DELETE /subscriptions — Cancel subscription', () => {
|
|
test('rejects unauthenticated cancellation', async ({ request }) => {
|
|
const res = await request.delete('/subscriptions');
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
});
|