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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,31 @@ jobs:

- name: Test
run: dotnet test ./JobFlow.API/JobFlow.API.csproj -c Release --no-build

api-e2e-playwright:
name: API E2E (Playwright)
runs-on: ubuntu-latest
if: ${{ secrets.JOBFLOW_API_BASE_URL != '' && secrets.JOBFLOW_API_BEARER_TOKEN != '' }}

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

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: tests/e2e/package-lock.json

- name: Install E2E dependencies
working-directory: ./tests/e2e
run: npm ci

- name: Run API Playwright tests
working-directory: ./tests/e2e
env:
API_BASE_URL: ${{ secrets.JOBFLOW_API_BASE_URL }}
JOBFLOW_API_BEARER_TOKEN: ${{ secrets.JOBFLOW_API_BEARER_TOKEN }}
JOBFLOW_ORGANIZATION_ID: ${{ secrets.JOBFLOW_ORGANIZATION_ID }}
run: npm run test:e2e
8 changes: 8 additions & 0 deletions tests/e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# API endpoint for Playwright API E2E tests
API_BASE_URL=https://localhost:5099

# Seeded account JWT containing organizationId claim (required for business-flow tests)
JOBFLOW_API_BEARER_TOKEN=

# Optional explicit org id used for assertions and payload defaults
JOBFLOW_ORGANIZATION_ID=
47 changes: 47 additions & 0 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# JobFlow API E2E (Playwright)

This folder provides black-box API automation using Playwright's request client.

## Why Playwright for API

- Reuses one runner across UI and API automation.
- Fast HTTP smoke tests and contract checks.
- Good reporting and CI ergonomics.

## Setup

1. Start JobFlow API locally.
2. `cd tests/e2e`
3. `npm install`
4. Configure env (see `.env.example`):
- PowerShell: `$env:API_BASE_URL = "https://localhost:5099"`
- PowerShell: `$env:JOBFLOW_API_BEARER_TOKEN = "<seeded-jwt>"`
- PowerShell: `$env:JOBFLOW_ORGANIZATION_ID = "<org-guid>"`
5. `npm run test:e2e`

## Current coverage

- Swagger UI availability.
- OpenAPI document availability.
- Unknown route behavior.
- End-to-end business lifecycle:
- Create client
- Create estimate
- Upsert job
- Create and fetch invoice

## Seeded fixtures

- `fixtures.seed.example.json` documents required seed assumptions.
- `JOBFLOW_API_BEARER_TOKEN` should be a token whose claims include `organizationId`.
- Business-flow tests auto-skip when token is not set.

## Next workflow scenarios to automate

1. Firebase login handshake (`/api/auth/login-with-firebase`) with a seeded test identity.
2. Onboarding checklist fetch for a test organization.
3. Client create/list/update lifecycle.
4. Estimate create -> revise -> accept path.
5. Job create/schedule/assignment path.
6. Invoice issue/payment record path.
7. Subscription-gated endpoint behavior by plan.
15 changes: 15 additions & 0 deletions tests/e2e/fixtures.seed.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"seededIdentity": {
"description": "User whose JWT includes organizationId claim",
"source": "Firebase login -> API login-with-firebase",
"requiredEnv": [
"JOBFLOW_API_BEARER_TOKEN",
"JOBFLOW_ORGANIZATION_ID"
]
},
"sampleClient": {
"firstName": "E2E",
"lastName": "Client",
"emailDomain": "example.test"
}
}
96 changes: 96 additions & 0 deletions tests/e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "jobflow-api-e2e",
"private": true,
"version": "1.0.0",
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:e2e:report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@types/node": "^24.7.2"
}
}
17 changes: 17 additions & 0 deletions tests/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from '@playwright/test';

const baseURL = process.env.API_BASE_URL ?? 'https://localhost:7090';

export default defineConfig({
testDir: './specs',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL,
ignoreHTTPSErrors: true,
extraHTTPHeaders: {
Accept: 'application/json'
}
}
});
25 changes: 25 additions & 0 deletions tests/e2e/specs/api-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test, expect } from '@playwright/test';

test.describe('JobFlow API smoke', () => {
test('swagger ui is reachable', async ({ request }) => {
const response = await request.get('/swagger/index.html');
expect(response.ok()).toBeTruthy();

const html = await response.text();
expect(html.toLowerCase()).toContain('swagger');
});

test('openapi document is served', async ({ request }) => {
const response = await request.get('/swagger/v1/swagger.json');
expect(response.ok()).toBeTruthy();

const body = await response.json();
expect(body).toHaveProperty('paths');
expect(Object.keys(body.paths).length).toBeGreaterThan(0);
});

test('unknown route returns non-success status', async ({ request }) => {
const response = await request.get('/api/this-route-should-not-exist');
expect(response.status()).toBeGreaterThanOrEqual(400);
});
});
Loading