import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { GiteaClient, generateReleaseNotes, GiteaReleaseConfig } from "../core/gitea.js"; const defaultConfig: GiteaReleaseConfig = { baseUrl: "https://git.example.com", token: "test-token-123", owner: "testorg", repo: "testrepo", }; function makeReleaseResponse(overrides: Partial<{ id: number; tag_name: string; name: string; body: string; url: string; html_url: string; draft: boolean; prerelease: boolean; }> = {}): Record { return { id: overrides.id ?? 1, tag_name: overrides.tag_name ?? "v1.0.0", name: overrides.name ?? "v1.0.0", body: overrides.body ?? "Release notes", url: overrides.url ?? "https://git.example.com/api/v1/repos/testorg/testrepo/releases/1", html_url: overrides.html_url ?? "https://git.example.com/testorg/testrepo/releases/tag/v1.0.0", draft: overrides.draft ?? false, prerelease: overrides.prerelease ?? false, }; } describe("GiteaClient", () => { let originalFetch: typeof globalThis.fetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); describe("createRelease", () => { it("creates a release via POST", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => makeReleaseResponse({ tag_name: "v1.0.0", name: "v1.0.0" }), }); const release = await client.createRelease({ tag_name: "v1.0.0", name: "v1.0.0", body: "Initial release", }); expect(release.tag_name).toBe("v1.0.0"); expect(globalThis.fetch).toHaveBeenCalledTimes(1); const call = (globalThis.fetch as jest.Mock).mock.calls[0]; expect(call[0]).toContain("/releases"); expect(call[1].method).toBe("POST"); expect(call[1].headers.Authorization).toBe("token test-token-123"); }); it("throws on non-ok response", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: false, status: 409, text: async () => "Conflict: tag already exists", }); await expect(client.createRelease({ tag_name: "v1.0.0", name: "v1.0.0", body: "", })).rejects.toThrow("Gitea API error: 409"); }); }); describe("listReleases", () => { it("lists releases via GET", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => [ makeReleaseResponse({ id: 1, tag_name: "v1.0.0" }), makeReleaseResponse({ id: 2, tag_name: "v1.1.0" }), ], }); const releases = await client.listReleases(); expect(releases).toHaveLength(2); expect(releases[0].tag_name).toBe("v1.0.0"); expect(releases[1].tag_name).toBe("v1.1.0"); }); it("throws on non-ok response", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500, }); await expect(client.listReleases()).rejects.toThrow("Gitea API error: 500"); }); }); describe("getReleaseByTag", () => { it("returns release when found", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, json: async () => makeReleaseResponse({ tag_name: "v1.0.0" }), }); const release = await client.getReleaseByTag("v1.0.0"); expect(release).not.toBeNull(); expect(release!.tag_name).toBe("v1.0.0"); }); it("returns null on 404", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: false, status: 404, }); const release = await client.getReleaseByTag("v0.0.0"); expect(release).toBeNull(); }); it("throws on other non-ok status", async () => { const client = new GiteaClient(defaultConfig); globalThis.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500, }); await expect(client.getReleaseByTag("v1.0.0")).rejects.toThrow("Gitea API error: 500"); }); }); }); describe("generateReleaseNotes", () => { let dir: string; beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-gitea-test-")); }); afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); it("parses git log into categorized sections", () => { const gitDir = path.join(dir, "repo"); fs.mkdirSync(gitDir, { recursive: true }); const { execSync } = require("node:child_process"); execSync("git init", { cwd: gitDir, stdio: "pipe" }); execSync('git config user.email "test@test.com"', { cwd: gitDir, stdio: "pipe" }); execSync('git config user.name "Test"', { cwd: gitDir, stdio: "pipe" }); fs.writeFileSync(path.join(gitDir, "file1.txt"), "hello"); execSync("git add -A", { cwd: gitDir, stdio: "pipe" }); execSync('git commit -m "feat: add authentication"', { cwd: gitDir, stdio: "pipe" }); fs.writeFileSync(path.join(gitDir, "file2.txt"), "world"); execSync("git add -A", { cwd: gitDir, stdio: "pipe" }); execSync('git commit -m "fix: resolve login bug"', { cwd: gitDir, stdio: "pipe" }); execSync("git tag v1.0.0", { cwd: gitDir, stdio: "pipe" }); const notes = generateReleaseNotes(gitDir, null, "v1.0.0"); expect(notes).toContain("New Features"); expect(notes).toContain("add authentication"); expect(notes).toContain("Bug Fixes"); expect(notes).toContain("resolve login bug"); }); it("returns no-commits message when no commits found", () => { const nonExistent = path.join(dir, "nonexistent"); const notes = generateReleaseNotes(nonExistent, null, "v0.0.0"); expect(notes).toContain("No commits found"); }); });