feat(gaze): add Gaze subdomain landing page with direct download
Move the Gaze marketing home from the legacy /marketing/gaze route to a dedicated gaze subdomain landing page (src/routes/gaze). The new page adds a richer hero, feature highlights, and a direct .dmg download flow that resolves a signed S3 URL via the tRPC downloads.getDownloadUrl endpoint. Extract the download-resolution logic into a shared, testable downloadAsset helper (src/lib/download-asset) so current and future subdomain landing pages (InputHalo, Lineage, …) share one code path. The helper is pure over an injected DownloadApi + redirect sink, keeping it unit-testable without importing solid-js / CSRF cookie code. Replace the old marketing/gaze route with a permanent 308 redirect to https://gaze.freno.me so existing inbound links keep resolving to the canonical home. Add unit tests covering the tRPC call shape, redirect sink, and error handling.
This commit is contained in:
88
src/lib/download-asset.test.ts
Normal file
88
src/lib/download-asset.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Unit tests for the shared `downloadAsset` helper (task 05).
|
||||
*
|
||||
* The helper is a pure function over an injected `DownloadApi` + redirect sink,
|
||||
* so these tests verify the tRPC call shape, redirect, and error handling
|
||||
* without importing `~/lib/api` (which pulls in solid-js / CSRF cookie code).
|
||||
*/
|
||||
import { describe, it, expect, mock } from "bun:test";
|
||||
import { downloadAsset, type DownloadApi } from "~/lib/download-asset";
|
||||
|
||||
function makeFakeApi(
|
||||
url: string,
|
||||
seen: { input: { asset_name: string } }[] = []
|
||||
): DownloadApi {
|
||||
return {
|
||||
downloads: {
|
||||
getDownloadUrl: {
|
||||
query: async (input: { asset_name: string }) => {
|
||||
seen.push({ input });
|
||||
return { downloadURL: url };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("downloadAsset", () => {
|
||||
it("queries getDownloadUrl with the provided asset_name", async () => {
|
||||
const seen: { input: { asset_name: string } }[] = [];
|
||||
const api = makeFakeApi("https://s3/gaze.dmg", seen);
|
||||
await downloadAsset({ api, assetName: "gaze", redirect: () => {} });
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0]!.input.asset_name).toBe("gaze");
|
||||
});
|
||||
|
||||
it("redirects to the returned signed URL", async () => {
|
||||
const api = makeFakeApi("https://s3/gaze.dmg");
|
||||
const sink = mock((u: string) => {});
|
||||
await downloadAsset({ api, assetName: "gaze", redirect: sink });
|
||||
expect(sink).toHaveBeenCalledTimes(1);
|
||||
expect(sink).toHaveBeenCalledWith("https://s3/gaze.dmg");
|
||||
});
|
||||
|
||||
it("routes failures to onError without throwing", async () => {
|
||||
const api: DownloadApi = {
|
||||
downloads: {
|
||||
getDownloadUrl: {
|
||||
query: async () => {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const sink = mock((u: string) => {});
|
||||
const errSink = mock((e: unknown) => {});
|
||||
await expect(
|
||||
downloadAsset({ api, assetName: "gaze", redirect: sink, onError: errSink })
|
||||
).resolves.toBeUndefined();
|
||||
expect(sink).not.toHaveBeenCalled();
|
||||
expect(errSink).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("swallows errors silently when no onError is provided", async () => {
|
||||
const api: DownloadApi = {
|
||||
downloads: {
|
||||
getDownloadUrl: {
|
||||
query: async () => {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const sink = mock((u: string) => {});
|
||||
await expect(
|
||||
downloadAsset({ api, assetName: "gaze", redirect: sink })
|
||||
).resolves.toBeUndefined();
|
||||
expect(sink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("works for any asset name (not hard-coded to gaze)", async () => {
|
||||
const seen: { input: { asset_name: string } }[] = [];
|
||||
const api = makeFakeApi("https://s3/inputhalo.dmg", seen);
|
||||
const sink = mock((u: string) => {});
|
||||
await downloadAsset({ api, assetName: "inputhalo", redirect: sink });
|
||||
expect(seen[0]!.input.asset_name).toBe("inputhalo");
|
||||
expect(sink).toHaveBeenCalledWith("https://s3/inputhalo.dmg");
|
||||
});
|
||||
});
|
||||
76
src/lib/download-asset.ts
Normal file
76
src/lib/download-asset.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Pure, testable helper for triggering a signed-S3 download via the tRPC
|
||||
* `downloads.getDownloadUrl` endpoint.
|
||||
*
|
||||
* Extracted (task 05) so the Gaze landing page's download button — and any
|
||||
* other subdomain landing page that needs the same flow (InputHalo, Lineage,
|
||||
* …) — can share a single code path AND be unit-tested without importing
|
||||
* `~/lib/api` (which transitively imports solid-js / CSRF cookie access).
|
||||
*
|
||||
* The component layer supplies the concrete `api` (dynamic-imported at click
|
||||
* time) and the `redirect` sink (defaults to `window.location.href = url`).
|
||||
* Tests inject a fake `api` and a capture sink.
|
||||
*/
|
||||
|
||||
/** Structural shape of the tRPC downloads proxy this helper depends on. */
|
||||
export interface DownloadApi {
|
||||
downloads: {
|
||||
getDownloadUrl: {
|
||||
query: (input: { asset_name: string }) => Promise<{
|
||||
downloadURL: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** A `(url) => void` sink the helper calls with the signed S3 URL. */
|
||||
export type DownloadRedirect = (url: string) => void;
|
||||
|
||||
export interface DownloadAssetOptions {
|
||||
/** tRPC proxy (or fake) exposing `downloads.getDownloadUrl.query`. */
|
||||
api: DownloadApi;
|
||||
/** Asset key known to the downloads router (e.g. `"gaze"`). */
|
||||
assetName: string;
|
||||
/** Called with the signed URL. Defaults to `window.location.href = url`. */
|
||||
redirect?: DownloadRedirect;
|
||||
/** Invoked on failure; defaults to a no-op (component shows its own UI). */
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
/** Default redirect sink — browser navigation to the signed S3 URL. */
|
||||
const defaultRedirect: DownloadRedirect = (url) => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = url;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the latest signed download URL for `assetName` and redirect the
|
||||
* browser to it. Never throws — failures are routed to `onError`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import("~/lib/api").then(({ api }) => {
|
||||
* downloadAsset({ api, assetName: "gaze" });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function downloadAsset(
|
||||
options: DownloadAssetOptions
|
||||
): Promise<void> {
|
||||
const {
|
||||
api,
|
||||
assetName,
|
||||
redirect = defaultRedirect,
|
||||
onError
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const data = await api.downloads.getDownloadUrl.query({
|
||||
asset_name: assetName
|
||||
});
|
||||
redirect(data.downloadURL);
|
||||
} catch (error) {
|
||||
if (onError) onError(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user