|
| 1 | +import "mocha"; |
| 2 | +import { expect } from "chai"; |
| 3 | +import fs from "fs"; |
| 4 | +import path from "path"; |
| 5 | +import { ComposeEditor } from "@dappnode/dockercompose"; |
| 6 | +import { Compose } from "@dappnode/types"; |
| 7 | +import { yamlDump, getDockerComposePath, parseEnvironment } from "@dappnode/utils"; |
| 8 | +import { params } from "@dappnode/params"; |
| 9 | +import { persistDappmanagerSettings } from "../../../src/installer/getInstallerPackageData.js"; |
| 10 | + |
| 11 | +const dnpName = params.dappmanagerDnpName; |
| 12 | +const isCore = true; |
| 13 | + |
| 14 | +/** |
| 15 | + * Helper to write a compose file to the path ComposeFileEditor expects |
| 16 | + */ |
| 17 | +function writeInstalledCompose(compose: Compose): string { |
| 18 | + const composePath = getDockerComposePath(dnpName, isCore); |
| 19 | + const dir = path.dirname(composePath); |
| 20 | + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); |
| 21 | + fs.writeFileSync(composePath, yamlDump(compose)); |
| 22 | + return composePath; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Cleanup the installed compose file |
| 27 | + */ |
| 28 | +function removeInstalledCompose(): void { |
| 29 | + const composePath = getDockerComposePath(dnpName, isCore); |
| 30 | + if (fs.existsSync(composePath)) fs.unlinkSync(composePath); |
| 31 | +} |
| 32 | + |
| 33 | +/** |
| 34 | + * Build a minimal dappmanager compose |
| 35 | + */ |
| 36 | +function buildDappmanagerCompose(overrides?: { |
| 37 | + environment?: Record<string, string> | string[]; |
| 38 | + volumes?: string[]; |
| 39 | +}): Compose { |
| 40 | + return { |
| 41 | + version: "3.5", |
| 42 | + services: { |
| 43 | + "dappmanager.dnp.dappnode.eth": { |
| 44 | + image: "dappmanager.dnp.dappnode.eth:0.2.71", |
| 45 | + container_name: "DAppNodeCore-dappmanager.dnp.dappnode.eth", |
| 46 | + volumes: overrides?.volumes ?? [ |
| 47 | + "/run/dbus/system_bus_socket:/run/dbus/system_bus_socket", |
| 48 | + "dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/", |
| 49 | + "/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE", |
| 50 | + "/var/run/docker.sock:/var/run/docker.sock" |
| 51 | + ], |
| 52 | + environment: overrides?.environment ?? { |
| 53 | + LOG_LEVEL: "info" |
| 54 | + } |
| 55 | + } |
| 56 | + }, |
| 57 | + networks: { |
| 58 | + dncore_network: { external: true } |
| 59 | + } |
| 60 | + }; |
| 61 | +} |
| 62 | + |
| 63 | +describe("persistDappmanagerSettings", () => { |
| 64 | + afterEach(() => { |
| 65 | + removeInstalledCompose(); |
| 66 | + }); |
| 67 | + |
| 68 | + it("Should be a no-op for non-dappmanager packages", () => { |
| 69 | + const compose = new ComposeEditor(buildDappmanagerCompose(), { dnpName }); |
| 70 | + const before = JSON.stringify(compose.compose); |
| 71 | + |
| 72 | + // Call with a different dnpName |
| 73 | + persistDappmanagerSettings(compose, "other.dnp.dappnode.eth", isCore); |
| 74 | + |
| 75 | + expect(JSON.stringify(compose.compose)).to.equal(before); |
| 76 | + }); |
| 77 | + |
| 78 | + it("Should be a no-op when no installed compose exists (fresh install)", () => { |
| 79 | + // Don't write any compose to disk |
| 80 | + removeInstalledCompose(); |
| 81 | + |
| 82 | + const newCompose = buildDappmanagerCompose(); |
| 83 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 84 | + const before = JSON.stringify(compose.compose); |
| 85 | + |
| 86 | + // Should not throw |
| 87 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 88 | + |
| 89 | + expect(JSON.stringify(compose.compose)).to.equal(before); |
| 90 | + }); |
| 91 | + |
| 92 | + it("Should persist DISABLE_HOST_SCRIPTS from installed compose", () => { |
| 93 | + // Write installed compose with DISABLE_HOST_SCRIPTS=true |
| 94 | + writeInstalledCompose( |
| 95 | + buildDappmanagerCompose({ |
| 96 | + environment: { |
| 97 | + LOG_LEVEL: "info", |
| 98 | + DISABLE_HOST_SCRIPTS: "true" |
| 99 | + } |
| 100 | + }) |
| 101 | + ); |
| 102 | + |
| 103 | + // New compose does NOT have DISABLE_HOST_SCRIPTS |
| 104 | + const newCompose = buildDappmanagerCompose({ |
| 105 | + environment: { LOG_LEVEL: "info" } |
| 106 | + }); |
| 107 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 108 | + |
| 109 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 110 | + |
| 111 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 112 | + const envs = parseEnvironment(service.environment || []); |
| 113 | + expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true"); |
| 114 | + // Original env preserved |
| 115 | + expect(envs["LOG_LEVEL"]).to.equal("info"); |
| 116 | + }); |
| 117 | + |
| 118 | + it("Should persist DAPPNODE_CORE_DIR from installed compose", () => { |
| 119 | + writeInstalledCompose( |
| 120 | + buildDappmanagerCompose({ |
| 121 | + environment: { |
| 122 | + LOG_LEVEL: "info", |
| 123 | + DAPPNODE_CORE_DIR: "/custom/path/DNCORE" |
| 124 | + } |
| 125 | + }) |
| 126 | + ); |
| 127 | + |
| 128 | + const newCompose = buildDappmanagerCompose({ |
| 129 | + environment: { LOG_LEVEL: "info" } |
| 130 | + }); |
| 131 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 132 | + |
| 133 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 134 | + |
| 135 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 136 | + const envs = parseEnvironment(service.environment || []); |
| 137 | + expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/path/DNCORE"); |
| 138 | + }); |
| 139 | + |
| 140 | + it("Should persist both DISABLE_HOST_SCRIPTS and DAPPNODE_CORE_DIR", () => { |
| 141 | + writeInstalledCompose( |
| 142 | + buildDappmanagerCompose({ |
| 143 | + environment: { |
| 144 | + LOG_LEVEL: "info", |
| 145 | + DISABLE_HOST_SCRIPTS: "true", |
| 146 | + DAPPNODE_CORE_DIR: "/custom/path/DNCORE" |
| 147 | + } |
| 148 | + }) |
| 149 | + ); |
| 150 | + |
| 151 | + const newCompose = buildDappmanagerCompose({ |
| 152 | + environment: { LOG_LEVEL: "debug" } |
| 153 | + }); |
| 154 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 155 | + |
| 156 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 157 | + |
| 158 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 159 | + const envs = parseEnvironment(service.environment || []); |
| 160 | + expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true"); |
| 161 | + expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/path/DNCORE"); |
| 162 | + // Original env kept |
| 163 | + expect(envs["LOG_LEVEL"]).to.equal("debug"); |
| 164 | + }); |
| 165 | + |
| 166 | + it("Should update DNCORE volume host path to match DAPPNODE_CORE_DIR", () => { |
| 167 | + const customDir = "/custom/path/DNCORE"; |
| 168 | + writeInstalledCompose( |
| 169 | + buildDappmanagerCompose({ |
| 170 | + environment: { |
| 171 | + DAPPNODE_CORE_DIR: customDir |
| 172 | + } |
| 173 | + }) |
| 174 | + ); |
| 175 | + |
| 176 | + // New compose has default volume path |
| 177 | + const newCompose = buildDappmanagerCompose({ |
| 178 | + environment: { LOG_LEVEL: "info" }, |
| 179 | + volumes: [ |
| 180 | + "dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/", |
| 181 | + "/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE", |
| 182 | + "/var/run/docker.sock:/var/run/docker.sock" |
| 183 | + ] |
| 184 | + }); |
| 185 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 186 | + |
| 187 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 188 | + |
| 189 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 190 | + // The DNCORE volume should now point to the custom dir |
| 191 | + expect(service.volumes).to.include(`${customDir}:/usr/src/app/DNCORE`); |
| 192 | + // Other volumes untouched |
| 193 | + expect(service.volumes).to.include("dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo"); |
| 194 | + expect(service.volumes).to.include("/var/run/docker.sock:/var/run/docker.sock"); |
| 195 | + }); |
| 196 | + |
| 197 | + it("Should not modify volumes when DAPPNODE_CORE_DIR is not set in installed compose", () => { |
| 198 | + // Installed compose with DISABLE_HOST_SCRIPTS but no DAPPNODE_CORE_DIR |
| 199 | + writeInstalledCompose( |
| 200 | + buildDappmanagerCompose({ |
| 201 | + environment: { |
| 202 | + DISABLE_HOST_SCRIPTS: "true" |
| 203 | + } |
| 204 | + }) |
| 205 | + ); |
| 206 | + |
| 207 | + const volumes = [ |
| 208 | + "dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/", |
| 209 | + "/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE", |
| 210 | + "/var/run/docker.sock:/var/run/docker.sock" |
| 211 | + ]; |
| 212 | + const newCompose = buildDappmanagerCompose({ |
| 213 | + environment: { LOG_LEVEL: "info" }, |
| 214 | + volumes |
| 215 | + }); |
| 216 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 217 | + |
| 218 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 219 | + |
| 220 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 221 | + // Default DNCORE path preserved |
| 222 | + expect(service.volumes).to.include("/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE"); |
| 223 | + }); |
| 224 | + |
| 225 | + it("Should be a no-op when installed compose has no relevant envs", () => { |
| 226 | + // Installed compose with no special envs |
| 227 | + writeInstalledCompose( |
| 228 | + buildDappmanagerCompose({ |
| 229 | + environment: { |
| 230 | + LOG_LEVEL: "info", |
| 231 | + SOME_OTHER_VAR: "value" |
| 232 | + } |
| 233 | + }) |
| 234 | + ); |
| 235 | + |
| 236 | + const newCompose = buildDappmanagerCompose({ |
| 237 | + environment: { LOG_LEVEL: "debug" } |
| 238 | + }); |
| 239 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 240 | + const envsBefore = parseEnvironment(compose.compose.services["dappmanager.dnp.dappnode.eth"].environment || []); |
| 241 | + |
| 242 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 243 | + |
| 244 | + const envsAfter = parseEnvironment(compose.compose.services["dappmanager.dnp.dappnode.eth"].environment || []); |
| 245 | + expect(envsAfter).to.deep.equal(envsBefore); |
| 246 | + }); |
| 247 | + |
| 248 | + it("Should handle installed compose with environment as array format", () => { |
| 249 | + writeInstalledCompose( |
| 250 | + buildDappmanagerCompose({ |
| 251 | + environment: ["LOG_LEVEL=info", "DISABLE_HOST_SCRIPTS=true", "DAPPNODE_CORE_DIR=/custom/dir"] |
| 252 | + }) |
| 253 | + ); |
| 254 | + |
| 255 | + const newCompose = buildDappmanagerCompose({ |
| 256 | + environment: { LOG_LEVEL: "info" } |
| 257 | + }); |
| 258 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 259 | + |
| 260 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 261 | + |
| 262 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 263 | + const envs = parseEnvironment(service.environment || []); |
| 264 | + expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true"); |
| 265 | + expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/dir"); |
| 266 | + }); |
| 267 | + |
| 268 | + it("Should not overwrite DNCORE volume if no volume matches the container path", () => { |
| 269 | + const customDir = "/custom/path/DNCORE"; |
| 270 | + writeInstalledCompose( |
| 271 | + buildDappmanagerCompose({ |
| 272 | + environment: { |
| 273 | + DAPPNODE_CORE_DIR: customDir |
| 274 | + } |
| 275 | + }) |
| 276 | + ); |
| 277 | + |
| 278 | + // New compose with no DNCORE volume |
| 279 | + const newCompose = buildDappmanagerCompose({ |
| 280 | + environment: { LOG_LEVEL: "info" }, |
| 281 | + volumes: ["dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/", "/var/run/docker.sock:/var/run/docker.sock"] |
| 282 | + }); |
| 283 | + const compose = new ComposeEditor(newCompose, { dnpName }); |
| 284 | + |
| 285 | + persistDappmanagerSettings(compose, dnpName, isCore); |
| 286 | + |
| 287 | + const service = compose.compose.services["dappmanager.dnp.dappnode.eth"]; |
| 288 | + // No DNCORE volume should be added — only existing volumes modified |
| 289 | + expect(service.volumes).to.have.lengthOf(2); |
| 290 | + expect(service.volumes).to.include("dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo"); |
| 291 | + expect(service.volumes).to.include("/var/run/docker.sock:/var/run/docker.sock"); |
| 292 | + }); |
| 293 | +}); |
0 commit comments