Skip to content

feat: add web testing foundation with infra-backed suites#349

Open
KMKoushik wants to merge 1 commit intomainfrom
feat/web-testing-foundation
Open

feat: add web testing foundation with infra-backed suites#349
KMKoushik wants to merge 1 commit intomainfrom
feat/web-testing-foundation

Conversation

@KMKoushik
Copy link
Member

@KMKoushik KMKoushik commented Feb 7, 2026

Summary

  • add a Vitest-based testing foundation for apps/web with dedicated unit, tRPC, API, and integration suite configs plus root/app test scripts
  • introduce reusable test harness utilities (env bootstrap, setup hooks, DB/Redis reset helpers, and entity factories) and initial coverage across utility logic, tRPC middleware flows, API handlers, and integration paths
  • add test infrastructure and automation, including Docker Compose services for Postgres/Redis, testing docs, .env.test.example, and a GitHub Actions workflow that runs unit + tRPC + API + integration suites
  • harden Redis test lifecycle by recreating the client when the shared connection was ended

Verification

  • pnpm --filter=web test:unit
  • pnpm --filter=web test:trpc
  • pnpm --filter=web test:api
  • pnpm test:infra:up
  • pnpm --filter=web test:integration:full
  • pnpm test:infra:down
  • pnpm --filter=web test

Summary by cubic

Adds a Vitest-based test foundation for apps/web with unit, tRPC, API, and infra-backed integration suites, plus Dockerized Postgres/Redis and CI to run them. Also fixes Redis client lifecycle to prevent ended-connection issues during tests.

  • New Features

    • Vitest configs for unit, tRPC, API, and integration suites with dedicated scripts at root and app.
    • Test harness utilities: env bootstrap, global setup hooks, Postgres/Redis reset helpers, and entity factories.
    • Infra for integration tests via Docker Compose (Postgres/Redis), .env.test.example, and TESTING.md.
    • GitHub Actions workflow that brings up services and runs all suites on PRs/push.
    • Initial coverage across utilities, tRPC middleware, API handlers, public API (Hono), and services (including idempotency), in both unit and integration layers.
  • Bug Fixes

    • Recreate Redis client when the shared connection is ended to stabilize test runs.

Written for commit 9b752f3. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Tests
    • Established comprehensive automated testing infrastructure with unit, integration, tRPC, and API test suites
    • Added continuous integration workflow that automatically runs tests on pull requests and pushes to main
    • Configured isolated test environments with dedicated PostgreSQL and Redis services
    • Introduced test documentation and shared helper utilities for standardized testing

@vercel
Copy link

vercel bot commented Feb 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
unsend-marketing Ready Ready Preview, Comment Feb 7, 2026 11:03pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

Walkthrough

This pull request introduces a comprehensive testing infrastructure for the web application. It adds a GitHub Actions workflow for running tests on pull requests and pushes to main, deploys PostgreSQL 16 and Redis 7 services for testing, establishes Vitest-based unit, tRPC, API, and integration test configurations, and provides test setup files, factories, and helper utilities. Environment configuration files, Docker Compose orchestration for test services, and multiple test files demonstrating the testing patterns across different layers are included. Test scripts are registered in package.json files for both the root workspace and web application.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: establishing a comprehensive testing foundation for the web app with infrastructure-backed test suites. It accurately reflects the main objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9b752f3
Status: ✅  Deploy successful!
Preview URL: https://644be922.usesend.pages.dev
Branch Preview URL: https://feat-web-testing-foundation.usesend.pages.dev

View logs

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 29 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/workflows/test-web.yml:
- Around line 12-23: Add NEXTAUTH_SECRET to the test environment variables so
unsubscribe hash verification succeeds: set NEXTAUTH_SECRET: "test-secret" in
the env block currently defining NODE_ENV/NEXTAUTH_URL/... in the CI workflow
(the .github workflow env) and also add NEXTAUTH_SECRET: "test-secret" to the
defaultEnv object in the test setup (the apps/web test/setup/setup-env.ts file)
so functions createUnsubUrl, createOneClickUnsubUrl and
unsubscribeContactFromLink have the secret during integration tests.

In `@apps/web/src/lib/usage.unit.test.ts`:
- Line 7: Rename the misspelled function getUsageUinits to getUsageUnits in the
implementation and update both call sites accordingly (the test referencing
getUsageUinits in this test file and the usage-job.ts call). Search for the
symbol getUsageUinits, rename the function declaration to getUsageUnits, and
replace all references (including imports and tests) so imports, calls, and
exports use getUsageUnits consistently.

In `@apps/web/src/test/setup/setup-env.ts`:
- Line 9: The default value for NEXT_PUBLIC_IS_CLOUD in setup-env.ts is "true"
but .env.test.example uses "false", causing inconsistent test defaults; update
the NEXT_PUBLIC_IS_CLOUD entry in apps/web/src/test/setup/setup-env.ts to match
the canonical value ("false") used in apps/web/.env.test.example so tests and
example envs align, ensuring the exported/default env object that contains
NEXT_PUBLIC_IS_CLOUD uses "false".

In `@apps/web/vitest.config.ts`:
- Around line 4-27: The test config in defineConfig (the test block in
vitest.config.ts) lacks an explicit include/exclude so bare "vitest run" will
pick up integration tests; update the test block (where test.environment,
globals, setupFiles, etc. are defined) to add either an include array that only
matches unit tests (e.g., "src/**/*.test.{ts,tsx}" and/or
"src/**/*.spec.{ts,tsx}") or add an exclude pattern for integration tests (e.g.,
"**/*.integration.test.{ts,tsx}") so integration tests are not run by default
without infrastructure.
🧹 Nitpick comments (6)
apps/web/src/test/setup/setup-env.ts (1)

14-17: Empty-string env vars are silently overwritten.

!process.env[key] is falsy for "", so an explicitly exported FOO="" would be replaced by the default. Using process.env[key] === undefined (or !(key in process.env)) preserves intentional empty overrides.

Proposed fix
 for (const [key, value] of Object.entries(defaultEnv)) {
-  if (!process.env[key]) {
+  if (process.env[key] === undefined) {
     process.env[key] = value;
   }
 }
apps/web/src/server/api/trpc.integration.test.ts (2)

119-122: Avoid calling caller.teamPing() twice — each invocation re-executes the procedure.

Both assertions independently invoke the procedure, doubling the DB round-trips and risking divergent results if the first call has side effects. Capture the rejection once:

Proposed fix
-    await expect(caller.teamPing()).rejects.toBeInstanceOf(TRPCError);
-    await expect(caller.teamPing()).rejects.toMatchObject({
-      code: "NOT_FOUND",
-    });
+    await expect(caller.teamPing()).rejects.toSatisfy((err: unknown) => {
+      expect(err).toBeInstanceOf(TRPCError);
+      expect(err).toMatchObject({ code: "NOT_FOUND" });
+      return true;
+    });

2-3: Consolidate vitest imports into a single statement.

vi can be imported alongside the other vitest exports on line 2.

Proposed fix
-import { afterAll, beforeEach, describe, expect, it } from "vitest";
-import { vi } from "vitest";
+import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
apps/web/src/server/service/api-service.integration.test.ts (1)

25-29: Consider using the createTeam factory for consistency with other integration tests.

Other integration tests (e.g., trpc.integration.test.ts) use the factories from ~/test/factories/core. Using db.team.create directly here works but diverges from the established pattern.

Also applies to: 46-50

apps/web/src/test/setup/setup-tests.ts (1)

3-6: vi.clearAllMocks() is redundant when followed by vi.restoreAllMocks().

restoreAllMocks already clears all mock state (calls, instances, results) and restores original implementations. The clearAllMocks call can be removed.

Proposed fix
 afterEach(() => {
-  vi.clearAllMocks();
   vi.restoreAllMocks();
 });
.github/workflows/test-web.yml (1)

3-7: Consider adding path filters to avoid running tests on unrelated changes.

This workflow triggers on every pull request regardless of which files changed. Adding path filters would save CI minutes for docs-only or non-web changes.

Example
 on:
   pull_request:
+    paths:
+      - "apps/web/**"
+      - "packages/**"
+      - "docker/testing/**"
+      - ".github/workflows/test-web.yml"
   push:
     branches:
       - main

Comment on lines +12 to +23
env:
NODE_ENV: test
NEXTAUTH_URL: http://localhost:3000
DATABASE_URL: postgresql://usesend:password@127.0.0.1:5432/usesend_test
REDIS_URL: redis://127.0.0.1:6379/15
AWS_ACCESS_KEY: test-access-key
AWS_SECRET_KEY: test-secret-key
AWS_DEFAULT_REGION: us-east-1
NEXT_PUBLIC_IS_CLOUD: "true"
API_RATE_LIMIT: "2"
AUTH_EMAIL_RATE_LIMIT: "5"
RUN_INTEGRATION: "true"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if NEXTAUTH_SECRET is referenced in test-adjacent or server code
rg -n "NEXTAUTH_SECRET" --type=ts -g '!node_modules/**' apps/web/src/

Repository: usesend/useSend

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Also check the workflow file itself to understand the full context
cat -n .github/workflows/test-web.yml | head -50

Repository: usesend/useSend

Length of output: 1615


🏁 Script executed:

#!/bin/bash
# Check for NEXTAUTH_SECRET in configuration files and test setup
rg -n "NEXTAUTH_SECRET" --type-add ts --type-list | head -5
rg "NEXTAUTH_SECRET" -t js -t ts -t jsx -t tsx --max-count=20

Repository: usesend/useSend

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Check for tests that use campaign-service
find apps/web -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "campaign" 2>/dev/null || echo "No campaign tests found"

Repository: usesend/useSend

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Check the campaign-service file to see context of NEXTAUTH_SECRET usage
sed -n '535,570p' apps/web/src/server/service/campaign-service.ts
sed -n '625,635p' apps/web/src/server/service/campaign-service.ts

Repository: usesend/useSend

Length of output: 1381


🏁 Script executed:

#!/bin/bash
# Check if there's any test configuration that might set NEXTAUTH_SECRET
find apps/web -name "jest.config.*" -o -name "vitest.config.*" -o -name "setup.*test*"
cat apps/web/jest.config.* 2>/dev/null || cat apps/web/vitest.config.* 2>/dev/null || echo "No test config found"

Repository: usesend/useSend

Length of output: 731


🏁 Script executed:

#!/bin/bash
# Check the setup files for NEXTAUTH_SECRET configuration
cat apps/web/src/test/setup/setup-env.ts

Repository: usesend/useSend

Length of output: 602


🏁 Script executed:

#!/bin/bash
cat apps/web/src/test/setup/setup-tests.ts

Repository: usesend/useSend

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Check if there are any integration or server tests that might call these functions
find apps/web/src -name "*.test.ts" -o -name "*.spec.ts" | head -20

Repository: usesend/useSend

Length of output: 674


🏁 Script executed:

#!/bin/bash
# Check the integration tests to see if they might call campaign functions
grep -l "campaign\|unsubscribe" apps/web/src/server/api/trpc.integration.test.ts apps/web/src/server/service/*.integration.test.ts 2>/dev/null || echo "No matches found in integration tests"

Repository: usesend/useSend

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Check if campaign service is imported or used anywhere in tests
rg "campaign-service|createUnsubUrl|unsubscribeContactFromLink" --type ts apps/web/src

Repository: usesend/useSend

Length of output: 1767


🏁 Script executed:

#!/bin/bash
# Check if there are any route handlers that use the campaign functions
rg "createUnsubUrl|createOneClickUnsubUrl|unsubscribeContactFromLink" --type ts apps/web/src/app

Repository: usesend/useSend

Length of output: 522


🏁 Script executed:

#!/bin/bash
# Check if env.NEXTAUTH_SECRET is accessed conditionally or if it always fails
grep -B3 -A3 "env.NEXTAUTH_SECRET" apps/web/src/server/service/campaign-service.ts | head -30

Repository: usesend/useSend

Length of output: 845


🏁 Script executed:

#!/bin/bash
# Check the unsubscribe page to see if it's tested or how it handles missing env vars
cat apps/web/src/app/unsubscribe/page.tsx

Repository: usesend/useSend

Length of output: 1430


Add NEXTAUTH_SECRET to test environment variables.

NEXTAUTH_SECRET is required at runtime by the campaign unsubscribe functions (createUnsubUrl, createOneClickUnsubUrl, unsubscribeContactFromLink) and is called by server routes in apps/web/src/app/unsubscribe/page.tsx and apps/web/src/app/api/unsubscribe-oneclick/route.ts. Without it defined in the CI environment, any integration test exercising these routes will fail with "Invalid unsubscribe link" errors because hash verification will fail.

Add NEXTAUTH_SECRET: "test-secret" to the env block in .github/workflows/test-web.yml and to the defaultEnv object in apps/web/src/test/setup/setup-env.ts.

🧰 Tools
🪛 Checkov (3.2.334)

[medium] 15-16: Basic Auth Credentials

(CKV_SECRET_4)

🤖 Prompt for AI Agents
In @.github/workflows/test-web.yml around lines 12 - 23, Add NEXTAUTH_SECRET to
the test environment variables so unsubscribe hash verification succeeds: set
NEXTAUTH_SECRET: "test-secret" in the env block currently defining
NODE_ENV/NEXTAUTH_URL/... in the CI workflow (the .github workflow env) and also
add NEXTAUTH_SECRET: "test-secret" to the defaultEnv object in the test setup
(the apps/web test/setup/setup-env.ts file) so functions createUnsubUrl,
createOneClickUnsubUrl and unsubscribeContactFromLink have the secret during
integration tests.

getCost,
getUsageDate,
getUsageTimestamp,
getUsageUinits,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'getUsageUinits' --type=ts

Repository: usesend/useSend

Length of output: 473


🏁 Script executed:

sed -n '55,65p' apps/web/src/lib/usage.ts

Repository: usesend/useSend

Length of output: 317


Rename misspelled function: getUsageUinitsgetUsageUnits.

The function definition in ~/lib/usage.ts contains a typo (Uinits instead of Units). Update the function name and its two call sites in usage-job.ts and this test file.

🤖 Prompt for AI Agents
In `@apps/web/src/lib/usage.unit.test.ts` at line 7, Rename the misspelled
function getUsageUinits to getUsageUnits in the implementation and update both
call sites accordingly (the test referencing getUsageUinits in this test file
and the usage-job.ts call). Search for the symbol getUsageUinits, rename the
function declaration to getUsageUnits, and replace all references (including
imports and tests) so imports, calls, and exports use getUsageUnits
consistently.

AWS_ACCESS_KEY: "test-access-key",
AWS_SECRET_KEY: "test-secret-key",
AWS_DEFAULT_REGION: "us-east-1",
NEXT_PUBLIC_IS_CLOUD: "true",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

NEXT_PUBLIC_IS_CLOUD value inconsistent with .env.test.example.

This file defaults NEXT_PUBLIC_IS_CLOUD to "true", but apps/web/.env.test.example sets it to "false". Since setup-env.ts is the runtime bootstrap that applies when no env file is loaded, tests will behave as cloud-mode by default, which may surprise contributors who expect the example file to reflect the actual defaults.

Pick one canonical value and align both files.

🤖 Prompt for AI Agents
In `@apps/web/src/test/setup/setup-env.ts` at line 9, The default value for
NEXT_PUBLIC_IS_CLOUD in setup-env.ts is "true" but .env.test.example uses
"false", causing inconsistent test defaults; update the NEXT_PUBLIC_IS_CLOUD
entry in apps/web/src/test/setup/setup-env.ts to match the canonical value
("false") used in apps/web/.env.test.example so tests and example envs align,
ensuring the exported/default env object that contains NEXT_PUBLIC_IS_CLOUD uses
"false".

Comment on lines +4 to +27
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: "node",
globals: true,
setupFiles: [
"./src/test/setup/setup-env.ts",
"./src/test/setup/setup-tests.ts",
],
clearMocks: true,
restoreMocks: true,
mockReset: true,
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/test/**",
"src/env.js",
],
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Base config has no include filter — bare vitest run will match all test files.

The test script (vitest run) uses this base config, which has no include pattern. Vitest's default glob will match *.integration.test.ts files too. If integration tests don't guard themselves with an early RUN_INTEGRATION check, they'll run (and likely fail) without infrastructure.

Consider adding an explicit include that excludes integration tests, or an exclude for integration patterns, so that the bare test command is safe to run without Docker services:

Suggested change
   test: {
     environment: "node",
     globals: true,
+    include: ["src/**/*.test.ts"],
+    exclude: ["src/**/*.integration.test.ts"],
     setupFiles: [
🤖 Prompt for AI Agents
In `@apps/web/vitest.config.ts` around lines 4 - 27, The test config in
defineConfig (the test block in vitest.config.ts) lacks an explicit
include/exclude so bare "vitest run" will pick up integration tests; update the
test block (where test.environment, globals, setupFiles, etc. are defined) to
add either an include array that only matches unit tests (e.g.,
"src/**/*.test.{ts,tsx}" and/or "src/**/*.spec.{ts,tsx}") or add an exclude
pattern for integration tests (e.g., "**/*.integration.test.{ts,tsx}") so
integration tests are not run by default without infrastructure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant