105 lines
5.1 KiB
TypeScript
105 lines
5.1 KiB
TypeScript
import { create } from "@bufbuild/protobuf";
|
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import type { ReactNode } from "react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useResolvedRelationMemos } from "@/components/MemoMetadata/Relation/useResolvedRelationMemos";
|
|
import { findMemoInCollectionQueries, memoKeys, useMemo, useUpdateMemo } from "@/hooks/useMemoQueries";
|
|
import { ListMemosResponseSchema, MemoRelation_Type, MemoRelationSchema, MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
|
|
|
|
const api = vi.hoisted(() => ({ getMemo: vi.fn(), updateMemo: vi.fn() }));
|
|
vi.mock("@/connect", () => ({ memoServiceClient: api }));
|
|
const setup = () => {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false, retryDelay: 0 }, mutations: { retry: false } } });
|
|
const wrapper = ({ children }: { children: ReactNode }) => <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
|
return { client, wrapper };
|
|
};
|
|
|
|
describe("memo access transitions", () => {
|
|
beforeEach(() => vi.resetAllMocks());
|
|
|
|
it.each([
|
|
Code.PermissionDenied,
|
|
Code.NotFound,
|
|
])("discards denied parent content and snippets while preserving the comment (%s)", async (code) => {
|
|
const { client, wrapper } = setup();
|
|
const parent = create(MemoSchema, { name: "memos/parent", content: "secret", snippet: "secret" });
|
|
const comment = create(MemoSchema, {
|
|
name: "memos/comment",
|
|
parent: parent.name,
|
|
content: "my comment",
|
|
relations: [
|
|
create(MemoRelationSchema, {
|
|
type: MemoRelation_Type.COMMENT,
|
|
memo: { name: "memos/comment" },
|
|
relatedMemo: { name: parent.name, snippet: "secret" },
|
|
}),
|
|
],
|
|
});
|
|
client.setQueryData(memoKeys.detail(parent.name), parent, { updatedAt: 1 });
|
|
client.setQueryData(memoKeys.detail(comment.name), comment);
|
|
client.setQueryData(memoKeys.list({}), create(ListMemosResponseSchema, { memos: [parent, comment] }));
|
|
api.getMemo.mockRejectedValue(new ConnectError("denied", code));
|
|
const { result } = renderHook(() => useMemo(parent.name), { wrapper });
|
|
await waitFor(() => expect(result.current.isUnavailable).toBe(true));
|
|
expect(result.current.data).toBeUndefined();
|
|
expect(client.getQueryData(memoKeys.detail(parent.name))).toBeNull();
|
|
expect(findMemoInCollectionQueries(client, parent.name)).toBeUndefined();
|
|
expect(client.getQueryData(memoKeys.detail(comment.name))).toMatchObject({ content: "my comment", parent: parent.name, relations: [] });
|
|
expect(api.getMemo).toHaveBeenCalledOnce();
|
|
api.getMemo.mockResolvedValue(parent);
|
|
await act(async () => {
|
|
await result.current.refetch();
|
|
});
|
|
await waitFor(() => expect(result.current.isUnavailable).toBe(false));
|
|
expect(result.current.data?.content).toBe("secret");
|
|
client.clear();
|
|
});
|
|
|
|
it("resolves readable relations even when another relation is denied", async () => {
|
|
const { client, wrapper } = setup();
|
|
api.getMemo.mockImplementation(({ name }) =>
|
|
name === "memos/denied"
|
|
? Promise.reject(new ConnectError("denied", Code.PermissionDenied))
|
|
: Promise.resolve(create(MemoSchema, { name, snippet: "Readable" })),
|
|
);
|
|
const { result } = renderHook(() => useResolvedRelationMemos(["memos/denied", "memos/readable"]), { wrapper });
|
|
await waitFor(() => expect(result.current["memos/readable"]?.snippet).toBe("Readable"));
|
|
expect(result.current["memos/denied"]).toBeNull();
|
|
client.clear();
|
|
});
|
|
|
|
it("retains transient errors as retryable errors instead of marking a memo unavailable", async () => {
|
|
const { client, wrapper } = setup();
|
|
api.getMemo.mockRejectedValue(new ConnectError("offline", Code.Unavailable));
|
|
const { result } = renderHook(() => useMemo("memos/parent"), { wrapper });
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
expect(result.current.isUnavailable).toBe(false);
|
|
expect(api.getMemo).toHaveBeenCalledTimes(2);
|
|
client.clear();
|
|
});
|
|
|
|
it("does not optimistically move a memo before the server accepts it", async () => {
|
|
const { client, wrapper } = setup();
|
|
const memo = create(MemoSchema, { name: "memos/moving", space: "spaces/a" });
|
|
client.setQueryData(memoKeys.detail(memo.name), memo);
|
|
let reject!: (error: Error) => void;
|
|
api.updateMemo.mockImplementation(
|
|
() =>
|
|
new Promise((_, fail) => {
|
|
reject = fail;
|
|
}),
|
|
);
|
|
const { result } = renderHook(() => useUpdateMemo(), { wrapper });
|
|
act(() => result.current.mutate({ update: { name: memo.name, space: "spaces/b" }, updateMask: ["space"] }));
|
|
await waitFor(() => expect(api.updateMemo).toHaveBeenCalledOnce());
|
|
expect(client.getQueryData(memoKeys.detail(memo.name))).toEqual(memo);
|
|
await act(async () => {
|
|
reject(new ConnectError("denied", Code.PermissionDenied));
|
|
});
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
expect(client.getQueryData(memoKeys.detail(memo.name))).toEqual(memo);
|
|
client.clear();
|
|
});
|
|
});
|