Use correct rageshake URL when running in embedded package + tests (#3132)

* Use correct rageshake URL when running in embedded package

It was incorrectly trying to use the one from config.json

* Refactor to add tests

* Empty mock config
This commit is contained in:
Hugh Nimmo-Smith
2025-03-31 16:38:25 +01:00
committed by GitHub
parent e4c222a4e8
commit d1753c33f5
4 changed files with 235 additions and 78 deletions

View File

@@ -30,7 +30,7 @@ import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider"; import { Slider } from "../Slider";
import { DeviceSelection } from "./DeviceSelection"; import { DeviceSelection } from "./DeviceSelection";
import { DeveloperSettingsTab } from "./DeveloperSettingsTab"; import { DeveloperSettingsTab } from "./DeveloperSettingsTab";
import { isRageshakeAvailable } from "./submit-rageshake"; import { useSubmitRageshake } from "./submit-rageshake";
type SettingsTab = type SettingsTab =
| "audio" | "audio"
@@ -71,6 +71,8 @@ export const SettingsModal: FC<Props> = ({
const [showDeveloperSettingsTab] = useSetting(developerMode); const [showDeveloperSettingsTab] = useSetting(developerMode);
const { available: isRageshakeAvailable } = useSubmitRageshake();
const audioTab: Tab<SettingsTab> = { const audioTab: Tab<SettingsTab> = {
key: "audio", key: "audio",
name: t("common.audio"), name: t("common.audio"),
@@ -148,7 +150,7 @@ export const SettingsModal: FC<Props> = ({
const tabs = [audioTab, videoTab]; const tabs = [audioTab, videoTab];
if (widget === null) tabs.push(profileTab); if (widget === null) tabs.push(profileTab);
tabs.push(preferencesTab); tabs.push(preferencesTab);
if (isRageshakeAvailable() || import.meta.env.VITE_PACKAGE === "full") { if (isRageshakeAvailable || import.meta.env.VITE_PACKAGE === "full") {
// for full package we want to show the analytics consent checkbox // for full package we want to show the analytics consent checkbox
// even if rageshake is not available // even if rageshake is not available
tabs.push(feedbackTab); tabs.push(feedbackTab);

View File

@@ -15,78 +15,12 @@ import {
beforeEach, beforeEach,
} from "vitest"; } from "vitest";
import { import { getRageshakeSubmitUrl } from "./submit-rageshake";
getRageshakeSubmitUrl,
isRageshakeAvailable,
} from "./submit-rageshake";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
import { mockConfig } from "../utils/test"; import { mockConfig } from "../utils/test";
vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() })); vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() }));
describe("isRageshakeAvailable", () => {
beforeEach(() => {
(getUrlParams as Mock).mockReturnValue({});
mockConfig({});
});
afterEach(() => {
vi.unstubAllEnvs();
vi.clearAllMocks();
});
describe("embedded package", () => {
beforeEach(() => {
vi.stubEnv("VITE_PACKAGE", "embedded");
});
it("returns false with no rageshakeSubmitUrl URL param", () => {
expect(isRageshakeAvailable()).toBe(false);
});
it("ignores config value and returns false with no rageshakeSubmitUrl URL param", () => {
mockConfig({
rageshake: {
submit_url: "https://config.example.com.localhost",
},
});
expect(isRageshakeAvailable()).toBe(false);
});
it("returns true with rageshakeSubmitUrl URL param", () => {
(getUrlParams as Mock).mockReturnValue({
rageshakeSubmitUrl: "https://url.example.com.localhost",
});
expect(isRageshakeAvailable()).toBe(true);
});
});
describe("full package", () => {
beforeEach(() => {
vi.stubEnv("VITE_PACKAGE", "full");
});
it("returns false with no config value", () => {
expect(isRageshakeAvailable()).toBe(false);
});
it("ignores rageshakeSubmitUrl URL param and returns false with no config value", () => {
(getUrlParams as Mock).mockReturnValue({
rageshakeSubmitUrl: "https://url.example.com.localhost",
});
expect(isRageshakeAvailable()).toBe(false);
});
it("returns true with config value", () => {
mockConfig({
rageshake: {
submit_url: "https://config.example.com.localhost",
},
});
expect(isRageshakeAvailable()).toBe(true);
});
});
});
describe("getRageshakeSubmitUrl", () => { describe("getRageshakeSubmitUrl", () => {
beforeEach(() => { beforeEach(() => {
(getUrlParams as Mock).mockReturnValue({}); (getUrlParams as Mock).mockReturnValue({});

View File

@@ -131,11 +131,9 @@ export function getRageshakeSubmitUrl(): string | undefined {
return undefined; return undefined;
} }
export function isRageshakeAvailable(): boolean { export function useSubmitRageshake(
return !!getRageshakeSubmitUrl(); injectedGetRageshakeSubmitUrl = getRageshakeSubmitUrl,
} ): {
export function useSubmitRageshake(): {
submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>; submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
sending: boolean; sending: boolean;
sent: boolean; sent: boolean;
@@ -158,7 +156,8 @@ export function useSubmitRageshake(): {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
async (opts) => { async (opts) => {
if (!getRageshakeSubmitUrl()) { const submitUrl = injectedGetRageshakeSubmitUrl();
if (!submitUrl) {
throw new Error("No rageshake URL is configured"); throw new Error("No rageshake URL is configured");
} }
@@ -292,7 +291,7 @@ export function useSubmitRageshake(): {
); );
} }
const res = await fetch(Config.get().rageshake!.submit_url, { const res = await fetch(submitUrl, {
method: "POST", method: "POST",
body, body,
}); });
@@ -309,7 +308,7 @@ export function useSubmitRageshake(): {
logger.error(error); logger.error(error);
} }
}, },
[client, sending], [client, sending, injectedGetRageshakeSubmitUrl],
); );
return { return {
@@ -317,7 +316,7 @@ export function useSubmitRageshake(): {
sending, sending,
sent, sent,
error, error,
available: isRageshakeAvailable(), available: !!injectedGetRageshakeSubmitUrl(),
}; };
} }

View File

@@ -0,0 +1,222 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
expect,
describe,
it,
vi,
beforeEach,
afterEach,
type Mock,
} from "vitest";
import { useState, type ReactElement } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { type MatrixClient } from "matrix-js-sdk/lib/client";
import { useSubmitRageshake, getRageshakeSubmitUrl } from "./submit-rageshake";
import { ClientContextProvider } from "../ClientContext";
import { getUrlParams } from "../UrlParams";
import { mockConfig } from "../utils/test";
vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() }));
const TestComponent = ({
sendLogs,
getRageshakeSubmitUrl,
}: {
sendLogs: boolean;
getRageshakeSubmitUrl: () => string | undefined;
}): ReactElement => {
const [clickError, setClickError] = useState<Error | null>(null);
const { available, sending, sent, submitRageshake, error } =
useSubmitRageshake(getRageshakeSubmitUrl);
const onClick = (): void => {
submitRageshake({
sendLogs,
}).catch((e) => {
setClickError(e);
});
};
return (
<div>
<p data-testid="available">{available ? "true" : "false"}</p>
<p data-testid="sending">{sending ? "true" : "false"}</p>
<p data-testid="sent">{sent ? "true" : "false"}</p>
<p data-testid="error">{error?.message}</p>
<p data-testid="clickError">{clickError?.message}</p>
<button onClick={onClick} data-testid="submit">
submit
</button>
</div>
);
};
function renderWithMockClient(
getRageshakeSubmitUrl: () => string | undefined,
sendLogs: boolean,
): void {
const client = vi.mocked<MatrixClient>({
getUserId: vi.fn().mockReturnValue("@user:localhost"),
getUser: vi.fn().mockReturnValue(null),
credentials: {
userId: "@user:localhost",
},
getCrypto: vi.fn().mockReturnValue(undefined),
} as unknown as MatrixClient);
render(
<ClientContextProvider
value={{
state: "valid",
disconnected: false,
supportedFeatures: {
reactions: true,
thumbnails: true,
},
setClient: vi.fn(),
authenticated: {
client,
isPasswordlessUser: true,
changePassword: vi.fn(),
logout: vi.fn(),
},
}}
>
<TestComponent
sendLogs={sendLogs}
getRageshakeSubmitUrl={getRageshakeSubmitUrl}
/>
</ClientContextProvider>,
);
}
describe("useSubmitRageshake", () => {
describe("available", () => {
beforeEach(() => {
(getUrlParams as Mock).mockReturnValue({});
mockConfig({});
});
afterEach(() => {
vi.unstubAllEnvs();
vi.clearAllMocks();
});
describe("embedded package", () => {
beforeEach(() => {
vi.stubEnv("VITE_PACKAGE", "embedded");
});
it("returns false with no rageshakeSubmitUrl URL param", () => {
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("false");
});
it("ignores config value and returns false with no rageshakeSubmitUrl URL param", () => {
mockConfig({
rageshake: {
submit_url: "https://config.example.com.localhost",
},
});
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("false");
});
it("returns true with rageshakeSubmitUrl URL param", () => {
(getUrlParams as Mock).mockReturnValue({
rageshakeSubmitUrl: "https://url.example.com.localhost",
});
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("true");
});
});
describe("full package", () => {
beforeEach(() => {
mockConfig({});
vi.stubEnv("VITE_PACKAGE", "full");
});
it("returns false with no config value", () => {
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("false");
});
it("ignores rageshakeSubmitUrl URL param and returns false with no config value", () => {
(getUrlParams as Mock).mockReturnValue({
rageshakeSubmitUrl: "https://url.example.com.localhost",
});
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("false");
});
it("returns true with config value", () => {
mockConfig({
rageshake: {
submit_url: "https://config.example.com.localhost",
},
});
renderWithMockClient(getRageshakeSubmitUrl, false);
expect(screen.getByTestId("available").textContent).toBe("true");
});
});
});
describe("when rageshake is available", () => {
beforeEach(() => {
mockConfig({});
vi.unstubAllGlobals();
});
it("starts unsent", () => {
renderWithMockClient(() => "https://rageshake.localhost/foo", false);
expect(screen.getByTestId("sending").textContent).toBe("false");
expect(screen.getByTestId("sent").textContent).toBe("false");
});
it("submitRageshake fetches expected URL", async () => {
const fetchFn = vi.fn().mockResolvedValue({
status: 200,
});
vi.stubGlobal("fetch", fetchFn);
renderWithMockClient(() => "https://rageshake.localhost/foo", false);
screen.getByTestId("submit").click();
await waitFor(() => {
expect(screen.getByTestId("sent").textContent).toBe("true");
});
expect(fetchFn).toHaveBeenCalledExactlyOnceWith(
"https://rageshake.localhost/foo",
expect.objectContaining({
method: "POST",
}),
);
expect(screen.getByTestId("clickError").textContent).toBe("");
expect(screen.getByTestId("error").textContent).toBe("");
});
});
describe("when rageshake is not available", () => {
it("starts unsent", () => {
renderWithMockClient(() => undefined, false);
expect(screen.getByTestId("sending").textContent).toBe("false");
expect(screen.getByTestId("sent").textContent).toBe("false");
});
it("submitRageshake throws error", async () => {
renderWithMockClient(() => undefined, false);
screen.getByTestId("submit").click();
await waitFor(() => {
expect(screen.getByTestId("clickError").textContent).toBe(
"No rageshake URL is configured",
);
});
});
});
});