diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33a2303..51ca101 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout Code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f with: node-version: "24" cache: "npm" @@ -28,4 +28,4 @@ jobs: run: npm run lint - name: Run Tests - run: npm test + run: npm run test:all diff --git a/README.md b/README.md index 39c679c..8463f92 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ The worker exposes three main services: We've structured the app to separate the core logic from the specific runtime environment (Cloudflare, Node, etc.). -- **Application Logic**: Found in `src/versions`, `src/central-alerts`, etc. These feature modules don't know they are running on Cloudflare. -- **Platform Layer**: Located in `src/platform`. This defines interfaces for things like Cache, Database, and Environment variables. +- **Application Logic**: Found in `src/services/versions/v1`, `src/services/central-alerts/v1`, etc. These feature modules don't know they are running on Cloudflare. +- **Platform Layer**: Located in `src/lib`. This defines interfaces for things like Cache, Database, and Environment variables. - **Adapters**: - - `src/platform/adapters/cloudflare`: Real implementations using KV and D1. - - `src/platform/adapters/node`: Reference implementations (useful for testing or alternative deployments). +- `src/lib/adapters/cloudflare`: Real implementations using KV and D1. +- `src/lib/adapters/node`: Reference implementations (useful for testing or alternative deployments). ## APIs diff --git a/package.json b/package.json index 8bbccad..bb6f7e1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "wrangler dev", "format": "prettier --write .", "format:check": "prettier --check .", - "init:db": "npx tsx src/central-alerts/v1/scripts/init-db.ts", + "init:db": "npx tsx src/services/central-alerts/v1/scripts/init-db.ts", "lint": "eslint", "lint:fix": "eslint --fix", "test": "vitest run", diff --git a/src/index.ts b/src/app/index.ts similarity index 81% rename from src/index.ts rename to src/app/index.ts index bd8b545..11845ae 100644 --- a/src/index.ts +++ b/src/app/index.ts @@ -1,11 +1,11 @@ import { Hono } from "hono"; import { contextStorage } from "hono/context-storage"; import { HTTPException } from "hono/http-exception"; -import centralAlertsV1 from "./central-alerts/v1"; -import releasesV1 from "./releases/v1"; -import versionsV1 from "./versions/v1"; -import { platformMiddleware } from "./platform/middleware"; -import { createCloudflareBindings } from "./platform/adapters/cloudflare"; +import centralAlertsV1 from "../services/central-alerts/v1"; +import releasesV1 from "../services/releases/v1"; +import versionsV1 from "../services/versions/v1"; +import { platformMiddleware } from "../lib/middleware"; +import { createCloudflareBindings } from "../lib/adapters/cloudflare"; const app = new Hono<{ Bindings: CloudflareBindings }>(); diff --git a/src/platform/adapters/cloudflare/cache.ts b/src/lib/adapters/cloudflare/cache.ts similarity index 100% rename from src/platform/adapters/cloudflare/cache.ts rename to src/lib/adapters/cloudflare/cache.ts diff --git a/src/platform/adapters/cloudflare/database.ts b/src/lib/adapters/cloudflare/database.ts similarity index 100% rename from src/platform/adapters/cloudflare/database.ts rename to src/lib/adapters/cloudflare/database.ts diff --git a/src/platform/adapters/cloudflare/environment.ts b/src/lib/adapters/cloudflare/environment.ts similarity index 100% rename from src/platform/adapters/cloudflare/environment.ts rename to src/lib/adapters/cloudflare/environment.ts diff --git a/src/platform/adapters/cloudflare/index.ts b/src/lib/adapters/cloudflare/index.ts similarity index 100% rename from src/platform/adapters/cloudflare/index.ts rename to src/lib/adapters/cloudflare/index.ts diff --git a/src/platform/adapters/node/cache.ts b/src/lib/adapters/node/cache.ts similarity index 100% rename from src/platform/adapters/node/cache.ts rename to src/lib/adapters/node/cache.ts diff --git a/src/platform/adapters/node/database.ts b/src/lib/adapters/node/database.ts similarity index 100% rename from src/platform/adapters/node/database.ts rename to src/lib/adapters/node/database.ts diff --git a/src/platform/adapters/node/environment.ts b/src/lib/adapters/node/environment.ts similarity index 100% rename from src/platform/adapters/node/environment.ts rename to src/lib/adapters/node/environment.ts diff --git a/src/platform/adapters/node/index.ts b/src/lib/adapters/node/index.ts similarity index 100% rename from src/platform/adapters/node/index.ts rename to src/lib/adapters/node/index.ts diff --git a/src/platform/context.ts b/src/lib/context.ts similarity index 100% rename from src/platform/context.ts rename to src/lib/context.ts diff --git a/src/platform/interfaces.ts b/src/lib/interfaces.ts similarity index 100% rename from src/platform/interfaces.ts rename to src/lib/interfaces.ts diff --git a/src/platform/middleware.ts b/src/lib/middleware.ts similarity index 100% rename from src/platform/middleware.ts rename to src/lib/middleware.ts diff --git a/src/central-alerts/v1/README.md b/src/services/central-alerts/v1/README.md similarity index 95% rename from src/central-alerts/v1/README.md rename to src/services/central-alerts/v1/README.md index 055bc3d..1f6728a 100644 --- a/src/central-alerts/v1/README.md +++ b/src/services/central-alerts/v1/README.md @@ -46,4 +46,4 @@ The `type` field accepts: `success`, `info`, `warning`, `danger` ## Database -Uses D1 database binding `DB_CENTRAL_ALERTS`. Initialize with the setup script in `src/central-alerts/v1/scripts/`. +Uses D1 database binding `DB_CENTRAL_ALERTS`. Initialize with the setup script in `src/services/central-alerts/v1/scripts/`. diff --git a/src/central-alerts/v1/database.ts b/src/services/central-alerts/v1/database.ts similarity index 95% rename from src/central-alerts/v1/database.ts rename to src/services/central-alerts/v1/database.ts index 350e89f..de9f2cc 100644 --- a/src/central-alerts/v1/database.ts +++ b/src/services/central-alerts/v1/database.ts @@ -1,5 +1,5 @@ import { CentralAlert } from "./interfaces"; -import { IDatabase } from "../../platform/interfaces"; +import { IDatabase } from "../../../lib/interfaces"; export interface DatabaseError { message: string; diff --git a/src/central-alerts/v1/db/init.sql b/src/services/central-alerts/v1/db/init.sql similarity index 100% rename from src/central-alerts/v1/db/init.sql rename to src/services/central-alerts/v1/db/init.sql diff --git a/src/central-alerts/v1/index.ts b/src/services/central-alerts/v1/index.ts similarity index 93% rename from src/central-alerts/v1/index.ts rename to src/services/central-alerts/v1/index.ts index 07d34ca..d461b69 100644 --- a/src/central-alerts/v1/index.ts +++ b/src/services/central-alerts/v1/index.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import { trimTrailingSlash } from "hono/trailing-slash"; import { CentralAlertsDatabase } from "./database"; -import { getPlatform } from "../../platform/middleware"; +import { getPlatform } from "../../../lib/middleware"; const centralAlertsV1 = new Hono<{ Bindings: CloudflareBindings }>(); diff --git a/src/central-alerts/v1/interfaces.ts b/src/services/central-alerts/v1/interfaces.ts similarity index 100% rename from src/central-alerts/v1/interfaces.ts rename to src/services/central-alerts/v1/interfaces.ts diff --git a/src/central-alerts/v1/scripts/init-db.ts b/src/services/central-alerts/v1/scripts/init-db.ts similarity index 100% rename from src/central-alerts/v1/scripts/init-db.ts rename to src/services/central-alerts/v1/scripts/init-db.ts diff --git a/src/releases/v1/README.md b/src/services/releases/v1/README.md similarity index 100% rename from src/releases/v1/README.md rename to src/services/releases/v1/README.md diff --git a/src/releases/v1/index.ts b/src/services/releases/v1/index.ts similarity index 96% rename from src/releases/v1/index.ts rename to src/services/releases/v1/index.ts index 48571c2..b2e8e07 100644 --- a/src/releases/v1/index.ts +++ b/src/services/releases/v1/index.ts @@ -7,7 +7,7 @@ import { trimTrailingSlash } from "hono/trailing-slash"; import { diff as semverDiff, compare as semverCompare } from "semver"; import { FOSSBillingVersion } from "./interfaces"; import { getReleases } from "../../versions/v1"; -import { getPlatform } from "../../platform/middleware"; +import { getPlatform } from "../../../lib/middleware"; const releasesV1 = new Hono<{ Bindings: CloudflareBindings; strict: true }>(); diff --git a/src/releases/v1/interfaces.ts b/src/services/releases/v1/interfaces.ts similarity index 100% rename from src/releases/v1/interfaces.ts rename to src/services/releases/v1/interfaces.ts diff --git a/src/versions/v1/README.md b/src/services/versions/v1/README.md similarity index 100% rename from src/versions/v1/README.md rename to src/services/versions/v1/README.md diff --git a/src/versions/v1/index.ts b/src/services/versions/v1/index.ts similarity index 98% rename from src/versions/v1/index.ts rename to src/services/versions/v1/index.ts index 28ef89b..1ca28b1 100644 --- a/src/versions/v1/index.ts +++ b/src/services/versions/v1/index.ts @@ -13,8 +13,8 @@ import { valid as semverValid } from "semver"; import { Releases, ReleaseDetails } from "./interfaces"; -import { getPlatform } from "../../platform/middleware"; -import { ICache } from "../../platform/interfaces"; +import { getPlatform } from "../../../lib/middleware"; +import { ICache } from "../../../lib/interfaces"; // Cache for UPDATE_TOKEN to avoid repeated KV lookups let updateTokenCache: string | null = null; diff --git a/src/versions/v1/interfaces.ts b/src/services/versions/v1/interfaces.ts similarity index 100% rename from src/versions/v1/interfaces.ts rename to src/services/versions/v1/interfaces.ts diff --git a/test/unit/index.test.ts b/test/app/index.test.ts similarity index 99% rename from test/unit/index.test.ts rename to test/app/index.test.ts index 3c18afa..5d704dc 100644 --- a/test/unit/index.test.ts +++ b/test/app/index.test.ts @@ -10,7 +10,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../src/index"; +import app from "../../src/app/index"; import { mockD1Database } from "../utils/d1-mock"; import { ApiResponse, diff --git a/test/integration/app.test.ts b/test/integration/app.test.ts new file mode 100644 index 0000000..cb69be6 --- /dev/null +++ b/test/integration/app.test.ts @@ -0,0 +1,299 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + env, + createExecutionContext, + waitOnExecutionContext +} from "cloudflare:test"; +import app from "../../src/app/index"; +import { mockGitHubReleases, mockComposerJson } from "../mocks/github-releases"; +import { setupGitHubApiMock } from "../utils/mock-helpers"; +import { mockD1Database } from "../utils/d1-mock"; +import { + ApiResponse, + CentralAlertsResponse, + MockGitHubRequest, + ReleasesResponse, + VersionsResponse +} from "../utils/test-types"; + +vi.mock("@octokit/request", () => ({ + request: vi.fn() +})); + +import { request as ghRequest } from "@octokit/request"; + +describe("FOSSBilling API Worker - Full App Integration", () => { + beforeEach(async () => { + await env.CACHE_KV.delete("gh-fossbilling-releases"); + await env.AUTH_KV.put("UPDATE_TOKEN", "test-update-token-12345"); + + env.DB_CENTRAL_ALERTS = mockD1Database; + + vi.clearAllMocks(); + setupGitHubApiMock( + vi.mocked(ghRequest) as MockGitHubRequest, + mockGitHubReleases, + mockComposerJson + ); + }); + + describe("Service Discovery and Routing", () => { + it("should route to all three services correctly", async () => { + const ctx1 = createExecutionContext(); + const versionsResponse = await app.request("/versions/v1", {}, env, ctx1); + await waitOnExecutionContext(ctx1); + + const ctx2 = createExecutionContext(); + const releasesResponse = await app.request("/releases/v1", {}, env, ctx2); + await waitOnExecutionContext(ctx2); + + const ctx3 = createExecutionContext(); + const alertsResponse = await app.request( + "/central-alerts/v1/list", + {}, + env, + ctx3 + ); + await waitOnExecutionContext(ctx3); + + expect(versionsResponse.status).toBe(200); + expect(releasesResponse.status).toBe(200); + + const versionsData = (await versionsResponse.json()) as VersionsResponse; + const releasesData = (await releasesResponse.json()) as ReleasesResponse; + const alertsData = (await alertsResponse.json()) as CentralAlertsResponse; + + expect(versionsData).toHaveProperty("result"); + expect(versionsData).toHaveProperty("error_code", 0); + expect(releasesData).toHaveProperty("result"); + expect(releasesData.result.versions).toBeInstanceOf(Array); + expect(alertsData).toHaveProperty("result"); + }); + + it("should return 404 for unknown routes", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/unknown/path", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(404); + }); + + it("should return service information at root path", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = (await response.json()) as ApiResponse; + expect(data.result).toBe(null); + expect(data.error_code).toBe(0); + expect(data.message).toContain("FOSSBilling API Worker"); + }); + }); + + describe("Cross-Service Communication", () => { + it("should allow releases service to fetch from versions service", async () => { + const originalFetch = global.fetch; + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ + result: { + "0.5.0": { version: "0.5.0" }, + "0.6.0": { version: "0.6.0" } + } + }) + } as Response); + + const ctx = createExecutionContext(); + const response = await app.request("/releases/v1", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = (await response.json()) as ReleasesResponse; + expect(data.result.versions.length).toBeGreaterThan(0); + + global.fetch = originalFetch; + }); + }); + + describe("Context Storage Middleware", () => { + it("should make environment bindings available to all services", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/versions/v1", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + + const cached = await env.CACHE_KV.get("gh-fossbilling-releases"); + expect(cached).toBeTruthy(); + }); + + it("should provide KV namespace for central alerts", async () => { + const ctx = createExecutionContext(); + const response = await app.request( + "/central-alerts/v1/list", + {}, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = (await response.json()) as CentralAlertsResponse; + expect(data.result.alerts).toBeInstanceOf(Array); + }); + + it("should handle UPDATE_TOKEN from KV storage", async () => { + const ctx = createExecutionContext(); + const response = await app.request( + "/versions/v1/update", + { + headers: { + Authorization: "Bearer test-update-token-12345" + } + }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + }); + }); + + describe("Error Handling Across Services", () => { + it("should handle 404 for invalid service routes", async () => { + const endpoints = [ + "/versions/v1/invalid-endpoint", + "/releases/v1/invalid", + "/central-alerts/v1/invalid" + ]; + + for (const endpoint of endpoints) { + const ctx = createExecutionContext(); + const response = await app.request(endpoint, {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(404); + } + }); + + it("should handle unauthorized update requests", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/versions/v1/update", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(401); + }); + }); + + describe("Consistent API Response Format", () => { + it("should maintain consistent response format across all services", async () => { + const endpoints = [ + { path: "/versions/v1", fields: ["result", "error_code", "message"] }, + { path: "/central-alerts/v1/list", fields: ["result"] }, + { path: "/releases/v1", fields: ["result", "error"] } + ]; + + for (const { path, fields } of endpoints) { + const ctx = createExecutionContext(); + const response = await app.request(path, {}, env, ctx); + await waitOnExecutionContext(ctx); + + const data = await response.json(); + for (const field of fields) { + expect(data).toHaveProperty(field); + } + } + }); + }); + + describe("HTTP Method Handling", () => { + it("should handle GET requests across all services", async () => { + const endpoints = [ + "/versions/v1", + "/versions/v1/latest", + "/central-alerts/v1/list", + "/releases/v1" + ]; + + for (const endpoint of endpoints) { + const ctx = createExecutionContext(); + const response = await app.request( + endpoint, + { method: "GET" }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect([200, 301]).toContain(response.status); + } + }); + + it("should return 404 for unsupported methods", async () => { + const ctx = createExecutionContext(); + const response = await app.request( + "/versions/v1", + { method: "POST" }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(404); + }); + + it("should handle OPTIONS preflight requests", async () => { + const endpoints = ["/versions/v1", "/releases/v1"]; + + for (const endpoint of endpoints) { + const ctx = createExecutionContext(); + const response = await app.request( + endpoint, + { method: "OPTIONS" }, + env, + ctx + ); + await waitOnExecutionContext(ctx); + + expect([204, 405]).toContain(response.status); + if (response.status === 204) { + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + } + } + }); + }); + + describe("Headers and Middleware", () => { + it("should include CORS headers on all responses", async () => { + const endpoints = ["/versions/v1", "/releases/v1"]; + + for (const endpoint of endpoints) { + const ctx = createExecutionContext(); + const response = await app.request(endpoint, {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + } + }); + + it("should include deprecation headers on releases service", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/releases/v1", {}, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.headers.get("Deprecation")).toBe("true"); + expect(response.headers.get("Sunset")).toBeTruthy(); + }); + + it("should include ETag headers on cacheable responses", async () => { + const ctx = createExecutionContext(); + const response = await app.request("/versions/v1", {}, env, ctx); + await waitOnExecutionContext(ctx); + + const etag = response.headers.get("ETag"); + expect(etag).toBeTruthy(); + }); + }); +}); diff --git a/test/integration/versions.test.ts b/test/integration/versions/index.test.ts similarity index 98% rename from test/integration/versions.test.ts rename to test/integration/versions/index.test.ts index 34c1536..df03a65 100644 --- a/test/integration/versions.test.ts +++ b/test/integration/versions/index.test.ts @@ -4,17 +4,17 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../src"; +import app from "../../../src/app"; import { mockGitHubReleases, mockComposerJson -} from "../fixtures/github-releases"; -import { setupGitHubApiMock } from "../utils/mock-helpers"; +} from "../../mocks/github-releases"; +import { setupGitHubApiMock } from "../../utils/mock-helpers"; import { MockGitHubRequest, VersionsResponse, ApiResponse -} from "../utils/test-types"; +} from "../../utils/test-types"; vi.mock("@octokit/request", () => ({ request: vi.fn() diff --git a/test/node/bindings.test.ts b/test/lib/adapters/node/bindings.test.ts similarity index 96% rename from test/node/bindings.test.ts rename to test/lib/adapters/node/bindings.test.ts index 55f30df..17f6593 100644 --- a/test/node/bindings.test.ts +++ b/test/lib/adapters/node/bindings.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, afterEach } from "vitest"; -import { createNodeBindings } from "../../src/platform/adapters/node/index"; +import { createNodeBindings } from "../../../../src/lib/adapters/node/index"; describe("createNodeBindings - path normalization", () => { afterEach(() => { diff --git a/test/node/cache.test.ts b/test/lib/adapters/node/cache.test.ts similarity index 98% rename from test/node/cache.test.ts rename to test/lib/adapters/node/cache.test.ts index 8e17382..f758f0c 100644 --- a/test/node/cache.test.ts +++ b/test/lib/adapters/node/cache.test.ts @@ -5,7 +5,7 @@ import { SQLiteCacheAdapter, createMemoryCache, createFileCache -} from "../../src/platform/adapters/node/cache"; +} from "../../../../src/lib/adapters/node/cache"; describe("SQLiteCacheAdapter - Memory", () => { let cache: SQLiteCacheAdapter; diff --git a/test/mocks/README.md b/test/mocks/README.md new file mode 100644 index 0000000..f9e2cf7 --- /dev/null +++ b/test/mocks/README.md @@ -0,0 +1,13 @@ +# Test Mocks + +This directory contains mock implementations and mock data used across the test suite. + +## Contents + +- `mock-adapters.ts` - Mock implementations for database, cache, and environment adapters +- `github-releases.ts` - Mock GitHub API release data +- `releases.ts` - Processed release mock data for testing releases API + +## Usage + +Mock adapters are used in unit tests to simulate Cloudflare Workers bindings (D1, KV, environment variables) without requiring actual Cloudflare infrastructure. diff --git a/test/fixtures/github-releases.ts b/test/mocks/github-releases.ts similarity index 100% rename from test/fixtures/github-releases.ts rename to test/mocks/github-releases.ts diff --git a/test/fixtures/mock-adapters.ts b/test/mocks/mock-adapters.ts similarity index 98% rename from test/fixtures/mock-adapters.ts rename to test/mocks/mock-adapters.ts index 293eba4..2125e5b 100644 --- a/test/fixtures/mock-adapters.ts +++ b/test/mocks/mock-adapters.ts @@ -3,7 +3,7 @@ import { IPreparedStatement, ICache, IEnvironment -} from "../../src/platform/interfaces"; +} from "../../src/lib/interfaces"; export class MockDatabaseAdapter implements IDatabase { private data = new Map(); diff --git a/test/fixtures/releases.ts b/test/mocks/releases.ts similarity index 96% rename from test/fixtures/releases.ts rename to test/mocks/releases.ts index 0102f21..03955c0 100644 --- a/test/fixtures/releases.ts +++ b/test/mocks/releases.ts @@ -6,7 +6,7 @@ * @license AGPL-3.0 */ -import { Releases } from "../../src/versions/v1/interfaces"; +import { Releases } from "../../src/services/versions/v1/interfaces"; export const mockReleases: Releases = { "0.5.0": { diff --git a/test/node/README.md b/test/node/README.md deleted file mode 100644 index dc03f6e..0000000 --- a/test/node/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Node.js Adapter Testing - -This directory contains Node.js-specific unit tests for adapters that use Node.js built-in modules (like `node:sqlite`) which are not supported in the Cloudflare Workers test environment. - -## Running Tests - -```bash -# Run only Node.js tests -npm run test:node - -# Run all tests (Cloudflare + Node.js) -npm run test:all - -# Run Cloudflare tests only (default) -npm test -``` - -## Test Structure - -- `test/unit/` - Cloudflare Workers environment tests (via `@cloudflare/vitest-pool-workers`) -- `test/node/` - Node.js environment tests (via Vitest with `environment: "node"`) -- `test/integration/` - Integration tests running in Cloudflare Workers environment - -## Why Separate Test Environments? - -The Cloudflare Workers runtime does not support Node.js built-in modules like: - -- `node:sqlite` -- `node:fs` -- `node:path` - -These tests ensure the SQLite-based adapters work correctly in Node.js environments while keeping the main test suite focused on the Cloudflare Workers deployment target. diff --git a/test/unit/central-alerts/database.test.ts b/test/services/central-alerts/v1/database.test.ts similarity index 97% rename from test/unit/central-alerts/database.test.ts rename to test/services/central-alerts/v1/database.test.ts index 6e09439..0f48a84 100644 --- a/test/unit/central-alerts/database.test.ts +++ b/test/services/central-alerts/v1/database.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll } from "vitest"; -import { CentralAlertsDatabase } from "../../../src/central-alerts/v1/database"; +import { CentralAlertsDatabase } from "../../../../src/services/central-alerts/v1/database"; // Mock D1 Database for testing class MockD1Database { @@ -254,8 +254,7 @@ describe("CentralAlertsDatabase", () => { beforeAll(() => { mockD1 = new MockD1Database(); - // @ts-expect-error - We're mocking the D1 database - db = new CentralAlertsDatabase(mockD1); + db = new CentralAlertsDatabase(mockD1 as never); }); describe("getAllAlerts", () => { diff --git a/test/unit/central-alerts/v1/index.test.ts b/test/services/central-alerts/v1/index.test.ts similarity index 99% rename from test/unit/central-alerts/v1/index.test.ts rename to test/services/central-alerts/v1/index.test.ts index 5fbd592..0008d95 100644 --- a/test/unit/central-alerts/v1/index.test.ts +++ b/test/services/central-alerts/v1/index.test.ts @@ -4,7 +4,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockD1Database } from "../../../utils/d1-mock"; import type { CentralAlertsResponse } from "../../../utils/test-types"; diff --git a/test/unit/releases/v1/index.test.ts b/test/services/releases/v1/index.test.ts similarity index 92% rename from test/unit/releases/v1/index.test.ts rename to test/services/releases/v1/index.test.ts index 87bc53e..076986b 100644 --- a/test/unit/releases/v1/index.test.ts +++ b/test/services/releases/v1/index.test.ts @@ -4,16 +4,18 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; -import { mockReleases } from "../../../fixtures/releases"; +import app from "../../../../src/app"; +import { mockReleases } from "../../../mocks/releases"; import { suppressConsole } from "../../../utils/mock-helpers"; import type { ReleasesResponse } from "../../../utils/test-types"; -import { getReleases } from "../../../../src/versions/v1"; -import { Releases } from "../../../../src/versions/v1/interfaces"; +import { getReleases } from "../../../../src/services/versions/v1"; +import { Releases } from "../../../../src/services/versions/v1/interfaces"; -vi.mock("../../../../src/versions/v1", async (importOriginal) => { +vi.mock("../../../../src/services/versions/v1", async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal< + typeof import("../../../../src/services/versions/v1") + >(); return { ...actual, getReleases: vi.fn() diff --git a/test/unit/versions/v1/errors.test.ts b/test/services/versions/v1/errors.test.ts similarity index 99% rename from test/unit/versions/v1/errors.test.ts rename to test/services/versions/v1/errors.test.ts index 03152a8..611e7e2 100644 --- a/test/unit/versions/v1/errors.test.ts +++ b/test/services/versions/v1/errors.test.ts @@ -4,11 +4,11 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson -} from "../../../fixtures/github-releases"; +} from "../../../mocks/github-releases"; import { suppressConsole } from "../../../utils/mock-helpers"; import { MockGitHubRequest, diff --git a/test/unit/versions/v1/index.test.ts b/test/services/versions/v1/index.test.ts similarity index 99% rename from test/unit/versions/v1/index.test.ts rename to test/services/versions/v1/index.test.ts index b1f5a04..a62cbd3 100644 --- a/test/unit/versions/v1/index.test.ts +++ b/test/services/versions/v1/index.test.ts @@ -4,12 +4,12 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson -} from "../../../fixtures/github-releases"; +} from "../../../mocks/github-releases"; import { suppressConsole, setupGitHubApiMock diff --git a/test/unit/versions/v1/middleware.test.ts b/test/services/versions/v1/middleware.test.ts similarity index 99% rename from test/unit/versions/v1/middleware.test.ts rename to test/services/versions/v1/middleware.test.ts index 3dabbd9..7930cb3 100644 --- a/test/unit/versions/v1/middleware.test.ts +++ b/test/services/versions/v1/middleware.test.ts @@ -4,11 +4,11 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson -} from "../../../fixtures/github-releases"; +} from "../../../mocks/github-releases"; import { suppressConsole, setupGitHubApiMock diff --git a/test/utils/d1-mock.ts b/test/utils/d1-mock.ts index 3bd3121..ae60a96 100644 --- a/test/utils/d1-mock.ts +++ b/test/utils/d1-mock.ts @@ -2,7 +2,7 @@ * Mock D1 Database for testing * Implements the D1Database interface with in-memory storage */ -import { CentralAlert } from "../../src/central-alerts/v1/interfaces"; +import { CentralAlert } from "../../src/services/central-alerts/v1/interfaces"; type DatabaseAlert = Omit & { created_at: string; diff --git a/test/utils/test-types.ts b/test/utils/test-types.ts index e47fdc4..6f6285d 100644 --- a/test/utils/test-types.ts +++ b/test/utils/test-types.ts @@ -3,7 +3,7 @@ */ import { vi } from "vitest"; -import type { CentralAlert } from "../../src/central-alerts/v1/interfaces"; +import type { CentralAlert } from "../../src/services/central-alerts/v1/interfaces"; // API Response Types export interface ApiResponse { @@ -131,7 +131,7 @@ export interface TestEnv { } // Export CentralAlert type -export type { CentralAlert } from "../../src/central-alerts/v1/interfaces"; +export type { CentralAlert } from "../../src/services/central-alerts/v1/interfaces"; // Spy Types export type FetchSpy = ReturnType; diff --git a/vitest.config.ts b/vitest.config.ts index 3e3de81..a46722f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { // Exclude Node.js tests from Cloudflare Workers environment - exclude: ["**/node_modules/**", "**/test/node/**"], + exclude: ["**/node_modules/**", "**/test/lib/adapters/node/**"], // Test timeout configuration testTimeout: 30000, // 30 seconds max per test diff --git a/vitest.node.config.ts b/vitest.node.config.ts index efe1ff4..ff5d943 100644 --- a/vitest.node.config.ts +++ b/vitest.node.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", - include: ["test/node/**/*.test.ts"], + include: ["test/lib/adapters/node/**/*.test.ts"], testTimeout: 10000 } }); diff --git a/wrangler.jsonc b/wrangler.jsonc index 9dd90fb..1e42af5 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -5,7 +5,7 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "api-worker", - "main": "src/index.ts", + "main": "src/app/index.ts", "compatibility_flags": ["nodejs_compat"], "compatibility_date": "2025-09-15", "observability": {