feat: 80% real frontend coverage

This commit is contained in:
Julian Sommer
2026-03-11 10:56:42 +10:00
parent 40e09f0886
commit 7af16b75b9
12 changed files with 1021 additions and 1 deletions
@@ -0,0 +1,34 @@
import { render, screen } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { DetailCard } from "./detail-card"
describe("DetailCard", () => {
it("renders children", () => {
render(<DetailCard>Card content</DetailCard>)
expect(screen.getByText("Card content")).toBeInTheDocument()
})
it("renders header", () => {
render(<DetailCard.Header>Section Title</DetailCard.Header>)
expect(
screen.getByRole("heading", { name: "Section Title" }),
).toBeInTheDocument()
})
it("renders content", () => {
render(<DetailCard.Content>Inner content</DetailCard.Content>)
expect(screen.getByText("Inner content")).toBeInTheDocument()
})
it("composes root, header, and content together", () => {
render(
<DetailCard>
<DetailCard.Header>Title</DetailCard.Header>
<DetailCard.Content>Body</DetailCard.Content>
</DetailCard>,
)
expect(screen.getByRole("heading", { name: "Title" })).toBeInTheDocument()
expect(screen.getByText("Body")).toBeInTheDocument()
})
})
@@ -0,0 +1,27 @@
import { render, screen } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { DetailRow } from "./detail-row"
describe("DetailRow", () => {
it("renders label and value", () => {
render(<DetailRow label="Instance ID" value="i-abc123" />)
expect(screen.getByText("Instance ID")).toBeInTheDocument()
expect(screen.getByText("i-abc123")).toBeInTheDocument()
})
it("renders dash when value is undefined", () => {
render(<DetailRow label="Subnet" />)
expect(screen.getByText("—")).toBeInTheDocument()
})
it("renders dash when value is empty string", () => {
render(<DetailRow label="Subnet" value="" />)
expect(screen.getByText("—")).toBeInTheDocument()
})
it("renders ReactNode as value", () => {
render(<DetailRow label="Status" value={<span>running</span>} />)
expect(screen.getByText("running")).toBeInTheDocument()
})
})
@@ -0,0 +1,81 @@
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { describe, expect, it, vi } from "vitest"
import { FormActions } from "./form-actions"
describe("FormActions", () => {
it("renders submit and cancel buttons", () => {
render(
<FormActions
isSubmitting={false}
isPending={false}
onCancel={vi.fn()}
submitLabel="Create"
pendingLabel="Creating..."
/>,
)
expect(screen.getByRole("button", { name: "Create" })).toBeInTheDocument()
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument()
})
it("shows pending label when submitting", () => {
render(
<FormActions
isSubmitting={true}
isPending={false}
onCancel={vi.fn()}
submitLabel="Create"
pendingLabel="Creating..."
/>,
)
expect(
screen.getByRole("button", { name: "Creating..." }),
).toBeInTheDocument()
})
it("shows pending label when isPending", () => {
render(
<FormActions
isSubmitting={false}
isPending={true}
onCancel={vi.fn()}
submitLabel="Create"
pendingLabel="Creating..."
/>,
)
expect(
screen.getByRole("button", { name: "Creating..." }),
).toBeInTheDocument()
})
it("disables both buttons when submitting", () => {
render(
<FormActions
isSubmitting={true}
isPending={false}
onCancel={vi.fn()}
submitLabel="Create"
pendingLabel="Creating..."
/>,
)
expect(screen.getByRole("button", { name: "Creating..." })).toBeDisabled()
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled()
})
it("calls onCancel when cancel is clicked", async () => {
const onCancel = vi.fn()
const user = userEvent.setup()
render(
<FormActions
isSubmitting={false}
isPending={false}
onCancel={onCancel}
submitLabel="Create"
pendingLabel="Creating..."
/>,
)
await user.click(screen.getByRole("button", { name: "Cancel" }))
expect(onCancel).toHaveBeenCalledOnce()
})
})
@@ -0,0 +1,126 @@
import { act, render, renderHook, screen } from "@testing-library/react"
import type { ReactNode } from "react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { ThemeProvider, useTheme } from "./theme-provider"
function wrapper({ children }: { children: ReactNode }) {
return <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
}
beforeEach(() => {
localStorage.clear()
document.documentElement.classList.remove("light", "dark")
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockReturnValue({ matches: false }),
})
})
describe("ThemeProvider", () => {
it("renders children", () => {
render(
<ThemeProvider>
<p>Hello</p>
</ThemeProvider>,
)
expect(screen.getByText("Hello")).toBeInTheDocument()
})
it("applies light class to documentElement", () => {
render(
<ThemeProvider defaultTheme="light">
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("light")).toBe(true)
})
it("applies dark class to documentElement", () => {
render(
<ThemeProvider defaultTheme="dark">
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("dark")).toBe(true)
})
it("uses system theme when set to system", () => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockReturnValue({ matches: true }),
})
render(
<ThemeProvider defaultTheme="system">
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("dark")).toBe(true)
})
it("reads stored theme from localStorage", () => {
localStorage.setItem("ui-theme", "dark")
render(
<ThemeProvider>
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("dark")).toBe(true)
})
it("ignores invalid stored theme", () => {
localStorage.setItem("ui-theme", "invalid")
render(
<ThemeProvider defaultTheme="light">
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("light")).toBe(true)
})
it("uses custom storageKey", () => {
localStorage.setItem("custom-key", "dark")
render(
<ThemeProvider storageKey="custom-key">
<p>Test</p>
</ThemeProvider>,
)
expect(document.documentElement.classList.contains("dark")).toBe(true)
})
})
describe("useTheme", () => {
it("throws when used outside ThemeProvider", () => {
expect(() => renderHook(() => useTheme())).toThrow(
"useTheme must be used within a ThemeProvider",
)
})
it("returns current theme", () => {
const { result } = renderHook(() => useTheme(), { wrapper })
expect(result.current.theme).toBe("light")
})
it("setTheme updates theme and localStorage", () => {
const { result } = renderHook(() => useTheme(), { wrapper })
act(() => {
result.current.setTheme("dark")
})
expect(result.current.theme).toBe("dark")
expect(localStorage.getItem("ui-theme")).toBe("dark")
})
it("removes previous class when theme changes", () => {
const { result } = renderHook(() => useTheme(), { wrapper })
expect(document.documentElement.classList.contains("light")).toBe(true)
act(() => {
result.current.setTheme("dark")
})
expect(document.documentElement.classList.contains("dark")).toBe(true)
expect(document.documentElement.classList.contains("light")).toBe(false)
})
})
@@ -0,0 +1,54 @@
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import type { ReactNode } from "react"
import { describe, expect, it } from "vitest"
import { ThemeProvider, useTheme } from "./theme-provider"
import { ThemeSwitch } from "./theme-switch"
function ThemeDisplay() {
const { theme } = useTheme()
return <span data-testid="theme">{theme}</span>
}
function renderWithTheme(defaultTheme: "light" | "dark" | "system" = "light") {
return render(
<ThemeProvider defaultTheme={defaultTheme}>
<ThemeSwitch />
<ThemeDisplay />
</ThemeProvider>,
)
}
describe("ThemeSwitch", () => {
it("renders toggle button", () => {
renderWithTheme()
expect(
screen.getByRole("button", { name: "Toggle theme" }),
).toBeInTheDocument()
})
it("toggles from light to dark", async () => {
const user = userEvent.setup()
renderWithTheme("light")
await user.click(screen.getByRole("button", { name: "Toggle theme" }))
expect(screen.getByTestId("theme")).toHaveTextContent("dark")
})
it("toggles from dark to light", async () => {
const user = userEvent.setup()
renderWithTheme("dark")
await user.click(screen.getByRole("button", { name: "Toggle theme" }))
expect(screen.getByTestId("theme")).toHaveTextContent("light")
})
it("toggles from system to dark", async () => {
const user = userEvent.setup()
renderWithTheme("system")
await user.click(screen.getByRole("button", { name: "Toggle theme" }))
expect(screen.getByTestId("theme")).toHaveTextContent("dark")
})
})
@@ -0,0 +1,269 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { renderHook, waitFor } from "@testing-library/react"
import type { ReactNode } from "react"
import { afterEach, describe, expect, it, vi } from "vitest"
const mockSend = vi.fn().mockResolvedValue({})
vi.mock("@/lib/awsClient", () => ({
getIamClient: () => ({ send: mockSend }),
}))
import {
useAttachUserPolicy,
useCreateAccessKey,
useCreatePolicy,
useCreateUser,
useDeleteAccessKey,
useDeletePolicy,
useDeleteUser,
useDetachUserPolicy,
useUpdateAccessKey,
} from "./iam"
let queryClient: QueryClient
function wrapper({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
function createQueryClient() {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return queryClient
}
afterEach(() => {
mockSend.mockClear()
})
describe("useCreateUser", () => {
it("sends CreateUserCommand with userName", async () => {
createQueryClient()
const { result } = renderHook(() => useCreateUser(), { wrapper })
result.current.mutate({ userName: "admin" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
Path: undefined,
})
})
it("includes Path when provided", async () => {
createQueryClient()
const { result } = renderHook(() => useCreateUser(), { wrapper })
result.current.mutate({ userName: "admin", path: "/engineering/" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
Path: "/engineering/",
})
})
it("invalidates users query on success", async () => {
createQueryClient()
const spy = vi.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(() => useCreateUser(), { wrapper })
result.current.mutate({ userName: "admin" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(spy).toHaveBeenCalledWith({ queryKey: ["iam", "users"] })
})
})
describe("useDeleteUser", () => {
it("sends DeleteUserCommand with userName", async () => {
createQueryClient()
const { result } = renderHook(() => useDeleteUser(), { wrapper })
result.current.mutate("admin")
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ UserName: "admin" })
})
})
describe("useCreateAccessKey", () => {
it("sends CreateAccessKeyCommand with userName", async () => {
createQueryClient()
const { result } = renderHook(() => useCreateAccessKey(), { wrapper })
result.current.mutate("admin")
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ UserName: "admin" })
})
it("invalidates access-keys query on success", async () => {
createQueryClient()
const spy = vi.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(() => useCreateAccessKey(), { wrapper })
result.current.mutate("admin")
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(spy).toHaveBeenCalledWith({ queryKey: ["iam", "access-keys"] })
})
})
describe("useDeleteAccessKey", () => {
it("sends DeleteAccessKeyCommand with userName and accessKeyId", async () => {
createQueryClient()
const { result } = renderHook(() => useDeleteAccessKey(), { wrapper })
result.current.mutate({ userName: "admin", accessKeyId: "AKIA123" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
AccessKeyId: "AKIA123",
})
})
})
describe("useUpdateAccessKey", () => {
it("sends UpdateAccessKeyCommand with status", async () => {
createQueryClient()
const { result } = renderHook(() => useUpdateAccessKey(), { wrapper })
result.current.mutate({
userName: "admin",
accessKeyId: "AKIA123",
status: "Inactive",
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
AccessKeyId: "AKIA123",
Status: "Inactive",
})
})
})
describe("useCreatePolicy", () => {
it("sends CreatePolicyCommand with policy data", async () => {
createQueryClient()
const { result } = renderHook(() => useCreatePolicy(), { wrapper })
result.current.mutate({
policyName: "ReadOnly",
policyDocument: '{"Version":"2012-10-17"}',
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
PolicyName: "ReadOnly",
Description: undefined,
PolicyDocument: '{"Version":"2012-10-17"}',
})
})
it("includes Description when provided", async () => {
createQueryClient()
const { result } = renderHook(() => useCreatePolicy(), { wrapper })
result.current.mutate({
policyName: "ReadOnly",
description: "Read-only access",
policyDocument: "{}",
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input.Description).toBe(
"Read-only access",
)
})
it("invalidates policies query on success", async () => {
createQueryClient()
const spy = vi.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(() => useCreatePolicy(), { wrapper })
result.current.mutate({ policyName: "ReadOnly", policyDocument: "{}" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(spy).toHaveBeenCalledWith({ queryKey: ["iam", "policies"] })
})
})
describe("useDeletePolicy", () => {
it("sends DeletePolicyCommand with policyArn", async () => {
createQueryClient()
const { result } = renderHook(() => useDeletePolicy(), { wrapper })
result.current.mutate("arn:aws:iam::123:policy/ReadOnly")
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
PolicyArn: "arn:aws:iam::123:policy/ReadOnly",
})
})
})
describe("useAttachUserPolicy", () => {
it("sends AttachUserPolicyCommand with userName and policyArn", async () => {
createQueryClient()
const { result } = renderHook(() => useAttachUserPolicy(), { wrapper })
result.current.mutate({ userName: "admin", policyArn: "arn:test" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
PolicyArn: "arn:test",
})
})
it("invalidates attached-user-policies query on success", async () => {
createQueryClient()
const spy = vi.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(() => useAttachUserPolicy(), { wrapper })
result.current.mutate({ userName: "admin", policyArn: "arn:test" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(spy).toHaveBeenCalledWith({
queryKey: ["iam", "attached-user-policies"],
})
})
})
describe("useDetachUserPolicy", () => {
it("sends DetachUserPolicyCommand with userName and policyArn", async () => {
createQueryClient()
const { result } = renderHook(() => useDetachUserPolicy(), { wrapper })
result.current.mutate({ userName: "admin", policyArn: "arn:test" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
UserName: "admin",
PolicyArn: "arn:test",
})
})
it("invalidates attached-user-policies query on success", async () => {
createQueryClient()
const spy = vi.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(() => useDetachUserPolicy(), { wrapper })
result.current.mutate({ userName: "admin", policyArn: "arn:test" })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(spy).toHaveBeenCalledWith({
queryKey: ["iam", "attached-user-policies"],
})
})
})
@@ -0,0 +1,171 @@
import { afterEach, describe, expect, it, vi } from "vitest"
const mockSend = vi.fn().mockResolvedValue({})
vi.mock("@/lib/awsClient", () => ({
getIamClient: () => ({ send: mockSend }),
}))
import {
iamAccessKeysQueryOptions,
iamAttachedUserPoliciesQueryOptions,
iamPoliciesQueryOptions,
iamPolicyQueryOptions,
iamPolicyVersionQueryOptions,
iamUserQueryOptions,
iamUsersQueryOptions,
} from "./iam"
describe("query keys", () => {
it("iamUsersQueryOptions has correct key", () => {
expect(iamUsersQueryOptions.queryKey).toEqual(["iam", "users"])
})
it("iamUserQueryOptions includes userName in key", () => {
expect(iamUserQueryOptions("admin").queryKey).toEqual([
"iam",
"users",
"admin",
])
})
it("iamAccessKeysQueryOptions includes userName in key", () => {
expect(iamAccessKeysQueryOptions("admin").queryKey).toEqual([
"iam",
"access-keys",
"admin",
])
})
it("iamPoliciesQueryOptions has correct key", () => {
expect(iamPoliciesQueryOptions.queryKey).toEqual(["iam", "policies"])
})
it("iamPolicyQueryOptions includes policyArn in key", () => {
expect(
iamPolicyQueryOptions("arn:aws:iam::123:policy/ReadOnly").queryKey,
).toEqual(["iam", "policies", "arn:aws:iam::123:policy/ReadOnly"])
})
it("iamPolicyVersionQueryOptions includes policyArn and versionId in key", () => {
expect(
iamPolicyVersionQueryOptions("arn:aws:iam::123:policy/ReadOnly", "v1")
.queryKey,
).toEqual([
"iam",
"policy-versions",
"arn:aws:iam::123:policy/ReadOnly",
"v1",
])
})
it("iamAttachedUserPoliciesQueryOptions includes userName in key", () => {
expect(iamAttachedUserPoliciesQueryOptions("admin").queryKey).toEqual([
"iam",
"attached-user-policies",
"admin",
])
})
})
describe("staleTime", () => {
it("users use staleTime", () => {
expect(iamUsersQueryOptions.staleTime).toBe(300_000)
})
it("user uses staleTime", () => {
expect(iamUserQueryOptions("admin").staleTime).toBe(300_000)
})
it("access keys use staleTime", () => {
expect(iamAccessKeysQueryOptions("admin").staleTime).toBe(300_000)
})
it("policies use staleTime", () => {
expect(iamPoliciesQueryOptions.staleTime).toBe(300_000)
})
it("policy uses staleTime", () => {
expect(iamPolicyQueryOptions("arn:test").staleTime).toBe(300_000)
})
it("policy version uses staleTime", () => {
expect(iamPolicyVersionQueryOptions("arn:test", "v1").staleTime).toBe(
300_000,
)
})
it("attached user policies use staleTime", () => {
expect(iamAttachedUserPoliciesQueryOptions("admin").staleTime).toBe(
300_000,
)
})
})
describe("queryFn", () => {
afterEach(() => {
mockSend.mockClear()
})
it("iamUsersQueryOptions sends ListUsersCommand", async () => {
const queryFn = iamUsersQueryOptions.queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend).toHaveBeenCalledOnce()
expect(mockSend.mock.calls[0]?.[0].input).toEqual({})
})
it("iamUserQueryOptions sends GetUserCommand with userName", async () => {
const queryFn = iamUserQueryOptions("admin").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ UserName: "admin" })
})
it("iamAccessKeysQueryOptions sends ListAccessKeysCommand", async () => {
const queryFn = iamAccessKeysQueryOptions("admin").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ UserName: "admin" })
})
it("iamPoliciesQueryOptions sends ListPoliciesCommand with Local scope", async () => {
const queryFn = iamPoliciesQueryOptions.queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ Scope: "Local" })
})
it("iamPolicyQueryOptions sends GetPolicyCommand with policyArn", async () => {
const queryFn = iamPolicyQueryOptions("arn:test").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
PolicyArn: "arn:test",
})
})
it("iamPolicyVersionQueryOptions sends GetPolicyVersionCommand", async () => {
const queryFn = iamPolicyVersionQueryOptions("arn:test", "v1").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
PolicyArn: "arn:test",
VersionId: "v1",
})
})
it("iamAttachedUserPoliciesQueryOptions sends ListAttachedUserPoliciesCommand", async () => {
const queryFn = iamAttachedUserPoliciesQueryOptions("admin").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({ UserName: "admin" })
})
})
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it, vi } from "vitest"
const mockSend = vi.fn().mockResolvedValue({})
vi.mock("@/lib/awsClient", () => ({
getS3Client: () => ({ send: mockSend }),
}))
import { s3BucketObjectsQueryOptions, s3BucketsQueryOptions } from "./s3"
describe("query keys", () => {
it("s3BucketsQueryOptions has correct key", () => {
expect(s3BucketsQueryOptions.queryKey).toEqual(["s3", "buckets"])
})
it("s3BucketObjectsQueryOptions includes bucket and empty prefix in key", () => {
expect(s3BucketObjectsQueryOptions("my-bucket").queryKey).toEqual([
"s3",
"buckets",
"my-bucket",
"objects",
"",
])
})
it("s3BucketObjectsQueryOptions includes prefix in key", () => {
expect(
s3BucketObjectsQueryOptions("my-bucket", "photos/").queryKey,
).toEqual(["s3", "buckets", "my-bucket", "objects", "photos/"])
})
})
describe("queryFn", () => {
afterEach(() => {
mockSend.mockClear()
})
it("s3BucketsQueryOptions sends ListBucketsCommand", async () => {
const queryFn = s3BucketsQueryOptions.queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend).toHaveBeenCalledOnce()
expect(mockSend.mock.calls[0]?.[0].input).toEqual({})
})
it("s3BucketObjectsQueryOptions sends ListObjectsV2Command with bucket and delimiter", async () => {
const queryFn = s3BucketObjectsQueryOptions("my-bucket").queryFn as (
ctx: never,
) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
Bucket: "my-bucket",
Prefix: undefined,
Delimiter: "/",
})
})
it("s3BucketObjectsQueryOptions sends prefix when provided", async () => {
const queryFn = s3BucketObjectsQueryOptions("my-bucket", "docs/")
.queryFn as (ctx: never) => Promise<unknown>
await queryFn({} as never)
expect(mockSend.mock.calls[0]?.[0].input).toEqual({
Bucket: "my-bucket",
Prefix: "docs/",
Delimiter: "/",
})
})
})
@@ -0,0 +1,24 @@
import { render, screen } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { PolicyDocumentViewer } from "./policy-document-viewer"
describe("PolicyDocumentViewer", () => {
it("formats valid JSON with indentation", () => {
const doc = '{"Version":"2012-10-17","Statement":[]}'
const { container } = render(<PolicyDocumentViewer document={doc} />)
const pre = container.querySelector("pre")
expect(pre?.textContent).toBe(JSON.stringify(JSON.parse(doc), null, 2))
})
it("renders invalid JSON as-is", () => {
render(<PolicyDocumentViewer document="not valid json" />)
expect(screen.getByText("not valid json")).toBeInTheDocument()
})
it("renders in a pre element", () => {
render(<PolicyDocumentViewer document="{}" />)
const pre = screen.getByText("{}").closest("pre")
expect(pre).toBeInTheDocument()
})
})
@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest"
import { createPolicySchema, createUserSchema } from "./iam"
describe("createUserSchema", () => {
it("accepts a valid user name", () => {
const result = createUserSchema.safeParse({ userName: "admin" })
expect(result.success).toBe(true)
})
it("rejects empty user name", () => {
const result = createUserSchema.safeParse({ userName: "" })
expect(result.success).toBe(false)
})
it("rejects user name over 64 chars", () => {
const result = createUserSchema.safeParse({ userName: "a".repeat(65) })
expect(result.success).toBe(false)
})
it("rejects user name with invalid characters", () => {
const result = createUserSchema.safeParse({ userName: "user name!" })
expect(result.success).toBe(false)
})
it("accepts user name with allowed special characters", () => {
const result = createUserSchema.safeParse({ userName: "user+=,.@-test" })
expect(result.success).toBe(true)
})
it("allows optional path", () => {
const result = createUserSchema.safeParse({
userName: "admin",
path: "/engineering/",
})
expect(result.success).toBe(true)
})
})
describe("createPolicySchema", () => {
it("accepts valid policy params", () => {
const result = createPolicySchema.safeParse({
policyName: "ReadOnly",
policyDocument: '{"Version":"2012-10-17","Statement":[]}',
})
expect(result.success).toBe(true)
})
it("rejects empty policy name", () => {
const result = createPolicySchema.safeParse({
policyName: "",
policyDocument: "{}",
})
expect(result.success).toBe(false)
})
it("rejects policy name over 128 chars", () => {
const result = createPolicySchema.safeParse({
policyName: "a".repeat(129),
policyDocument: "{}",
})
expect(result.success).toBe(false)
})
it("rejects invalid JSON in policy document", () => {
const result = createPolicySchema.safeParse({
policyName: "ReadOnly",
policyDocument: "not json",
})
expect(result.success).toBe(false)
})
it("rejects empty policy document", () => {
const result = createPolicySchema.safeParse({
policyName: "ReadOnly",
policyDocument: "",
})
expect(result.success).toBe(false)
})
it("allows optional description", () => {
const result = createPolicySchema.safeParse({
policyName: "ReadOnly",
description: "Read-only access",
policyDocument: "{}",
})
expect(result.success).toBe(true)
})
})
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest"
import { createBucketSchema } from "./s3"
describe("createBucketSchema", () => {
it("accepts a valid bucket name", () => {
const result = createBucketSchema.safeParse({ bucketName: "my-bucket" })
expect(result.success).toBe(true)
})
it("rejects bucket name under 3 characters", () => {
const result = createBucketSchema.safeParse({ bucketName: "ab" })
expect(result.success).toBe(false)
})
it("rejects bucket name over 63 characters", () => {
const result = createBucketSchema.safeParse({
bucketName: "a".repeat(64),
})
expect(result.success).toBe(false)
})
it("rejects uppercase letters", () => {
const result = createBucketSchema.safeParse({ bucketName: "MyBucket" })
expect(result.success).toBe(false)
})
it("rejects names starting with a period", () => {
const result = createBucketSchema.safeParse({ bucketName: ".my-bucket" })
expect(result.success).toBe(false)
})
it("rejects names ending with a hyphen", () => {
const result = createBucketSchema.safeParse({ bucketName: "my-bucket-" })
expect(result.success).toBe(false)
})
it("rejects consecutive periods", () => {
const result = createBucketSchema.safeParse({ bucketName: "my..bucket" })
expect(result.success).toBe(false)
})
it("rejects adjacent period and hyphen", () => {
const result = createBucketSchema.safeParse({ bucketName: "my.-bucket" })
expect(result.success).toBe(false)
})
it("rejects adjacent hyphen and period", () => {
const result = createBucketSchema.safeParse({ bucketName: "my-.bucket" })
expect(result.success).toBe(false)
})
it("accepts bucket name with periods and hyphens", () => {
const result = createBucketSchema.safeParse({
bucketName: "my.test-bucket.v2",
})
expect(result.success).toBe(true)
})
it("accepts all-numeric bucket name", () => {
const result = createBucketSchema.safeParse({ bucketName: "123" })
expect(result.success).toBe(true)
})
})
+13 -1
View File
@@ -48,8 +48,20 @@ export default defineConfig({
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
coverage: {
all: true,
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/components/ui/**",
"src/layouts/**",
"src/routes/*.{ts,tsx}",
"src/routes/**/!(-components)/*.{ts,tsx}",
"src/test/**",
"src/**/*.test.*",
"src/routeTree.gen.ts",
"src/main.tsx",
],
thresholds: {
statements: 70,
lines: 70,
},
},
},