Files
galaxy-game/ui/frontend/tests/galaxy-client.test.ts
T
Ilia Denisov 8565942392
Build · Site / build (push) Successful in 8s
Tests · Go / test (push) Successful in 2m22s
Tests · UI / test (push) Failing after 2m42s
feat(deploy): single-origin path-based deployment + project site
Serve the whole stack behind one host: site at /, game UI at /game/,
gateway REST at /api + /healthz, Connect at /rpc (prefix stripped by the
edge Caddy). The built artifact is domain-agnostic — the UI talks to the
gateway same-origin via relative URLs, so the same bundle runs under any
host with no rebuild and with CORS disabled.

- Rename the Connect proto service galaxy.gateway.v1.EdgeGateway ->
  edge.v1.Gateway; regenerate Go + TS; public path /rpc/edge.v1.Gateway.
- Move the game UI under base path /game (env BASE_PATH); make the
  manifest, service-worker scope, WASM loader, and all navigation
  base-aware via a withBase helper.
- Relative API + /rpc Connect prefix; Vite dev proxy mirrors the strip.
- Rewrite the edge Caddy (dev + prod) for path-based routing; empty CORS
  allow-lists (same-origin); single host.
- New VitePress project site (site/): i18n en/ru with switcher, LaTeX
  math, minimal monospace theme; built and served at /.
- dev-deploy compose/Makefile + CI (dev-deploy, prod-build, new
  site-build) build and seed the site; probes hit /, /game/, /healthz.
- Sync docs (ARCHITECTURE, gateway README/openapi, dev-deploy &
  local-dev READMEs, CLAUDE.md, ui/PLAN).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 18:19:07 +02:00

218 lines
7.2 KiB
TypeScript

// Verifies the orchestration order in `GalaxyClient.executeCommand`:
//
// core.signRequest(fields)
// → signer(canonicalBytes)
// → edge.executeCommand({ envelope, signature })
// → core.verifyPayloadHash(response payload, hash)
// → core.verifyResponse(envelope, signature)
// → return response.payloadBytes
//
// Tests use a hand-rolled mock `Core` and a Connect router transport
// from `@connectrpc/connect`, which lets us assert what the gateway
// would have received without standing up a real Connect server.
import { create } from "@bufbuild/protobuf";
import { createClient, createRouterTransport } from "@connectrpc/connect";
import { describe, expect, test, vi } from "vitest";
import { GalaxyClient } from "../src/api/galaxy-client";
import {
Gateway,
ExecuteCommandResponseSchema,
type ExecuteCommandRequest,
} from "../src/proto/edge/v1/edge_gateway_pb";
import type {
Core,
RequestSigningFields,
ResponseSigningFields,
} from "../src/platform/core/index";
import { makeFakeCore } from "./fake-core";
const FIXED_REQUEST_ID = "req-test-1";
const FIXED_TIMESTAMP = 1_700_000_000_000n;
describe("GalaxyClient.executeCommand", () => {
test("orchestrates sign → fetch → verify and returns the payload", async () => {
const canonicalBytes = new Uint8Array([0xca, 0xfe]);
const signature = new Uint8Array(64).fill(0x55);
const responsePayload = new TextEncoder().encode("hello-from-server");
const responseHash = new Uint8Array(32).fill(0x77);
const responseSignature = new Uint8Array(64).fill(0x99);
const responsePublicKey = new Uint8Array(32).fill(0x11);
const core = mockCore({
signRequestImpl: () => canonicalBytes,
verifyResponseImpl: () => true,
verifyPayloadHashImpl: () => true,
});
const signer = vi.fn(async () => signature);
const sha256 = vi.fn(async () => new Uint8Array(32).fill(0x33));
let captured: ExecuteCommandRequest | undefined;
const transport = createRouterTransport(({ service }) => {
service(Gateway, {
executeCommand(req) {
captured = req;
return create(ExecuteCommandResponseSchema, {
protocolVersion: "v1",
requestId: FIXED_REQUEST_ID,
timestampMs: FIXED_TIMESTAMP,
resultCode: "ok",
payloadBytes: responsePayload,
payloadHash: responseHash,
signature: responseSignature,
});
},
subscribeEvents() {
throw new Error("not used in this test");
},
});
});
const edge = createClient(Gateway, transport);
const client = new GalaxyClient({
core,
edge,
signer,
sha256,
deviceSessionId: "device-session-1",
gatewayResponsePublicKey: responsePublicKey,
clock: () => FIXED_TIMESTAMP,
requestIdFactory: () => FIXED_REQUEST_ID,
});
const out = await client.executeCommand(
"user.account.get",
new TextEncoder().encode("client-payload"),
);
expect(out.resultCode).toBe("ok");
expect(Array.from(out.payloadBytes)).toEqual(Array.from(responsePayload));
expect(signer).toHaveBeenCalledWith(canonicalBytes);
expect(sha256).toHaveBeenCalledTimes(1);
expect(core.signRequest).toHaveBeenCalledTimes(1);
const verifyHashCall = vi.mocked(core.verifyPayloadHash).mock.calls[0]!;
expect(Array.from(verifyHashCall[0])).toEqual(Array.from(responsePayload));
expect(Array.from(verifyHashCall[1])).toEqual(Array.from(responseHash));
const verifyRespCall = vi.mocked(core.verifyResponse).mock.calls[0]!;
expect(Array.from(verifyRespCall[0])).toEqual(Array.from(responsePublicKey));
expect(Array.from(verifyRespCall[1])).toEqual(Array.from(responseSignature));
expect(verifyRespCall[2]).toMatchObject({
protocolVersion: "v1",
requestId: FIXED_REQUEST_ID,
resultCode: "ok",
});
expect(captured?.deviceSessionId).toBe("device-session-1");
expect(captured?.messageType).toBe("user.account.get");
expect(captured?.requestId).toBe(FIXED_REQUEST_ID);
expect(Array.from(captured?.signature ?? [])).toEqual(
Array.from(signature),
);
});
test("throws when the response signature fails verification", async () => {
const core = mockCore({
signRequestImpl: () => new Uint8Array(),
verifyResponseImpl: () => false,
verifyPayloadHashImpl: () => true,
});
const transport = createRouterTransport(({ service }) => {
service(Gateway, {
executeCommand: () =>
create(ExecuteCommandResponseSchema, {
protocolVersion: "v1",
requestId: FIXED_REQUEST_ID,
timestampMs: FIXED_TIMESTAMP,
resultCode: "ok",
payloadBytes: new Uint8Array(),
payloadHash: new Uint8Array(32),
signature: new Uint8Array(64),
}),
subscribeEvents() {
throw new Error("not used in this test");
},
});
});
const client = new GalaxyClient({
core,
edge: createClient(Gateway, transport),
signer: async () => new Uint8Array(64),
sha256: async () => new Uint8Array(32),
deviceSessionId: "device-session-1",
gatewayResponsePublicKey: new Uint8Array(32),
clock: () => FIXED_TIMESTAMP,
requestIdFactory: () => FIXED_REQUEST_ID,
});
await expect(
client.executeCommand("user.account.get", new Uint8Array()),
).rejects.toThrow(/invalid response signature/);
});
test("throws when the response payload_hash does not match", async () => {
const core = mockCore({
signRequestImpl: () => new Uint8Array(),
verifyResponseImpl: () => true,
verifyPayloadHashImpl: () => false,
});
const transport = createRouterTransport(({ service }) => {
service(Gateway, {
executeCommand: () =>
create(ExecuteCommandResponseSchema, {
protocolVersion: "v1",
requestId: FIXED_REQUEST_ID,
timestampMs: FIXED_TIMESTAMP,
resultCode: "ok",
payloadBytes: new Uint8Array(),
payloadHash: new Uint8Array(32),
signature: new Uint8Array(64),
}),
subscribeEvents() {
throw new Error("not used in this test");
},
});
});
const client = new GalaxyClient({
core,
edge: createClient(Gateway, transport),
signer: async () => new Uint8Array(64),
sha256: async () => new Uint8Array(32),
deviceSessionId: "device-session-1",
gatewayResponsePublicKey: new Uint8Array(32),
clock: () => FIXED_TIMESTAMP,
requestIdFactory: () => FIXED_REQUEST_ID,
});
await expect(
client.executeCommand("user.account.get", new Uint8Array()),
).rejects.toThrow(/payload_hash mismatch/);
});
});
interface MockCoreOptions {
signRequestImpl: (fields: RequestSigningFields) => Uint8Array;
verifyResponseImpl: (
publicKey: Uint8Array,
signature: Uint8Array,
fields: ResponseSigningFields,
) => boolean;
verifyPayloadHashImpl: (
payload: Uint8Array,
hash: Uint8Array,
) => boolean;
}
function mockCore(opts: MockCoreOptions): Core & {
signRequest: ReturnType<typeof vi.fn>;
verifyResponse: ReturnType<typeof vi.fn>;
verifyPayloadHash: ReturnType<typeof vi.fn>;
verifyEvent: ReturnType<typeof vi.fn>;
} {
return {
// `GalaxyClient` does not exercise the calc bridge, so the calc
// methods come from the shared fake; only the signing/verify
// methods need spies for the orchestration-order assertions.
...makeFakeCore(),
signRequest: vi.fn(opts.signRequestImpl),
verifyResponse: vi.fn(opts.verifyResponseImpl),
verifyEvent: vi.fn(() => true),
verifyPayloadHash: vi.fn(opts.verifyPayloadHashImpl),
};
}