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
119 changes: 119 additions & 0 deletions backend/src/integration/chaos.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Chaos/failure-injection tests for RPC and DB outages (#877).
* Tests graceful degradation and recovery under infrastructure failures.
*
* Run with: npm run test:integration
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import app from '../index.js';

describe('Chaos Injection: RPC and DB Resilience', () => {
let server;

beforeEach(async () => {
server = app.listen(3000);
await new Promise((resolve) => setTimeout(resolve, 500));
});

afterEach(async () => {
await new Promise((resolve) => server.close(resolve));
});

describe('RPC Failure Scenarios', () => {
it('should handle RPC timeout gracefully', async () => {
// Test: when Stellar RPC times out, API returns 503 with retry hint
const res = await fetch('http://localhost:3000/health');
expect([200, 503]).toContain(res.status);
const body = await res.json();
expect(body).toHaveProperty('status');
});

it('should return cached data when RPC is down', async () => {
// Test: API calls that require RPC should serve cached data if available
const res = await fetch('http://localhost:3000/api/campaigns');
expect([200, 503]).toContain(res.status);
if (res.status === 200) {
const body = await res.json();
expect(body).toHaveProperty('campaigns');
}
});

it('should report RPC pool saturation', async () => {
// Test: health check should report RPC pool status
const res = await fetch('http://localhost:3000/health');
const body = await res.json();
expect(body).toHaveProperty('rpc');
expect(body.rpc).toHaveProperty('poolHealth');
});
});

describe('Database Failure Scenarios', () => {
it('should handle DB connection errors', async () => {
// Test: when DB is down, read-heavy endpoints return 503
const res = await fetch('http://localhost:3000/api/campaigns');
expect([200, 503]).toContain(res.status);
});

it('should reject writes when DB is unavailable', async () => {
// Test: POST/PUT endpoints should fail explicitly when DB down
const res = await fetch('http://localhost:3000/api/campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' }),
});
expect([201, 503, 500]).toContain(res.status);
});

it('should eventually recover when DB comes back', async () => {
// Test: after DB recovery, subsequent requests succeed
const res = await fetch('http://localhost:3000/health');
expect([200, 503]).toContain(res.status);
});
});

describe('Graceful Degradation', () => {
it('should serve health checks even under stress', async () => {
// Test: /health endpoint responds even when other services fail
const res = await fetch('http://localhost:3000/health');
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toHaveProperty('status');
});

it('should timeout gracefully on slow responses', async () => {
// Test: requests timeout with proper error message
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

const res = await fetch('http://localhost:3000/health', {
signal: controller.signal,
}).catch(() => null);

clearTimeout(timeout);
expect(res === null || res.ok).toBe(true);
});
});

describe('Recovery Scenarios', () => {
it('should recover from transient RPC errors', async () => {
// Test: after a transient error, retry succeeds
for (let i = 0; i < 3; i++) {
const res = await fetch('http://localhost:3000/health');
expect([200, 503]).toContain(res.status);
await new Promise((resolve) => setTimeout(resolve, 100));
}
});

it('should recover from DB reconnection', async () => {
// Test: after DB reconnect, operations resume
const res = await fetch('http://localhost:3000/health');
expect([200, 503]).toContain(res.status);

if (res.status === 200) {
const body = await res.json();
expect(body.status).toBeDefined();
}
});
});
});
52 changes: 52 additions & 0 deletions chaos/test-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# chaos/test-runner.sh — Run integration tests under various failure scenarios (#877)
#
# Usage: bash chaos/test-runner.sh [scenario]
# Without args, runs all scenarios.

set -euo pipefail

BACKEND_CONTAINER="${BACKEND_CONTAINER:-trivela-backend-1}"
CHAOS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INJECT="${CHAOS_DIR}/inject.sh"
TEST_CMD="npm run test:integration"

run_test_scenario() {
local scenario=$1
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Testing under: $scenario"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# Inject failure
bash "$INJECT" "$scenario" || true

# Wait for failure to propagate
sleep 2

# Run tests (don't fail on test errors yet)
if eval "$TEST_CMD"; then
echo "✓ Tests passed under $scenario"
else
echo "✗ Tests failed under $scenario (may indicate missing resilience)"
fi

# Restore
bash "$INJECT" restore

# Wait for recovery
sleep 2

echo ""
}

if [[ $# -eq 0 ]]; then
# Run all scenarios
run_test_scenario "network-partition-backend"
run_test_scenario "db-latency"
run_test_scenario "stellar-rpc-timeout"
else
# Run specified scenario
run_test_scenario "$1"
fi

echo "✓ Chaos testing complete"
99 changes: 99 additions & 0 deletions frontend/RESPONSIVE_DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Mobile-Responsive Design Guidelines (#867)

Trivela is designed mobile-first with progressive enhancement for larger screens.

## Breakpoints

- **Mobile**: 0 – 768px
- **Tablet**: 769px – 1024px
- **Desktop**: 1025px+

## Mobile-First CSS

Write styles for mobile first, then enhance for larger screens:

```css
/* Mobile (default) */
.card {
display: block;
padding: 12px;
}

/* Tablet and up */
@media (min-width: 769px) {
.card {
padding: 16px;
}
}

/* Desktop and up */
@media (min-width: 1025px) {
.card {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 24px;
}
}
```

## Touch-Friendly UI

On mobile, ensure interactive elements meet WCAG guidelines:

- **Minimum touch target**: 48×48px
- **Spacing**: At least 12px between interactive elements
- **Font size**: ≥ 16px (prevents zoom on iOS)

```css
button {
min-height: 48px;
min-width: 48px;
}
```

## Responsive Text

Use `clamp()` for fluid typography:

```css
h1 {
font-size: clamp(1.5rem, 4vw, 2rem);
}
```

## Safe Areas

For notched devices (iPhone X+), use CSS safe-area:

```css
body {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
```

## Landscape Orientation

Handle landscape on mobile:

```css
@media (max-height: 600px) and (orientation: landscape) {
h1, h2, h3 {
margin: 0.25rem 0;
}
}
```

## Testing

Test with:

- Chrome DevTools device emulation
- Real iOS/Android devices
- Different orientations (portrait, landscape)
- Various screen sizes (320px–1440px)

## Service Worker / Offline

The PWA service worker caches essential assets. See [PwaStatus.jsx](frontend/src/components/PwaStatus.jsx) for offline UI.
9 changes: 8 additions & 1 deletion frontend/src/ClaimRewards.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import TransactionStatus from './components/TransactionStatus';
import { useOptimisticAction } from './hooks/useOptimisticAction';
import { analytics } from './lib/analytics';
import { getContractErrorMessage, extractContractErrorCode } from './lib/contractErrors';

/**
* ClaimRewards — lets the user enter a points amount and either claim
Expand Down Expand Up @@ -176,7 +177,13 @@ export default function ClaimRewards({ walletAddress, hasPayoutAsset = false, on

{isError && error && (
<p id={feedbackId} className="claim-error" role="alert">
{error.message}
{(() => {
const contractErrorCode = extractContractErrorCode(error);
if (contractErrorCode !== null) {
return getContractErrorMessage(contractErrorCode);
}
return error.message || 'An error occurred. Please try again.';
})()}
{error.recovery ? ` ${error.recovery}.` : ''}
</p>
)}
Expand Down
Loading
Loading