diff --git a/dist/index.js b/dist/index.js index 450f2d7..a03bad8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -47949,8 +47949,13 @@ class DockerComposeInstallerService { this.manualInstallerAdapter = manualInstallerAdapter; } async install({ composeVersion, cwd, githubToken }) { - const currentVersion = await this.getInstalledVersion(cwd); - const needsInstall = !currentVersion || (composeVersion && composeVersion !== currentVersion); + const currentVersion = await this.version({ cwd }); + const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null; + const normalizedRequestedVersion = composeVersion + ? this.normalizeVersion(composeVersion) + : null; + const needsInstall = !currentVersion || + (composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion); if (!needsInstall) { return currentVersion; } @@ -47962,21 +47967,24 @@ class DockerComposeInstallerService { targetVersion = await this.getLatestVersion(githubToken); } await this.installVersion(targetVersion); - return this.version({ cwd }); - } - async getInstalledVersion(cwd) { - try { - return await this.version({ cwd }); - } - catch { - return null; + const installedVersion = await this.version({ cwd }); + if (!installedVersion || + this.normalizeVersion(installedVersion) !== this.normalizeVersion(targetVersion)) { + throw new Error(`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`); } + return installedVersion; } async version({ cwd }) { - const result = await (0,dist.version)({ - cwd, - }); - return result.data.version; + try { + const result = await (0,dist.version)({ + cwd, + }); + return result.data.version; + } + catch { + // If version check fails (e.g., Docker Compose not installed), return null + return null; + } } async getLatestVersion(githubToken) { const octokit = getOctokit(githubToken); @@ -47986,6 +47994,9 @@ class DockerComposeInstallerService { }); return response.data.tag_name; } + normalizeVersion(version) { + return version.replace(/^v/i, ""); + } async installVersion(version) { switch (process.platform) { case "linux": diff --git a/src/services/docker-compose-installer.service.test.ts b/src/services/docker-compose-installer.service.test.ts index d6c680b..27492a3 100644 --- a/src/services/docker-compose-installer.service.test.ts +++ b/src/services/docker-compose-installer.service.test.ts @@ -21,6 +21,52 @@ describe("DockerComposeInstallerService", () => { let mockAgent: MockAgent; let service: InstanceType; + const composeVersionResponse = (version: string) => ({ + exitCode: 0, + out: "", + err: "", + data: { + version, + }, + }); + + const installCompose = (composeVersion: string | null, githubToken: string | null) => + service.install({ + composeVersion, + cwd: "/path/to/cwd", + githubToken, + }); + + const setPlatform = (platform: NodeJS.Platform) => { + Object.defineProperty(process, "platform", { + value: platform, + }); + }; + + const mockLatestRelease = (version: string) => { + const mockClient = mockAgent.get("https://api.github.com"); + mockClient + .intercept({ + path: "/repos/docker/compose/releases/latest", + method: "GET", + }) + .reply( + 200, + { + tag_name: version, + }, + { + headers: { + "content-type": "application/json", + }, + } + ); + setGlobalDispatcher(mockClient); + Object.defineProperty(globalThis, "fetch", { + value: jest.fn(), + }); + }; + beforeEach(() => { jest.clearAllMocks(); mockAgent = new MockAgent(); @@ -36,21 +82,10 @@ describe("DockerComposeInstallerService", () => { describe("install", () => { it("should return current version when no version is provided", async () => { // Arrange - versionMock.mockResolvedValue({ - exitCode: 0, - out: "", - err: "", - data: { - version: "2.0.0", - }, - }); + versionMock.mockResolvedValue(composeVersionResponse("2.0.0")); // Act - const result = await service.install({ - composeVersion: null, - cwd: "/path/to/cwd", - githubToken: null, - }); + const result = await installCompose(null, null); // Assert expect(result).toBe("2.0.0"); @@ -59,21 +94,10 @@ describe("DockerComposeInstallerService", () => { it("should not install anything when expected version is already installed", async () => { // Arrange - versionMock.mockResolvedValue({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValue(composeVersionResponse("1.2.3")); // Act - const result = await service.install({ - composeVersion: "1.2.3", - cwd: "/path/to/cwd", - githubToken: null, - }); + const result = await installCompose("v1.2.3", null); // Assert expect(result).toBe("1.2.3"); @@ -82,35 +106,14 @@ describe("DockerComposeInstallerService", () => { it("should install the requested version if it is not already installed", async () => { // Arrange - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3")); const expectedVersion = "1.3.0"; - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: expectedVersion, - }, - }); - - Object.defineProperty(process, "platform", { - value: "linux", - }); + versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion)); + setPlatform("linux"); // Act - const result = await service.install({ - composeVersion: expectedVersion, - cwd: "/path/to/cwd", - githubToken: null, - }); + const result = await installCompose(expectedVersion, null); // Assert expect(result).toBe(expectedVersion); @@ -119,58 +122,15 @@ describe("DockerComposeInstallerService", () => { it("should install the latest version if requested", async () => { // Arrange - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3")); const latestVersion = "v1.4.0"; - - const mockClient = mockAgent.get("https://api.github.com"); - mockClient - .intercept({ - path: "/repos/docker/compose/releases/latest", - method: "GET", - }) - .reply( - 200, - { - tag_name: latestVersion, - }, - { - headers: { - "content-type": "application/json", - }, - } - ); - setGlobalDispatcher(mockClient); - - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: latestVersion, - }, - }); - - Object.defineProperty(process, "platform", { - value: "linux", - }); - Object.defineProperty(globalThis, "fetch", { - value: jest.fn(), - }); + mockLatestRelease(latestVersion); + versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion)); + setPlatform("linux"); // Act - const result = await service.install({ - composeVersion: "latest", - cwd: "/path/to/cwd", - githubToken: "token", - }); + const result = await installCompose("latest", "token"); // Assert expect(result).toBe(latestVersion); @@ -179,58 +139,26 @@ describe("DockerComposeInstallerService", () => { it("should throw an error if the latest version if requested and no Github token is provided", async () => { // Arrange - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3")); // Act & Assert - await expect( - service.install({ - composeVersion: "latest", - cwd: "/path/to/cwd", - githubToken: null, - }) - ).rejects.toThrow("GitHub token is required to install the latest version"); + await expect(installCompose("latest", null)).rejects.toThrow( + "GitHub token is required to install the latest version" + ); }); it("should throw an error on unsupported platforms", async () => { // Arrange - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3")); const expectedVersion = "1.3.0"; - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: expectedVersion, - }, - }); - - Object.defineProperty(process, "platform", { - value: "win32", - }); + versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion)); + setPlatform("win32"); // Act & Assert - await expect( - service.install({ - composeVersion: expectedVersion, - cwd: "/path/to/cwd", - githubToken: null, - }) - ).rejects.toThrow(`Unsupported platform: win32`); + await expect(installCompose(expectedVersion, null)).rejects.toThrow( + `Unsupported platform: win32` + ); expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled(); }); @@ -242,25 +170,11 @@ describe("DockerComposeInstallerService", () => { const installedVersion = "2.0.0"; // After installation, version() returns the new version - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: installedVersion, - }, - }); - - Object.defineProperty(process, "platform", { - value: "linux", - }); + versionMock.mockResolvedValueOnce(composeVersionResponse(installedVersion)); + setPlatform("linux"); // Act - const result = await service.install({ - composeVersion: installedVersion, - cwd: "/path/to/cwd", - githubToken: "token", - }); + const result = await installCompose(installedVersion, "token"); // Assert expect(result).toBe(installedVersion); @@ -271,58 +185,15 @@ describe("DockerComposeInstallerService", () => { // Arrange: first call to version() doesn't find versionMock.mockRejectedValueOnce(new Error("version check failed")); // second call finds newly installed version - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: "v1.4.0", - }, - }); + versionMock.mockResolvedValueOnce(composeVersionResponse("v1.4.0")); const latestVersion = "v1.4.0"; - - const mockClient = mockAgent.get("https://api.github.com"); - mockClient - .intercept({ - path: "/repos/docker/compose/releases/latest", - method: "GET", - }) - .reply( - 200, - { - tag_name: latestVersion, - }, - { - headers: { - "content-type": "application/json", - }, - } - ); - setGlobalDispatcher(mockClient); - - versionMock.mockResolvedValueOnce({ - exitCode: 0, - out: "", - err: "", - data: { - version: latestVersion, - }, - }); - - Object.defineProperty(process, "platform", { - value: "linux", - }); - Object.defineProperty(globalThis, "fetch", { - value: jest.fn(), - }); + mockLatestRelease(latestVersion); + versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion)); + setPlatform("linux"); // Act - const result = await service.install({ - composeVersion: "latest", - cwd: "/path/to/cwd", - githubToken: "token", - }); + const result = await installCompose("latest", "token"); // Assert expect(result).toBe(latestVersion); @@ -332,41 +203,38 @@ describe("DockerComposeInstallerService", () => { it("should throw if Compose is missing and no GitHub token is provided", async () => { // Arrange: first call to version() doesn't find versionMock.mockRejectedValueOnce(new Error("version check failed")); + setPlatform("linux"); - Object.defineProperty(process, "platform", { - value: "linux", - }); - - await expect( - service.install({ - composeVersion: "latest", - cwd: "/path/to/cwd", - githubToken: null, - }) - ).rejects.toThrow("GitHub token is required to install the latest version"); + await expect(installCompose("latest", null)).rejects.toThrow( + "GitHub token is required to install the latest version" + ); }); it("should not install when the version is already installed and no version is specified", async () => { // Arrange - versionMock.mockResolvedValue({ - exitCode: 0, - out: "", - err: "", - data: { - version: "1.2.3", - }, - }); + versionMock.mockResolvedValue(composeVersionResponse("1.2.3")); // Act - const result = await service.install({ - composeVersion: "", - cwd: "/path/to/cwd", - githubToken: null, - }); + const result = await installCompose("", null); // Assert expect(result).toBe("1.2.3"); expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled(); }); + + it("should throw when installed version does not match target", async () => { + // Arrange + versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3")); + + const targetVersion = "v1.4.0"; + versionMock.mockResolvedValueOnce(composeVersionResponse("1.3.0")); + setPlatform("linux"); + + // Act & Assert + await expect(installCompose(targetVersion, "token")).rejects.toThrow( + `Failed to install Docker Compose version "${targetVersion}", installed version is "1.3.0"` + ); + expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(targetVersion); + }); }); }); diff --git a/src/services/docker-compose-installer.service.ts b/src/services/docker-compose-installer.service.ts index 78c22ee..f4485da 100644 --- a/src/services/docker-compose-installer.service.ts +++ b/src/services/docker-compose-installer.service.ts @@ -17,9 +17,16 @@ export class DockerComposeInstallerService { constructor(private readonly manualInstallerAdapter: ManualInstallerAdapter) {} async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise { - const currentVersion = await this.getInstalledVersion(cwd); + const currentVersion = await this.version({ cwd }); - const needsInstall = !currentVersion || (composeVersion && composeVersion !== currentVersion); + const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null; + const normalizedRequestedVersion = composeVersion + ? this.normalizeVersion(composeVersion) + : null; + + const needsInstall = + !currentVersion || + (composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion); if (!needsInstall) { return currentVersion; } @@ -35,24 +42,32 @@ export class DockerComposeInstallerService { await this.installVersion(targetVersion); - return this.version({ cwd }); + const installedVersion = await this.version({ cwd }); + + if ( + !installedVersion || + this.normalizeVersion(installedVersion) !== this.normalizeVersion(targetVersion) + ) { + throw new Error( + `Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"` + ); + } + + return installedVersion; } - private async getInstalledVersion(cwd: string): Promise { + private async version({ cwd }: VersionInputs): Promise { try { - return await this.version({ cwd }); + const result = await version({ + cwd, + }); + return result.data.version; } catch { + // If version check fails (e.g., Docker Compose not installed), return null return null; } } - private async version({ cwd }: VersionInputs): Promise { - const result = await version({ - cwd, - }); - return result.data.version; - } - private async getLatestVersion(githubToken: string): Promise { const octokit = github.getOctokit(githubToken); @@ -64,6 +79,10 @@ export class DockerComposeInstallerService { return response.data.tag_name; } + private normalizeVersion(version: string): string { + return version.replace(/^v/i, ""); + } + private async installVersion(version: string): Promise { switch (process.platform) { case "linux":