import { IncrementType, Tag, GitService, CoreService, ConventionalCommitAnalyzer, TagService, TagIncrementer, LocalGitService, LocalCoreService, } from "./main"; // Mock child_process.execSync jest.mock("child_process", () => ({ execSync: jest.fn(), })); import { execSync } from "child_process"; const mockExecSync = execSync as jest.MockedFunction; // Mock interfaces for testing class MockGitService implements GitService { constructor( private mockTags: { name: string }[] = [], private mockCommit: { commit: { message: string } } = { commit: { message: "test commit" } } ) {} async listTags(perPage: number): Promise<{ name: string }[]> { return this.mockTags.slice(0, perPage); } async getCommit(ref: string): Promise<{ commit: { message: string } }> { // Use ref parameter to avoid unused parameter warning if (!ref) { throw new Error("Ref is required"); } return this.mockCommit; } async testConnection(): Promise { // Mock successful connection } setMockTags(tags: { name: string }[]) { this.mockTags = tags; } setMockCommit(commit: { commit: { message: string } }) { this.mockCommit = commit; } } class MockCoreService implements CoreService { private inputs: Record = {}; private outputs: Record = {}; private logs: string[] = []; private warnings: string[] = []; private failures: string[] = []; setInput(name: string, value: string) { this.inputs[name] = value; } getInput(name: string): string { return this.inputs[name] || ""; } info(message: string): void { this.logs.push(`INFO: ${message}`); } warning(message: string): void { this.warnings.push(`WARNING: ${message}`); } setOutput(name: string, value: string): void { this.outputs[name] = value; } setFailed(message: string): void { this.failures.push(message); } getOutputs(): Record { return { ...this.outputs }; } getLogs(): string[] { return [...this.logs]; } getWarnings(): string[] { return [...this.warnings]; } getFailures(): string[] { return [...this.failures]; } clear() { this.outputs = {}; this.logs = []; this.warnings = []; this.failures = []; } } describe("TagService", () => { let tagService: TagService; beforeEach(() => { tagService = new TagService(); }); describe("bumpTag", () => { const baseTag: Tag = { major: 1, minor: 2, patch: 3 }; it("should increment major version", () => { const result = tagService.bumpTag(baseTag, IncrementType.MAJOR); expect(result).toEqual({ major: 2, minor: 0, patch: 0 }); }); it("should increment minor version", () => { const result = tagService.bumpTag(baseTag, IncrementType.MINOR); expect(result).toEqual({ major: 1, minor: 3, patch: 0 }); }); it("should increment patch version", () => { const result = tagService.bumpTag(baseTag, IncrementType.PATCH); expect(result).toEqual({ major: 1, minor: 2, patch: 4 }); }); it("should handle zero versions", () => { const zeroTag: Tag = { major: 0, minor: 0, patch: 0 }; const result = tagService.bumpTag(zeroTag, IncrementType.MINOR); expect(result).toEqual({ major: 0, minor: 1, patch: 0 }); }); }); describe("renderTag", () => { const tag: Tag = { major: 1, minor: 2, patch: 3 }; it("should render tag without prefix", () => { const result = tagService.renderTag(tag); expect(result).toBe("1.2.3"); }); it("should render tag with prefix", () => { const result = tagService.renderTag(tag, "v"); expect(result).toBe("v1.2.3"); }); it("should render tag with prerelease suffix", () => { const result = tagService.renderTag(tag, "", "-rc-abc123"); expect(result).toBe("1.2.3-rc-abc123"); }); it("should render tag with prefix and prerelease suffix", () => { const result = tagService.renderTag(tag, "v", "-rc-abc123"); expect(result).toBe("v1.2.3-rc-abc123"); }); }); describe("parseTag", () => { it("should parse valid tag without prefix", () => { const result = tagService.parseTag("1.2.3"); expect(result).toEqual({ major: 1, minor: 2, patch: 3 }); }); it("should parse valid tag with prefix", () => { const result = tagService.parseTag("v1.2.3"); expect(result).toEqual({ major: 1, minor: 2, patch: 3 }); }); it("should throw error for invalid format", () => { expect(() => tagService.parseTag("invalid")).toThrow( "Invalid tag format: invalid. Expected semantic version (e.g., 1.2.3)" ); }); it("should throw error for non-numeric versions", () => { expect(() => tagService.parseTag("1.a.3")).toThrow( "Invalid tag format: 1.a.3. Expected semantic version (e.g., 1.2.3)" ); }); it("should throw error for incomplete version", () => { expect(() => tagService.parseTag("1.2")).toThrow( "Invalid tag format: 1.2. Expected semantic version (e.g., 1.2.3)" ); }); }); }); describe("TagIncrementer", () => { let tagIncrementer: TagIncrementer; let mockGitService: MockGitService; let mockCoreService: MockCoreService; let mockCommitAnalyzer: ConventionalCommitAnalyzer; let mockTagService: TagService; beforeEach(() => { mockGitService = new MockGitService(); mockCoreService = new MockCoreService(); mockCommitAnalyzer = new ConventionalCommitAnalyzer(); mockTagService = new TagService(); tagIncrementer = new TagIncrementer( mockGitService, mockCoreService, mockCommitAnalyzer, mockTagService ); }); afterEach(() => { mockCoreService.clear(); }); describe("getBaseTag", () => { it("should return provided tag when available", async () => { const result = await tagIncrementer.getBaseTag("v1.2.3"); expect(result).toBe("v1.2.3"); expect(mockCoreService.getLogs()).toContain("INFO: Using provided tag: v1.2.3"); }); it("should fetch latest tag when no tag provided", async () => { mockGitService.setMockTags([{ name: "v2.0.0" }]); const result = await tagIncrementer.getBaseTag(undefined); expect(result).toBe("v2.0.0"); expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v2.0.0"); }); it("should default to 0.0.0 when no tags found", async () => { mockGitService.setMockTags([]); const result = await tagIncrementer.getBaseTag(undefined); expect(result).toBe("0.0.0"); expect(mockCoreService.getWarnings()).toContain( "WARNING: No tags found in repository, defaulting to 0.0.0" ); }); it("should skip pre-release tags and use latest real tag", async () => { mockGitService.setMockTags([ { name: "v2.0.0-rc-26035c6e13" }, { name: "v1.2.3" }, { name: "v1.1.4-rc-aa512edb16" }, { name: "v1.0.0" }, ]); const result = await tagIncrementer.getBaseTag(undefined); expect(result).toBe("v1.2.3"); expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v1.2.3"); }); it("should default to 0.0.0 when only pre-release tags found", async () => { mockGitService.setMockTags([ { name: "v2.0.0-rc-26035c6e13" }, { name: "v1.1.4-rc-aa512edb16" }, ]); const result = await tagIncrementer.getBaseTag(undefined); expect(result).toBe("0.0.0"); expect(mockCoreService.getWarnings()).toContain( "WARNING: No real tags found (only pre-release tags), defaulting to 0.0.0" ); }); }); describe("determineIncrementType", () => { it("should analyze commit and determine increment type", async () => { mockGitService.setMockCommit({ commit: { message: "feat: new feature" }, }); const result = await tagIncrementer.determineIncrementType("ref"); expect(result).toBe(IncrementType.MINOR); }); it("should fallback to PATCH on an non-conventional commit message", async () => { mockGitService.setMockCommit({ commit: { message: "just some tests, nothing special" }, }); const result = await tagIncrementer.determineIncrementType("ref"); expect(result).toBe(IncrementType.PATCH); }); }); describe("incrementTag", () => { it("should increment tag correctly for patch increment", async () => { mockGitService.setMockTags([{ name: "v1.2.3" }]); mockGitService.setMockCommit({ commit: { message: "fix: bug fix" }, }); const result = await tagIncrementer.incrementTag(undefined, "ref"); expect(result).toBe("1.2.4"); expect(mockCoreService.getOutputs()).toEqual({}); }); it("should increment tag correctly for minor increment", async () => { mockGitService.setMockTags([{ name: "v1.2.3" }]); mockGitService.setMockCommit({ commit: { message: "feat: new feature" }, }); const result = await tagIncrementer.incrementTag(undefined, "ref"); expect(result).toBe("1.3.0"); }); it("should increment tag correctly for major increment", async () => { mockGitService.setMockTags([{ name: "v1.2.3" }]); mockGitService.setMockCommit({ commit: { message: "feat!: breaking change" }, }); const result = await tagIncrementer.incrementTag(undefined, "ref"); expect(result).toBe("2.0.0"); }); it("should use provided tag when available", async () => { mockGitService.setMockCommit({ commit: { message: "fix: bug fix" }, }); const result = await tagIncrementer.incrementTag("v2.1.0", "ref"); expect(result).toBe("2.1.1"); }); it("should create prerelease tag when prerelease is true", async () => { mockGitService.setMockTags([{ name: "v1.2.3" }]); mockGitService.setMockCommit({ commit: { message: "feat: new feature" }, }); const result = await tagIncrementer.incrementTag(undefined, "ref", 50, true, "abc123"); expect(result).toBe("1.3.0-rc-abc123"); }); it("should not create prerelease tag when prerelease is false", async () => { mockGitService.setMockTags([{ name: "v1.2.3" }]); mockGitService.setMockCommit({ commit: { message: "feat: new feature" }, }); const result = await tagIncrementer.incrementTag(undefined, "ref", 50, false, "abc123"); expect(result).toBe("1.3.0"); }); }); }); describe("LocalGitService", () => { let localGitService: LocalGitService; beforeEach(() => { localGitService = new LocalGitService(); mockExecSync.mockClear(); }); describe("listTags", () => { it("should call git command to list tags", async () => { mockExecSync.mockReturnValue("v1.0.0\n1.1.0\n2.0.0-rc-26035c6e13\n" as any); const result = await localGitService.listTags(5); expect(mockExecSync).toHaveBeenCalledWith("git tag --sort=-version:refname", { encoding: "utf8", }); expect(result).toEqual([ { name: "v1.0.0" }, { name: "1.1.0" }, { name: "2.0.0-rc-26035c6e13" }, ]); }); it("should return empty array when no tags exist", async () => { mockExecSync.mockReturnValue("" as any); const result = await localGitService.listTags(5); expect(result).toEqual([]); }); }); describe("getCommit", () => { it("should call git command to get commit", async () => { mockExecSync.mockReturnValue("test commit message" as any); const result = await localGitService.getCommit("abc123"); expect(mockExecSync).toHaveBeenCalledWith("git log -1 --pretty=%B abc123", { encoding: "utf8", }); expect(result).toEqual({ commit: { message: "test commit message" } }); }); }); describe("testConnection", () => { it("should call git command to test connection", async () => { mockExecSync.mockReturnValue(".git" as any); await localGitService.testConnection(); expect(mockExecSync).toHaveBeenCalledWith("git rev-parse --git-dir", { stdio: "pipe" }); }); it("should throw error when not in git repository", async () => { mockExecSync.mockImplementation(() => { throw new Error("fatal: not a git repository"); }); await expect(localGitService.testConnection()).rejects.toThrow( "Not in a git repository. Please ensure this action runs after checkout." ); }); }); }); describe("LocalCoreService", () => { let localCoreService: LocalCoreService; beforeEach(() => { localCoreService = new LocalCoreService(); }); // Note: These tests would require mocking the @actions/core module // In a real implementation, you might want to use jest.mock() to mock the entire module // For now, we'll just test that the methods exist and can be called it("should have all required methods", () => { expect(typeof localCoreService.getInput).toBe("function"); expect(typeof localCoreService.info).toBe("function"); expect(typeof localCoreService.warning).toBe("function"); expect(typeof localCoreService.setOutput).toBe("function"); expect(typeof localCoreService.setFailed).toBe("function"); }); }); // Integration test example describe("TagIncrementer Integration", () => { it("should handle complete tag increment flow", async () => { const mockGitService = new MockGitService( [{ name: "v2.0.0-rc-26035c6e13" }, { name: "v1.1.4-rc-aa512edb16" }, { name: "v1.2.3" }], { commit: { message: "feat: new feature" }, } ); const mockCoreService = new MockCoreService(); const commitAnalyzer = new ConventionalCommitAnalyzer(); const tagService = new TagService(); const tagIncrementer = new TagIncrementer( mockGitService, mockCoreService, commitAnalyzer, tagService ); const result = await tagIncrementer.incrementTag(undefined, "ref"); expect(result).toBe("1.3.0"); expect(mockCoreService.getLogs()).toContain("INFO: Determined increment type: minor"); expect(mockCoreService.getLogs()).toContain("INFO: New tag: 1.3.0"); expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v1.2.3"); }); }); describe("Prerelease Validation", () => { it("should validate prerelease configuration correctly", () => { // Test cases for prerelease validation logic const testCases = [ { prerelease: true, githubSha: "abc123", shouldFail: false }, { prerelease: true, githubSha: "", shouldFail: true }, { prerelease: false, githubSha: "abc123", shouldFail: false }, { prerelease: false, githubSha: "", shouldFail: false }, ]; testCases.forEach(({ prerelease, githubSha, shouldFail }) => { const result = prerelease && !githubSha; expect(result).toBe(shouldFail); }); }); });