Skip to content
Merged
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
73 changes: 73 additions & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Visual Regression Tests

on:
pull_request:
paths:
- 'frontend/src/**'
- 'frontend/.storybook/**'
- 'frontend/tests/visual/**'
- 'frontend/package*.json'
push:
branches:
- main
paths:
- 'frontend/src/**'
- 'frontend/.storybook/**'
- 'frontend/tests/visual/**'
workflow_dispatch:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
visual-tests:
name: Visual Regression
runs-on: ubuntu-latest
timeout-minutes: 15

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install root dependencies
run: npm install

- name: Install frontend dependencies
working-directory: frontend
run: npm install

- name: Install Playwright browsers
working-directory: frontend
run: npx playwright install --with-deps chromium

- name: Run visual regression tests
working-directory: frontend
run: npm run test:visual
continue-on-error: true

- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-regression-report
path: frontend/playwright-report-visual/
retention-days: 30

- name: Upload diff images
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-diffs
path: |
frontend/tests/visual/__snapshots__/**/*-diff.png
frontend/tests/visual/__snapshots__/**/*-actual.png
retention-days: 30
if-no-files-found: ignore
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ Thumbs.db

# Turborepo
.turbo

# Playwright
**/playwright-report-visual/
**/test-results/
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,64 @@ npm run test:backend
npm run build:frontend
# 2. Run the tests
npm run test:frontend

# Frontend visual regression tests (Playwright + Storybook)
cd frontend
npm run test:visual # Run visual tests
npm run test:visual:update # Update baseline snapshots
npm run test:visual:report # View detailed HTML report
```

The frontend E2E tests use **Playwright**. They run against a local preview server
(`npm run preview`). Ensure the backend is running if you want the tests to hit real API endpoints,
otherwise they will show the "empty state" as expected in a isolated environment.

### Load Testing

Load tests validate backend performance under synthetic traffic using [k6](https://k6.io/):

```bash
# Install k6: brew install k6 (macOS) or see https://k6.io/docs/get-started/installation/

# Run from repo root (starts backend automatically via npm script)
npm run load-test # default: read-campaigns scenario
LOAD_SCENARIO=burst-registration npm run load-test # user registration spike
LOAD_SCENARIO=claim-storm API_KEY=sk_dev npm run load-test # reward claim surge
```

Available scenarios (`load-tests/scenarios/`):

- **read-campaigns** (100 VUs, 30s) - Read-heavy GET requests
- **write-campaigns** (10 VUs, 30s) - POST campaign creation
- **mixed-read-write** (80R+20W VUs, 60s) - Combined traffic
- **burst-registration** (0→200 VUs, 70s) - Registration spike
- **claim-storm** (0→150 VUs, 75s) - Concurrent reward claims

See [`load-tests/README.md`](load-tests/README.md) for thresholds, CI integration, and custom
scenarios.

### Visual Regression Testing

Visual regression tests capture screenshots of Storybook components and detect unintended UI
changes:

```bash
cd frontend

# Run tests (starts Storybook automatically)
npm run test:visual

# Update snapshots after intentional design changes
npm run test:visual:update

# View detailed test report with diffs
npm run test:visual:report
```

Tests run automatically in CI on PRs that touch frontend code. See
[`frontend/tests/visual/README.md`](frontend/tests/visual/README.md) for adding new tests and
troubleshooting.

---

## Tech Stack
Expand Down
16 changes: 5 additions & 11 deletions backend/scripts/build-redoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@ import fs from 'fs';

const root = process.cwd();

const output = path.resolve(
root,
'../frontend/public/api-docs.html'
);
const output = path.resolve(root, '../frontend/public/api-docs.html');

fs.mkdirSync(path.dirname(output), {
recursive: true,
});

execSync(
`npx redoc-cli bundle openapi.yaml -o "${output}"`,
{
stdio: 'inherit',
}
);
execSync(`npx redoc-cli bundle openapi.yaml -o "${output}"`, {
stdio: 'inherit',
});

console.log('✓ Static Redoc generated');
console.log('✓ Static Redoc generated');
25 changes: 8 additions & 17 deletions backend/scripts/security-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,7 @@ const DANGEROUS_PATTERNS = [
];

// Patterns that indicate proper security practices
const SECURE_PATTERNS = [
/sanitize|escape/i,
/DOMPurify/,
/validator\./,
/zod.*parse/,
/safeParse/,
];
const SECURE_PATTERNS = [/sanitize|escape/i, /DOMPurify/, /validator\./, /zod.*parse/, /safeParse/];

/**
* Recursively read all JS/TS files from a directory
Expand All @@ -76,21 +70,18 @@ function getAllJsFiles(dir) {

// Skip node_modules, dist, build, etc.
if (
entry.name.startsWith('.')
|| entry.name === 'node_modules'
|| entry.name === 'dist'
|| entry.name === 'build'
|| entry.name === 'coverage'
entry.name.startsWith('.') ||
entry.name === 'node_modules' ||
entry.name === 'dist' ||
entry.name === 'build' ||
entry.name === 'coverage'
) {
continue;
}

if (entry.isDirectory()) {
walk(fullPath);
} else if (
extname(entry.name) === '.js'
|| extname(entry.name) === '.ts'
) {
} else if (extname(entry.name) === '.js' || extname(entry.name) === '.ts') {
files.push(fullPath);
}
}
Expand Down Expand Up @@ -185,7 +176,7 @@ function printResults() {
console.log(' ✓ URL parameter sanitization');
console.log(' ✓ Log injection prevention');
console.log(' ✓ No dangerouslySetInnerHTML usage');
console.log(' ✓ Error messages don\'t reflect user input');
console.log(" ✓ Error messages don't reflect user input");
console.log('='.repeat(60) + '\n');

return ISSUES.length > 0 ? 1 : 0;
Expand Down
6 changes: 2 additions & 4 deletions backend/tests/integration/auditLogRepository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ describe('sqliteAuditLogRepository — integration tests (real SQLite)', () => {

it('filters by actor (via entity filter)', () => {
const all = auditLogs.list();
const admin1Entries = all.filter(
(entry) => entry.actor === 'admin-1',
);
const admin1Entries = all.filter((entry) => entry.actor === 'admin-1');
assert.equal(admin1Entries.length, 4);
});

Expand Down Expand Up @@ -132,4 +130,4 @@ describe('sqliteAuditLogRepository — integration tests (real SQLite)', () => {
}
});
});
});
});
39 changes: 34 additions & 5 deletions backend/tests/integration/campaignRepository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,39 @@ describe('sqliteCampaignRepository — integration tests (real SQLite)', () => {

before(() => {
allCreated = seedCampaigns(campaigns, [
{ name: 'Active DeFi', slug: 'active-defi', active: true, tags: ['defi'], category: 'DeFi', rewardPerAction: 10 },
{ name: 'Inactive NFT', slug: 'inactive-nft', active: false, tags: ['nft'], category: 'NFT', rewardPerAction: 20 },
{ name: 'Featured Community', slug: 'featured-community', active: true, featured: true, tags: ['community'], category: 'Community', rewardPerAction: 30 },
{ name: 'Active Airdrop', slug: 'active-airdrop', active: true, tags: ['airdrop', 'defi'], category: 'Airdrop', rewardPerAction: 40 },
{
name: 'Active DeFi',
slug: 'active-defi',
active: true,
tags: ['defi'],
category: 'DeFi',
rewardPerAction: 10,
},
{
name: 'Inactive NFT',
slug: 'inactive-nft',
active: false,
tags: ['nft'],
category: 'NFT',
rewardPerAction: 20,
},
{
name: 'Featured Community',
slug: 'featured-community',
active: true,
featured: true,
tags: ['community'],
category: 'Community',
rewardPerAction: 30,
},
{
name: 'Active Airdrop',
slug: 'active-airdrop',
active: true,
tags: ['airdrop', 'defi'],
category: 'Airdrop',
rewardPerAction: 40,
},
]);
});

Expand Down Expand Up @@ -326,4 +355,4 @@ describe('sqliteCampaignRepository — integration tests (real SQLite)', () => {
}
});
});
});
});
2 changes: 1 addition & 1 deletion backend/tests/integration/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ export function seedAuditLogs(auditLogs, data = []) {
timestamp: item.timestamp ?? new Date().toISOString(),
}),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[],
[]
],
"auth": [[], [], []],
"ledger": {
"protocol_version": 25,
"sequence_number": 0,
Expand Down Expand Up @@ -116,4 +112,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[],
[]
],
"auth": [[], [], []],
"ledger": {
"protocol_version": 25,
"sequence_number": 0,
Expand Down Expand Up @@ -116,4 +112,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,4 @@
]
},
"events": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,4 @@
]
},
"events": []
}
}
Loading
Loading