Skip to content

Commit bee7273

Browse files
adding tests for helper and main
1 parent 9a59833 commit bee7273

3 files changed

Lines changed: 454 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { jest } from "@jest/globals";
2+
import { Dependencies, OctokitClient, Logger } from "../../types/Dependencies.js";
3+
4+
5+
export function createMockLogger(): Logger & { [K in keyof Logger]: jest.Mock } {
6+
return {
7+
info: jest.fn<any>(),
8+
error: jest.fn<any>(),
9+
warning: jest.fn<any>(),
10+
debug: jest.fn<any>(),
11+
};
12+
}
13+
14+
// creates a mock OctokitClient with sensible defaults
15+
export function createMockOctokit(overrides: Partial<DeepPartial<OctokitClient>> = {}): OctokitClient {
16+
return {
17+
rest: {
18+
repos: {
19+
get: overrides.rest?.repos?.get as OctokitClient["rest"]["repos"]["get"] ??
20+
jest.fn<any>().mockResolvedValue({
21+
data: {
22+
name: "test-repo",
23+
description: "A test repository",
24+
html_url: "https://github.com/test-owner/test-repo",
25+
private: false,
26+
forks_count: 5,
27+
topics: ["test", "automation"],
28+
created_at: "2024-01-01T00:00:00Z",
29+
updated_at: "2024-06-01T00:00:00Z",
30+
default_branch: "main",
31+
},
32+
}),
33+
listLanguages: overrides.rest?.repos?.listLanguages as OctokitClient["rest"]["repos"]["listLanguages"] ??
34+
jest.fn<any>().mockResolvedValue({
35+
data: { TypeScript: 5000, JavaScript: 2000 },
36+
}),
37+
getContent: overrides.rest?.repos?.getContent as OctokitClient["rest"]["repos"]["getContent"] ??
38+
jest.fn<any>().mockResolvedValue({
39+
data: { sha: "abc123" },
40+
}),
41+
createOrUpdateFileContents: overrides.rest?.repos?.createOrUpdateFileContents as OctokitClient["rest"]["repos"]["createOrUpdateFileContents"] ??
42+
jest.fn<any>().mockResolvedValue({
43+
data: { commit: { sha: "def456" } },
44+
}),
45+
},
46+
},
47+
createPullRequest: overrides.createPullRequest as OctokitClient["createPullRequest"] ??
48+
jest.fn<any>().mockResolvedValue({
49+
data: { html_url: "https://github.com/test-owner/test-repo/pull/1" },
50+
}),
51+
};
52+
}
53+
54+
// creates a full mock Dependencies object with sensible defaults
55+
export function createMockDeps(overrides: Partial<Dependencies> = {}): Dependencies {
56+
const mockOctokit = createMockOctokit();
57+
58+
return {
59+
owner: "test-owner",
60+
repo: "test-repo",
61+
githubToken: "fake-github-token",
62+
adminToken: "",
63+
branch: "main",
64+
skipPR: false,
65+
isArchived: false,
66+
67+
octokit: mockOctokit,
68+
adminOctokit: null,
69+
70+
exec: jest.fn<any>().mockResolvedValue({
71+
stdout: JSON.stringify({ estimatedScheduleMonths: 2.5 }),
72+
stderr: "",
73+
}),
74+
readFile: jest.fn<any>().mockRejectedValue(new Error("File not found")),
75+
76+
log: createMockLogger(),
77+
setOutput: jest.fn<any>(),
78+
setFailed: jest.fn<any>(),
79+
80+
...overrides,
81+
};
82+
}
83+
84+
// helper type for deep partial overrides
85+
type DeepPartial<T> = {
86+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
87+
};

src/__tests__/unit/helper.test.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { describe, it, expect, jest, beforeEach } from "@jest/globals";
2+
import { createHelpers } from "../../helper.js";
3+
import { createMockDeps, createMockOctokit } from "../fixtures/mock-deps.js";
4+
import { Dependencies } from "../../types/Dependencies.js";
5+
6+
describe("createHelpers - calculateMetaData", () => {
7+
let deps: Dependencies;
8+
9+
beforeEach(() => {
10+
deps = createMockDeps();
11+
});
12+
13+
it("returns metadata from GitHub API and SCC", async () => {
14+
const helpers = createHelpers(deps);
15+
const result = await helpers.calculateMetaData();
16+
17+
expect(result.name).toBe("test-repo");
18+
expect(result.description).toBe("A test repository");
19+
expect(result.repositoryURL).toBe("https://github.com/test-owner/test-repo");
20+
expect(result.repositoryVisibility).toBe("public");
21+
expect(result.languages).toEqual(["TypeScript", "JavaScript"]);
22+
expect(result.laborHours).toBeGreaterThan(0);
23+
expect(result.reuseFrequency?.forks).toBe(5);
24+
expect(result.tags).toEqual(["test", "automation"]);
25+
});
26+
27+
it("reports private visibility for private repos", async () => {
28+
const mockOctokit = createMockOctokit({
29+
rest: {
30+
repos: {
31+
get: jest.fn<any>().mockResolvedValue({
32+
data: {
33+
name: "private-repo",
34+
description: "Secret stuff",
35+
html_url: "https://github.com/test-owner/private-repo",
36+
private: true,
37+
forks_count: 0,
38+
topics: [],
39+
created_at: "2024-01-01T00:00:00Z",
40+
updated_at: "2024-06-01T00:00:00Z",
41+
default_branch: "main",
42+
},
43+
}),
44+
},
45+
},
46+
});
47+
48+
deps = createMockDeps({ octokit: mockOctokit });
49+
const helpers = createHelpers(deps);
50+
const result = await helpers.calculateMetaData();
51+
52+
expect(result.repositoryVisibility).toBe("private");
53+
});
54+
55+
it("handles SCC failure gracefully", async () => {
56+
deps = createMockDeps({
57+
exec: jest.fn<any>().mockRejectedValue(new Error("scc not found")),
58+
});
59+
60+
const helpers = createHelpers(deps);
61+
await expect(helpers.calculateMetaData()).rejects.toThrow("scc not found");
62+
expect(deps.log.error).toHaveBeenCalled();
63+
});
64+
});
65+
66+
describe("createHelpers - getBaseBranch", () => {
67+
it("returns configured branch when provided", async () => {
68+
const deps = createMockDeps({ branch: "develop" });
69+
const helpers = createHelpers(deps);
70+
71+
expect(await helpers.getBaseBranch()).toBe("develop");
72+
});
73+
74+
it("fetches default branch from API when not configured", async () => {
75+
const deps = createMockDeps({ branch: "" });
76+
const helpers = createHelpers(deps);
77+
78+
expect(await helpers.getBaseBranch()).toBe("main");
79+
expect(deps.octokit.rest.repos.get).toHaveBeenCalled();
80+
});
81+
});
82+
83+
describe("createHelpers - readJSON", () => {
84+
it("parses valid JSON from file", async () => {
85+
const mockData = { name: "test", version: "1.0" };
86+
const deps = createMockDeps({
87+
readFile: jest.fn<any>().mockResolvedValue(JSON.stringify(mockData)),
88+
});
89+
const helpers = createHelpers(deps);
90+
91+
const result = await helpers.readJSON("/some/path/code.json");
92+
expect(result).toEqual(mockData);
93+
});
94+
95+
it("returns null for missing files", async () => {
96+
const deps = createMockDeps({
97+
readFile: jest.fn<any>().mockRejectedValue(new Error("ENOENT")),
98+
});
99+
const helpers = createHelpers(deps);
100+
101+
const result = await helpers.readJSON("/missing/code.json");
102+
expect(result).toBeNull();
103+
});
104+
105+
it("returns null for invalid JSON", async () => {
106+
const deps = createMockDeps({
107+
readFile: jest.fn<any>().mockResolvedValue("not json {{{"),
108+
});
109+
const helpers = createHelpers(deps);
110+
111+
const result = await helpers.readJSON("/bad/code.json");
112+
expect(result).toBeNull();
113+
});
114+
});
115+
116+
describe("createHelpers - sendPR", () => {
117+
it("creates a PR and sets outputs", async () => {
118+
const deps = createMockDeps();
119+
const helpers = createHelpers(deps);
120+
121+
const fakeCodeJSON = { name: "test" } as any;
122+
await helpers.sendPR(fakeCodeJSON, "main");
123+
124+
expect(deps.octokit.createPullRequest).toHaveBeenCalledWith(
125+
expect.objectContaining({
126+
owner: "test-owner",
127+
repo: "test-repo",
128+
base: "main",
129+
title: "Update code.json",
130+
}),
131+
);
132+
expect(deps.setOutput).toHaveBeenCalledWith("updated", true);
133+
expect(deps.setOutput).toHaveBeenCalledWith("method_used", "pull_request");
134+
});
135+
136+
it("uses archival title and labels when archived", async () => {
137+
const deps = createMockDeps({ isArchived: true });
138+
const helpers = createHelpers(deps);
139+
140+
await helpers.sendPR({ name: "test" } as any, "main");
141+
142+
expect(deps.octokit.createPullRequest).toHaveBeenCalledWith(
143+
expect.objectContaining({
144+
title: "Update code.json for archival",
145+
labels: ["archived"],
146+
}),
147+
);
148+
});
149+
});
150+
151+
describe("createHelpers - pushDirectlyWithFallback", () => {
152+
it("falls back to PR when no admin token", async () => {
153+
const deps = createMockDeps({ adminToken: "" });
154+
const helpers = createHelpers(deps);
155+
156+
await helpers.pushDirectlyWithFallback({ name: "test" } as any, "main");
157+
158+
// Should have fallen back to PR
159+
expect(deps.octokit.createPullRequest).toHaveBeenCalled();
160+
expect(deps.log.error).toHaveBeenCalledWith(
161+
expect.stringContaining("ADMIN_TOKEN is not provided"),
162+
);
163+
});
164+
165+
it("pushes directly when admin token is available", async () => {
166+
const adminOctokit = createMockOctokit();
167+
const deps = createMockDeps({
168+
adminToken: "admin-pat-token",
169+
adminOctokit,
170+
});
171+
const helpers = createHelpers(deps);
172+
173+
await helpers.pushDirectlyWithFallback({ name: "test" } as any, "main");
174+
175+
expect(adminOctokit.rest.repos.createOrUpdateFileContents).toHaveBeenCalled();
176+
expect(deps.setOutput).toHaveBeenCalledWith("method_used", "direct_push");
177+
});
178+
});
179+
180+
describe("createHelpers - validateOnly", () => {
181+
it("fails when code.json is missing", async () => {
182+
const deps = createMockDeps({
183+
readFile: jest.fn<any>().mockRejectedValue(new Error("ENOENT")),
184+
});
185+
const helpers = createHelpers(deps);
186+
187+
await helpers.validateOnly();
188+
189+
expect(deps.setFailed).toHaveBeenCalledWith(
190+
expect.stringContaining("code.json file not found"),
191+
);
192+
});
193+
194+
it("succeeds for valid code.json", async () => {
195+
const validCodeJSON = await import("../fixtures/test-code.json");
196+
const deps = createMockDeps({
197+
readFile: jest.fn<any>().mockResolvedValue(JSON.stringify(validCodeJSON.default ?? validCodeJSON)),
198+
});
199+
const helpers = createHelpers(deps);
200+
201+
await helpers.validateOnly();
202+
203+
expect(deps.setFailed).not.toHaveBeenCalled();
204+
expect(deps.log.info).toHaveBeenCalledWith("code.json is valid!");
205+
});
206+
});

0 commit comments

Comments
 (0)