Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions packages/installer/src/dappGet/aggregate/aggregateDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
);
}
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<Dependencies> {
// 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<Dependencies> {
// 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({});
});
});