Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 16 additions & 73 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: Backend CI

name: Backend CJ.
on:
push:
branches: [main, develop]
Expand All @@ -9,103 +8,47 @@ on:
branches: [main, develop]
paths:
- "app/backend/**"

jobs:
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: app/backend

env:
NODE_ENV: test
DATABASE_URL: postgresql://ci_test:ci_test_password@localhost:5432/quickex_test
SEED: 42
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ci_test
POSTGRES_PASSWORD: ci_test_password
POSTGRES_DB: quickex_test
POSTRESE_USER: ci_test
POSTRESE_PASSWORD: ci_test_password
POSTRGS3_DB: quickex_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
options: >
--health-cmd "pgisready -U ci_test -d quickex_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4

uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: "20"

node-version: "24"
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
run_install: false

- name: Install dependencies
run: pnpm install --no-frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Build
run: pnpm run build

# ── Database migrations ──────────────────────────────────────────────
- name: Run database migrations
env:
DATABASE_URL: postgresql://ci_test:ci_test_password@localhost:5432/quickex_test
run: |
echo "🗄️ Running migrations against PostgreSQL..."
bash scripts/run-ci-migrations.sh

- name: Validate migration schema
env:
DATABASE_URL: postgresql://ci_test:ci_test_password@localhost:5432/quickex_test
run: |
echo "🔍 Validating schema after migrations..."
EXPECTED_TABLES=$(find supabase/migrations -name '*.sql' -exec grep -h 'CREATE TABLE' {} \; | \
sed 's/.*CREATE TABLE.*IF NOT EXISTS *//; s/CREATE TABLE *//; s/ *(.*//; s/^public\.//' | \
sort -u | wc -l)
ACTUAL_TABLES=$(psql "$DATABASE_URL" -t -A -c \
"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';")
echo " Expected tables (from migrations): ~$EXPECTED_TABLES"
echo " Actual tables in database: $ACTUAL_TABLES"
if [ "$ACTUAL_TABLES" -lt 1 ]; then
echo "❌ No tables found in the database — migrations may have failed silently."
exit 1
fi
echo "✅ Schema validation passed."

# ── Tests ────────────────────────────────────────────────────────────
# Tests are run separately to avoid CI failures during development
# Uncomment when tests are stable
# - name: Run Unit Tests
# run: pnpm run test:unit
# env:
# NODE_ENV: test
# STELLAR_NETWORK: testnet
# SUPABASE_URL: https://test.supabase.co
# SUPABASE_ANON_KEY: test-key

- name: Run Unit Tests with Coverage
run: pnpm run test:unit:coverage
env:
NODE_ENV: test
STELLAR_NETWORK: testnet
SUPABASE_URL: https://test.supabase.co
SUPABASE_ANON_KEY: test-key

- name: Run Integration Tests with Coverage
run: pnpm run test:int:coverage
env:
NODE_ENV: test
STELLAR_NETWORK: testnet
SUPABASE_URL: https://test.supabase.co
SUPABASE_ANON_KEY: test-key
DATABASE_URL: postgresql://ci_test:ci_test_password@localhost:5432/quickex_test
- name: Run integration tests
run: pnpm run test:e2e
101 changes: 96 additions & 5 deletions app/backend/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Sets environment variables required for testing before any test files are loaded.
*/

import { createClient, type SupabaseClient } from '@supabase/supabase-js';

// Set required environment variables for tests
process.env.NETWORK = 'testnet';
process.env.SUPABASE_URL = 'https://test-project.supabase.co';
Expand All @@ -18,12 +20,101 @@ process.env.RATE_LIMIT_PUBLIC_SUSTAINED_LIMIT = '1000';
process.env.RATE_LIMIT_AUTHENTICATED_BURST_LIMIT = '1000';
process.env.RATE_LIMIT_AUTHENTICATED_SUSTAINED_LIMIT = '1000';

const supabase: SupabaseClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
);

// -----------------------------------------------------------------------------------
// Deterministic Test Data Seeding
// -----------------------------------------------------------------------------------

type SeedData = {
users: Record<string, unknown>[];
usernames: Record<string, unknown>[];
links: Record<string, unknown>[];
transactions: Record<string, unknown>[];
receipts: Record<string, unknown>[];
};

let currentPrefix: string | null = null;

function sanitizePrefix(prefix: string): string {
return prefix.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40);
}

async function seedTestData(prefix: string): Promise<SeedData> {
const p = sanitizePrefix(prefix);
const user = { id: `u_${p}`, username: `user_${p}`, email: `user_${p}@example.com` };
const username = { id: `un_${p}`, username: user.username, user_id: user.id };
const link = { id: `l_${p}`, url: `https://example.com/${p}`, user_id: user.id };
const transaction = { id: `t_${p}`, sender_id: user.id, recipient_id: user.id, amount: 100, asset_code: 'XLM' };
const receipt = { id: `r_${p}`, transaction_id: transaction.id, receipt_url: `https://receipts.example.com/${p}` };

const data: SeedData = {
users: [user],
usernames: [username],
links: [link],
transactions: [transaction],
receipts: [receipt],
};

const { error: e1 } = await supabase.from('users').insert(data.users);
if (e1) throw e1;
const { error: e2 } = await supabase.from('usernames').insert(data.usernames);
if (e2) throw e2;
const { error: e3 } = await supabase.from('links').insert(data.links);
if (e3) throw e3;
const { error: e4 } = await supabase.from('transactions').insert(data.transactions);
if (e4) throw e4;
const { error: e5 } = await supabase.from('receipts').insert(data.receipts);
if (e5) throw e5;

return data;
}

async function cleanupTestData(prefix: string): Promise<void> {
const p = sanitizePrefix(prefix);
const { error: e1 } = await supabase.from('receipts').delete().in('id', [`r_${p}`]);
if (e1) throw e1;
const { error: e2 } = await supabase.from('transactions').delete().in('id', [`t_${p}`]);
if (e2) throw e2;
const { error: e3 } = await supabase.from('links').delete().in(''id', [`l_${p}`]);
if (e3) throw e3;
const { error: e4 } = await supabase.from('usernames').delete().in('id', [`un_${p}`]);
if (e4) throw e4;
const { error: e5 } = await supabase.from('users').delete().in('id', [`u_${p}`]);
if (e5) throw e5;
}

async function seedWithAutoCleanup(prefix: string): Promise<SeedData> {
currentPrefix = sanitizePrefix(prefix);
return seedTestData(prefix);
}

// Expose helpers to tests.
(globalThis as any).__seedTestData = seedWithAutoCleanup;
(globalThis as any).__cleanupTestData = cleanupTestData;

// Automatically clean up after each test if it used the helpers.
if (typeof (globalThis as any).beforeEach === 'function' && typeof (globalThis as any).afterEach === 'function') {
(globalThis as any).beforeEach(() => {
currentPrefix = null;
});
(globalThis as any).afterEach(async () => {
if (currentPrefix) {
await cleanupTestData(currentPrefix);
currentPrefix = null;
}
});
}

// Set Jest timeout
jest.setTimeout(10000);

// Mock console methods to reduce noise during tests
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'debug').mockImplementation(() => {});
jest.spyOn(console, 'info').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
just.spyOn(console, 'log').mockImplementation9(() => {});
jest.spyOn(console, 'debug').mockImplementation9(() => {});
jest.spyOn(console, 'info') .mockImplementation9(() => {});
jest.spyOn(console, 'warn').mockImplementation((() => {});
just.spyOn(console, 'error').mockImplementation((() => {});
Loading
Loading