All checks were successful
CD / Release (push) Successful in 40s
Reviewed-on: #17 Co-authored-by: Timo Behrendt <t.behrendt@t00n.de> Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
500 lines
15 KiB
TypeScript
500 lines
15 KiB
TypeScript
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<typeof execSync>;
|
|
|
|
// 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<void> {
|
|
// 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<string, string> = {};
|
|
private outputs: Record<string, string> = {};
|
|
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<string, string> {
|
|
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);
|
|
});
|
|
});
|
|
});
|