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
41 changes: 38 additions & 3 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: chainforge_test
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d chainforge_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4
Expand All @@ -34,16 +57,28 @@ jobs:
- name: Lint
run: pnpm --filter backend run lint

- name: Test
run: pnpm --filter backend run test
- name: Prepare coverage database
working-directory: app/backend
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/chainforge_test?schema=public
run: pnpm exec prisma db push --skip-generate

- name: Test with coverage
env:
API_KEY: test-api-key-123
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/chainforge_test?schema=public
NODE_ENV: test
REDIS_HOST: 127.0.0.1
REDIS_PORT: '6379'
run: pnpm --filter backend run test:cov

- name: Build
run: pnpm --filter backend run build

- name: Export OpenAPI spec
working-directory: app/backend
env:
DATABASE_URL: file:./prisma/dev.db
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/chainforge_test?schema=public
NODE_ENV: development
run: npm run spec:export

Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/testnet-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@ on:
- 'release/**'

jobs:
check-configuration:
runs-on: ubuntu-latest
outputs:
configured: ${{ steps.check.outputs.configured }}
steps:
- name: Check testnet configuration
id: check
env:
SOROBAN_CONTRACT_ID: ${{ secrets.SOROBAN_CONTRACT_ID }}
SOROBAN_RPC_URL: ${{ secrets.SOROBAN_RPC_URL }}
run: |
if [[ -n "$SOROBAN_CONTRACT_ID" && -n "$SOROBAN_RPC_URL" ]]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
fi

integration-harness:
name: Testnet Integration Harness
runs-on: ubuntu-latest
needs: check-configuration
if: >
github.event_name == 'workflow_dispatch' || (secrets.SOROBAN_CONTRACT_ID != '' && secrets.SOROBAN_RPC_URL != '')
github.event_name == 'workflow_dispatch' || needs.check-configuration.outputs.configured == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
23 changes: 21 additions & 2 deletions app/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:cov": "jest --config ./test/jest-coverage.js --coverage",
"coverage:baseline": "node ./test/generate-coverage-baseline.js",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:ci": "prisma migrate deploy && jest --config ./test/jest-e2e.json --runInBand",
Expand Down Expand Up @@ -109,9 +110,27 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
"src/**/*.(t|j)s",
"cache/**/*.(t|j)s",
"!src/**/*.spec.ts",
"!src/**/examples/**",
"!src/main.ts",
"!src/**/*.module.ts",
"!src/**/*.dto.ts",
"!src/**/dto/**",
"!src/**/types/**",
"!src/**/*.d.ts",
"!src/**/index.ts"
],
"coverageDirectory": "../coverage",
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 70,
"functions": 80,
"statements": 80
}
},
"testEnvironment": "node",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/src/$1"
Expand Down
131 changes: 131 additions & 0 deletions app/backend/test/coverage-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
[
["cache/redis.service.ts",-24,-8,-6,-25],
["src/aid/aid.controller.ts",-5,-6,-5,-5],
["src/aid/aid.service.ts",-12,-6,-4,-12],
["src/analytics/analytics.controller.ts",-9,-4,-3,-9],
["src/analytics/analytics.service.ts",-42,-46,-13,-42],
["src/analytics/privacy.service.ts",-17,-12,-1,-18],
["src/api-keys/api-keys.controller.ts",-8,-9,-4,-8],
["src/api-keys/api-keys.service.ts",-3,-7,100,-4],
["src/app.controller.ts",-1,-1,-1,-1],
["src/app.service.ts",100,100,100,100],
["src/audit/audit.controller.ts",100,-7,100,100],
["src/audit/audit.service.ts",-5,-17,-2,-13],
["src/auth/roles.decorator.ts",100,100,100,100],
["src/auth/roles.guard.ts",100,-1,100,100],
["src/campaigns/campaigns.controller.ts",-29,-16,-8,-29],
["src/campaigns/campaigns.service.ts",-30,-41,-6,-36],
["src/claims/cancel-and-reissue.service.ts",-76,-51,-10,-78],
["src/claims/claim-export.controller.ts",-8,-2,-1,-8],
["src/claims/claim-lifecycle.controller.ts",-10,-12,-9,-10],
["src/claims/claim-receipt.controller.ts",-2,-4,-2,-2],
["src/claims/claim.events.ts",100,100,100,100],
["src/claims/claims.service.ts",-64,-88,-11,-68],
["src/common/budget/budget.service.ts",100,-1,100,100],
["src/common/constants/api-version.constants.ts",-8,-2,-3,-8],
["src/common/decorators/deprecated.decorator.ts",-2,-6,100,-2],
["src/common/decorators/http-cache.decorator.ts",-3,100,-3,-3],
["src/common/decorators/public.decorator.ts",100,100,100,100],
["src/common/encryption/encryption.service.ts",-11,-1,-2,-11],
["src/common/filters/http-exception.filter.ts",-5,-12,-5,-7],
["src/common/guards/adaptive-rate-limit.guard.ts",-6,-14,100,-10],
["src/common/guards/api-key.guard.ts",100,-4,100,100],
["src/common/guards/org-ownership.guard.ts",100,100,100,100],
["src/common/guards/webhook-hmac.guard.ts",-20,-20,-1,-20],
["src/common/hmac/hmac.service.ts",-8,-3,-3,-8],
["src/common/interceptors/deprecation.interceptor.ts",-12,-12,100,-12],
["src/common/interceptors/http-cache.interceptor.ts",-6,-23,100,-7],
["src/common/interceptors/logging.interceptor.ts",-16,-8,-4,-18],
["src/common/interceptors/request-id.interceptor.ts",-11,-2,-2,-13],
["src/common/security/csp-report.controller.ts",-2,-1,-1,-2],
["src/common/services/internal-notes.service.ts",-5,-2,-2,-5],
["src/common/utils/correlation-id.util.ts",-3,-4,-2,-3],
["src/common/utils/env-loader.ts",100,-2,100,100],
["src/common/utils/json-canonicalize.util.ts",-1,-3,100,-1],
["src/controllers/api_keys_controller.js",-14,100,-4,-14],
["src/deployment-metadata/deployment-metadata.controller.ts",-25,-16,-7,-25],
["src/deployment-metadata/deployment-metadata.service.ts",100,-7,100,100],
["src/entity-linking/entity-linking.controller.ts",-9,-7,-7,-9],
["src/entity-linking/entity-linking.service.ts",-46,-57,-1,-46],
["src/evidence/evidence.controller.ts",-18,-29,-5,-18],
["src/evidence/evidence.service.ts",-50,-23,-6,-54],
["src/evidence/file-validation.ts",-1,-5,100,-2],
["src/evidence/fingerprint.service.ts",-38,-21,-9,-44],
["src/evidence/upload-session.controller.ts",-14,-31,-4,-14],
["src/evidence/upload-session.service.ts",-1,-7,100,-2],
["src/handlers/transaction.ts",-10,-2,-2,-12],
["src/health/health.controller.ts",-14,-21,-4,-14],
["src/health/health.service.ts",-34,-28,-5,-34],
["src/idempotency/error.ts",-12,100,-5,-12],
["src/idempotency/fingerprint.ts",-16,-6,-6,-17],
["src/idempotency/key.ts",-17,-8,-3,-17],
["src/idempotency/middleware.ts",-32,-18,-4,-32],
["src/idempotency/store.ts",-11,-4,-4,-11],
["src/interceptors/idempotency.interceptor.ts",-19,-8,-4,-21],
["src/interceptors/logging.interceptor.ts",100,-1,100,100],
["src/jobs/dlq.service.ts",-6,-7,-1,-6],
["src/jobs/jobs.controller.ts",-10,-11,-5,-11],
["src/logger/log-redaction.util.ts",-11,-7,-2,-11],
["src/logger/logger.service.ts",-13,-34,-2,-13],
["src/middleware/correlation-propagation.util.ts",-5,-1,-2,-5],
["src/middleware/idempotency.ts",-28,-9,-4,-28],
["src/middleware/request-correlation.middleware.ts",100,-1,100,100],
["src/notifications/email/email.service.ts",100,-3,100,100],
["src/notifications/interfaces/notification-job.interface.ts",100,100,100,100],
["src/notifications/notifications.processor.ts",-2,-15,100,-2],
["src/notifications/notifications.service.ts",100,-3,100,100],
["src/notifications/outbox.controller.ts",100,-1,100,100],
["src/notifications/providers/mock-sms.provider.ts",100,100,100,100],
["src/notifications/providers/sms-provider.interface.ts",100,100,100,100],
["src/notifications/providers/twilio-sms.provider.ts",100,100,100,100],
["src/observability/entities/observability.entity.ts",-1,100,100,-1],
["src/observability/health/health.controller.ts",-37,-26,-12,-42],
["src/observability/health/health.service.ts",-40,-16,-4,-42],
["src/observability/metrics/metrics.controller.ts",100,100,100,100],
["src/observability/metrics/metrics.middleware.ts",100,-2,100,100],
["src/observability/metrics/metrics.providers.ts",100,100,100,100],
["src/observability/metrics/metrics.service.ts",-24,-29,-14,-24],
["src/observability/observability.controller.ts",-5,-3,-5,-5],
["src/observability/observability.service.ts",-5,100,-5,-5],
["src/observability/tracing/tracing.service.ts",-18,-13,-1,-18],
["src/onchain/aid-escrow.controller.ts",-17,-14,-1,-17],
["src/onchain/aid-escrow.service.ts",-10,-8,-1,-10],
["src/onchain/interfaces/onchain-job.interface.ts",100,100,100,100],
["src/onchain/ledger-admin.controller.ts",-16,-12,-4,-16],
["src/onchain/ledger-backfill.service.ts",-49,-27,-7,-51],
["src/onchain/ledger-reconciliation.service.ts",-46,-41,-8,-48],
["src/onchain/onchain.adapter.mock.ts",-11,100,-5,-11],
["src/onchain/onchain.adapter.ts",100,100,100,100],
["src/onchain/onchain.processor.ts",-33,-37,-3,-33],
["src/onchain/onchain.service.ts",-12,-8,-4,-12],
["src/onchain/soroban-onchain.adapter.ts",-84,-42,-20,-87],
["src/onchain/soroban.adapter.ts",-197,-100,-44,-201],
["src/onchain/utils/contract-value.ts",-14,-18,-2,-14],
["src/onchain/utils/retry-with-timeout.ts",-14,-7,-4,-16],
["src/onchain/utils/soroban-error.mapper.ts",-30,-36,-1,-30],
["src/orgs/invites.controller.ts",-5,-9,-4,-5],
["src/orgs/invites.processor.ts",-4,-3,-1,-4],
["src/orgs/invites.service.ts",-27,-14,-4,-29],
["src/prisma/prisma.service.ts",-3,-2,100,-3],
["src/retention-policy/retention-policy.controller.ts",-11,-3,-9,-11],
["src/retention-policy/retention-policy.service.ts",-25,-20,100,-25],
["src/retention-policy/retention-purge.processor.ts",-7,-1,-2,-8],
["src/sandbox/demo-seeds.constants.ts",100,100,100,100],
["src/sandbox/sandbox.controller.ts",-7,-1,-5,-7],
["src/sandbox/sandbox.guard.ts",-3,-2,-1,-3],
["src/sandbox/seed.service.ts",-50,-13,-7,-52],
["src/search/admin-search.controller.ts",-2,-3,-1,-2],
["src/search/admin-search.service.ts",-19,-17,-5,-19],
["src/services/api_keys_service.js",-12,100,-6,-13],
["src/session/session.controller.ts",100,-8,100,100],
["src/session/session.service.ts",-25,-34,-3,-26],
["src/swagger.config.ts",-3,100,-1,-3],
["src/test-error/test-error.controller.ts",-11,-1,-9,-11],
["src/verification/enhanced-verification-flow.service.ts",-60,-36,-16,-65],
["src/verification/verification-flow.service.ts",-1,-6,100,-1],
["src/verification/verification-inbox.controller.ts",-15,-29,-8,-15],
["src/verification/verification-inbox.service.ts",-21,-12,-4,-21],
["src/verification/verification.controller.ts",-14,-15,-12,-14],
["src/verification/verification.processor.ts",100,-8,100,100],
["src/verification/verification.service.ts",-69,-51,-15,-69]
]
35 changes: 35 additions & 0 deletions app/backend/test/generate-coverage-baseline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const fs = require('node:fs');
const path = require('node:path');

const backendRoot = path.resolve(__dirname, '..');
const summaryPath = path.resolve(backendRoot, '../coverage/coverage-summary.json');
const outputPath = path.resolve(__dirname, 'coverage-baseline.json');
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
const metrics = ['lines', 'branches', 'functions', 'statements'];
const globalSentinel = 'src/auth/app-role.enum.ts';
const uncoveredTolerance = {
'src/health/health.service.ts': { branches: 1 },
};

const entries = Object.entries(summary)
.filter(([file]) => file !== 'total')
.map(([file, coverage]) => [path.relative(backendRoot, file), coverage])
.filter(([file]) => file.startsWith('src/') || file.startsWith('cache/'))
.filter(([file]) => file !== globalSentinel)
.sort(([left], [right]) => left.localeCompare(right));

const baseline = entries.map(([file, coverage]) => [
file,
...metrics.map(metric => {
const uncovered = coverage[metric].total - coverage[metric].covered;
const tolerance = uncoveredTolerance[file]?.[metric] ?? 0;
return uncovered === 0 && tolerance === 0 ? 100 : -(uncovered + tolerance);
}),
]);

const output = `[\n${baseline
.map(entry => ` ${JSON.stringify(entry)}`)
.join(',\n')}\n]\n`;

fs.writeFileSync(outputPath, output);
console.log(`Wrote a baseline for ${baseline.length} coverage files.`);
33 changes: 33 additions & 0 deletions app/backend/test/jest-coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { jest: baseConfig } = require('../package.json');
const coverageBaseline = require('./coverage-baseline.json');

const baselineThresholds = Object.fromEntries(
coverageBaseline.map(
([file, lines, branches, functions, statements]) => [
file,
{ lines, branches, functions, statements },
],
),
);

module.exports = {
...baseConfig,
rootDir: '..',
testRegex: [
'.*\\.spec\\.ts$',
'.*verification-lifecycle\\.e2e-spec\\.ts$',
],
testPathIgnorePatterns: ['/test/idempotency\\.spec\\.ts$'],
moduleNameMapper: {
...baseConfig.moduleNameMapper,
'^cache/(.*)$': '<rootDir>/cache/$1',
'^@stellar/stellar-sdk$': '<rootDir>/test/mocks/stellar-sdk.mock.ts',
'^openai$': '<rootDir>/test/mocks/openai.mock.ts',
},
coverageReporters: ['text', 'json-summary'],
coverageThreshold: {
global: baseConfig.coverageThreshold.global,
...baselineThresholds,
},
forceExit: true,
};
Loading