Skip to content
Open
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
47 changes: 47 additions & 0 deletions .github/workflows/backend-testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: ArenaX Backend Testing Suite

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
test-and-audit:
runs-on: ubuntu-latest

services:
docker:
image: docker:dind

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install Dependencies
run: cd backend && npm ci

- name: Unit & Integration Tests
run: cd backend && npm test -- --coverage

- name: Security & Vulnerability Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high

- name: Performance Benchmarking
run: cd backend && npm run test:bench

- name: Upload Coverage
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: backend/coverage/
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "arenax-game-state"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "20.0.0"

[dev-dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }
8 changes: 4 additions & 4 deletions ISSUES_BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ Create comprehensive API documentation and client SDKs.
### Mobile & PWA Features

#### Issue #24: Progressive Web App (PWA)
**Labels:** 🎨 frontend, 📱 mobile, 🐛 low
**Labels:** frontend, mobile, low

**Description:**
Enhance the frontend as a Progressive Web App.
Expand All @@ -579,7 +579,7 @@ Enhance the frontend as a Progressive Web App.
**Estimated Effort:** 2-3 days

#### Issue #25: Mobile Responsiveness
**Labels:** 🎨 frontend, 📱 mobile, 🐛 low
**Labels:** frontend, 📱 mobile, 🐛 low

**Description:**
Ensure full mobile responsiveness across all features.
Expand All @@ -599,8 +599,8 @@ Ensure full mobile responsiveness across all features.

### DevOps & Infrastructure

#### Issue #26: Comprehensive Testing Suite
**Labels:** 🧪 testing, 🚀 devops, 📋 medium
#### Issue #26: Comprehensive Testing Suite [DONE] ✅
**Labels:** testing, devops, medium

**Description:**
Implement comprehensive testing infrastructure.
Expand Down
42 changes: 42 additions & 0 deletions backend/DataFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { faker } from '@faker-js/faker';

/**
* Automated Test Data Generation Utility
*/
export const DataFactory = {
createUser: () => ({
id: faker.string.uuid(),
username: faker.internet.userName(),
email: faker.internet.email(),
stellarPublicKey: 'GC' + faker.string.alphanumeric(54).toUpperCase(),
}),

createTournament: (status: 'upcoming' | 'ongoing' | 'completed' = 'upcoming') => ({
id: faker.string.uuid(),
title: faker.company.catchPhrase() + ' Tournament',
entryFee: faker.number.int({ min: 10, max: 100 }),
status,
}),
};

/**
* Mock External Service Dependencies (Stellar SDK)
*/
export const StellarMock = {
server: {
loadAccount: jest.fn().mockResolvedValue({
sequenceNumber: () => '12345',
balances: [{ asset_type: 'native', balance: '100.00' }],
}),
submitTransaction: jest.fn().mockResolvedValue({
hash: 'mock-tx-hash-' + faker.string.alphanumeric(10),
successful: true,
}),
},
Keypair: {
random: () => ({
publicKey: () => 'G' + faker.string.alphanumeric(55),
secret: () => 'S' + faker.string.alphanumeric(55),
}),
},
};
41 changes: 41 additions & 0 deletions backend/TestEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis';

/**
* Managed Test Environment using TestContainers
* Ensures clean database isolation for integration tests
*/
export class TestEnvironment {
private pgContainer?: StartedPostgreSqlContainer;
private redisContainer?: StartedRedisContainer;

async start() {
// Start PostgreSQL
this.pgContainer = await new PostgreSqlContainer('postgres:15-alpine')
.withDatabase('arenax_test')
.withUsername('test_user')
.withPassword('test_pass')
.start();

// Start Redis
this.redisContainer = await new RedisContainer('redis:7-alpine').start();

process.env.DATABASE_URL = this.pgContainer.getConnectionUri();
process.env.REDIS_URL = `redis://${this.redisContainer.getHost()}:${this.redisContainer.getMappedPort(6379)}`;
}

async stop() {
await this.pgContainer?.stop();
await this.redisContainer?.stop();
}

getPgConnection() {
return this.pgContainer?.getConnectionUri();
}

getRedisUrl() {
return `redis://${this.redisContainer?.getHost()}:${this.redisContainer?.getMappedPort(6379)}`;
}
}

export const testEnv = new TestEnvironment();
39 changes: 39 additions & 0 deletions backend/auth.api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import request from 'supertest';
import { app } from '@/app'; // Assuming Actix-web/Express app is exported
import { testEnv } from './TestEnvironment';

describe('Authentication API Integration', () => {
beforeAll(async () => {
await testEnv.start();
});

afterAll(async () => {
await testEnv.stop();
});

describe('POST /api/v1/auth/register', () => {
it('should register a new user successfully', async () => {
const response = await request(app)
.post('/api/v1/auth/register')
.send({
username: 'tester',
email: 'test@arenax.gg',
password: 'SecurePassword123!'
});

expect(response.status).toBe(201);
expect(response.body).toHaveProperty('token');
});

it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/v1/auth/register')
.send({
username: 'tester',
email: 'invalid-email',
password: 'pass'
});
expect(response.status).toBe(400);
});
});
});
49 changes: 49 additions & 0 deletions backend/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test';

test.describe('ArenaX E2E Tests', () => {
test('user registration and login flow', async ({ page }) => {
// Navigate to registration page
await page.goto('http://localhost:3000/register');

// Fill registration form
await page.fill('[data-testid="username"]', 'testuser');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'SecurePass123!');
await page.click('[data-testid="register-button"]');

// Should redirect to dashboard or show success
await expect(page).toHaveURL(/dashboard|home/);

// Logout
await page.click('[data-testid="logout-button"]');

// Login
await page.goto('http://localhost:3000/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'SecurePass123!');
await page.click('[data-testid="login-button"]');

// Should be logged in
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});

test('tournament creation and joining', async ({ page }) => {
// Assume logged in
await page.goto('http://localhost:3000/tournaments');

// Create tournament
await page.click('[data-testid="create-tournament"]');
await page.fill('[data-testid="tournament-name"]', 'Test Tournament');
await page.selectOption('[data-testid="game-type"]', 'arena-battle');
await page.click('[data-testid="submit-tournament"]');

// Should see tournament in list
await expect(page.locator('text=Test Tournament')).toBeVisible();

// Join tournament
await page.click('[data-testid="join-tournament"]');

// Should show joined status
await expect(page.locator('[data-testid="joined-status"]')).toBeVisible();
});
});
27 changes: 27 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>', '<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/?(*.)+(spec|test).ts'],
testPathIgnorePatterns: ['e2e.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/types/**/*.ts'
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
43 changes: 43 additions & 0 deletions backend/load-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
stages: [
{ duration: '2m', target: 100 }, // below normal load
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, // normal load
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, // around the breaking point
{ duration: '5m', target: 300 },
{ duration: '2m', target: 400 }, // beyond the breaking point
{ duration: '5m', target: 400 },
{ duration: '10m', target: 0 }, // scale down. Recovery stage.
],
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';

export default function () {
const responses = http.batch([
['GET', `${BASE_URL}/api/v1/health`],
['GET', `${BASE_URL}/api/v1/tournaments`],
['POST', `${BASE_URL}/api/v1/auth/login`, JSON.stringify({
email: 'test@example.com',
password: 'password'
}), { headers: { 'Content-Type': 'application/json' } }],
]);

check(responses[0], {
'health check status is 200': (r) => r.status === 200,
});

check(responses[1], {
'tournaments status is 200': (r) => r.status === 200,
});

check(responses[2], {
'login status is 200 or 401': (r) => r.status === 200 || r.status === 401,
});

sleep(1);
}
48 changes: 48 additions & 0 deletions backend/load-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
config:
target: 'http://localhost:8080'
phases:
- duration: 60
arrivalRate: 5
name: Warm up
- duration: 120
arrivalRate: 5
rampTo: 50
name: Ramp up load
- duration: 60
arrivalRate: 50
name: Sustained load
defaults:
headers:
Content-Type: 'application/json'

scenarios:
- name: 'Health check'
weight: 30
requests:
- get:
url: '/api/v1/health'

- name: 'User registration'
weight: 20
requests:
- post:
url: '/api/v1/auth/register'
json:
username: 'loadtestuser{{ $randomInt }}'
email: 'loadtest{{ $randomInt }}@example.com'
password: 'SecurePass123!'

- name: 'Get tournaments'
weight: 25
requests:
- get:
url: '/api/v1/tournaments'

- name: 'User login'
weight: 25
requests:
- post:
url: '/api/v1/auth/login'
json:
email: 'test@example.com'
password: 'password'
Loading
Loading