diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml
index e29288d..5021eac 100644
--- a/.github/workflows/accessibility.yml
+++ b/.github/workflows/accessibility.yml
@@ -226,56 +226,39 @@ jobs:
echo "SERVER_PID=$!" >> $GITHUB_ENV
sleep 5
- - name: Create Axe accessibility test
- working-directory: ./frontend
- run: |
- mkdir -p tests/accessibility
- cat > tests/accessibility/axe.spec.ts << 'EOF'
- import { test, expect } from '@playwright/test';
- import AxeBuilder from '@axe-core/playwright';
-
- test('should not have accessibility violations', async ({ page }) => {
- await page.goto('http://localhost:3001');
-
- const accessibilityScanResults = await new AxeBuilder({ page })
- .withTags(['wcag2a', 'wcag2aa'])
- .analyze();
-
- // Log violations for debugging but don't fail the test
- if (accessibilityScanResults.violations.length > 0) {
- console.log('Accessibility violations found:', accessibilityScanResults.violations.length);
- accessibilityScanResults.violations.forEach((violation, index) => {
- console.log(`Violation ${index + 1}: ${violation.id} - ${violation.description}`);
- });
- }
-
- // We expect no violations, but test continues even if there are some
- expect(accessibilityScanResults.violations.length).toBeLessThanOrEqual(10);
- });
- EOF
-
- name: Run Axe accessibility tests
working-directory: ./frontend
run: |
- cat > playwright.config.ts << 'EOF'
+ # Create a specific config for axe tests
+ cat > playwright.axe.config.ts << 'EOF'
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/accessibility',
+ testMatch: '**/axe.spec.ts',
fullyParallel: false,
forbidOnly: !!process.env.CI,
- retries: 1,
+ retries: 2,
workers: 1,
- reporter: [['html'], ['json', { outputFile: 'test-results.json' }]],
+ reporter: [
+ ['html', { outputFolder: 'playwright-report-axe' }],
+ ['json', { outputFile: 'axe-results.json' }],
+ ['list']
+ ],
use: {
baseURL: 'http://localhost:3001',
trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ },
+ timeout: 60000,
+ expect: {
+ timeout: 10000,
},
- timeout: 30000,
});
EOF
- npx playwright test || echo "Axe tests completed with violations"
+ echo "Running Axe accessibility tests..."
+ npx playwright test --config=playwright.axe.config.ts || echo "Axe tests completed with violations"
continue-on-error: true
- name: Upload Axe results
@@ -284,8 +267,10 @@ jobs:
with:
name: axe-results-${{ github.run_number }}
path: |
- frontend/test-results.json
- frontend/playwright-report/
+ frontend/axe-results.json
+ frontend/playwright-report-axe/
+ frontend/playwright.axe.config.ts
+ frontend/test-results/
retention-days: 7
- name: Stop frontend server
@@ -323,7 +308,7 @@ jobs:
run: |
echo "Installing workspace dependencies..."
npm install
- npm install puppeteer
+ npm install puppeteer serve
- name: Build frontend
working-directory: ./frontend
@@ -369,7 +354,18 @@ jobs:
run: |
npx serve -s dist -l 3002 &
echo "SERVER_PID=$!" >> $GITHUB_ENV
- sleep 5
+ echo "Waiting for server to start..."
+ sleep 10
+
+ # Check if server is running
+ for i in {1..5}; do
+ if curl -f http://localhost:3002 > /dev/null 2>&1; then
+ echo "Server is ready!"
+ break
+ fi
+ echo "Waiting for server... attempt $i"
+ sleep 2
+ done
- name: Run WAVE-style tests
run: |
@@ -391,7 +387,24 @@ jobs:
try {
const page = await browser.newPage();
- await page.goto('http://localhost:3002', { waitUntil: 'networkidle2', timeout: 30000 });
+
+ // Retry logic for connection
+ let connected = false;
+ let retries = 3;
+ while (retries > 0 && !connected) {
+ try {
+ await page.goto('http://localhost:3002', { waitUntil: 'networkidle2', timeout: 30000 });
+ connected = true;
+ } catch (error) {
+ console.log(`Connection attempt failed. Retries left: ${retries - 1}`);
+ retries--;
+ if (retries > 0) {
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ } else {
+ throw error;
+ }
+ }
+ }
const pageResults = await page.evaluate(() => {
const errors = [];
@@ -519,7 +532,7 @@ jobs:
run: |
echo "Installing workspace dependencies..."
npm install
- npm install puppeteer
+ npm install puppeteer serve
- name: Build frontend
working-directory: ./frontend
@@ -565,7 +578,18 @@ jobs:
run: |
npx serve -s dist -l 3003 &
echo "SERVER_PID=$!" >> $GITHUB_ENV
- sleep 5
+ echo "Waiting for server to start..."
+ sleep 10
+
+ # Check if server is running
+ for i in {1..5}; do
+ if curl -f http://localhost:3003 > /dev/null 2>&1; then
+ echo "Server is ready!"
+ break
+ fi
+ echo "Waiting for server... attempt $i"
+ sleep 2
+ done
- name: Run color contrast tests
run: |
@@ -587,7 +611,24 @@ jobs:
try {
const page = await browser.newPage();
- await page.goto('http://localhost:3003', { waitUntil: 'networkidle2', timeout: 30000 });
+
+ // Retry logic for connection
+ let connected = false;
+ let retries = 3;
+ while (retries > 0 && !connected) {
+ try {
+ await page.goto('http://localhost:3003', { waitUntil: 'networkidle2', timeout: 30000 });
+ connected = true;
+ } catch (error) {
+ console.log(`Connection attempt failed. Retries left: ${retries - 1}`);
+ retries--;
+ if (retries > 0) {
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ } else {
+ throw error;
+ }
+ }
+ }
const contrastResults = await page.evaluate(() => {
const elements = document.querySelectorAll('*');
@@ -751,64 +792,14 @@ jobs:
echo "SERVER_PID=$!" >> $GITHUB_ENV
sleep 5
- - name: Create keyboard navigation test
- working-directory: ./frontend
- run: |
- mkdir -p tests/accessibility
- cat > tests/accessibility/keyboard.spec.ts << 'EOF'
- import { test, expect } from '@playwright/test';
-
- test('keyboard navigation should work', async ({ page }) => {
- await page.goto('http://localhost:3004');
-
- // Find all interactive elements
- const interactiveElements = await page.locator('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])').all();
-
- console.log(`Found ${interactiveElements.length} interactive elements`);
-
- // Test Tab navigation through first few elements
- const elementsToTest = Math.min(interactiveElements.length, 5);
-
- for (let i = 0; i < elementsToTest; i++) {
- await page.keyboard.press('Tab');
- await page.waitForTimeout(100);
-
- // Verify an element is focused
- const focusedElement = await page.locator(':focus').first();
- const isVisible = await focusedElement.isVisible().catch(() => false);
-
- if (isVisible) {
- console.log(`Element ${i + 1} is focusable`);
- }
- }
-
- // Test should pass even if some elements aren't perfectly focusable
- expect(elementsToTest).toBeGreaterThan(0);
- });
- EOF
-
- name: Run keyboard navigation tests
working-directory: ./frontend
run: |
- cat > playwright.config.ts << 'EOF'
- import { defineConfig } from '@playwright/test';
-
- export default defineConfig({
- testDir: './tests/accessibility',
- fullyParallel: false,
- forbidOnly: !!process.env.CI,
- retries: 1,
- workers: 1,
- reporter: [['html'], ['json', { outputFile: 'keyboard-results.json' }]],
- use: {
- baseURL: 'http://localhost:3004',
- trace: 'on-first-retry',
- },
- timeout: 30000,
- });
- EOF
-
- npx playwright test keyboard.spec.ts || echo "Keyboard tests completed"
+ echo "Running keyboard navigation tests using existing CI config..."
+ npx playwright test --config=playwright.keyboard.ci.config.ts || {
+ echo "Keyboard navigation tests completed with issues"
+ echo "Check the report for details"
+ }
continue-on-error: true
- name: Upload keyboard navigation results
@@ -818,7 +809,10 @@ jobs:
name: keyboard-results-${{ github.run_number }}
path: |
frontend/keyboard-results.json
- frontend/playwright-report/
+ frontend/playwright-report-keyboard/
+ frontend/playwright.keyboard.ci.config.ts
+ frontend/test-results/
+ frontend/keyboard-ci-results.json
retention-days: 7
- name: Stop frontend server
diff --git a/.github/workflows/api-testing.yml b/.github/workflows/api-testing.yml
new file mode 100644
index 0000000..d602e79
--- /dev/null
+++ b/.github/workflows/api-testing.yml
@@ -0,0 +1,698 @@
+name: API Testing
+
+on:
+ pull_request:
+ branches: [main, develop]
+ paths:
+ - "backend/**"
+ - ".github/workflows/api-testing.yml"
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ actions: read
+ checks: write
+
+jobs:
+ postman-newman-tests:
+ name: Postman/Newman API Tests
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:15-alpine
+ env:
+ POSTGRES_USER: admin
+ POSTGRES_PASSWORD: admin123
+ POSTGRES_DB: connectkit
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+
+ redis:
+ image: redis:7-alpine
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 6379:6379
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Install backend dependencies
+ run: |
+ cd backend
+ npm ci --include=dev
+
+ - name: Start backend
+ run: |
+ cd backend
+ npm run dev &
+ sleep 5
+ timeout 60 bash -c 'until curl -f http://localhost:3001/health; do sleep 2; done'
+ echo "β
Backend is ready"
+ env:
+ NODE_ENV: test
+ PORT: 3001
+ DB_HOST: localhost
+ DB_PORT: 5432
+ DB_USER: admin
+ DB_PASSWORD: admin123
+ DB_NAME: connectkit
+ REDIS_HOST: localhost
+ REDIS_PORT: 6379
+ JWT_SECRET: test-jwt-secret-that-is-long-enough-for-validation
+ JWT_REFRESH_SECRET: test-refresh-secret-that-is-long-enough-for-validation
+ ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+
+ - name: Install Newman
+ run: npm install -g newman newman-reporter-htmlextra newman-reporter-json-summary
+
+ - name: Create Postman collection
+ run: |
+ mkdir -p tests/api
+ cat > tests/api/connectkit-api.postman_collection.json << 'EOF'
+ {
+ "info": {
+ "name": "ConnectKit API Tests",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Health Check",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Status code is 200', () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test('Response time is less than 500ms', () => {",
+ " pm.expect(pm.response.responseTime).to.be.below(500);",
+ "});",
+ "",
+ "pm.test('Health check returns status', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response).to.have.property('status');",
+ " pm.expect(response.status).to.equal('healthy');",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "url": "{{API_URL}}/health"
+ }
+ },
+ {
+ "name": "Authentication",
+ "item": [
+ {
+ "name": "Register User",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "const randomEmail = `test${Date.now()}@example.com`;",
+ "pm.collectionVariables.set('testEmail', randomEmail);"
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('User registration successful', () => {",
+ " pm.response.to.have.status(201);",
+ "});",
+ "",
+ "pm.test('Response contains user data', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response).to.have.property('user');",
+ " pm.expect(response.user).to.have.property('email');",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "url": "{{API_URL}}/auth/register",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\\n \\"email\\": \\"{{testEmail}}\\",\\n \\"password\\": \\"TestPassword123!\\",\\n \\"name\\": \\"Test User\\"\\n}"
+ }
+ }
+ },
+ {
+ "name": "Login User",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Login successful', () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test('Response contains token', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response).to.have.property('token');",
+ " pm.collectionVariables.set('authToken', response.token);",
+ "});",
+ "",
+ "pm.test('Token is valid JWT', () => {",
+ " const response = pm.response.json();",
+ " const tokenParts = response.token.split('.');",
+ " pm.expect(tokenParts).to.have.lengthOf(3);",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "url": "{{API_URL}}/auth/login",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\\n \\"email\\": \\"{{testEmail}}\\",\\n \\"password\\": \\"TestPassword123!\\"\\n}"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "Contacts",
+ "item": [
+ {
+ "name": "Create Contact",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Contact created successfully', () => {",
+ " pm.response.to.have.status(201);",
+ "});",
+ "",
+ "pm.test('Response contains contact ID', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response).to.have.property('id');",
+ " pm.collectionVariables.set('contactId', response.id);",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "url": "{{API_URL}}/contacts",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{authToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\\n \\"firstName\\": \\"John\\",\\n \\"lastName\\": \\"Doe\\",\\n \\"email\\": \\"john.doe@example.com\\",\\n \\"phone\\": \\"+1234567890\\"\\n}"
+ }
+ }
+ },
+ {
+ "name": "Get Contacts List",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Contacts retrieved successfully', () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test('Response is paginated', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response).to.have.property('data');",
+ " pm.expect(response).to.have.property('pagination');",
+ " pm.expect(response.data).to.be.an('array');",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "url": "{{API_URL}}/contacts",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{authToken}}"
+ }
+ ]
+ }
+ },
+ {
+ "name": "Update Contact",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Contact updated successfully', () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test('Updated fields are correct', () => {",
+ " const response = pm.response.json();",
+ " pm.expect(response.firstName).to.equal('Jane');",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "PUT",
+ "url": "{{API_URL}}/contacts/{{contactId}}",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{authToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\\n \\"firstName\\": \\"Jane\\"\\n}"
+ }
+ }
+ },
+ {
+ "name": "Delete Contact",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test('Contact deleted successfully', () => {",
+ " pm.response.to.have.status(204);",
+ "});"
+ ]
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "url": "{{API_URL}}/contacts/{{contactId}}",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{authToken}}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "variable": [
+ {
+ "key": "API_URL",
+ "value": "http://localhost:3001/api",
+ "type": "string"
+ }
+ ]
+ }
+ EOF
+
+ - name: Run Newman tests
+ run: |
+ newman run tests/api/connectkit-api.postman_collection.json \
+ --environment-var API_URL=http://localhost:3001/api \
+ --reporters cli,json,htmlextra \
+ --reporter-json-export results/newman-results.json \
+ --reporter-htmlextra-export results/newman-report.html
+ continue-on-error: true
+
+ - name: Upload Newman results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: newman-results-${{ github.run_number }}
+ path: results/
+ retention-days: 30
+
+ rest-assured-tests:
+ name: REST Assured API Tests
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:15-alpine
+ env:
+ POSTGRES_USER: admin
+ POSTGRES_PASSWORD: admin123
+ POSTGRES_DB: connectkit
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+
+ redis:
+ image: redis:7-alpine
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 6379:6379
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Install backend dependencies
+ run: |
+ cd backend
+ npm ci --include=dev
+
+ - name: Start backend
+ run: |
+ cd backend
+ npm run dev &
+ sleep 5
+ timeout 60 bash -c 'until curl -f http://localhost:3001/health; do sleep 2; done'
+ echo "β
Backend is ready"
+ env:
+ NODE_ENV: test
+ PORT: 3001
+ DB_HOST: localhost
+ DB_PORT: 5432
+ DB_USER: admin
+ DB_PASSWORD: admin123
+ DB_NAME: connectkit
+ REDIS_HOST: localhost
+ REDIS_PORT: 6379
+ JWT_SECRET: test-jwt-secret-that-is-long-enough-for-validation
+ JWT_REFRESH_SECRET: test-refresh-secret-that-is-long-enough-for-validation
+ ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+
+ - name: Install test dependencies
+ run: npm install axios
+
+ - name: Create REST Assured test script
+ run: |
+ mkdir -p tests/api
+ cat > tests/api/rest-assured-test.js << 'EOF'
+ const axios = require('axios');
+ const assert = require('assert');
+
+ const API_URL = process.env.API_URL || 'http://localhost:3001/api';
+
+ async function runTests() {
+ console.log('π§ͺ Running REST Assured style API tests...\n');
+
+ try {
+ // Test 1: Health Check
+ console.log('Test 1: Health Check');
+ const healthRes = await axios.get(`${API_URL}/health`);
+ assert.strictEqual(healthRes.status, 200, 'Health check should return 200');
+ assert.strictEqual(healthRes.data.status, 'healthy', 'Status should be healthy');
+ console.log('β
Health check passed\n');
+
+ // Test 2: Authentication Flow
+ console.log('Test 2: Authentication Flow');
+ const email = `test${Date.now()}@example.com`;
+
+ // Register
+ const registerRes = await axios.post(`${API_URL}/auth/register`, {
+ email,
+ password: 'TestPassword123!',
+ name: 'Test User'
+ });
+ assert.strictEqual(registerRes.status, 201, 'Registration should return 201');
+ console.log('β
Registration passed');
+
+ // Login
+ const loginRes = await axios.post(`${API_URL}/auth/login`, {
+ email,
+ password: 'TestPassword123!'
+ });
+ assert.strictEqual(loginRes.status, 200, 'Login should return 200');
+ assert(loginRes.data.token, 'Login should return token');
+ const token = loginRes.data.token;
+ console.log('β
Login passed\n');
+
+ // Test 3: Contact CRUD
+ console.log('Test 3: Contact CRUD Operations');
+
+ // Create
+ const createRes = await axios.post(`${API_URL}/contacts`, {
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john@example.com',
+ phone: '+1234567890'
+ }, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ assert.strictEqual(createRes.status, 201, 'Create contact should return 201');
+ const contactId = createRes.data.id;
+ console.log('β
Create contact passed');
+
+ // Read
+ const getRes = await axios.get(`${API_URL}/contacts/${contactId}`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ assert.strictEqual(getRes.status, 200, 'Get contact should return 200');
+ console.log('β
Get contact passed');
+
+ // Update
+ const updateRes = await axios.put(`${API_URL}/contacts/${contactId}`, {
+ firstName: 'Jane'
+ }, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ assert.strictEqual(updateRes.status, 200, 'Update contact should return 200');
+ console.log('β
Update contact passed');
+
+ // Delete
+ const deleteRes = await axios.delete(`${API_URL}/contacts/${contactId}`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ assert.strictEqual(deleteRes.status, 204, 'Delete contact should return 204');
+ console.log('β
Delete contact passed\n');
+
+ console.log('π All REST Assured tests passed!');
+ } catch (error) {
+ console.error('β Test failed:', error.message);
+ if (error.response) {
+ console.error('Response status:', error.response.status);
+ console.error('Response data:', error.response.data);
+ }
+ process.exit(1);
+ }
+ }
+
+ runTests();
+ EOF
+
+ - name: Run REST Assured style tests
+ run: node tests/api/rest-assured-test.js
+ env:
+ API_URL: http://localhost:3001/api
+ continue-on-error: true
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: rest-assured-results-${{ github.run_number }}
+ path: tests/api/
+ retention-days: 30
+
+ api-security-tests:
+ name: API Security Testing
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:15-alpine
+ env:
+ POSTGRES_USER: admin
+ POSTGRES_PASSWORD: admin123
+ POSTGRES_DB: connectkit
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+
+ redis:
+ image: redis:7-alpine
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 6379:6379
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Install backend dependencies
+ run: |
+ cd backend
+ npm ci --include=dev
+
+ - name: Start backend
+ run: |
+ cd backend
+ npm run dev &
+ sleep 5
+ timeout 60 bash -c 'until curl -f http://localhost:3001/health; do sleep 2; done'
+ echo "β
Backend is ready"
+ env:
+ NODE_ENV: test
+ PORT: 3001
+ DB_HOST: localhost
+ DB_PORT: 5432
+ DB_USER: admin
+ DB_PASSWORD: admin123
+ DB_NAME: connectkit
+ REDIS_HOST: localhost
+ REDIS_PORT: 6379
+ JWT_SECRET: test-jwt-secret-that-is-long-enough-for-validation
+ JWT_REFRESH_SECRET: test-refresh-secret-that-is-long-enough-for-validation
+ ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+
+ - name: Run API security tests
+ run: |
+ echo "## π API Security Tests" >> $GITHUB_STEP_SUMMARY
+ API_URL="http://localhost:3001/api"
+
+ # Test 1: SQL Injection
+ echo "### SQL Injection Test" >> $GITHUB_STEP_SUMMARY
+ RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$API_URL/contacts?search=' OR '1'='1")
+ if [ "$RESPONSE" = "400" ] || [ "$RESPONSE" = "401" ]; then
+ echo "β
Protected against SQL injection" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "β οΈ Potential SQL injection vulnerability (status: $RESPONSE)" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # Test 2: XSS Prevention
+ echo "### XSS Prevention Test" >> $GITHUB_STEP_SUMMARY
+ XSS_PAYLOAD=''
+ RESPONSE=$(curl -s -X POST "$API_URL/auth/register" \
+ -H "Content-Type: application/json" \
+ -d "{\"email\":\"$XSS_PAYLOAD\",\"password\":\"test\"}")
+ if echo "$RESPONSE" | grep -q "