All checks were successful
CI / verify (push) Successful in 59s
Adds specs for the files added or modified in the recent merge that
weren't yet covered: oauthReturnPath, withATProtoImages, link,
displayLanguage, and the useGitHubContent / useImageUpload /
useNoteFreshness hooks. Locks in the new pullLatest contract
({raw, failureStatus}) and the fetchLatestSha branch semantics
(ok/unauthorized/offline).
198 lines
5.5 KiB
TypeScript
198 lines
5.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
vi.mock("@/modules/repo/services/octo", () => ({
|
|
getOctokit: vi.fn(),
|
|
runWithAuthRetry: vi.fn()
|
|
}))
|
|
|
|
vi.mock("@/utils/notif", () => ({
|
|
confirmMessage: vi.fn(),
|
|
errorMessage: vi.fn()
|
|
}))
|
|
|
|
import { getOctokit, runWithAuthRetry } from "@/modules/repo/services/octo"
|
|
import { confirmMessage, errorMessage } from "@/utils/notif"
|
|
|
|
import { useGitHubContent } from "./useGitHubContent.hook"
|
|
|
|
const make = () => useGitHubContent({ user: "alice", repo: "notes" })
|
|
|
|
describe("useGitHubContent.fetchLatestSha", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(runWithAuthRetry).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it("returns the sha from a file response", async () => {
|
|
vi.mocked(runWithAuthRetry).mockResolvedValue({
|
|
data: { sha: "abc123" }
|
|
} as never)
|
|
|
|
expect(await make().fetchLatestSha("a.md")).toEqual({
|
|
kind: "ok",
|
|
sha: "abc123"
|
|
})
|
|
})
|
|
|
|
it("returns sha=null when the path resolves to a directory (array response)", async () => {
|
|
vi.mocked(runWithAuthRetry).mockResolvedValue({ data: [] } as never)
|
|
|
|
expect(await make().fetchLatestSha("dir/")).toEqual({
|
|
kind: "ok",
|
|
sha: null
|
|
})
|
|
})
|
|
|
|
it("returns sha=null when the data object has no sha field", async () => {
|
|
vi.mocked(runWithAuthRetry).mockResolvedValue({
|
|
data: { type: "submodule" }
|
|
} as never)
|
|
|
|
expect(await make().fetchLatestSha("path")).toEqual({
|
|
kind: "ok",
|
|
sha: null
|
|
})
|
|
})
|
|
|
|
it("returns kind=unauthorized on 401", async () => {
|
|
vi.mocked(runWithAuthRetry).mockRejectedValue(
|
|
Object.assign(new Error("Unauthorized"), { status: 401 })
|
|
)
|
|
|
|
expect(await make().fetchLatestSha("path")).toEqual({
|
|
kind: "unauthorized"
|
|
})
|
|
})
|
|
|
|
it("returns kind=offline on non-401 errors (404, network, etc.)", async () => {
|
|
vi.mocked(runWithAuthRetry).mockRejectedValue(
|
|
Object.assign(new Error("Not found"), { status: 404 })
|
|
)
|
|
expect(await make().fetchLatestSha("path")).toEqual({ kind: "offline" })
|
|
|
|
vi.mocked(runWithAuthRetry).mockRejectedValue(new Error("network"))
|
|
expect(await make().fetchLatestSha("path")).toEqual({ kind: "offline" })
|
|
})
|
|
})
|
|
|
|
describe("useGitHubContent.updateFile / createFile (putFile)", () => {
|
|
const mockOctokitRequest = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
mockOctokitRequest.mockReset()
|
|
vi.mocked(getOctokit).mockResolvedValue({
|
|
request: mockOctokitRequest
|
|
} as never)
|
|
vi.mocked(confirmMessage).mockReset()
|
|
vi.mocked(errorMessage).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it("returns the new sha and calls confirmMessage on success", async () => {
|
|
mockOctokitRequest.mockResolvedValue({
|
|
data: { content: { sha: "new-sha" } }
|
|
})
|
|
|
|
const result = await make().updateFile({
|
|
content: "# hello",
|
|
path: "a.md",
|
|
sha: "old-sha"
|
|
})
|
|
|
|
expect(result).toEqual({ sha: "new-sha", conflict: false })
|
|
expect(confirmMessage).toHaveBeenCalledWith("✅ Note saved")
|
|
})
|
|
|
|
it("flags conflict=true on 409 and 422 responses", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
|
|
for (const status of [409, 422]) {
|
|
mockOctokitRequest.mockRejectedValueOnce(
|
|
Object.assign(new Error("Conflict"), { status })
|
|
)
|
|
const result = await make().createFile({ content: "x", path: "a.md" })
|
|
expect(result).toEqual({ sha: null, conflict: true })
|
|
}
|
|
expect(errorMessage).toHaveBeenCalledWith(
|
|
"⚠ Conflict: this note changed on GitHub"
|
|
)
|
|
})
|
|
|
|
it("returns conflict=false and calls errorMessage on non-conflict errors", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
mockOctokitRequest.mockRejectedValue(
|
|
Object.assign(new Error("Server error"), { status: 500 })
|
|
)
|
|
|
|
const result = await make().updateFile({
|
|
content: "x",
|
|
path: "a.md",
|
|
sha: "old"
|
|
})
|
|
|
|
expect(result).toEqual({ sha: null, conflict: false })
|
|
expect(errorMessage).toHaveBeenCalledWith("❌ Note could not be saved")
|
|
})
|
|
})
|
|
|
|
describe("useGitHubContent.uploadBinaryFile", () => {
|
|
const mockOctokitRequest = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
mockOctokitRequest.mockReset()
|
|
vi.mocked(getOctokit).mockResolvedValue({
|
|
request: mockOctokitRequest
|
|
} as never)
|
|
vi.mocked(errorMessage).mockReset()
|
|
vi.mocked(confirmMessage).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it("uploads pre-base64-encoded content without re-encoding", async () => {
|
|
mockOctokitRequest.mockResolvedValue({
|
|
data: { content: { sha: "img-sha" } }
|
|
})
|
|
|
|
const result = await make().uploadBinaryFile({
|
|
base64: "ZGF0YQ==",
|
|
path: "img/cover.png"
|
|
})
|
|
|
|
expect(result).toEqual({ sha: "img-sha", conflict: false })
|
|
expect(mockOctokitRequest).toHaveBeenCalledWith(
|
|
"PUT /repos/{owner}/{repo}/contents/{+path}",
|
|
expect.objectContaining({
|
|
path: "img/cover.png",
|
|
content: "ZGF0YQ=="
|
|
})
|
|
)
|
|
expect(confirmMessage).toHaveBeenCalledWith("✅ Image uploaded")
|
|
})
|
|
|
|
it("surfaces a conflict on existing file (422)", async () => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
mockOctokitRequest.mockRejectedValue(
|
|
Object.assign(new Error("Exists"), { status: 422 })
|
|
)
|
|
|
|
const result = await make().uploadBinaryFile({
|
|
base64: "Zm9v",
|
|
path: "img.png"
|
|
})
|
|
|
|
expect(result).toEqual({ sha: null, conflict: true })
|
|
expect(errorMessage).toHaveBeenCalledWith(
|
|
"⚠ A file already exists at this path on GitHub"
|
|
)
|
|
})
|
|
})
|