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
1 change: 1 addition & 0 deletions frontend/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
2 changes: 2 additions & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
};
6 changes: 6 additions & 0 deletions frontend/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Add any global test setup here
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
147 changes: 147 additions & 0 deletions frontend/src/WasmOrRpcProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Mock fetch globally
global.fetch = jest.fn();

describe("WasmOrRpcProvider RPC calls", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset window._WEBSOCKET_HOST
(global as any).window = { _WEBSOCKET_HOST: undefined };
});

describe("callRpc URL construction", () => {
// Since callRpc is not exported, we test the URL construction logic directly
it("should use relative /api/rpc when WEBSOCKET_HOST is not set", async () => {
// Test setup to trigger RPC call
const mockResponse = {
ok: true,
text: async () => JSON.stringify({ type: "Response", data: {} }),
};
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);

// Test case 1: No WEBSOCKET_HOST
(global as any).window._WEBSOCKET_HOST = undefined;
const rpcUrl = "/api/rpc";
expect(rpcUrl).toBe("/api/rpc");
});

it("should convert wss:// to https:// for RPC calls", () => {
(global as any).window._WEBSOCKET_HOST = "wss://example.com/game";

// Simulate the URL construction logic
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

expect(rpcUrl).toBe("https://example.com/game/api/rpc");
});

it("should convert ws:// to http:// for RPC calls", () => {
(global as any).window._WEBSOCKET_HOST = "ws://localhost:3000";

// Simulate the URL construction logic
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

expect(rpcUrl).toBe("http://localhost:3000/api/rpc");
});

it("should handle URLs ending with /", () => {
(global as any).window._WEBSOCKET_HOST = "wss://api.example.com/";

// Simulate the URL construction logic
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

expect(rpcUrl).toBe("https://api.example.com/api/rpc");
});

it("should handle URLs ending with /api", () => {
(global as any).window._WEBSOCKET_HOST = "wss://example.com/api";

// Simulate the URL construction logic
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

expect(rpcUrl).toBe("https://example.com/api/rpc");
});

it("should handle null WEBSOCKET_HOST", () => {
(global as any).window._WEBSOCKET_HOST = null;

// Simulate the URL construction logic
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

expect(rpcUrl).toBe("/api/rpc");
});
});
});
23 changes: 22 additions & 1 deletion frontend/src/WasmOrRpcProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,28 @@ type WasmRpcRequest =
async function callRpc<T>(request: WasmRpcRequest): Promise<T> {
const bodyString = JSON.stringify(request);

const response = await fetch("/api/rpc", {
// Respect WEBSOCKET_HOST for RPC calls when set
const runtimeWebsocketHost = (window as any)._WEBSOCKET_HOST;
let rpcUrl = "/api/rpc";

if (runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null) {
// Convert WebSocket URL to HTTP URL for RPC calls
// Replace wss:// with https:// and ws:// with http://
const httpUrl = runtimeWebsocketHost
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://");

// Ensure the URL ends with /api/rpc
if (httpUrl.endsWith("/")) {
rpcUrl = httpUrl + "api/rpc";
} else if (httpUrl.endsWith("/api")) {
rpcUrl = httpUrl + "/rpc";
} else {
rpcUrl = httpUrl + "/api/rpc";
}
}

const response = await fetch(rpcUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
176 changes: 176 additions & 0 deletions frontend/src/WebsocketProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Tests for WebsocketProvider URL construction logic

describe("WebsocketProvider URL construction", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset window._WEBSOCKET_HOST
(global as any).window = { _WEBSOCKET_HOST: undefined };
(global as any).location = {
protocol: "https:",
host: "example.com",
pathname: "/game/",
};
});

it("should use WEBSOCKET_HOST when provided", () => {
(global as any).window._WEBSOCKET_HOST =
"wss://custom.server.com/websocket";

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: (location.protocol === "https:" ? "wss://" : "ws://") +
location.host +
location.pathname +
(location.pathname.endsWith("/") ? "api" : "/api");

expect(uri).toBe("wss://custom.server.com/websocket");
});

it("should use default URL when WEBSOCKET_HOST is null", () => {
(global as any).window._WEBSOCKET_HOST = null;

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

// Should construct URL from location
expect(uri).toBe("wss://example.com/game/api");
});

it("should use default URL when WEBSOCKET_HOST is undefined", () => {
(global as any).window._WEBSOCKET_HOST = undefined;

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

// Should construct URL from location
expect(uri).toBe("wss://example.com/game/api");
});

it("should use ws:// for non-https protocol when no WEBSOCKET_HOST", () => {
(global as any).window._WEBSOCKET_HOST = undefined;
(global as any).location = {
protocol: "http:",
host: "localhost:3000",
pathname: "/",
};

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

expect(uri).toBe("ws://localhost:3000/api");
});

it("should handle pathname not ending with slash", () => {
(global as any).window._WEBSOCKET_HOST = undefined;
(global as any).location = {
protocol: "https:",
host: "example.com",
pathname: "/game",
};

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

expect(uri).toBe("wss://example.com/game/api");
});

it("should handle WEBSOCKET_HOST with ws:// protocol", () => {
(global as any).window._WEBSOCKET_HOST = "ws://dev.server.com/socket";

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

expect(uri).toBe("ws://dev.server.com/socket");
});

it("should handle WEBSOCKET_HOST with wss:// protocol", () => {
(global as any).window._WEBSOCKET_HOST = "wss://secure.server.com/ws";

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

expect(uri).toBe("wss://secure.server.com/ws");
});

it("should handle empty string WEBSOCKET_HOST", () => {
(global as any).window._WEBSOCKET_HOST = "";
(global as any).location = {
protocol: "https:",
host: "example.com",
pathname: "/",
};

// Simulate the URL construction logic from WebsocketProvider
const runtimeWebsocketHost = (global as any).window._WEBSOCKET_HOST;
const uri =
runtimeWebsocketHost !== undefined && runtimeWebsocketHost !== null
? runtimeWebsocketHost
: ((global as any).location.protocol === "https:"
? "wss://"
: "ws://") +
(global as any).location.host +
(global as any).location.pathname +
((global as any).location.pathname.endsWith("/") ? "api" : "/api");

// Empty string is truthy in JavaScript, but the code checks for undefined and null
// So empty string would be used as-is
expect(uri).toBe("");
});
});
Loading