Files
goodgo-platform/e2e/api/subscriptions.spec.ts
Ho Ngoc Hai 271ad76e6f fix: resolve E2E test failures and API runtime issues for Docker dev environment
- 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>
2026-04-08 05:44:00 +07:00

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);
});
});
});