Skip to content
Open
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
9 changes: 5 additions & 4 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ export const CONTACT_SUPPORT_TEAM_MESSAGE =
export const DETAILED_UPGRADES_VIEW_ENABLED =
import.meta.env.VITE_DETAILED_UPGRADES_VIEW_ENABLED === "true";
export const IS_MSW_ENABLED = import.meta.env.VITE_MSW_ENABLED === "true";
export const MSW_ENDPOINTS_TO_INTERCEPT =
(import.meta.env.VITE_MSW_ENDPOINTS_TO_INTERCEPT ?? "")
.split(",")
.filter(Boolean) ?? [];
export const MSW_ENDPOINTS_TO_INTERCEPT = (
import.meta.env.VITE_MSW_ENDPOINTS_TO_INTERCEPT ?? ""
)
.split(",")
.filter(Boolean);
export const HOMEPAGE_PATH = ROUTES.overview.root();
export const DEFAULT_ACCESS_GROUP_NAME = "global";
export const BREAKPOINT_PX = {
Expand Down
15 changes: 4 additions & 11 deletions src/tests/browser.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { API_URL, API_URL_OLD, MSW_ENDPOINTS_TO_INTERCEPT } from "@/constants";
import type { RequestHandler } from "msw";
import { http, HttpResponse, passthrough } from "msw";
import { http, passthrough } from "msw";
import { setupWorker } from "msw/browser";
import fallbackHandlers from "./server/handlers";

const handlers: RequestHandler[] = [
http.all("*", ({ request }) => {
if (request.url.includes("sentry.is.canonical.com")) {
return passthrough();
}

return;
}),
http.all("*", async ({ request }) => {
if (!request.url.includes(API_URL) && !request.url.includes(API_URL_OLD)) {
return passthrough();
}
Expand All @@ -30,9 +23,9 @@ const handlers: RequestHandler[] = [

...fallbackHandlers,

http.all("*", async ({ request }) => {
console.log("Request not handled:", request.url);
return new HttpResponse(null, { status: 404 });
http.all("*", ({ request }) => {
console.warn("MSW: No handler matched, passing through:", request.url);
return passthrough();
}),
];

Expand Down
1 change: 0 additions & 1 deletion src/tests/endpointsToIntercept.json

This file was deleted.

36 changes: 35 additions & 1 deletion src/tests/server/handlers/_constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
import { HttpResponse } from "msw";

export const ENDPOINT_STATUS_API_ERROR_MESSAGE = `The endpoint status is set to "error".`;

const DEFAULT_ERROR_STATUS = 500;

/**
* @deprecated Use {@link createEndpointStatusError} instead.
* Keeping for backward compatibility during migration.
*/
export const ENDPOINT_STATUS_API_ERROR = HttpResponse.json(
{
error: "EndpointStatusError",
message: ENDPOINT_STATUS_API_ERROR_MESSAGE,
},
{ status: 500 },
{ status: DEFAULT_ERROR_STATUS },
);

/**
* Creates a fresh JSON-body error response for endpoint-status-driven error
* simulation. Use this where tests assert on the parsed error body (e.g.
* package-profiles, ubuntu-pro, activities list).
*
* Prefer this over the static `ENDPOINT_STATUS_API_ERROR` constant because
* response objects should not be shared across handler invocations.
*/
export const createEndpointStatusError = (status = DEFAULT_ERROR_STATUS) =>
HttpResponse.json(
{
error: "EndpointStatusError",
message: ENDPOINT_STATUS_API_ERROR_MESSAGE,
},
{ status },
);

/**
* Creates a fresh null-body error response for endpoint-status-driven error
* simulation. Use this where the handler originally used
* `throw new HttpResponse(null, { status: 500 })`.
*/
export const createEndpointStatusNetworkError = (
status = DEFAULT_ERROR_STATUS,
) => new HttpResponse(null, { status });

64 changes: 36 additions & 28 deletions src/tests/server/handlers/_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { HttpHandler } from "msw";
import { http, HttpResponse } from "msw";
import { API_URL } from "@/constants";
import { getEndpointStatus } from "@/tests/controllers/controller";
import { createEndpointStatusNetworkError } from "./_constants";

interface GeneratePaginatedResponseProps<D> {
data: D[];
Expand Down Expand Up @@ -73,6 +74,19 @@ export const isAction = (request: Request, actionName: string | string[]) => {
: actionName.includes(action);
};

/**
* Returns `true` when the global endpoint-status controller has a non-default
* status that applies to the given path. Pass the handler's own path so that
* a targeted `setEndpointStatus({ status: "error", path: "/features" })` only
* affects the matching handler.
*/
export function shouldApplyEndpointStatus(path?: string): boolean {
const { status, path: statusPath } = getEndpointStatus();
if (status === "default") return false;
if (!statusPath) return true;
return !!path && statusPath.includes(path);
}

interface GenerateGetListEndpointParams<T> {
readonly path: string;
readonly response: T[];
Expand All @@ -82,37 +96,31 @@ export function generateGetListEndpoint<T>({
path,
response,
}: GenerateGetListEndpointParams<T>): HttpHandler {
return http.get<never, never, ApiPaginatedResponse<T>>(
`${API_URL}${path}`,
() => {
const endpointStatus = getEndpointStatus();
return http.get(`${API_URL}${path}`, () => {
if (shouldApplyEndpointStatus(path)) {
const { status } = getEndpointStatus();

if (
!endpointStatus.path ||
(endpointStatus.path && endpointStatus.path === path)
) {
if (endpointStatus.status === "error") {
throw new HttpResponse(null, { status: 500 });
}

if (endpointStatus.status === "empty") {
return HttpResponse.json({
results: [],
count: 0,
next: null,
previous: null,
});
}
if (status === "error") {
throw createEndpointStatusNetworkError();
}

return HttpResponse.json({
results: response,
count: response.length,
next: null,
previous: null,
});
},
);
if (status === "empty") {
return HttpResponse.json({
results: [],
count: 0,
next: null,
previous: null,
});
}
}

return HttpResponse.json({
results: response,
count: response.length,
next: null,
previous: null,
});
});
}

export const parseArray = (paramValue: string | null): string[] => {
Expand Down
7 changes: 0 additions & 7 deletions src/tests/server/handlers/accessGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ export default [
return HttpResponse.json(accessGroups);
}),

http.get(API_URL_OLD, ({ request }) => {
if (!isAction(request, "ChangeComputersAccessGroup")) {
return;
}

return HttpResponse.json({ success: true });
}),

http.get(API_URL_OLD, ({ request }) => {
if (
Expand Down
44 changes: 19 additions & 25 deletions src/tests/server/handlers/activity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { API_URL, API_URL_OLD } from "@/constants";
import type { Activity, GetActivitiesParams } from "@/features/activities";
import type { Activity } from "@/features/activities";
import { getEndpointStatus } from "@/tests/controllers/controller";
import {
activities,
activityTypes,
INVALID_ACTIVITY_SEARCH_QUERY,
} from "@/tests/mocks/activity";
import type { ApiPaginatedResponse } from "@/types/api/ApiPaginatedResponse";
import { http, HttpResponse } from "msw";
import { ENDPOINT_STATUS_API_ERROR } from "./_constants";
import { generatePaginatedResponse, isAction } from "./_helpers";
import { createEndpointStatusError, createEndpointStatusNetworkError } from "./_constants";
import { generatePaginatedResponse, isAction, shouldApplyEndpointStatus } from "./_helpers";

const STATUS_QUERY_REGEX = /(?:^|\s)status:([^\s]+)/;
const TYPE_QUERY_REGEX = /(?:^|\s)type:([^\s]+)/;
Expand Down Expand Up @@ -48,13 +47,14 @@ const parseActivitiesQuery = (
};

export default [
http.get<never, GetActivitiesParams, ApiPaginatedResponse<Activity>>(
http.get(
`${API_URL}activities`,
async ({ request }) => {
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw ENDPOINT_STATUS_API_ERROR;
if (shouldApplyEndpointStatus("activities")) {
const { status } = getEndpointStatus();
if (status === "error") {
throw createEndpointStatusError();
}
}

const url = new URL(request.url);
Expand Down Expand Up @@ -97,17 +97,15 @@ export default [
},
),

http.get<{ id: string }, GetActivitiesParams, Activity>(
http.get(
`${API_URL}activities/:id`,
async ({ params: { id } }) => {
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw new HttpResponse(null, { status: 500 });
if (shouldApplyEndpointStatus("activities/:id")) {
throw createEndpointStatusNetworkError();
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldApplyEndpointStatus("activities/:id") becomes true for any non-default status (including empty), but this handler now throws a 500 unconditionally when it matches. Previously only status === "error" triggered the 500. Please read status from getEndpointStatus() and only throw createEndpointStatusNetworkError() when status === "error" (and optionally handle empty explicitly if desired).

Suggested change
throw createEndpointStatusNetworkError();
const { status } = getEndpointStatus();
if (status === "error") {
throw createEndpointStatusNetworkError();
}

Copilot uses AI. Check for mistakes.
}

return HttpResponse.json<Activity>(
activities.find((activity) => activity.id === parseInt(id)) ?? {
activities.find((activity) => activity.id === parseInt(id as string)) ?? {
activity_status: "succeeded",
approval_time: null,
children: [],
Expand Down Expand Up @@ -162,26 +160,22 @@ export default [
]);
}),

http.post<never, { activity_ids: number[] }, number[]>(
http.post(
`${API_URL}activities/revert`,
async () => {
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw ENDPOINT_STATUS_API_ERROR;
if (shouldApplyEndpointStatus("activities/revert")) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the activities/revert handler, shouldApplyEndpointStatus("activities/revert") is used as the sole condition to throw an error response. Since shouldApplyEndpointStatus returns true for both status: "error" and status: "empty", this will incorrectly throw a 500 for the empty case. Please gate the throw on getEndpointStatus().status === "error" (and keep empty behavior non-500).

Suggested change
if (shouldApplyEndpointStatus("activities/revert")) {
if (
shouldApplyEndpointStatus("activities/revert") &&
getEndpointStatus().status === "error"
) {

Copilot uses AI. Check for mistakes.
throw createEndpointStatusError();
}

return HttpResponse.json([activities[0].id, activities[1].id]);
},
),

http.post<never, { activity_ids: number[] }, number[]>(
http.post(
`${API_URL}activities/reapply`,
async () => {
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw ENDPOINT_STATUS_API_ERROR;
if (shouldApplyEndpointStatus("activities/reapply")) {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the activities/reapply handler, shouldApplyEndpointStatus("activities/reapply") currently triggers a 500 for any non-default status (including empty). This changes behavior from only erroring on status === "error". Please fetch the endpoint status and only throw createEndpointStatusError() when status === "error".

Suggested change
if (shouldApplyEndpointStatus("activities/reapply")) {
const status = getEndpointStatus("activities/reapply");
if (status === "error") {

Copilot uses AI. Check for mistakes.
throw createEndpointStatusError();
}

return HttpResponse.json([activities[0].id, activities[1].id]);
Expand Down
17 changes: 6 additions & 11 deletions src/tests/server/handlers/aptSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { API_URL, API_URL_OLD } from "@/constants";
import type { APTSource, GetAPTSourcesParams } from "@/features/apt-sources";
import { getEndpointStatus } from "@/tests/controllers/controller";
import { aptSources } from "@/tests/mocks/apt-sources";
import { isAction } from "@/tests/server/handlers/_helpers";
import { isAction, shouldApplyEndpointStatus } from "@/tests/server/handlers/_helpers";
import { http, HttpResponse } from "msw";
import { ENDPOINT_STATUS_API_ERROR } from "./_constants";
import { createEndpointStatusNetworkError } from "./_constants";

export default [
http.get<never, GetAPTSourcesParams, APTSource[]>(
Expand All @@ -19,15 +19,10 @@ export default [
),

http.delete(`${API_URL}repository/apt-source/:sourceId`, () => {
const endpointStatus = getEndpointStatus();

if (
!endpointStatus.path ||
(endpointStatus.path &&
endpointStatus.path === "repository/apt-source/:sourceId")
) {
if (endpointStatus.status === "error") {
throw HttpResponse.json(ENDPOINT_STATUS_API_ERROR, { status: 500 });
if (shouldApplyEndpointStatus("repository/apt-source/:sourceId")) {
const { status } = getEndpointStatus();
if (status === "error") {
throw createEndpointStatusNetworkError();
}
Comment on lines 21 to 26
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint previously attempted to return a JSON error response (via HttpResponse.json(...)) when endpoint-status was set to error. Switching to createEndpointStatusNetworkError() changes the error body to null, which can affect client error parsing and make this handler inconsistent with other mutation handlers that use createEndpointStatusError(). Consider using createEndpointStatusError() here if tests/UI expect the standard { error, message } body.

Copilot uses AI. Check for mistakes.
}

Expand Down
3 changes: 2 additions & 1 deletion src/tests/server/handlers/distributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getEndpointStatus } from "@/tests/controllers/controller";
import { distributions } from "@/tests/mocks/distributions";
import { http, HttpResponse } from "msw";
import { isAction } from "@/tests/server/handlers/_helpers";
import { createEndpointStatusNetworkError } from "./_constants";

export default [
http.get<never, GetDistributionsParams, Distribution[]>(
Expand All @@ -16,7 +17,7 @@ export default [
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw new HttpResponse(null, { status: 500 });
throw createEndpointStatusNetworkError();
}

if (endpointStatus.status === "empty") {
Expand Down
7 changes: 4 additions & 3 deletions src/tests/server/handlers/eventsLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { API_URL } from "@/constants";
import type { EventLog, GetEventsLogParams } from "@/features/events-log";
import { getEndpointStatus } from "@/tests/controllers/controller";
import { eventsLog } from "@/tests/mocks/eventsLog";
import { generatePaginatedResponse } from "@/tests/server/handlers/_helpers";
import { generatePaginatedResponse, shouldApplyEndpointStatus } from "@/tests/server/handlers/_helpers";
import { createEndpointStatusNetworkError } from "./_constants";
import type { ApiPaginatedResponse } from "@/types/api/ApiPaginatedResponse";
import { http, HttpResponse } from "msw";

Expand All @@ -12,8 +13,8 @@ export default [
async ({ request }) => {
const endpointStatus = getEndpointStatus();

if (endpointStatus.status === "error") {
throw new HttpResponse(null, { status: 500 });
if (shouldApplyEndpointStatus("events") && endpointStatus.status === "error") {
throw createEndpointStatusNetworkError();
}

const url = new URL(request.url);
Expand Down
6 changes: 3 additions & 3 deletions src/tests/server/handlers/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { API_URL } from "@/constants";
import { features } from "@/tests/mocks/features";
import { getEndpointStatus } from "@/tests/controllers/controller";
import type { Feature } from "@/types/Feature";
import type { ApiPaginatedResponse } from "@/types/api/ApiPaginatedResponse";
import { generatePaginatedResponse } from "@/tests/server/handlers/_helpers";
import { createEndpointStatusNetworkError } from "./_constants";

export default [
http.get<never, never, ApiPaginatedResponse<Feature>>(
http.get(
`${API_URL}features`,
() => {
const endpointStatus = getEndpointStatus();
Expand All @@ -23,7 +23,7 @@ export default [
}

if (endpointStatus.status === "error") {
throw new HttpResponse(null, { status: 500 });
throw createEndpointStatusNetworkError();
}

return HttpResponse.json(
Expand Down
Loading
Loading