From 828893643a112d21e9b5b462866397500ec1c412 Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Tue, 23 Dec 2025 22:30:41 +0000 Subject: [PATCH 1/4] Refactor platform and service module structure Moved platform-related code from `src/platform` to `src/lib` and service modules from `src/*` to `src/services/*` for improved organization and separation of concerns. Updated all imports and references throughout the codebase and tests to reflect the new structure. --- README.md | 6 +++--- src/{ => app}/index.ts | 10 +++++----- src/{platform => lib}/adapters/cloudflare/cache.ts | 0 .../adapters/cloudflare/database.ts | 0 .../adapters/cloudflare/environment.ts | 0 src/{platform => lib}/adapters/cloudflare/index.ts | 0 src/{platform => lib}/adapters/node/cache.ts | 0 src/{platform => lib}/adapters/node/database.ts | 0 src/{platform => lib}/adapters/node/environment.ts | 0 src/{platform => lib}/adapters/node/index.ts | 0 src/{platform => lib}/context.ts | 0 src/{platform => lib}/interfaces.ts | 0 src/{platform => lib}/middleware.ts | 0 src/{ => services}/central-alerts/v1/README.md | 0 src/{ => services}/central-alerts/v1/database.ts | 2 +- src/{ => services}/central-alerts/v1/db/init.sql | 0 src/{ => services}/central-alerts/v1/index.ts | 2 +- src/{ => services}/central-alerts/v1/interfaces.ts | 0 .../central-alerts/v1/scripts/init-db.ts | 0 src/{ => services}/releases/v1/README.md | 0 src/{ => services}/releases/v1/index.ts | 2 +- src/{ => services}/releases/v1/interfaces.ts | 0 src/{ => services}/versions/v1/README.md | 0 src/{ => services}/versions/v1/index.ts | 4 ++-- src/{ => services}/versions/v1/interfaces.ts | 0 test/fixtures/mock-adapters.ts | 2 +- test/fixtures/releases.ts | 2 +- test/integration/versions.test.ts | 2 +- test/node/bindings.test.ts | 2 +- test/node/cache.test.ts | 2 +- test/unit/central-alerts/database.test.ts | 2 +- test/unit/central-alerts/v1/index.test.ts | 2 +- test/unit/index.test.ts | 2 +- test/unit/releases/v1/index.test.ts | 12 +++++++----- test/unit/versions/v1/errors.test.ts | 2 +- test/unit/versions/v1/index.test.ts | 2 +- test/unit/versions/v1/middleware.test.ts | 2 +- test/utils/d1-mock.ts | 2 +- test/utils/test-types.ts | 4 ++-- wrangler.jsonc | 2 +- 40 files changed, 35 insertions(+), 33 deletions(-) rename src/{ => app}/index.ts (81%) rename src/{platform => lib}/adapters/cloudflare/cache.ts (100%) rename src/{platform => lib}/adapters/cloudflare/database.ts (100%) rename src/{platform => lib}/adapters/cloudflare/environment.ts (100%) rename src/{platform => lib}/adapters/cloudflare/index.ts (100%) rename src/{platform => lib}/adapters/node/cache.ts (100%) rename src/{platform => lib}/adapters/node/database.ts (100%) rename src/{platform => lib}/adapters/node/environment.ts (100%) rename src/{platform => lib}/adapters/node/index.ts (100%) rename src/{platform => lib}/context.ts (100%) rename src/{platform => lib}/interfaces.ts (100%) rename src/{platform => lib}/middleware.ts (100%) rename src/{ => services}/central-alerts/v1/README.md (100%) rename src/{ => services}/central-alerts/v1/database.ts (95%) rename src/{ => services}/central-alerts/v1/db/init.sql (100%) rename src/{ => services}/central-alerts/v1/index.ts (93%) rename src/{ => services}/central-alerts/v1/interfaces.ts (100%) rename src/{ => services}/central-alerts/v1/scripts/init-db.ts (100%) rename src/{ => services}/releases/v1/README.md (100%) rename src/{ => services}/releases/v1/index.ts (96%) rename src/{ => services}/releases/v1/interfaces.ts (100%) rename src/{ => services}/versions/v1/README.md (100%) rename src/{ => services}/versions/v1/index.ts (98%) rename src/{ => services}/versions/v1/interfaces.ts (100%) diff --git a/README.md b/README.md index 39c679c..f5128d7 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ 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. +- **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/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 100% rename from src/central-alerts/v1/README.md rename to src/services/central-alerts/v1/README.md 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/fixtures/mock-adapters.ts b/test/fixtures/mock-adapters.ts index 293eba4..2125e5b 100644 --- a/test/fixtures/mock-adapters.ts +++ b/test/fixtures/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/fixtures/releases.ts index 0102f21..03955c0 100644 --- a/test/fixtures/releases.ts +++ b/test/fixtures/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/integration/versions.test.ts b/test/integration/versions.test.ts index 34c1536..a6547b6 100644 --- a/test/integration/versions.test.ts +++ b/test/integration/versions.test.ts @@ -4,7 +4,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../src"; +import app from "../../src/app"; import { mockGitHubReleases, mockComposerJson diff --git a/test/node/bindings.test.ts b/test/node/bindings.test.ts index 55f30df..ded0b09 100644 --- a/test/node/bindings.test.ts +++ b/test/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/node/cache.test.ts index 8e17382..416c0a3 100644 --- a/test/node/cache.test.ts +++ b/test/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/unit/central-alerts/database.test.ts b/test/unit/central-alerts/database.test.ts index 6e09439..a5f1429 100644 --- a/test/unit/central-alerts/database.test.ts +++ b/test/unit/central-alerts/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 { diff --git a/test/unit/central-alerts/v1/index.test.ts b/test/unit/central-alerts/v1/index.test.ts index 5fbd592..0008d95 100644 --- a/test/unit/central-alerts/v1/index.test.ts +++ b/test/unit/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/index.test.ts b/test/unit/index.test.ts index 3c18afa..5d704dc 100644 --- a/test/unit/index.test.ts +++ b/test/unit/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/unit/releases/v1/index.test.ts b/test/unit/releases/v1/index.test.ts index 87bc53e..cd76567 100644 --- a/test/unit/releases/v1/index.test.ts +++ b/test/unit/releases/v1/index.test.ts @@ -4,16 +4,18 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockReleases } from "../../../fixtures/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/unit/versions/v1/errors.test.ts index 03152a8..6ada66e 100644 --- a/test/unit/versions/v1/errors.test.ts +++ b/test/unit/versions/v1/errors.test.ts @@ -4,7 +4,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson diff --git a/test/unit/versions/v1/index.test.ts b/test/unit/versions/v1/index.test.ts index b1f5a04..eac337e 100644 --- a/test/unit/versions/v1/index.test.ts +++ b/test/unit/versions/v1/index.test.ts @@ -4,7 +4,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, diff --git a/test/unit/versions/v1/middleware.test.ts b/test/unit/versions/v1/middleware.test.ts index 3dabbd9..87ba0f4 100644 --- a/test/unit/versions/v1/middleware.test.ts +++ b/test/unit/versions/v1/middleware.test.ts @@ -4,7 +4,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; -import app from "../../../../src"; +import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson 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/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": { From a0a37bc6f0e724b8af156870996d4d8b34395c7c Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Tue, 23 Dec 2025 23:33:54 +0000 Subject: [PATCH 2/4] Refactor test directory structure and update imports Moved and renamed test files to improve organization, grouping mocks and adapters under `test/mocks` and `test/lib/adapters/node`. Updated import paths in affected test files. Adjusted Vitest configuration to match new test locations and exclude Node.js adapter tests from Cloudflare Workers environment. Added integration test for app and removed obsolete README. --- package.json | 2 +- test/{unit => app}/index.test.ts | 0 test/integration/app.test.ts | 299 ++++++++++++++++++ .../index.test.ts} | 8 +- test/{ => lib/adapters}/node/bindings.test.ts | 2 +- test/{ => lib/adapters}/node/cache.test.ts | 2 +- test/mocks/README.md | 13 + test/{fixtures => mocks}/github-releases.ts | 0 test/{fixtures => mocks}/mock-adapters.ts | 0 test/{fixtures => mocks}/releases.ts | 0 test/node/README.md | 32 -- .../central-alerts/v1}/database.test.ts | 5 +- .../central-alerts/v1/index.test.ts | 0 .../releases/v1/index.test.ts | 2 +- .../versions/v1/errors.test.ts | 2 +- .../versions/v1/index.test.ts | 2 +- .../versions/v1/middleware.test.ts | 2 +- vitest.config.ts | 2 +- vitest.node.config.ts | 2 +- 19 files changed, 327 insertions(+), 48 deletions(-) rename test/{unit => app}/index.test.ts (100%) create mode 100644 test/integration/app.test.ts rename test/integration/{versions.test.ts => versions/index.test.ts} (98%) rename test/{ => lib/adapters}/node/bindings.test.ts (96%) rename test/{ => lib/adapters}/node/cache.test.ts (98%) create mode 100644 test/mocks/README.md rename test/{fixtures => mocks}/github-releases.ts (100%) rename test/{fixtures => mocks}/mock-adapters.ts (100%) rename test/{fixtures => mocks}/releases.ts (100%) delete mode 100644 test/node/README.md rename test/{unit/central-alerts => services/central-alerts/v1}/database.test.ts (97%) rename test/{unit => services}/central-alerts/v1/index.test.ts (100%) rename test/{unit => services}/releases/v1/index.test.ts (98%) rename test/{unit => services}/versions/v1/errors.test.ts (99%) rename test/{unit => services}/versions/v1/index.test.ts (99%) rename test/{unit => services}/versions/v1/middleware.test.ts (99%) 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/test/unit/index.test.ts b/test/app/index.test.ts similarity index 100% rename from test/unit/index.test.ts rename to test/app/index.test.ts 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 a6547b6..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/app"; +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 ded0b09..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/lib/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 416c0a3..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/lib/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 100% rename from test/fixtures/mock-adapters.ts rename to test/mocks/mock-adapters.ts diff --git a/test/fixtures/releases.ts b/test/mocks/releases.ts similarity index 100% rename from test/fixtures/releases.ts rename to test/mocks/releases.ts 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 a5f1429..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/services/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 100% rename from test/unit/central-alerts/v1/index.test.ts rename to test/services/central-alerts/v1/index.test.ts diff --git a/test/unit/releases/v1/index.test.ts b/test/services/releases/v1/index.test.ts similarity index 98% rename from test/unit/releases/v1/index.test.ts rename to test/services/releases/v1/index.test.ts index cd76567..076986b 100644 --- a/test/unit/releases/v1/index.test.ts +++ b/test/services/releases/v1/index.test.ts @@ -5,7 +5,7 @@ import { waitOnExecutionContext } from "cloudflare:test"; import app from "../../../../src/app"; -import { mockReleases } from "../../../fixtures/releases"; +import { mockReleases } from "../../../mocks/releases"; import { suppressConsole } from "../../../utils/mock-helpers"; import type { ReleasesResponse } from "../../../utils/test-types"; import { getReleases } from "../../../../src/services/versions/v1"; 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 6ada66e..611e7e2 100644 --- a/test/unit/versions/v1/errors.test.ts +++ b/test/services/versions/v1/errors.test.ts @@ -8,7 +8,7 @@ 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 eac337e..a62cbd3 100644 --- a/test/unit/versions/v1/index.test.ts +++ b/test/services/versions/v1/index.test.ts @@ -9,7 +9,7 @@ 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 87ba0f4..7930cb3 100644 --- a/test/unit/versions/v1/middleware.test.ts +++ b/test/services/versions/v1/middleware.test.ts @@ -8,7 +8,7 @@ import app from "../../../../src/app"; import { mockGitHubReleases, mockComposerJson -} from "../../../fixtures/github-releases"; +} from "../../../mocks/github-releases"; import { suppressConsole, setupGitHubApiMock 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 } }); From 97199e6541a5fca05d70fcedf6968598dac67c4b Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Tue, 23 Dec 2025 23:39:20 +0000 Subject: [PATCH 3/4] Update documentation paths for service modules Corrected references to application logic and setup script paths in README files to reflect the updated directory structure under src/services. --- README.md | 2 +- src/services/central-alerts/v1/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5128d7..8463f92 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ 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. +- **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/lib/adapters/cloudflare`: Real implementations using KV and D1. diff --git a/src/services/central-alerts/v1/README.md b/src/services/central-alerts/v1/README.md index 055bc3d..1f6728a 100644 --- a/src/services/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/`. From ef4fb6449eb039a13922c57f4022d9957988fd25 Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Tue, 23 Dec 2025 23:44:45 +0000 Subject: [PATCH 4/4] Update CI workflow test and action usage Removes version comments from actions/checkout and actions/setup-node steps for clarity. Changes test command from 'npm test' to 'npm run test:all' to run all tests in the CI workflow. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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