diff --git a/packages/installer/src/dappGet/aggregate/aggregateDependencies.ts b/packages/installer/src/dappGet/aggregate/aggregateDependencies.ts index 49e7f52314..41e6a4e5d2 100644 --- a/packages/installer/src/dappGet/aggregate/aggregateDependencies.ts +++ b/packages/installer/src/dappGet/aggregate/aggregateDependencies.ts @@ -53,31 +53,41 @@ export default async function aggregateDependencies({ // Already checked, skip. Otherwise lock request to prevent duplicate fetches if (hasVersion(dnps, name, version)) return; else setVersion(dnps, name, version, {}); - // 2. Get dependencies of this specific version - // dependencies = { dnp-name-1: "semverRange", dnp-name-2: "/ipfs/Qmf53..."} - const dependencies = await dappGetFetcher - .dependencies(dappnodeInstaller, name, version) - .then(sanitizeDependencies) - .catch((e: Error) => { - e.message += `Error fetching ${name}@${version}`; - throw e; - }); + + try { + // 2. Get dependencies of this specific version + // dependencies = { dnp-name-1: "semverRange", dnp-name-2: "/ipfs/Qmf53..."} + const dependencies = await dappGetFetcher + .dependencies(dappnodeInstaller, name, version) + .then(sanitizeDependencies); - // 3. Store dependencies - setVersion(dnps, name, version, dependencies); - // 4. Fetch sub-dependencies recursively - await Promise.all( - Object.keys(dependencies).map(async (dependencyName) => { - await aggregateDependencies({ - dappnodeInstaller, - name: dependencyName, - versionRange: dependencies[dependencyName], - dnps, - recursiveCount, - dappGetFetcher - }); - }) - ); + // 3. Store dependencies + setVersion(dnps, name, version, dependencies); + // 4. Fetch sub-dependencies recursively + await Promise.all( + Object.keys(dependencies).map(async (dependencyName) => { + await aggregateDependencies({ + dappnodeInstaller, + name: dependencyName, + versionRange: dependencies[dependencyName], + dnps, + recursiveCount, + dappGetFetcher + }); + }) + ); + } catch (e: unknown) { + // Skip versions whose dependencies can't be fetched instead of failing the entire aggregation + // Remove the version from dnps as it's not usable + if (dnps[name] && dnps[name].versions && dnps[name].versions[version] !== undefined) { + delete dnps[name].versions[version]; + // If no versions remain for this package, remove the package entirely + if (Object.keys(dnps[name].versions).length === 0) { + delete dnps[name]; + } + } + // Continue with other versions by not re-throwing the error + } }) ); } diff --git a/packages/installer/test/unit/dappGet/aggregate/aggregateDependencies.test.ts b/packages/installer/test/unit/dappGet/aggregate/aggregateDependencies.test.ts index 34cf1c1e9b..6556821ec1 100644 --- a/packages/installer/test/unit/dappGet/aggregate/aggregateDependencies.test.ts +++ b/packages/installer/test/unit/dappGet/aggregate/aggregateDependencies.test.ts @@ -1,6 +1,8 @@ import "mocha"; import { expect } from "chai"; import { DappGetFetcherMock, MockDnps } from "../testHelpers.js"; +import { Dependencies } from "@dappnode/types"; +import { DappnodeInstaller } from "../../../../src/dappnodeInstaller.js"; import aggregateDependencies from "../../../../src/dappGet/aggregate/aggregateDependencies.js"; import { dappnodeInstaller } from "../../../testUtils.js"; @@ -108,4 +110,103 @@ describe("dappGet/aggregate/aggregateDependencies", () => { } }); }); + + it("should skip versions whose dependencies can't be fetched", async () => { + // Create a mock fetcher that fails for specific versions + class DappGetFetcherWithFailures extends DappGetFetcherMock { + async dependencies(dappnodeInstaller: DappnodeInstaller, name: string, version: string): Promise { + // Simulate failure for specific version + if (name === "kovan.dnp.dappnode.eth" && version === "0.1.1") { + throw new Error(`Failed to fetch dependencies for ${name}@${version}`); + } + return super.dependencies(dappnodeInstaller, name, version); + } + } + + const mockDnps: MockDnps = { + "kovan.dnp.dappnode.eth": { + "0.1.0": { "dependency.dnp.dappnode.eth": "^0.1.1" }, + "0.1.1": { "dependency.dnp.dappnode.eth": "^0.1.1" }, // This will fail + "0.1.2": { "dependency.dnp.dappnode.eth": "^0.1.1" } + }, + "dependency.dnp.dappnode.eth": { + "0.1.1": {}, + "0.1.2": {} + } + }; + + const dappGetFetcher = new DappGetFetcherWithFailures(mockDnps); + + const dnpName = "kovan.dnp.dappnode.eth"; + const versionRange = "^0.1.0"; // This will match multiple versions + const dnps = {}; + + // This should not throw an error despite version 0.1.1 failing + await aggregateDependencies({ + dappnodeInstaller, + name: dnpName, + versionRange, + dnps, + dappGetFetcher + }); + + // Should only contain versions 0.1.0 and 0.1.2 (0.1.1 should be skipped due to fetch failure) + expect(dnps).to.deep.equal({ + "kovan.dnp.dappnode.eth": { + versions: { + "0.1.0": { "dependency.dnp.dappnode.eth": "^0.1.1" }, + "0.1.2": { "dependency.dnp.dappnode.eth": "^0.1.1" } + } + }, + "dependency.dnp.dappnode.eth": { + versions: { + "0.1.1": {}, + "0.1.2": {} + } + } + }); + }); + + it("should handle case where all versions fail to fetch dependencies", async () => { + // Create a mock fetcher that fails for all versions of a specific package + class DappGetFetcherWithAllFailures extends DappGetFetcherMock { + async dependencies(dappnodeInstaller: DappnodeInstaller, name: string, version: string): Promise { + // Simulate failure for all versions of this specific package + if (name === "broken.dnp.dappnode.eth") { + throw new Error(`Failed to fetch dependencies for ${name}@${version}`); + } + return super.dependencies(dappnodeInstaller, name, version); + } + } + + const mockDnps: MockDnps = { + "broken.dnp.dappnode.eth": { + "0.1.0": { "dependency.dnp.dappnode.eth": "^0.1.1" }, // This will fail + "0.1.1": { "dependency.dnp.dappnode.eth": "^0.1.1" }, // This will fail + "0.1.2": { "dependency.dnp.dappnode.eth": "^0.1.1" } // This will fail + }, + "dependency.dnp.dappnode.eth": { + "0.1.1": {}, + "0.1.2": {} + } + }; + + const dappGetFetcher = new DappGetFetcherWithAllFailures(mockDnps); + + const dnpName = "broken.dnp.dappnode.eth"; + const versionRange = "^0.1.0"; // This will match multiple versions but all will fail + const dnps = {}; + + // This should not throw an error despite all versions failing + await aggregateDependencies({ + dappnodeInstaller, + name: dnpName, + versionRange, + dnps, + dappGetFetcher + }); + + // Should be empty since all versions failed to fetch dependencies + expect(dnps).to.deep.equal({}); + }); });