Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ Everything is built on [Hono](https://hono.dev), making it lightweight and fast.

## What it does

The worker exposes three main services:
The worker exposes two main services:

- **Versions Service** (`/versions/v1`)
The source of truth for FOSSBilling updates. It fetches release data from GitHub, caches it for performance, and helps instances decide if they need to update.

- **Central Alerts** (`/central-alerts/v1`)
Allows the project to push critical notifications to all FOSSBilling installations—useful for security hotfixes or major announcements.

- **Releases Service** (`/releases/v1`)
_Legacy._ This is kept around to support older FOSSBilling versions that haven't updated to the new update system yet. It sends deprecation headers and will eventually be removed.

## Architecture

We've structured the app to separate the core logic from the specific runtime environment (Cloudflare, Node, etc.).
Expand Down
2 changes: 0 additions & 2 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Hono } from "hono";
import { contextStorage } from "hono/context-storage";
import { HTTPException } from "hono/http-exception";
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";
Expand All @@ -17,7 +16,6 @@ app.use("*", async (c, next) => {
return middleware(c, next);
});

app.route("/releases/v1", releasesV1);
app.route("/central-alerts/v1", centralAlertsV1);
app.route("/versions/v1", versionsV1);

Expand Down
3 changes: 2 additions & 1 deletion src/services/central-alerts/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { trimTrailingSlash } from "hono/trailing-slash";
import { CentralAlertsDatabase } from "./database";
import { getPlatform } from "../../../lib/middleware";

const centralAlertsV1 = new Hono<{ Bindings: CloudflareBindings }>();

centralAlertsV1.use(trimTrailingSlash());
centralAlertsV1.use("/*", cors({ origin: "*" }), trimTrailingSlash());

centralAlertsV1.get("/list", async (c) => {
const platform = getPlatform(c);
Expand Down
59 changes: 0 additions & 59 deletions src/services/releases/v1/README.md

This file was deleted.

60 changes: 0 additions & 60 deletions src/services/releases/v1/index.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/services/releases/v1/interfaces.ts

This file was deleted.

60 changes: 0 additions & 60 deletions test/app/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import {
ApiResponse,
CentralAlertsResponse,
GitHubContentResponse,
MockFetchResponse,
MockGitHubRequest,
ReleasesResponse,
VersionInfo,
VersionsResponse
} from "../utils/test-types";
Expand Down Expand Up @@ -104,36 +102,6 @@ describe("FOSSBilling API Worker - Main App", () => {
expect(data).toHaveProperty("message");
});

it("should route /releases/v1 to releases service", async () => {
// Mock internal fetch call that releases service makes to versions service
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 MockFetchResponse);

const ctx = createExecutionContext();
const response = await app.request("/releases/v1", {}, env, ctx);
await waitOnExecutionContext(ctx);

expect(response.status).toBe(200);
const data: ReleasesResponse = await response.json();

// Should return releases API response format with deprecation headers
expect(data).toHaveProperty("result");
expect(data).toHaveProperty("error");
expect(response.headers.get("Deprecation")).toBeTruthy();
expect(response.headers.get("Sunset")).toBeTruthy();

// Restore fetch
global.fetch = originalFetch;
});

it("should route /central-alerts/v1/list to central alerts service", async () => {
const ctx = createExecutionContext();
const response = await app.request(
Expand Down Expand Up @@ -233,34 +201,6 @@ describe("FOSSBilling API Worker - Main App", () => {
expect(data.result.version).toBe("0.6.0");
});

it("should allow releases service to fetch from versions service", async () => {
// Mock internal fetch call that releases service makes
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 MockFetchResponse);

const ctx = createExecutionContext();
const response = await app.request("/releases/v1", {}, env, ctx);
await waitOnExecutionContext(ctx);

expect(response.status).toBe(200);
const data: ReleasesResponse = await response.json();

// Releases service internally calls versions service
expect(data.result).toBeTruthy();
expect(data.result.versions.length).toBeGreaterThan(0);

// Restore fetch
global.fetch = originalFetch;
});

it("should allow central alerts service to return static data", async () => {
const ctx = createExecutionContext();
const response = await app.request(
Expand Down
58 changes: 12 additions & 46 deletions test/integration/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
ApiResponse,
CentralAlertsResponse,
MockGitHubRequest,
ReleasesResponse,
VersionsResponse
} from "../utils/test-types";

Expand All @@ -38,35 +37,28 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
});

describe("Service Discovery and Routing", () => {
it("should route to all three services correctly", async () => {
it("should route to all 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
ctx2
);
await waitOnExecutionContext(ctx3);
await waitOnExecutionContext(ctx2);

expect(versionsResponse.status).toBe(200);
expect(releasesResponse.status).toBe(200);
expect(alertsResponse.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");
});

Expand All @@ -92,27 +84,13 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
});

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);

it("should allow services to share cached data", async () => {
const ctx = createExecutionContext();
const response = await app.request("/releases/v1", {}, env, ctx);
await app.request("/versions/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;
const cached = await env.CACHE_KV.get("gh-fossbilling-releases");
expect(cached).toBeTruthy();
});
});

Expand Down Expand Up @@ -165,7 +143,6 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
it("should handle 404 for invalid service routes", async () => {
const endpoints = [
"/versions/v1/invalid-endpoint",
"/releases/v1/invalid",
"/central-alerts/v1/invalid"
];

Expand All @@ -191,8 +168,7 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
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"] }
{ path: "/central-alerts/v1/list", fields: ["result"] }
];

for (const { path, fields } of endpoints) {
Expand All @@ -213,8 +189,7 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
const endpoints = [
"/versions/v1",
"/versions/v1/latest",
"/central-alerts/v1/list",
"/releases/v1"
"/central-alerts/v1/list"
];

for (const endpoint of endpoints) {
Expand Down Expand Up @@ -245,7 +220,7 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
});

it("should handle OPTIONS preflight requests", async () => {
const endpoints = ["/versions/v1", "/releases/v1"];
const endpoints = ["/versions/v1"];

for (const endpoint of endpoints) {
const ctx = createExecutionContext();
Expand All @@ -267,7 +242,7 @@ describe("FOSSBilling API Worker - Full App Integration", () => {

describe("Headers and Middleware", () => {
it("should include CORS headers on all responses", async () => {
const endpoints = ["/versions/v1", "/releases/v1"];
const endpoints = ["/versions/v1", "/central-alerts/v1/list"];

for (const endpoint of endpoints) {
const ctx = createExecutionContext();
Expand All @@ -278,15 +253,6 @@ describe("FOSSBilling API Worker - Full App Integration", () => {
}
});

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);
Expand Down
Loading