generated from nhcarrigan/template
feat: initial prototype attempt
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user