From ec74035c7030f96fbf42b4c4c25154403832faaf Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 25 Jul 2026 03:20:00 -0600 Subject: [PATCH] feat: add new test --- .../compose/network/service-networks.test.ts | 199 ++++++++++++++++++ packages/server/src/db/schema/network.ts | 7 - packages/server/src/services/network.ts | 4 - packages/server/src/utils/docker/domain.ts | 4 +- 4 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 apps/dokploy/__test__/compose/network/service-networks.test.ts diff --git a/apps/dokploy/__test__/compose/network/service-networks.test.ts b/apps/dokploy/__test__/compose/network/service-networks.test.ts new file mode 100644 index 000000000..85d912971 --- /dev/null +++ b/apps/dokploy/__test__/compose/network/service-networks.test.ts @@ -0,0 +1,199 @@ +import type { Compose, ComposeSpecification } from "@dokploy/server"; +import { + applyServiceNetworks, + declareUsedNetworksInRoot, + resolveServiceNetworks, +} from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { beforeEach, expect, test, type vi } from "vitest"; +import { parse } from "yaml"; + +const findManyMock = db.query.network.findMany as ReturnType; + +beforeEach(() => { + findManyMock.mockReset(); + findManyMock.mockResolvedValue([]); +}); + +const baseCompose = { + serverId: null, + isolatedDeployment: false, +} as unknown as Compose; + +const withServiceNetworks = ( + serviceNetworks: Compose["serviceNetworks"], +): Compose => ({ ...baseCompose, serviceNetworks }); + +test("applyServiceNetworks: no-op when serviceNetworks is empty", async () => { + const result = parse(` +services: + web: + image: nginx +`) as ComposeSpecification; + + const injected = await applyServiceNetworks(result, withServiceNetworks([])); + + expect(injected.size).toBe(0); + expect(result.services?.web?.networks).toBeUndefined(); + expect(findManyMock).not.toHaveBeenCalled(); +}); + +test("applyServiceNetworks: injects assigned network by networkId", async () => { + findManyMock.mockResolvedValue([{ networkId: "net-1", name: "shared-net" }]); + + const result = parse(` +services: + web: + image: nginx +`) as ComposeSpecification; + + const injected = await applyServiceNetworks( + result, + withServiceNetworks([ + { + serviceName: "web", + networkIds: ["net-1"], + detachDokployNetwork: false, + }, + ]), + ); + + expect(injected.has("shared-net")).toBe(true); + expect(result.services?.web?.networks).toContain("shared-net"); +}); + +test("applyServiceNetworks: detach removes dokploy-network and default", async () => { + const result = parse(` +services: + db: + image: postgres + networks: + - dokploy-network + - default +`) as ComposeSpecification; + + const injected = await applyServiceNetworks( + result, + withServiceNetworks([ + { serviceName: "db", networkIds: [], detachDokployNetwork: true }, + ]), + ); + + expect(injected.size).toBe(0); + expect(result.services?.db?.networks).not.toContain("dokploy-network"); + expect(result.services?.db?.networks).not.toContain("default"); +}); + +test("applyServiceNetworks: unknown networkId is skipped", async () => { + findManyMock.mockResolvedValue([]); + + const result = parse(` +services: + web: + image: nginx +`) as ComposeSpecification; + + const injected = await applyServiceNetworks( + result, + withServiceNetworks([ + { + serviceName: "web", + networkIds: ["missing"], + detachDokployNetwork: false, + }, + ]), + ); + + expect(injected.size).toBe(0); +}); + +test("applyServiceNetworks: skips services that don't exist in the compose", async () => { + findManyMock.mockResolvedValue([{ networkId: "net-1", name: "shared-net" }]); + + const result = parse(` +services: + web: + image: nginx +`) as ComposeSpecification; + + const injected = await applyServiceNetworks( + result, + withServiceNetworks([ + { + serviceName: "ghost", + networkIds: ["net-1"], + detachDokployNetwork: false, + }, + ]), + ); + + expect(injected.size).toBe(0); + expect(result.services?.web?.networks).toBeUndefined(); +}); + +test("declareUsedNetworksInRoot: declares dokploy-network only when used", () => { + const used = parse(` +services: + web: + image: nginx + networks: + - dokploy-network +`) as ComposeSpecification; + declareUsedNetworksInRoot(used, new Set()); + expect(used.networks).toHaveProperty("dokploy-network"); + + const unused = parse(` +services: + web: + image: nginx + networks: + - default +`) as ComposeSpecification; + declareUsedNetworksInRoot(unused, new Set()); + expect(unused.networks ?? {}).not.toHaveProperty("dokploy-network"); +}); + +test("declareUsedNetworksInRoot: declares injected networks that are used", () => { + const result = parse(` +services: + web: + image: nginx + networks: + - shared-net +`) as ComposeSpecification; + + declareUsedNetworksInRoot(result, new Set(["shared-net", "unused-net"])); + + expect(result.networks).toHaveProperty("shared-net"); + expect(result.networks ?? {}).not.toHaveProperty("unused-net"); +}); + +test("resolveServiceNetworks: returns dokploy-network by default", async () => { + const resolved = await resolveServiceNetworks({}); + expect(resolved).toEqual([{ Target: "dokploy-network" }]); + expect(findManyMock).not.toHaveBeenCalled(); +}); + +test("resolveServiceNetworks: omits dokploy-network when detached", async () => { + const resolved = await resolveServiceNetworks({ detachDokployNetwork: true }); + expect(resolved).toEqual([]); +}); + +test("resolveServiceNetworks: appends overlay networks by networkId", async () => { + findManyMock.mockResolvedValue([{ name: "overlay-a" }]); + + const resolved = await resolveServiceNetworks({ networkIds: ["net-a"] }); + + expect(resolved).toEqual([ + { Target: "dokploy-network" }, + { Target: "overlay-a" }, + ]); +}); + +test("resolveServiceNetworks: networkSwarm override takes precedence", async () => { + const override = [{ Target: "custom-net" }]; + const resolved = await resolveServiceNetworks({ networkSwarm: override }); + + expect(resolved).toBe(override); + expect(findManyMock).not.toHaveBeenCalled(); +}); diff --git a/packages/server/src/db/schema/network.ts b/packages/server/src/db/schema/network.ts index add0ef249..d64b2a088 100644 --- a/packages/server/src/db/schema/network.ts +++ b/packages/server/src/db/schema/network.ts @@ -6,13 +6,6 @@ import { z } from "zod"; import { organization } from "./account"; import { server } from "./server"; -/** - * Docker network driver types. Only bridge and overlay are supported: - * "host"/"none" are Docker singletons that cannot be created, and - * macvlan/ipvlan require driver options (parent interface) we don't expose. - * Scope is derived from the driver (bridge = local, overlay = swarm), and - * ingress/config-only networks are not manageable from Dokploy. - */ export const networkDriver = pgEnum("networkDriver", ["bridge", "overlay"]); export const network = pgTable("network", { diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index f2ccd05ae..364176034 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -7,8 +7,6 @@ import { IS_CLOUD } from "../constants"; import type { ApplicationNested } from "../utils/builders"; import { getRemoteDocker } from "../utils/servers/remote-docker"; -// Networks managed by Docker/Dokploy itself that must never be imported -// or deleted through the networks UI const RESERVED_NETWORKS = [ "bridge", "host", @@ -325,7 +323,6 @@ export const inspectNetwork = async (networkId: string) => { } }; -// Docker networks are immutable: there is no update, only create and remove. export const removeNetwork = async (networkId: string) => { const row = await findNetworkById(networkId); @@ -333,7 +330,6 @@ export const removeNetwork = async (networkId: string) => { try { await docker.getNetwork(row.name).remove(); } catch (error) { - // If the network is already gone from Docker, still clean up the DB row const statusCode = (error as { statusCode?: number })?.statusCode; if (statusCode !== 404) { throw new TRPCError({ diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index b2d0bea43..26acf2986 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -240,7 +240,7 @@ export const addDomainToCompose = async ( return result; }; -const applyServiceNetworks = async ( +export const applyServiceNetworks = async ( result: ComposeSpecification, compose: Compose, ) => { @@ -282,7 +282,7 @@ const applyServiceNetworks = async ( return injectedNetworkNames; }; -const declareUsedNetworksInRoot = ( +export const declareUsedNetworksInRoot = ( result: ComposeSpecification, injectedNetworkNames: Set, ) => {