mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 22:13:18 +09:00
fix(dashboard): drop the resource map ConfigMaps once the map is unreadable
React Query keeps the last successful list when a refetch fails. After the
resource map became forbidden or went away, the hook went on parsing that
old copy, so the ConfigMaps tab stayed with stale names, and a malformed
map kept its old error, instead of the tab going away as it does when the
map is unreadable from the start.
Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
(cherry picked from commit 8ce3f853af)
This commit is contained in:
1 parent
2610fb8565
commit
07be46d37c
3 files changed
+48
-7
No files matched your search
+22
-2
@@ -1,19 +1,24 @@
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { MemoryRouter, Routes, Route } from "react-router"
|
||||
import { K8sApiError } from "@cozystack/k8s-client"
|
||||
|
||||
// Drive useK8sGet's result per test; the page's loading/error guards are the unit under test.
|
||||
const h = vi.hoisted(() => ({
|
||||
get: { data: undefined as unknown, isLoading: true, error: undefined as unknown },
|
||||
configMaps: [] as unknown[],
|
||||
configMapsError: undefined as unknown,
|
||||
tabLabels: [] as string[],
|
||||
}))
|
||||
|
||||
vi.mock("@cozystack/k8s-client", () => ({
|
||||
vi.mock("@cozystack/k8s-client", async (importOriginal) => ({
|
||||
K8sApiError: (await importOriginal<typeof import("@cozystack/k8s-client")>()).K8sApiError,
|
||||
useK8sGet: () => h.get,
|
||||
useK8sDelete: () => ({ mutateAsync: vi.fn() }),
|
||||
// Presence probes (use-resource-presence.ts) — report empty lists.
|
||||
useK8sList: (ref: { plural: string }) => ({ data: { items: ref.plural === "configmaps" ? h.configMaps : [] }, isLoading: false }),
|
||||
useK8sList: (ref: { plural: string }) => ref.plural === "configmaps"
|
||||
? { data: { items: h.configMaps }, error: h.configMapsError, isLoading: false }
|
||||
: { data: { items: [] }, isLoading: false },
|
||||
}))
|
||||
vi.mock("../../lib/app-definitions.ts", () => ({
|
||||
useApplicationDefinitions: () => ({
|
||||
@@ -89,4 +94,19 @@ describe("ApplicationDetailPage configuration", () => {
|
||||
renderPage()
|
||||
expect(h.tabLabels).toContain("ConfigMaps")
|
||||
})
|
||||
|
||||
it("drops the ConfigMaps tab when the resource map stops being readable", () => {
|
||||
h.get = {
|
||||
data: { kind: "Postgres", metadata: { name: "demo", namespace: "tenant-test" } },
|
||||
isLoading: false,
|
||||
error: undefined,
|
||||
}
|
||||
h.configMaps = [{ metadata: { name: "demo-resourcemap" }, data: {
|
||||
resources: "- apiVersion: v1\n kind: ConfigMap\n name: postgres-demo-config",
|
||||
} }]
|
||||
h.configMapsError = new K8sApiError(403, "forbidden")
|
||||
h.tabLabels = []
|
||||
renderPage()
|
||||
expect(h.tabLabels).not.toContain("ConfigMaps")
|
||||
})
|
||||
})
|
||||
+24
-3
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import { K8sApiError, K8sClient } from "@cozystack/k8s-client"
|
||||
import type { ApplicationDefinition, ApplicationInstance } from "@cozystack/types"
|
||||
import { renderWithK8sProvider } from "../../test-utils/render.tsx"
|
||||
@@ -24,7 +24,7 @@ function Configuration() {
|
||||
return <ConfigMapsTab namespace={namespace} names={maps.names} error={maps.error} />
|
||||
}
|
||||
|
||||
function makeClient(error?: Error) {
|
||||
function makeClient(error?: Error, map = resources) {
|
||||
const client = new K8sClient()
|
||||
vi.spyOn(client, "watch").mockReturnValue(() => {})
|
||||
vi.spyOn(client, "list").mockImplementation(async (_group, _version, _plural, ns, options) => {
|
||||
@@ -32,7 +32,7 @@ function makeClient(error?: Error) {
|
||||
const name = options?.fieldSelector?.replace("metadata.name=", "")
|
||||
if (name === "foundationdb-demo-resourcemap") {
|
||||
return { apiVersion: "v1", kind: "ConfigMapList", metadata: { resourceVersion: "1" }, items: [
|
||||
{ metadata: { name, namespace }, data: { resources } },
|
||||
{ metadata: { name, namespace }, data: { resources: map } },
|
||||
] }
|
||||
}
|
||||
expect(name).toBe("foundationdb-demo-config")
|
||||
@@ -88,6 +88,27 @@ describe("application ConfigMaps", () => {
|
||||
expect(screen.queryByText("No configuration values.")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([403, 404])("drops the ConfigMaps when the resource map answers %i after a successful read", async (status) => {
|
||||
const client = makeClient()
|
||||
const { queryClient } = renderWithK8sProvider(<Configuration />, { client })
|
||||
expect(await screen.findByText(clusterFile)).toBeInTheDocument()
|
||||
vi.mocked(client.list).mockRejectedValue(new K8sApiError(status, "resource map unavailable"))
|
||||
await act(() => queryClient.invalidateQueries())
|
||||
expect(await screen.findByText("No ConfigMaps.")).toBeInTheDocument()
|
||||
expect(screen.queryByText(clusterFile)).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("drops a malformed-map error when the resource map turns forbidden", async () => {
|
||||
const client = makeClient(undefined, "not a list")
|
||||
const { queryClient } = renderWithK8sProvider(<Configuration />, { client })
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("must contain a list")
|
||||
vi.mocked(client.list).mockRejectedValue(new K8sApiError(403, "resource map unavailable"))
|
||||
await act(() => queryClient.invalidateQueries())
|
||||
expect(await screen.findByText("No ConfigMaps.")).toBeInTheDocument()
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("ignores non-ConfigMap and cross-namespace entries and removes duplicate names", () => {
|
||||
expect(configMapNames(JSON.stringify([
|
||||
{ apiVersion: "v1", kind: "ConfigMap", name: "connection" },
|
||||
|
||||
+2
-2
@@ -35,7 +35,8 @@ export function useApplicationConfigMaps(
|
||||
{ apiGroup: "", apiVersion: "v1", plural: "configmaps", namespace },
|
||||
{ enabled: !!name && !!namespace, fieldSelector: `metadata.name=${name}`, retry: false },
|
||||
)
|
||||
const resources = query.data?.items.find((item) => item.metadata.name === name)?.data?.resources
|
||||
const unavailable = query.error && query.error instanceof K8sApiError && [403, 404].includes(query.error.status)
|
||||
const resources = unavailable ? undefined : query.data?.items.find((item) => item.metadata.name === name)?.data?.resources
|
||||
const parsed = useMemo(() => {
|
||||
try {
|
||||
return { names: configMapNames(resources, namespace ?? ""), error: null }
|
||||
@@ -43,6 +44,5 @@ export function useApplicationConfigMaps(
|
||||
return { names: [], error: error instanceof Error ? error : new Error("Invalid resource map.") }
|
||||
}
|
||||
}, [resources, namespace])
|
||||
const unavailable = query.error && query.error instanceof K8sApiError && [403, 404].includes(query.error.status)
|
||||
return { ...parsed, isLoading: query.isLoading, error: parsed.error ?? (unavailable ? null : query.error) ?? null }
|
||||
}
|
||||
Reference in new issue
Block a user