generated from nhcarrigan/template
feat: initial prototype attempt
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-nested-callbacks -- Vitest structure requires nested callbacks */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("config", () => {
|
||||
const originalEnvironment = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.env = { ...originalEnvironment };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnvironment;
|
||||
});
|
||||
|
||||
it("should have the correct default values", async() => {
|
||||
expect.assertions(5);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const { config } = await import("../src/config.js");
|
||||
|
||||
expect(config.checkInterval).toBe("0 7 * * *");
|
||||
expect(config.giteaOrg).toBe("nhcarrigan");
|
||||
expect(config.giteaUrl).toBe("https://git.nhcarrigan.com");
|
||||
expect(config.npmRegistryUrl).toBe("https://registry.npmjs.org");
|
||||
expect(config.prBranchPrefix).toBe("dependencies/update-");
|
||||
});
|
||||
|
||||
it("should use GITEA_TOKEN from environment", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "my-secret-token";
|
||||
const { config } = await import("../src/config.js");
|
||||
|
||||
expect(config.giteaToken).toBe("my-secret-token");
|
||||
});
|
||||
|
||||
it("should default giteaToken to empty string when not set", async() => {
|
||||
expect.assertions(1);
|
||||
delete process.env.GITEA_TOKEN;
|
||||
const { config } = await import("../src/config.js");
|
||||
|
||||
expect(config.giteaToken).toBe("");
|
||||
});
|
||||
|
||||
it("should not throw when GITEA_TOKEN is set", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "valid-token";
|
||||
const { validateConfig } = await import("../src/config.js");
|
||||
|
||||
expect(() => {
|
||||
validateConfig();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw when GITEA_TOKEN is empty", async() => {
|
||||
expect.assertions(1);
|
||||
delete process.env.GITEA_TOKEN;
|
||||
const { validateConfig } = await import("../src/config.js");
|
||||
|
||||
expect(() => {
|
||||
validateConfig();
|
||||
}).toThrow("GITEA_TOKEN is required");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("index", () => {
|
||||
it("should export from the module", () => {
|
||||
expect.assertions(1);
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-lines-per-function -- Test suites require many test cases */
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Test data uses npm package names and destructured imports */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Required for mocking */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@nhcarrigan/logger", () => {
|
||||
return {
|
||||
|
||||
Logger: class MockLogger {
|
||||
public error = vi.fn();
|
||||
public log = vi.fn();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
interface MockNpmService {
|
||||
getPackageChangelog: ReturnType<typeof vi.fn>;
|
||||
getPackageInfo: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
const createMockNpmService = (): MockNpmService => {
|
||||
return {
|
||||
getPackageChangelog: vi.fn(),
|
||||
getPackageInfo: vi.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
describe("dependencyAnalyzerService", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should return empty array when no dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should find updates for dependencies", async() => {
|
||||
expect.assertions(2);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockResolvedValue({
|
||||
"dist-tags": { latest: "2.0.0" },
|
||||
"name": "test-package",
|
||||
"versions": {},
|
||||
});
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"test-package": "1.0.0",
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toStrictEqual({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
packageName: "test-package",
|
||||
type: "dependencies",
|
||||
});
|
||||
});
|
||||
|
||||
it("should skip file: protocol dependencies", async() => {
|
||||
expect.assertions(2);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"local-package": "file:../local-package",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
expect(mockNpmService.getPackageInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should skip git: protocol dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"git-package": "git:github.com/user/repo",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should skip http: protocol dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"http-package": "http://example.com/package.tgz",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should skip https: protocol dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"https-package": "https://example.com/package.tgz",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should skip github: shorthand dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"github-package": "github:user/repo",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should skip * version dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"star-package": "*",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should skip latest version dependencies", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"latest-package": "latest",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should not include packages that are already up-to-date", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockResolvedValue({
|
||||
"dist-tags": { latest: "1.0.0" },
|
||||
"name": "test-package",
|
||||
"versions": {},
|
||||
});
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"test-package": "1.0.0",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should handle packages not found on npm", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockResolvedValue(null);
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"non-existent": "1.0.0",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should handle version prefixes like ^", async() => {
|
||||
expect.assertions(2);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockResolvedValue({
|
||||
"dist-tags": { latest: "2.0.0" },
|
||||
"name": "test-package",
|
||||
"versions": {},
|
||||
});
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"test-package": "^1.0.0",
|
||||
},
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.currentVersion).toBe("^1.0.0");
|
||||
});
|
||||
|
||||
it("should handle npm errors gracefully", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockRejectedValue(new Error("Network error"));
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"test-package": "1.0.0",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should handle semver comparison errors", async() => {
|
||||
expect.assertions(1);
|
||||
const mockNpmService = createMockNpmService();
|
||||
mockNpmService.getPackageInfo.mockResolvedValue({
|
||||
"dist-tags": { latest: "invalid-version" },
|
||||
"name": "test-package",
|
||||
"versions": {},
|
||||
});
|
||||
const { DependencyAnalyzerService }
|
||||
= await import("../../src/services/dependencyAnalyzerService.js");
|
||||
const analyzerService = new DependencyAnalyzerService(
|
||||
mockNpmService as Parameters<typeof DependencyAnalyzerService>[0],
|
||||
);
|
||||
const result = await analyzerService.analyzePackageJson({
|
||||
dependencies: {
|
||||
"test-package": "also-invalid",
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-lines-per-function -- Test suites require many test cases */
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Test data uses npm package names and destructured imports */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Required for mocking */
|
||||
/* eslint-disable max-nested-callbacks -- Vitest structure requires nested callbacks */
|
||||
/* eslint-disable stylistic/max-len -- Test files have long import paths */
|
||||
/* eslint-disable max-lines -- Test suites require many test cases */
|
||||
/* eslint-disable vitest/no-conditional-in-test -- Discriminated unions require type narrowing */
|
||||
/* eslint-disable vitest/no-conditional-expect -- Discriminated unions require type narrowing */
|
||||
|
||||
import { readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Logger } from "@nhcarrigan/logger";
|
||||
|
||||
const mockExecAsync = vi.fn();
|
||||
|
||||
vi.mock("node:child_process", () => {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs/promises", () => {
|
||||
return {
|
||||
readFile: vi.fn(),
|
||||
rm: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:os", () => {
|
||||
return {
|
||||
tmpdir: vi.fn(() => {
|
||||
return "/tmp";
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:util", () => {
|
||||
return {
|
||||
promisify: vi.fn(() => {
|
||||
return mockExecAsync;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
interface MockClonedRepo {
|
||||
cleanup: ReturnType<typeof vi.fn>;
|
||||
path: string;
|
||||
repoName: string;
|
||||
}
|
||||
|
||||
const createMockClonedRepo = (): MockClonedRepo => {
|
||||
return {
|
||||
cleanup: vi.fn(),
|
||||
path: "/tmp/minori-test-repo-123",
|
||||
repoName: "test-repo",
|
||||
};
|
||||
};
|
||||
|
||||
const createMockLogger = (): Logger => {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
};
|
||||
|
||||
describe("gitService", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let mockLogger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
mockLogger = createMockLogger();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should clone a repository to a temporary directory", async() => {
|
||||
expect.assertions(3);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
const { cloneRepository } = await import("../../src/services/gitService.js");
|
||||
const result = await cloneRepository(
|
||||
mockLogger,
|
||||
"test-repo",
|
||||
"test-token",
|
||||
);
|
||||
expect(result.repoName).toBe("test-repo");
|
||||
expect(result.path).toMatch(/^\/tmp\/minori-test-repo-\d+$/u);
|
||||
expect(typeof result.cleanup).toBe("function");
|
||||
});
|
||||
|
||||
it("should configure git user email and name", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
const { cloneRepository } = await import("../../src/services/gitService.js");
|
||||
await cloneRepository(mockLogger, "test-repo", "test-token");
|
||||
expect(mockExecAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("git clone"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should cleanup temporary directory when cleanup is called", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
vi.mocked(rm).mockResolvedValue(undefined);
|
||||
const { cloneRepository } = await import("../../src/services/gitService.js");
|
||||
const result = await cloneRepository(
|
||||
mockLogger,
|
||||
"test-repo",
|
||||
"test-token",
|
||||
);
|
||||
await result.cleanup();
|
||||
expect(rm).toHaveBeenCalledWith(
|
||||
result.path,
|
||||
{ force: true, recursive: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("should create a new branch when it does not exist", async() => {
|
||||
expect.assertions(2);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: {
|
||||
"test-package": "1.0.0",
|
||||
},
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("created");
|
||||
if (result.status === "created") {
|
||||
expect(result.branchName).toBe("dependencies/update-test-package");
|
||||
}
|
||||
});
|
||||
|
||||
it("should update an existing branch when behind", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockImplementation((command: string) => {
|
||||
if (command.includes("git branch -r")) {
|
||||
return Promise.resolve({
|
||||
stderr: "",
|
||||
stdout: " origin/dependencies/update-test-package\n",
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ stderr: "", stdout: "" });
|
||||
});
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: { "test-package": "1.5.0" },
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("updated");
|
||||
});
|
||||
|
||||
it("should skip when branch is already up-to-date", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockImplementation((command: string) => {
|
||||
if (command.includes("git branch -r")) {
|
||||
return Promise.resolve({
|
||||
stderr: "",
|
||||
stdout: " origin/dependencies/update-test-package\n",
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ stderr: "", stdout: "" });
|
||||
});
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: { "test-package": "2.0.0" },
|
||||
}));
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("up-to-date");
|
||||
});
|
||||
|
||||
it("should fail when package is not found", async() => {
|
||||
expect.assertions(2);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: {},
|
||||
}));
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("failed");
|
||||
if (result.status === "failed") {
|
||||
expect(result.error).toContain("not found");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle git command errors", async() => {
|
||||
expect.assertions(1);
|
||||
const error = new Error("Git command failed") as Error & { stderr: string };
|
||||
error.stderr = "fatal: error";
|
||||
mockExecAsync.mockRejectedValueOnce(error);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("should update devDependencies", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
devDependencies: {
|
||||
"test-package": "1.0.0",
|
||||
},
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("created");
|
||||
});
|
||||
|
||||
it("should handle cleanup errors gracefully", async() => {
|
||||
expect.assertions(1);
|
||||
const error = new Error("Git command failed");
|
||||
mockExecAsync.
|
||||
mockRejectedValueOnce(error).
|
||||
mockRejectedValueOnce(new Error("Cleanup failed"));
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("should log git stderr when it does not contain warnings", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockImplementation((command: string) => {
|
||||
if (command.includes("git fetch")) {
|
||||
return Promise.resolve({ stderr: "some error message", stdout: "" });
|
||||
}
|
||||
return Promise.resolve({ stderr: "", stdout: "" });
|
||||
});
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: { "test-package": "1.0.0" },
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
"debug",
|
||||
expect.stringContaining("Git stderr"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log 'unknown' when package not found on existing branch", async() => {
|
||||
expect.assertions(1);
|
||||
mockExecAsync.mockImplementation((command: string) => {
|
||||
if (command.includes("git branch -r")) {
|
||||
return Promise.resolve({
|
||||
stderr: "",
|
||||
stdout: " origin/dependencies/update-test-package\n",
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ stderr: "", stdout: "" });
|
||||
});
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: { "other-package": "1.0.0" },
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
"info",
|
||||
expect.stringContaining("unknown"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should update both dependencies and devDependencies", async() => {
|
||||
expect.assertions(3);
|
||||
mockExecAsync.mockResolvedValue({ stderr: "", stdout: "" });
|
||||
vi.mocked(readFile).mockResolvedValue(JSON.stringify({
|
||||
dependencies: { "test-package": "1.0.0" },
|
||||
devDependencies: { "test-package": "1.0.0" },
|
||||
}));
|
||||
vi.mocked(writeFile).mockResolvedValue(undefined);
|
||||
const { createOrUpdateBranch } = await import("../../src/services/gitService.js");
|
||||
const mockClonedRepo = createMockClonedRepo();
|
||||
const result = await createOrUpdateBranch({
|
||||
branchName: "dependencies/update-test-package",
|
||||
clonedRepo: mockClonedRepo,
|
||||
logger: mockLogger,
|
||||
packageName: "test-package",
|
||||
targetVersion: "2.0.0",
|
||||
});
|
||||
expect(result.status).toBe("created");
|
||||
const writeCall = vi.mocked(writeFile).mock.calls[0];
|
||||
const writtenContent = JSON.parse(writeCall?.[1] as string) as {
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
};
|
||||
expect(writtenContent.dependencies["test-package"]).toBe("2.0.0");
|
||||
expect(writtenContent.devDependencies["test-package"]).toBe("2.0.0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-lines-per-function -- Test suites require many test cases */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Required for mocking */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports -- Dynamic imports */
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Environment variables and Gitea API format */
|
||||
/* eslint-disable max-nested-callbacks -- Vitest structure requires nested callbacks */
|
||||
/* eslint-disable vitest/require-to-throw-message -- Generic throw assertion */
|
||||
|
||||
import axios, { AxiosError, type AxiosResponse } from "axios";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GiteaService } from "../../src/services/giteaService.js";
|
||||
|
||||
vi.mock("axios", async() => {
|
||||
const actualAxios = await vi.importActual<typeof import("axios")>("axios");
|
||||
return {
|
||||
|
||||
AxiosError: actualAxios.AxiosError,
|
||||
default: {
|
||||
create: vi.fn(() => {
|
||||
return {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
};
|
||||
}),
|
||||
},
|
||||
isAxiosError: actualAxios.isAxiosError,
|
||||
};
|
||||
});
|
||||
|
||||
interface MockRepository {
|
||||
archived: boolean;
|
||||
cloneUrl: string;
|
||||
defaultBranch: string;
|
||||
disabled: boolean;
|
||||
fullName: string;
|
||||
id: number;
|
||||
mirror: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const createMockRepository = (
|
||||
overrides: Partial<MockRepository> & { name: string; id: number },
|
||||
): Record<string, unknown> => {
|
||||
return {
|
||||
archived: overrides.archived ?? false,
|
||||
|
||||
clone_url: overrides.cloneUrl ?? "url",
|
||||
|
||||
default_branch: overrides.defaultBranch ?? "main",
|
||||
disabled: overrides.disabled ?? false,
|
||||
|
||||
full_name: overrides.fullName ?? `nhcarrigan/${overrides.name}`,
|
||||
id: overrides.id,
|
||||
mirror: overrides.mirror ?? false,
|
||||
name: overrides.name,
|
||||
};
|
||||
};
|
||||
|
||||
describe("giteaService", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let giteaService: GiteaService;
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let mockGet: ReturnType<typeof vi.fn>;
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let mockPost: ReturnType<typeof vi.fn>;
|
||||
const originalEnvironment = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env = { ...originalEnvironment, GITEA_TOKEN: "test-token" };
|
||||
|
||||
mockGet = vi.fn();
|
||||
mockPost = vi.fn();
|
||||
vi.mocked(axios.create).mockReturnValue({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
} as unknown as ReturnType<typeof axios.create>);
|
||||
|
||||
giteaService = new GiteaService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnvironment;
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should throw when GITEA_TOKEN is not set", () => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "";
|
||||
expect(() => {
|
||||
return new GiteaService();
|
||||
}).toThrow("GITEA_TOKEN environment variable is required");
|
||||
});
|
||||
|
||||
it("should throw when GITEA_TOKEN is undefined", () => {
|
||||
expect.assertions(1);
|
||||
delete process.env.GITEA_TOKEN;
|
||||
expect(() => {
|
||||
return new GiteaService();
|
||||
}).toThrow("GITEA_TOKEN environment variable is required");
|
||||
});
|
||||
|
||||
it("should create axios client with correct configuration", () => {
|
||||
expect.assertions(1);
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
baseURL: "https://git.nhcarrigan.com/api/v1",
|
||||
headers: {
|
||||
|
||||
"Authorization": "token test-token",
|
||||
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a pull request", async() => {
|
||||
expect.assertions(2);
|
||||
const mockPullRequest = {
|
||||
base: { ref: "main", sha: "abc123" },
|
||||
body: "Test body",
|
||||
head: { ref: "feature", sha: "def456" },
|
||||
id: 1,
|
||||
number: 1,
|
||||
state: "open",
|
||||
title: "Test PR",
|
||||
};
|
||||
mockPost.mockResolvedValueOnce({ data: mockPullRequest });
|
||||
const result = await giteaService.createPullRequest({
|
||||
base: "main",
|
||||
body: "Test body",
|
||||
head: "feature",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
title: "Test PR",
|
||||
});
|
||||
expect(result).toStrictEqual(mockPullRequest);
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
"/repos/test-owner/test-repo/pulls",
|
||||
{ base: "main", body: "Test body", head: "feature", title: "Test PR" },
|
||||
);
|
||||
});
|
||||
|
||||
it("should return file content when found", async() => {
|
||||
expect.assertions(2);
|
||||
const mockFile = {
|
||||
content: "SGVsbG8gV29ybGQ=",
|
||||
encoding: "base64",
|
||||
path: "package.json",
|
||||
sha: "abc123",
|
||||
type: "file",
|
||||
};
|
||||
mockGet.mockResolvedValueOnce({ data: mockFile });
|
||||
const result = await giteaService.getFileContent({
|
||||
owner: "test-owner",
|
||||
path: "package.json",
|
||||
reference: "main",
|
||||
repo: "test-repo",
|
||||
});
|
||||
expect(result).toStrictEqual(mockFile);
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
"/repos/test-owner/test-repo/contents/package.json",
|
||||
{ params: { ref: "main" } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null when file is not found (404)", async() => {
|
||||
expect.assertions(1);
|
||||
const axiosError = new AxiosError("Not Found");
|
||||
axiosError.response = { status: 404 } as AxiosResponse;
|
||||
mockGet.mockRejectedValueOnce(axiosError);
|
||||
const result = await giteaService.getFileContent({
|
||||
owner: "test-owner",
|
||||
path: "non-existent.json",
|
||||
repo: "test-repo",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw for non-404 errors", async() => {
|
||||
expect.assertions(1);
|
||||
const axiosError = new AxiosError("Server Error");
|
||||
axiosError.response = { status: 500 } as AxiosResponse;
|
||||
mockGet.mockRejectedValueOnce(axiosError);
|
||||
await expect(
|
||||
giteaService.getFileContent({
|
||||
owner: "test-owner",
|
||||
path: "package.json",
|
||||
repo: "test-repo",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should fetch all repositories with pagination", async() => {
|
||||
expect.assertions(2);
|
||||
const page1 = Array.from({ length: 100 }, (_, index) => {
|
||||
return createMockRepository({
|
||||
cloneUrl: `https://git.nhcarrigan.com/nhcarrigan/repo-${String(index)}.git`,
|
||||
fullName: `nhcarrigan/repo-${String(index)}`,
|
||||
id: index,
|
||||
name: `repo-${String(index)}`,
|
||||
});
|
||||
});
|
||||
const page2 = [
|
||||
createMockRepository({
|
||||
cloneUrl: "https://git.nhcarrigan.com/nhcarrigan/repo-100.git",
|
||||
fullName: "nhcarrigan/repo-100",
|
||||
id: 100,
|
||||
name: "repo-100",
|
||||
}),
|
||||
];
|
||||
mockGet.
|
||||
mockResolvedValueOnce({ data: page1 }).
|
||||
mockResolvedValueOnce({ data: page2 }).
|
||||
mockResolvedValueOnce({ data: [] });
|
||||
const result = await giteaService.listOrgRepositories();
|
||||
expect(result).toHaveLength(101);
|
||||
expect(mockGet).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should filter out archived repositories", async() => {
|
||||
expect.assertions(2);
|
||||
const repos = [
|
||||
createMockRepository({ archived: true, id: 1, name: "archived-repo" }),
|
||||
createMockRepository({ id: 2, name: "active-repo" }),
|
||||
];
|
||||
mockGet.
|
||||
mockResolvedValueOnce({ data: repos }).
|
||||
mockResolvedValueOnce({ data: [] });
|
||||
const result = await giteaService.listOrgRepositories();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.name).toBe("active-repo");
|
||||
});
|
||||
|
||||
it("should filter out disabled repositories", async() => {
|
||||
expect.assertions(2);
|
||||
const repos = [
|
||||
createMockRepository({ disabled: true, id: 1, name: "disabled-repo" }),
|
||||
createMockRepository({ id: 2, name: "enabled-repo" }),
|
||||
];
|
||||
mockGet.
|
||||
mockResolvedValueOnce({ data: repos }).
|
||||
mockResolvedValueOnce({ data: [] });
|
||||
const result = await giteaService.listOrgRepositories();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.name).toBe("enabled-repo");
|
||||
});
|
||||
|
||||
it("should filter out mirror repositories", async() => {
|
||||
expect.assertions(2);
|
||||
const repos = [
|
||||
createMockRepository({ id: 1, mirror: true, name: "mirror-repo" }),
|
||||
createMockRepository({ id: 2, name: "source-repo" }),
|
||||
];
|
||||
mockGet.
|
||||
mockResolvedValueOnce({ data: repos }).
|
||||
mockResolvedValueOnce({ data: [] });
|
||||
const result = await giteaService.listOrgRepositories();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.name).toBe("source-repo");
|
||||
});
|
||||
|
||||
it("should list pull requests with default state", async() => {
|
||||
expect.assertions(2);
|
||||
const mockPullRequests = [
|
||||
{
|
||||
base: { ref: "main", sha: "abc" },
|
||||
body: "PR 1",
|
||||
head: { ref: "feature-1", sha: "def" },
|
||||
id: 1,
|
||||
number: 1,
|
||||
state: "open",
|
||||
title: "PR 1",
|
||||
},
|
||||
];
|
||||
mockGet.mockResolvedValueOnce({ data: mockPullRequests });
|
||||
const result = await giteaService.listPullRequests("owner", "repo");
|
||||
expect(result).toStrictEqual(mockPullRequests);
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
"/repos/owner/repo/pulls",
|
||||
{ params: { state: "open" } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should list pull requests with specified state", async() => {
|
||||
expect.assertions(1);
|
||||
mockGet.mockResolvedValueOnce({ data: [] });
|
||||
await giteaService.listPullRequests("owner", "repo", "all");
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
"/repos/owner/repo/pulls",
|
||||
{ params: { state: "all" } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should list closed pull requests", async() => {
|
||||
expect.assertions(1);
|
||||
mockGet.mockResolvedValueOnce({ data: [] });
|
||||
await giteaService.listPullRequests("owner", "repo", "closed");
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
"/repos/owner/repo/pulls",
|
||||
{ params: { state: "closed" } },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-lines-per-function -- Test suites require many test cases */
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Test data uses npm package names */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions -- Required for mocking */
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports -- Dynamic imports */
|
||||
/* eslint-disable vitest/require-to-throw-message -- Generic throw assertion */
|
||||
/* eslint-disable stylistic/max-len -- Test files have long URLs */
|
||||
|
||||
import axios, { AxiosError, type AxiosResponse } from "axios";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NpmService } from "../../src/services/npmService.js";
|
||||
|
||||
vi.mock("axios", async() => {
|
||||
const actualAxios = await vi.importActual<typeof import("axios")>("axios");
|
||||
return {
|
||||
|
||||
AxiosError: actualAxios.AxiosError,
|
||||
default: {
|
||||
create: vi.fn(() => {
|
||||
return {
|
||||
get: vi.fn(),
|
||||
};
|
||||
}),
|
||||
get: vi.fn(),
|
||||
},
|
||||
isAxiosError: actualAxios.isAxiosError,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@nhcarrigan/logger", () => {
|
||||
return {
|
||||
|
||||
Logger: class MockLogger {
|
||||
public error = vi.fn();
|
||||
public log = vi.fn();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const createMockPackageInfo = (
|
||||
version: string,
|
||||
repositoryUrl?: string,
|
||||
): Record<string, unknown> => {
|
||||
const versionData: Record<string, unknown> = { version };
|
||||
if (repositoryUrl !== undefined) {
|
||||
versionData.repository = { url: repositoryUrl };
|
||||
}
|
||||
return {
|
||||
"dist-tags": { latest: version },
|
||||
"name": "test-package",
|
||||
"versions": { [version]: versionData },
|
||||
};
|
||||
};
|
||||
|
||||
describe("npmService", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let npmService: NpmService;
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reassigned in beforeEach
|
||||
let mockGet: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
|
||||
mockGet = vi.fn();
|
||||
vi.mocked(axios.create).mockReturnValue({
|
||||
get: mockGet,
|
||||
} as unknown as ReturnType<typeof axios.create>);
|
||||
|
||||
npmService = new NpmService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should return package info when found", async() => {
|
||||
expect.assertions(2);
|
||||
const mockPackageInfo = {
|
||||
"dist-tags": { latest: "2.0.0" },
|
||||
"name": "test-package",
|
||||
"versions": {
|
||||
"2.0.0": {
|
||||
repository: { url: "https://github.com/test/test.git" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
};
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
const result = await npmService.getPackageInfo("test-package");
|
||||
expect(result).toStrictEqual(mockPackageInfo);
|
||||
expect(mockGet).toHaveBeenCalledWith("/test-package");
|
||||
});
|
||||
|
||||
it("should return null when package is not found (404)", async() => {
|
||||
expect.assertions(1);
|
||||
const axiosError = new AxiosError("Not Found");
|
||||
axiosError.response = { status: 404 } as AxiosResponse;
|
||||
mockGet.mockRejectedValueOnce(axiosError);
|
||||
const result = await npmService.getPackageInfo("non-existent-package");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw for non-404 errors", async() => {
|
||||
expect.assertions(1);
|
||||
const axiosError = new AxiosError("Server Error");
|
||||
axiosError.response = { status: 500 } as AxiosResponse;
|
||||
mockGet.mockRejectedValueOnce(axiosError);
|
||||
await expect(
|
||||
npmService.getPackageInfo("test-package"),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should encode scoped package names", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = {
|
||||
"dist-tags": { latest: "1.0.0" },
|
||||
"name": "@scope/package",
|
||||
"versions": {},
|
||||
};
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
await npmService.getPackageInfo("@scope/package");
|
||||
expect(mockGet).toHaveBeenCalledWith("/%40scope%2Fpackage");
|
||||
});
|
||||
|
||||
it("should return fallback when package info is null", async() => {
|
||||
expect.assertions(1);
|
||||
const axiosError = new AxiosError("Not Found");
|
||||
axiosError.response = { status: 404 } as AxiosResponse;
|
||||
mockGet.mockRejectedValueOnce(axiosError);
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "non-existent",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("No changelog available for non-existent");
|
||||
});
|
||||
|
||||
it("should return fallback when repository URL is undefined", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo("2.0.0");
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
|
||||
it("should return fallback when not a GitHub URL", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://gitlab.com/test/test.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
|
||||
it("should fetch GitHub releases and format changelog", async() => {
|
||||
expect.assertions(5);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://github.com/owner/repo.git",
|
||||
);
|
||||
const mockReleases = [
|
||||
|
||||
{ body: "Release notes for 2.0.0", tag_name: "v2.0.0" },
|
||||
|
||||
{ body: "Release notes for 1.5.0", tag_name: "v1.5.0" },
|
||||
|
||||
{ body: "Release notes for 1.0.0", tag_name: "v1.0.0" },
|
||||
];
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: mockReleases });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toContain("## Changelog");
|
||||
expect(result).toContain("### v2.0.0");
|
||||
expect(result).toContain("Release notes for 2.0.0");
|
||||
expect(result).toContain("### v1.5.0");
|
||||
expect(result).not.toContain("### v1.0.0");
|
||||
});
|
||||
|
||||
it("should return fallback when no relevant releases found", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://github.com/owner/repo.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: [] });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
|
||||
it("should handle GitHub API errors gracefully", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://github.com/owner/repo.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockRejectedValueOnce(new Error("GitHub API error"));
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
|
||||
it("should handle releases without body", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://github.com/owner/repo.git",
|
||||
);
|
||||
const mockReleases = [
|
||||
|
||||
{ tag_name: "v2.0.0" },
|
||||
];
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: mockReleases });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toContain("No release notes available");
|
||||
});
|
||||
|
||||
it("should normalise git+ prefixed URLs", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"git+https://github.com/owner/repo.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: [] });
|
||||
await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
"https://api.github.com/repos/owner/repo/releases",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should normalise git:// URLs", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"git://github.com/owner/repo.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: [] });
|
||||
await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
"https://api.github.com/repos/owner/repo/releases",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should normalise ssh:// URLs", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"ssh://git@github.com/owner/repo.git",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data: [] });
|
||||
await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
"https://api.github.com/repos/owner/repo/releases",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return fallback when GitHub URL has insufficient parts", async() => {
|
||||
expect.assertions(1);
|
||||
const mockPackageInfo = createMockPackageInfo(
|
||||
"2.0.0",
|
||||
"https://github.com/owner",
|
||||
);
|
||||
mockGet.mockResolvedValueOnce({ data: mockPackageInfo });
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
|
||||
it("should handle errors in getPackageChangelog", async() => {
|
||||
expect.assertions(1);
|
||||
mockGet.mockRejectedValueOnce(new Error("Network error"));
|
||||
const result = await npmService.getPackageChangelog({
|
||||
fromVersion: "1.0.0",
|
||||
packageName: "test-package",
|
||||
toVersion: "2.0.0",
|
||||
});
|
||||
expect(result).toBe("Updated from 1.0.0 to 2.0.0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable vitest/valid-expect -- Test expectations don't need messages */
|
||||
/* eslint-disable max-lines-per-function -- Test suites require many test cases */
|
||||
/* eslint-disable max-lines -- Test suites require many test cases */
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Test data uses npm package names and destructured imports */
|
||||
|
||||
/* eslint-disable max-nested-callbacks -- Vitest structure requires nested callbacks */
|
||||
/* eslint-disable max-classes-per-file -- Mock classes are needed for each service */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockGiteaGetFileContent = vi.fn();
|
||||
const mockGiteaListOrgRepositories = vi.fn();
|
||||
const mockGiteaCreatePullRequest = vi.fn();
|
||||
const mockGiteaListPullRequests = vi.fn();
|
||||
const mockNpmGetPackageChangelog = vi.fn();
|
||||
const mockNpmGetPackageInfo = vi.fn();
|
||||
const mockAnalyzePackageJson = vi.fn();
|
||||
const mockCloneRepository = vi.fn();
|
||||
const mockCreateOrUpdateBranch = vi.fn();
|
||||
|
||||
vi.mock("@nhcarrigan/logger", () => {
|
||||
return {
|
||||
|
||||
Logger: class MockLogger {
|
||||
public error = vi.fn();
|
||||
public log = vi.fn();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../src/services/giteaService.js", () => {
|
||||
return {
|
||||
|
||||
GiteaService: class MockGiteaService {
|
||||
public createPullRequest = mockGiteaCreatePullRequest;
|
||||
public getFileContent = mockGiteaGetFileContent;
|
||||
public listOrgRepositories = mockGiteaListOrgRepositories;
|
||||
public listPullRequests = mockGiteaListPullRequests;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../src/services/npmService.js", () => {
|
||||
return {
|
||||
|
||||
NpmService: class MockNpmService {
|
||||
public getPackageChangelog = mockNpmGetPackageChangelog;
|
||||
public getPackageInfo = mockNpmGetPackageInfo;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../src/services/dependencyAnalyzerService.js", () => {
|
||||
return {
|
||||
|
||||
DependencyAnalyzerService: class MockDependencyAnalyzerService {
|
||||
public analyzePackageJson = mockAnalyzePackageJson;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../src/services/gitService.js", () => {
|
||||
return {
|
||||
cloneRepository: mockCloneRepository,
|
||||
createOrUpdateBranch: mockCreateOrUpdateBranch,
|
||||
};
|
||||
});
|
||||
|
||||
const createMockRepo = (name: string): Record<string, unknown> => {
|
||||
return {
|
||||
archived: false,
|
||||
clone_url: "url",
|
||||
default_branch: "main",
|
||||
disabled: false,
|
||||
full_name: `nhcarrigan/${name}`,
|
||||
id: 1,
|
||||
mirror: false,
|
||||
name: name,
|
||||
};
|
||||
};
|
||||
|
||||
interface MockFileContent {
|
||||
content?: string;
|
||||
encoding: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const createMockFileContent = (
|
||||
packageJson: Record<string, unknown>,
|
||||
): MockFileContent => {
|
||||
return {
|
||||
content: Buffer.from(JSON.stringify(packageJson)).toString("base64"),
|
||||
encoding: "base64",
|
||||
path: "package.json",
|
||||
sha: "abc123",
|
||||
type: "file",
|
||||
};
|
||||
};
|
||||
|
||||
interface MockUpdate {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
packageName: string;
|
||||
type: "dependencies" | "devDependencies";
|
||||
}
|
||||
|
||||
const createMockUpdate = (): MockUpdate => {
|
||||
return {
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
packageName: "test-pkg",
|
||||
type: "dependencies",
|
||||
};
|
||||
};
|
||||
|
||||
const createMockClonedRepo = (
|
||||
cleanup: ReturnType<typeof vi.fn> = vi.fn(),
|
||||
): Record<string, unknown> => {
|
||||
return {
|
||||
cleanup: cleanup,
|
||||
path: "/tmp/test",
|
||||
repoName: "test-repo",
|
||||
};
|
||||
};
|
||||
|
||||
describe("updateOrchestratorService", () => {
|
||||
const originalEnvironment = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env = { ...originalEnvironment, GITEA_TOKEN: "test-token" };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnvironment;
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should throw when GITEA_TOKEN is not set", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "";
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
expect(() => {
|
||||
return new UpdateOrchestratorService();
|
||||
}).toThrow("GITEA_TOKEN environment variable is required");
|
||||
});
|
||||
|
||||
it("should throw when GITEA_TOKEN is undefined", async() => {
|
||||
expect.assertions(1);
|
||||
delete process.env.GITEA_TOKEN;
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
expect(() => {
|
||||
return new UpdateOrchestratorService();
|
||||
}).toThrow("GITEA_TOKEN environment variable is required");
|
||||
});
|
||||
|
||||
it("should create instance when GITEA_TOKEN is set", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "valid-token";
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
expect(() => {
|
||||
return new UpdateOrchestratorService();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should process all repositories", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue([]);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaListOrgRepositories).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should skip repositories without package.json", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("no-package") ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(null);
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaGetFileContent).toHaveBeenCalledWith({
|
||||
owner: "nhcarrigan",
|
||||
path: "package.json",
|
||||
reference: "main",
|
||||
repo: "no-package",
|
||||
});
|
||||
});
|
||||
|
||||
it("should create PRs for updates", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockNpmGetPackageChangelog.mockResolvedValue("## Changelog");
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
branchName: "dependencies/update-test-pkg",
|
||||
status: "created",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaCreatePullRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
base: "main",
|
||||
head: "dependencies/update-test-pkg",
|
||||
owner: "nhcarrigan",
|
||||
repo: "test-repo",
|
||||
title: "deps: update test-pkg to 2.0.0",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle repository processing errors", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("error-repo") ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockRejectedValue(new Error("API error"));
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await expect(
|
||||
service.checkAndUpdateAllRepositories(),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("should skip when branch is up-to-date", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
status: "up-to-date",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaCreatePullRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle failed branch updates", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
error: "Git error",
|
||||
status: "failed",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaCreatePullRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle failed branch updates without error message", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
status: "failed",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaCreatePullRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle package.json without content", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent: MockFileContent = {
|
||||
encoding: "base64",
|
||||
path: "package.json",
|
||||
sha: "abc123",
|
||||
type: "file",
|
||||
};
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await expect(
|
||||
service.checkAndUpdateAllRepositories(),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("should skip PR creation when branch was updated", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
branchName: "dependencies/update-test-pkg",
|
||||
status: "updated",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockGiteaCreatePullRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle PR creation errors", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockNpmGetPackageChangelog.mockResolvedValue("## Changelog");
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo());
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
branchName: "dependencies/update-test-pkg",
|
||||
status: "created",
|
||||
});
|
||||
mockGiteaCreatePullRequest.mockRejectedValue(
|
||||
new Error("PR creation failed"),
|
||||
);
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await expect(
|
||||
service.checkAndUpdateAllRepositories(),
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("should skip non-file type package.json", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent: MockFileContent = {
|
||||
content: "",
|
||||
encoding: "base64",
|
||||
path: "package.json",
|
||||
sha: "abc123",
|
||||
type: "dir",
|
||||
};
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockAnalyzePackageJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should cleanup cloned repo after processing", async() => {
|
||||
expect.assertions(1);
|
||||
process.env.GITEA_TOKEN = "test-token";
|
||||
const mockRepos = [ createMockRepo("test-repo") ];
|
||||
const mockFileContent = createMockFileContent({
|
||||
dependencies: { "test-pkg": "1.0.0" },
|
||||
});
|
||||
const mockUpdates = [ createMockUpdate() ];
|
||||
const mockCleanup = vi.fn();
|
||||
mockGiteaListOrgRepositories.mockResolvedValue(mockRepos);
|
||||
mockGiteaGetFileContent.mockResolvedValue(mockFileContent);
|
||||
mockAnalyzePackageJson.mockResolvedValue(mockUpdates);
|
||||
mockCloneRepository.mockResolvedValue(createMockClonedRepo(mockCleanup));
|
||||
mockCreateOrUpdateBranch.mockResolvedValue({
|
||||
status: "up-to-date",
|
||||
});
|
||||
const { UpdateOrchestratorService }
|
||||
= await import("../../src/services/updateOrchestratorService.js");
|
||||
const service = new UpdateOrchestratorService();
|
||||
await service.checkAndUpdateAllRepositories();
|
||||
expect(mockCleanup).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user