Skip to content

salahawad/testsuits

Repository files navigation

TestSuits — Modern Test Management System

A self-hosted test management platform for manual functional QA. Organise projects, run test suites, track executions, link defects to Jira, and store evidence in MinIO — a lightweight, Docker-Compose alternative to TestRail / Zephyr / qTest.

  • Multi-tenant — every object is scoped to a Company; cross-tenant access is blocked at the API layer.
  • Jira integration — Jira credentials are configured once per company; each project picks which Jira project to file bugs into. Create Jira bug on a failed execution only enables when the execution's project has a Jira binding and the company has Jira credentials saved.
  • Custom fields — per-project configurable fields (text, long text, number, select, checkbox) that attach to every test case in the project.
  • Shared / reusable steps — a per-project library of named steps; insert one into any case from the step editor.
  • Rich-text editor — WYSIWYG (TipTap) with a toolbar (bold, italic, inline code, bullet/numbered lists, links, undo/redo) on the fields where formatting actually helps forensic detail: step action/expected, preconditions, shared-step library entries, and — on execution detail — Actual result and Why it failed. Stored and rendered as Markdown, so plain-text content round-trips and existing rows stay readable. Link URLs are restricted to http(s)/mailto/teljavascript: and other schemes are blocked at both insert and render time.
  • Webhooks — configure outbound HTTP webhooks per project on events (run.created, run.completed, run.archived, execution.failed, execution.passed, execution_result.failed, execution_result.passed, jira.bug_created). Optional HMAC-SHA256 signing, delivery log, test-fire button.
  • Kanban run view — toggle between list and kanban on the Runs page; managers drag cards between columns to change a run's status.
  • Archive runs — managers can archive a test run to remove it from the active list. An Archived tab on the Runs page shows all archived runs; managers can restore them back to active.
  • Evidence storage — upload screenshots, videos and logs against cases or executions.
  • Full activity log — per-project event stream with filters.
  • CSV export of run results.
  • API tokens — personal access tokens (ts_… prefix, SHA-256 hashed at rest). Send as Authorization: Bearer <token> to call the API from CI. Token auth is read-only on the token management endpoints — tokens can't mint more tokens.
  • Email verification — new signups must verify their email address before signing in. A tokenized verification link (24 h TTL) is emailed on signup; the UI shows the link in dev mode. Invited, SAML, and SCIM-provisioned users are auto-verified.
  • Two-factor authentication (TOTP) — optional per-user 2FA. Users enable it from their Profile page (scan QR code with any TOTP app). When enabled, login requires a 6-digit code after the password step. A Trust this device checkbox lets users skip 2FA on the same browser for 30 days. Disabling 2FA requires the current password and revokes all trusted devices.
  • Remember me — unchecked (default) sessions expire in 24 hours; checked sessions last 30 days. The choice carries through the 2FA step so the full flow honours it.
  • Invite flow & password reset — managers invite teammates via one-time signed links; users reset their own password from the sign-in page. Links go through SMTP (Mailpit in dev, any SMTP provider in prod), and the UI also surfaces a copyable dev link for local testing.
  • Requirements & traceability — first-class Requirement objects per project, with many-to-many links to test cases. The Coverage Matrix has a Requirement dimension that pivots the latest execution status per case under each linked requirement; unlinked cells are blank and linked-but-never-executed cells show UNTESTED.
  • User locking — managers can lock any user from the Team page. Locked users cannot sign in, use the app, or call the API (JWT and API-token requests return 423). Unlock to restore access. The Team list shows each member's avatar (or initials fallback) and last login timestamp.
  • RolesADMIN (company settings, SSO/SCIM tokens, audit log), MANAGER (all project work), TESTER (runs/executions they own, created, or are assigned to; can execute and assign), VIEWER (read-only across the company).
  • SCIM 2.0 user provisioning — working. Issue a per-tenant SCIM token from Company Settings → SSO, point your IdP (Okta, Azure AD, JumpCloud) at /api/scim/v2. Groups and role-to-group mapping are not yet supported.
  • SAML SSO — scaffolding only, not production-ready. The per-company config UI and routes are in place, but /saml/:slug/login returns 501 and /saml/:slug/acs deliberately rejects requests until a real assertion parser is wired up (see comments in backend/src/routes/saml.ts for the exact steps — install @node-saml/node-saml and verify against cfg.x509Cert). Do not expose these routes to the public internet as-is.
  • Audit log/api/audit surfaces the tenant-wide activity stream with user/action/date filters and CSV export for compliance reviews.
  • Test case versioning — every PATCH /cases/:id snapshots the prior state to TestCaseRevision and GET /cases/:id/revisions returns the full audit trail. The case detail page exposes a History panel that lists revisions with author/date/changed-field summary; a per-version field-level diff view is on the roadmap.
  • Executive dashboard — stat tiles, 30-day execution & pass-rate trend, release-readiness per milestone, defect aging buckets, top failing cases.
  • i18n — English and French built in with a language switcher; sidebar collapses for more canvas. All API error, warning, and info messages are returned as stable UPPER_SNAKE_CASE machine keys (e.g. INVALID_CREDENTIALS, PROJECT_NOT_FOUND); the frontend translates them via t('errors.' + key). Validation messages, placeholders, and aria-labels are also served from the language files — no hardcoded English in .tsx files or API responses.
  • Dark mode — light by default. Toggle in the sidebar; the choice is saved in localStorage and applied pre-hydration so there's no flash on refresh. Toasts and the TipTap editor follow the theme automatically.

Stack

Layer Tech
Frontend React 18, Vite, TypeScript, Tailwind (with darkMode: "class"), TanStack Query, Zustand
Forms react-hook-form + Zod (shared schemas with the backend)
Feedback sonner toasts for async / server errors; inline ARIA-wired errors for field validation
Theming class-strategy dark mode; state in lib/theme.tsx; pre-hydration set in index.html
Backend Node.js 20, Express, TypeScript, Prisma, Zod, JWT auth
Database PostgreSQL 16
Object storage MinIO (S3-compatible)
Drag-and-drop dnd-kit (for reorderable test steps)
Orchestration Docker Compose

Prerequisites

  • Docker 24+ with Docker Compose v2
  • That's it — everything else runs inside containers.

Quick start

git clone https://github.com/<your-org>/testsuits.git
cd testsuits
cp .env.example .env
docker compose up --build

On first boot the API container runs prisma db push to create the schema and prisma db seed to insert the generic demo data. Open:

Service URL
Web UI http://localhost:5173
API http://localhost:4000/api
MinIO console http://localhost:9005 (minioadmin/minioadmin)
Mailpit (mail) http://localhost:8025
Postgres localhost:5434 (user/pass: testsuits)

Seeded accounts

The seed creates two isolated tenants so you can verify cross-company isolation immediately:

Tenant Email Password Role
Acme QA (primary demo) manager@acme.local acme123 MANAGER
Acme QA tester@acme.local acme123 TESTER
Globex QA (isolation test) manager@globex.local globex123 MANAGER
Globex QA tester@globex.local globex123 TESTER

The Acme tenant ships with two projects:

  • Acme Checkout — 3 suites (Cart, Payment, Shipping), 10 cases, 2 milestones, 4 runs covering smoke / full regression / offline / localisation, a failing execution linked to a sample Jira key, comments, activity-log entries, API token, and attachment metadata.
  • Acme Customer Portal — 2 suites (Account, Orders), 5 cases, 1 milestone, 1 smoke run with a failing profile-update execution.

The Globex tenant has a single project (BILLING) with a small run so the isolation test is still useful: log in as a Globex user and confirm you can't see Acme's data, then check that Globex's own project still renders on every page.

Configuration

All runtime configuration is controlled by .env. Copy .env.example and tweak as needed.

Required

Variable Purpose
POSTGRES_USER Postgres superuser (created on first boot)
POSTGRES_PASSWORD Postgres password
POSTGRES_DB Database name
DATABASE_URL Prisma connection string (uses the three vars above)
JWT_SECRET Signing secret for JWTs — required in production (API refuses to start with the default/empty value when NODE_ENV=production). Generate with openssl rand -hex 48.
API_PORT Port the Express API listens on (inside the container)
WEB_PORT Port the Vite dev server listens on
VITE_API_URL URL the browser uses to reach the API
CORS_ALLOWED_ORIGINS Comma-separated list of origins allowed to call the API. Leave empty in dev for auto-reflect. Required in production — an empty list denies all cross-origin requests.
TRUST_PROXY Number of reverse-proxy hops to trust for X-Forwarded-For (e.g. 1 behind a single LB). Without this, per-IP rate limits collapse every request onto the proxy IP.

Host port overrides

Change these if the defaults collide with something else on your host:

Variable Default Maps to
POSTGRES_HOST_PORT 5434 Postgres 5432 inside the container
MINIO_HOST_PORT 9004 MinIO S3 API 9000
MINIO_CONSOLE_HOST_PORT 9005 MinIO web console 9001

Object storage (MinIO)

Variable Purpose
MINIO_ROOT_USER MinIO admin user
MINIO_ROOT_PASSWORD MinIO admin password
MINIO_BUCKET Bucket name (created automatically by the minio-init service)
S3_PUBLIC_ENDPOINT URL the browser uses for signed downloads (host + MINIO_HOST_PORT)

Optional: auto-configure Jira

Set these to have the seed attach a JiraConfig to the Acme tenant on first boot. JIRA_PROJECT_KEY, if present, is bound to the CHECKOUT project so the per-project targeting is ready-to-go too. Omit these to configure Jira through the UI instead (Company Settings → Jira, then Project Settings → Jira binding).

JIRA_BASE_URL=https://your-org.atlassian.net
JIRA_USER=you@example.com
JIRA_TOKEN=your-atlassian-api-token
JIRA_PROJECT_KEY=PROJ

API tokens: id.atlassian.com/manage-profile/security/api-tokens.

Email

Outgoing email (password reset, team invites) is sent via SMTP. In dev the compose stack ships a Mailpit container that catches every message — no real mailbox required.

Variable Default Purpose
SMTP_URL smtp://mailpit:1025 SMTP connection string. Unset = log the body, don't send
MAIL_FROM TestSuits <no-reply@testsuits.local> From header on every message
APP_URL http://localhost:5173 Base URL used to build links (reset, invite) in email bodies
MAILPIT_SMTP_PORT 1025 Host-side SMTP port for Mailpit
MAILPIT_UI_PORT 8025 Host-side web UI for Mailpit

Open http://localhost:8025 to read every email the app sends. For production, set SMTP_URL to your provider's URL (e.g. smtps://postmaster@mg.example.com:KEY@smtp.mailgun.org:465) — no code changes needed.

Working with the app

Accessing from another device on your LAN

VITE_API_URL and S3_PUBLIC_ENDPOINT are URLs the browser resolves. When you open the UI from another machine, replace localhost with your host's LAN address, e.g.:

VITE_API_URL=http://192.168.1.20:4000/api
S3_PUBLIC_ENDPOINT=http://192.168.1.20:9004

Then restart: docker compose up -d --build web.

Re-seeding / resetting

Wipe everything and start from scratch:

docker compose down -v   # -v removes the postgres-data and minio-data volumes
docker compose up --build

Run just the seed against an already-running stack:

docker compose exec api npx tsx prisma/seed.ts

The seed is idempotent — rows that already exist are skipped.

Resetting a single tenant's demo

Log in as a manager, delete the project from the UI, then re-run the seed; it will re-create just the missing pieces.

Configuring Jira from the UI

Jira is split across two screens: credentials + templates live at the company level (they're reused by every project in the tenant), and the target Jira project lives on each project.

1. Company-level credentials (once per tenant)

  1. Log in as a manager.
  2. Open Company Settings → Jira.
  3. Fill in:
  4. Test connectionSave.

2. Per-project binding (once per project)

  1. Open the project → Settings → Jira.
  2. Pick the Jira project to file bugs into (the dropdown lists projects discovered via the company's saved credentials).
  3. Optionally override the issue type and pick a parent epic.
  4. Save.

Per-combination execution (platforms × connectivity × locale)

When a run is created with multi-select platforms (Web, Android, …), multi-select connectivities (Online, Offline), and a comma-separated locale field (en, fr, ar), each test case in the run is split into one TestExecutionResult row per combination. The Run Detail page renders a Result per combination matrix for the selected case:

  • Each row shows a combo label (e.g. Android · Offline · fr), a status pill, and pass/fail/blocked/skipped buttons.
  • FAILED requires both a failure reason and an actual result — the API rejects the save with RESULT_FAILED_REQUIRES_REASON_AND_DETAILS otherwise, and the cell stays red with a "needs reason & details" hint until both are filled.
  • A Mark all passed shortcut sets every remaining row to PASSED via POST /execution-results/bulk — the happy path for cases that work on every platform.
  • The case isn't considered done until every cell has a non-PENDING status; the parent TestExecution.status aggregates the children (FAILED if any failed, BLOCKED if any blocked, PENDING if any pending, else PASSED/SKIPPED).
  • The aggregate badge calls out the exact failing combos — so "passed on Windows, failed on Android" stays visible as Failed on: Android · Offline.

Runs created before this feature have no result rows and fall back to the legacy single-status editor — they keep working unchanged.

3. Filing bugs

On a per-combo matrix, every failing cell has its own Create Jira bug button that opens one ticket per failing combination — Android · Offline · en files a separate ticket from iOS · Online · fr, so each bug has precise reproduction context. The ticket description pulls the combo-specific failureReason and actualResult (both are required before the save is accepted, so the ticket is never blank on arrival). Legacy single-status executions still use the old Create Jira bug button on the parent row.

The button is only enabled when the company has Jira credentials saved and the execution's project has a Jira binding.

Email verification, invites & password reset

Outbound mail goes through SMTP. In development, the mailpit container in docker-compose.yml provides a local SMTP sink + web UI — open http://localhost:8025 to read mail sent by the app. In production, point SMTP_URL at your real provider (Mailgun, SES, Postmark, etc).

Signing up (email verification required)

  1. Fill in the signup form and submit.
  2. The server creates the company and user but does not sign you in. Instead, it emails a verification link valid for 24 hours. In dev mode the link is also shown in the UI.
  3. Click the verification link. The server marks the email as verified, issues a JWT, and logs you in.
  4. Until verified, login is blocked with a "please verify your email" message and a Resend button.

Invited users, SAML-authenticated users, and SCIM-provisioned users skip verification — their email ownership is already established.

Invite a teammate

  1. Log in as a manager.
  2. Open Team → Invite teammate.
  3. Fill in name, email, and role, and click Invite.
  4. The server issues a one-time link valid for 7 days and emails it to the invitee. In dev, the UI also shows the link for easy copy-paste.
  5. The invitee opens the link, sets their password, and is signed in directly — no second step, no shared passwords. The account is automatically marked as email-verified.

Forgot your password?

The sign-in page has a Forgot your password? link. Enter your email; if it matches an account, the server issues a one-time link valid for 1 hour and emails it. The response is identical for known and unknown emails (no user enumeration).

Two-factor authentication (TOTP)

2FA is opt-in per user and disabled by default.

Enabling 2FA

  1. Open Profile from the sidebar.
  2. In the Two-factor authentication section, click Enable 2FA.
  3. Scan the QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.), or enter the manual key.
  4. Type the 6-digit code from the app and click Verify.
  5. 2FA is now active for your account.

Signing in with 2FA

After entering your email and password, a second screen asks for the 6-digit code from your authenticator app. The challenge token is valid for 5 minutes. Check Trust this device for 30 days to skip the code step on future logins from the same browser.

Trusted devices

When you check "Trust this device" during 2FA, the server issues an opaque token (td_ prefix, SHA-256 hashed at rest, 30-day expiry). On subsequent logins the token is sent via X-Trust-Token header; if it's valid for the authenticated user, the 2FA step is bypassed. Trusted device tokens are revoked automatically when:

  • The user changes their password
  • The user resets their password
  • 2FA is disabled

Logout does not revoke trusted devices — the trust represents the machine, not the session. A forced session revocation (401) clears the trust token from the browser.

Disabling 2FA

In Profile → Two-factor authentication, enter your current password and click Disable 2FA. All trusted devices are revoked.

Remember me

The login page has a Remember me for 30 days checkbox. When unchecked (default), the JWT session expires in 24 hours. When checked, it lasts 30 days. The flag is carried through the 2FA challenge flow so the session duration is honoured regardless of whether 2FA is enabled.

API tokens (for CI / scripting)

Open API tokens from the sidebar. Create a token, give it a name, and copy the plaintext — the server stores only a SHA-256 hash and the plaintext is shown exactly once. Use it as:

curl -H "Authorization: Bearer ts_<your-token>" https://testsuits.example.com/api/runs

Tokens inherit the creator's role and company. Token-authenticated callers can hit every endpoint except the token management endpoints (GET/POST/DELETE /api/tokens) — those require a JWT session so a compromised token can't mint fresh credentials.

Revoke a token anytime from the same page. Revocation is immediate.

A CI seed token is created automatically by the seed for the primary manager; the plaintext is printed to the seed log so you can demo the flow end-to-end.

Requirements & traceability

Requirements are first-class per-project objects. Open a project → Requirements to create them; each one has an external reference (a URL or ID in your requirements system — Jira story, Confluence page, etc) and a title.

Link requirements to cases from Case → Edit → Linked requirements (multi-select). The legacy free-text requirements[] on a case still works and is shown in a separate panel; use the first-class links for anything that should surface in traceability reporting.

The Coverage Matrix picks up a new dimension: select Requirement on the Matrix page and the columns become the project's requirements. Each cell shows the latest non-PENDING execution status for the case, limited to the requirements the case is linked to. Cells for unlinked requirements are blank; cells for linked requirements that have never been executed show UNTESTED.

Domain model

Company
  ├── Users (MANAGER / TESTER)
  │     ├── emailVerifiedAt, totpSecret, totpEnabledAt, isLocked, lastLoginAt
  │     ├── owned TestRuns
  │     ├── assigned TestExecutions
  │     ├── Comments
  │     ├── ActivityLog entries
  │     ├── ApiTokens
  │     ├── EmailVerificationTokens
  │     └── TrustedDevices (td_ tokens, SHA-256, 30d TTL)
  ├── JiraConfig (optional, 1 per company — credentials + templates)
  └── Projects
        ├── jiraProjectKey / jiraIssueType / jiraParentEpicKey (per-project target)
        ├── customFields (JSON array — text/textarea/number/select/checkbox)
        ├── SharedSteps (reusable step library)
        ├── Webhooks (outbound HTTP on events; HMAC signing + delivery log)
        ├── Milestones
        ├── TestSuites (tree — suites can have sub-suites)
        │     └── TestCases
        │           ├── steps (ordered JSON array, drag-to-reorder, Markdown)
        │           ├── customFieldValues (keyed by project's customFields)
        │           ├── tags, priority, testLevel
        │           ├── Attachments
        │           └── Comments
        └── TestRuns
              ├── Milestone (optional)
              ├── status (PENDING/IN_PROGRESS/COMPLETED/ARCHIVED)
              ├── environment, platforms[], connectivities[], locale, dueDate
              └── TestExecutions
                    ├── status (aggregate — rolls up child TestExecutionResults)
                    ├── failureReason / actualResult (legacy runs only; per-combo runs use results[])
                    ├── assignee, durationMinutes
                    ├── jiraIssueKey / jiraIssueUrl (legacy runs only)
                    ├── Attachments, Comments
                    └── TestExecutionResults (one per platform × connectivity × locale)
                          ├── platform, connectivity, locale
                          ├── status (PENDING/PASSED/FAILED/BLOCKED/SKIPPED)
                          ├── failureReason, actualResult, notes (required when FAILED)
                          └── jiraIssueKey / jiraIssueUrl (one bug per failing combo)

Enums: Role (MANAGER, TESTER), RunStatus (PENDING/IN_PROGRESS/COMPLETED/ARCHIVED), Priority (LOW/MEDIUM/HIGH/CRITICAL), TestLevel (SMOKE/SANITY/REGRESSION/ADVANCED/EXPLORATORY), Platform (WEB/WINDOWS/MACOS/ANDROID/IOS), Connectivity (ONLINE/OFFLINE).

Role capabilities

Action ADMIN MANAGER TESTER VIEWER
View projects, suites, cases, milestones
View runs & executions only those they created or are assigned to
Create/edit projects, suites, cases, milestones
Create test runs
Archive / restore test runs
Execute tests (per-combo status, reason, actual result, notes, duration)
Assign / reassign executions (single & bulk)
Add / remove / change roles of teammates
Lock / unlock users
Company Jira credentials + templates view only view only
Per-project Jira binding (target project, epic) view only view only
Create / link / unlink Jira issues on executions ✔ (within their visible work)
Configure SSO / SAML, manage SCIM tokens
Export audit log

Folder layout

backend/
  prisma/
    schema.prisma      Data model
    seed.ts            Generic demo seed (shipped publicly)
    seed.hapster.ts    Local-only seed extension (gitignored — optional)
    migrations/        Generated Prisma migrations
  src/
    app.ts             Express setup + router mounting
    db.ts              Prisma client
    middleware/        auth, scope, error
    lib/               s3, jira, logger, activity
    routes/            auth, projects, suites, cases, runs, executions,
                       attachments, dashboard, jira, milestones,
                       comments, users, activity
  src/
    app.ts             Express setup + router mounting
    db.ts              Prisma client
    middleware/        auth, scope (tenant + tester visibility), logging, error
    lib/               s3, jira (discover + md→ADF), logger, activity, webhooks
    routes/            auth, twoFactor, users, companies, projects, suites,
                       cases, milestones, runs, executions, attachments,
                       comments, activity, dashboard, matrix, jira,
                       sharedSteps, webhooks
frontend/
  src/
    App.tsx            Router
    i18n/              en.json, fr.json — kept in sync (see CLAUDE.md)
    lib/               api, auth, status, logger, enums, markdown (safe renderer)
    components/        Layout (collapsible, company header), Comments,
                       ActivityFeed, LanguageSwitcher, PasswordInput,
                       CustomFieldsEditor, SharedStepsEditor, WebhooksEditor
    pages/             Login, VerifyEmail, Dashboard, Projects, ProjectDetail,
                       CompanySettings (company Jira creds + templates),
                       ProjectSettings (tabbed: Jira / custom fields /
                       shared steps / webhooks),
                       Team, Milestones, Matrix (coverage by platform /
                       connectivity / locale),
                       SuiteDetail, CaseDetail (Markdown + shared-step
                       library + custom fields), Runs (list / kanban toggle),
                       RunDetail, Profile (password, 2FA, avatar),
                       Tokens, Audit, SsoSettings
docker-compose.yml     postgres + minio + minio-init + mailpit + api + web

Local development

Docker is the recommended path. If you want to run the services natively (e.g. for faster hot-reload on the backend), point your local Node.js 20 at the Postgres and MinIO containers:

# One-time
cd backend && npm install
cd ../frontend && npm install

# Backend (reads DATABASE_URL from .env)
cd backend
npx prisma generate
npm run dev

# Frontend
cd ../frontend
npm run dev

You can still run just Postgres + MinIO in Docker:

docker compose up -d db minio minio-init

Extending the seed with local fixtures

prisma/seed.ts automatically loads prisma/seed.hapster.ts if the file is present. That filename is gitignored, as is anything matching seed.*.local.ts. Export a default async function:

// backend/prisma/seed.hapster.ts
import { PrismaClient } from "@prisma/client";

export default async function (prisma: PrismaClient) {
  // create your internal tenant, users, projects, etc.
}

Run it:

docker compose exec api npx tsx prisma/seed.ts

API surface (high level)

All routes live under /api. Authentication is required except the public endpoints called out below. Every authenticated route is scoped to the caller's company; cross-tenant reads return 404 (not 403) so existence is never leaked.

Authentication: send Authorization: Bearer <token> where <token> is either a JWT (interactive login) or an API token (ts_… prefix, SHA-256 hashed). API-token callers can't call the token management endpoints themselves — those require a JWT session.

Error responses use stable machine keys, not English prose. Every error is returned as { "error": "UPPER_SNAKE_CASE_KEY" } (e.g. INVALID_CREDENTIALS, PROJECT_NOT_FOUND, RUN_REQUIRES_CASES). Validation errors add a details object: { "error": "VALIDATION_FAILED", "details": { "formErrors": [], "fieldErrors": { ... } } }. Clients should match on these keys, not on human-readable text.

Area Routes
Auth (public) POST /auth/login, POST /auth/signup, POST /auth/forgot, POST /auth/reset, GET /auth/invite/:token, POST /auth/accept-invite, POST /auth/verify-email, POST /auth/resend-verification
Auth (mgr) POST /auth/invite
2FA (public) POST /2fa/authenticate
2FA (auth) GET /2fa/status, POST /2fa/setup, POST /2fa/confirm-setup, POST /2fa/disable
Tokens GET /tokens, POST /tokens, DELETE /tokens/:id — all require an interactive (JWT) session
Users GET /users, GET /users/me, POST /users (mgr), PATCH/DELETE /users/:id (mgr), PATCH /users/:id/lock (mgr)
Companies GET /companies/current, PATCH /companies/current (mgr)
Projects GET /projects, POST /projects (mgr), GET/PATCH/DELETE /projects/:id, GET/PUT /projects/:id/custom-fields (PUT mgr)
Requirements GET /requirements?projectId=…, GET /requirements/:id, POST /requirements (mgr), PATCH/DELETE /requirements/:id (mgr), POST/DELETE /requirements/:id/cases[/:caseId] (mgr)
Shared steps GET /shared-steps?projectId=…, POST /shared-steps (mgr), PATCH/DELETE /shared-steps/:id (mgr)
Webhooks GET /webhooks/events, GET /webhooks?projectId=…, POST/PATCH/DELETE /webhooks/:id (mgr), POST /webhooks/:id/test (mgr)
Suites POST /suites (mgr), GET/PATCH/DELETE /suites/:id
Cases POST /cases (mgr), GET /cases/:id, PATCH/DELETE /cases/:id (mgr), POST /cases/:id/clone (mgr)
Milestones GET /milestones, POST /milestones (mgr), PATCH/DELETE /milestones/:id (mgr)
Runs GET /runs, POST /runs (mgr), GET/PATCH/DELETE /runs/:id, GET /runs/:id/export.csv
Executions GET /executions/:id, PATCH /executions/:id (legacy runs only — per-combo runs must use /execution-results/*), POST /executions/bulk-assign
Execution results PATCH /execution-results/:id, POST /execution-results/bulk — per-combo status, failureReason, actualResult. FAILED requires both a reason and actual result.
Attachments POST /attachments (multipart), GET /attachments/:id/download, DELETE /attachments/:id
Comments GET /comments?caseId|executionId|runId=…, POST /comments, DELETE /comments/:id
Jira (company) GET/PUT/DELETE /jira/config (PUT/DELETE mgr), POST /jira/test, GET /jira/defaults/templates
Jira (discover) GET /jira/discover/projects, /jira/discover/issue-types, /jira/discover/epics
Jira (project) GET /jira/projects/:id/binding, PUT /jira/projects/:id/binding (mgr)
Jira (bugs) POST /jira/executions/:id/{create-bug,link,unlink} (legacy) · POST /jira/results/:id/{create-bug,link,unlink} (per-combo — one ticket per failing platform × connectivity × locale)
Matrix GET /matrix/projects/:id?dimension=platform|connectivity|locale|requirement
Activity GET /activity?projectId=…&entityType=…&entityId=…
Audit GET /audit?userId=…&action=…&from=…&to=…&format=csv (mgr)
Case history GET /cases/:id/revisions
Dashboard GET /dashboard — includes trend, releaseReadiness, defectAging
SAML (admin) GET/PUT /saml/config (admin); GET /saml/:slug/login, POST /saml/:slug/acs (public, IdP-facing)
SCIM tokens GET/POST/DELETE /scim-tokens (admin) — issue/revoke provisioning tokens
SCIM v2 GET/POST /scim/v2/Users, GET/PATCH/DELETE /scim/v2/Users/:id (Bearer SCIM token)
Client errors POST /_client-log (no auth) — frontend posts window.onerror / unhandledrejection via sendBeacon; per-IP rate-limited, routed through the server logger with source:"client" and session/user-correlation fields.

(mgr) = manager-level (MANAGER or ADMIN). (admin) = ADMIN-only.

Security notes

Authentication

  • JWTs are signed with JWT_SECRET. The API refuses to start when NODE_ENV=production and JWT_SECRET is unset or equal to the default placeholder — use openssl rand -hex 48 to generate a strong value.
  • API tokens (ts_… prefix) are stored as SHA-256 hashes — a compromised database row can't be used to impersonate the caller. Plaintext is shown exactly once at creation.
  • Token management endpoints require a JWT session — API-token callers can't mint, list, or revoke tokens, so a leaked token can't be used to create fresh credentials.
  • Email verification — new signups cannot log in until they verify their email via a tokenized link (24 h TTL, SHA-256 hashed at rest, single-use). Invited, SAML, and SCIM users are auto-verified. Resend is rate-limited and the response is identical for known/unknown emails.
  • Two-factor authentication — optional TOTP (RFC 6238). The secret is stored per-user; totpEnabledAt is null until setup is confirmed with a valid code. Login returns a short-lived challenge JWT (5 min) instead of a session when 2FA is active; the full session JWT is only issued after the TOTP code is verified. Disabling 2FA requires the current password.
  • Trusted devices — when a user checks "Trust this device" during 2FA, an opaque token (td_ prefix, 256-bit, SHA-256 hashed at rest, 30-day server-side expiry) is issued. On subsequent logins the token is sent via X-Trust-Token and validated against TrustedDevice.tokenHash + userId + expiresAt. The token only bypasses the 2FA step — the user's password is still verified. Tokens are revoked on password change, password reset, or 2FA disable; logout does not revoke them.
  • Remember me — controls JWT lifespan: unchecked = 24 h, checked = 30 d. The flag is forwarded through the 2FA challenge so the session duration is honoured end-to-end.
  • Password reset wipes the door on both sides: every outstanding JWT for that user is revoked (via passwordUpdatedAt compared against the JWT's iat), every API token is deleted, and every trusted device token is revoked. If a password leaks, a single reset kicks everyone off the account.
  • Login is timing-neutral — bcrypt runs even when the email is unknown so response latency can't be used to enumerate accounts.
  • Client error pipeline (POST /_client-log) is unauthenticated by design — browsers can't send JWTs on navigator.sendBeacon. It's rate-limited per IP, body-capped, and zod-validated. Identity fields (userId, userEmail, sessionId) are client-asserted and never used for authorization — only for correlating client errors to backend traces. Authz still lives on the JWT-protected /api/* routes.

Rate limiting

All credential-adjacent endpoints are rate-limited per IP:

Endpoint Window Max requests
POST /auth/login 1 min 10
POST /auth/signup 1 hour 5
POST /auth/forgot 1 hour 5
POST /auth/reset 1 min 10
POST /auth/invite 1 hour 30
GET /auth/invite/:t 1 min 30
POST /auth/accept-invite 1 min 10
POST /auth/verify-email 1 min 10
POST /auth/resend-verification 1 hour 5
POST /2fa/setup 1 min 10
POST /2fa/confirm-setup 1 min 10
POST /2fa/disable 1 min 10
POST /2fa/authenticate 1 min 10

When deployed behind a reverse proxy, set TRUST_PROXY (e.g. 1 for a single hop) so limits key off the real client IP instead of the proxy.

Password policy

Applied on signup, password reset, and invite-accept:

  • Minimum 10 characters, maximum 128.
  • Rejects a small list of common passwords (password, 12345678, qwerty, admin123, etc).
  • Login does not re-validate strength, so pre-policy accounts keep working until their next reset.

CORS

Set CORS_ALLOWED_ORIGINS to a comma-separated list of origins the browser may call from (scheme + host + port). In development, an empty list auto-reflects the request's Origin; in production, an empty list denies all cross-origin requests.

Other

  • Reset / invite tokens are cryptographically random 192-bit values, SHA-256 hashed at rest, and single-use. Raw tokens are never logged — only the token row ID is.
  • devToken field in /auth/forgot, /auth/invite, /auth/signup, and /auth/resend-verification responses is only populated when NODE_ENV !== "production", so the plaintext never leaks in prod even if the UI would still render it.
  • Expired PasswordResetToken and InviteToken rows are swept every 6 hours by a background task (backend/src/lib/cleanup.ts), so the tables don't grow unbounded.
  • Attachments are stored privately in MinIO; downloads go through signed URLs served by S3_PUBLIC_ENDPOINT.
  • .env is gitignored by default. Never commit a populated .env or seed.hapster.ts.
  • All multi-tenant reads are scoped by companyId at the query level (see backend/src/middleware/scope.ts). Cross-tenant lookups return 404 (not 403) so existence isn't leaked.

Troubleshooting

  • "Failed to resolve import @dnd-kit/core" — run docker compose up -d --build web. The web image bakes node_modules at build time; adding a dependency requires rebuilding.
  • Browser can't reach the API from another device — set VITE_API_URL to your host's LAN IP (not localhost) and rebuild the web container.
  • Signed download URLs failS3_PUBLIC_ENDPOINT must be the URL the browser can hit (host IP + MINIO_HOST_PORT), not the in-Docker MinIO address.
  • Port already in use — override the corresponding *_HOST_PORT in .env.
  • Seed did nothing on a fresh DB — check docker compose logs api | grep Seed. Every seed step logs either "created" or "skipping".

Webhooks

Each project can register outbound webhooks under Project Settings → Webhooks. Supported events:

Event Fires when
run.created a test run is created
run.completed a test run's status is set to COMPLETED
run.archived a test run is archived
execution.passed a legacy (single-status) execution transitions to PASSED
execution.failed a legacy (single-status) execution transitions to FAILED
execution_result.passed a per-combo result row transitions to PASSED
execution_result.failed a per-combo result row transitions to FAILED
jira.bug_created a Jira bug is auto-filed (payload includes resultId when the bug was filed per combo)

Request body:

{
  "event": "execution.failed",
  "projectId": "",
  "deliveredAt": "2026-04-15T09:00:00.000Z",
  "data": { "executionId": "", "runId": "", "caseId": "", "...": "..." }
}

If a signing secret is configured, every request carries x-testsuits-signature: <hex> where <hex> is HMAC-SHA256(secret, rawBody). Every delivery is recorded (status + error) and shown on the webhook row. Use the test button to fire a sample payload at any time.

Roadmap

  • SSO end-to-end against a real IdP — schema, admin UI, and the SP-initiated /login + /acs routes are shipped; the remaining gap is wiring @node-saml/node-saml into backend/src/routes/saml.ts to verify SAML assertions against the stored x509 cert
  • SCIM groups / role-to-group mapping (Users work today)
  • Playwright / Cypress reporter packages
  • Traceability matrix report
  • Test case versioning / history diff

License

Copyright © 2026 Salah AWAD. All rights reserved.

TestSuits is distributed under the Business Source License 1.1.

  • Free for personal use, evaluation, non-commercial projects, and your own internal business operations (using TestSuits to manage your own company's QA).
  • Requires a commercial agreement for offering TestSuits (or a derivative) to third parties as a hosted or managed service — for example, a multi-tenant SaaS.

On the Change Date (2030-04-15) this version of the code becomes available under the Apache License 2.0.

To request a commercial licence, see COMMERCIAL.md or email salah.awad@outlook.com. Contributions are accepted under DCO sign-off — see CONTRIBUTING.md.

About

Self-hosted manual test management — projects, runs, Jira integration, multi-tenant. React + Express + Postgres + MinIO.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages