Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Drizzle migration files are content-hashed by drizzle's migrator. Any line-ending
# drift (e.g. CRLF on Windows checkouts) changes the hash and triggers re-runs of
# already-applied migrations. Lock to LF so checkouts are byte-stable across hosts.
apps/dokploy/drizzle/*.sql text eol=lf
apps/dokploy/drizzle/meta/*.json text eol=lf
1 change: 1 addition & 0 deletions apps/dokploy/__test__/drop/drop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const baseApp: ApplicationNested = {
railpackVersion: "0.15.4",
applicationId: "",
previewLabels: [],
networkIds: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
Expand Down
96 changes: 96 additions & 0 deletions apps/dokploy/__test__/network/network-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
apiCreateNetwork,
apiFindOneNetwork,
apiRemoveNetwork,
} from "@dokploy/server/db/schema";
import { describe, expect, it } from "vitest";

describe("apiCreateNetwork", () => {
it("accepts a minimal create payload (just name)", () => {
const result = apiCreateNetwork.safeParse({ name: "my-net" });
expect(result.success).toBe(true);
});

it("rejects an empty name", () => {
const result = apiCreateNetwork.safeParse({ name: "" });
expect(result.success).toBe(false);
});

it("accepts all supported drivers", () => {
for (const driver of [
"bridge",
"host",
"overlay",
"macvlan",
"none",
"ipvlan",
] as const) {
const result = apiCreateNetwork.safeParse({ name: "net", driver });
expect(result.success, `driver=${driver}`).toBe(true);
}
});

it("rejects an unknown driver", () => {
const result = apiCreateNetwork.safeParse({ name: "net", driver: "lan" });
expect(result.success).toBe(false);
});

it("accepts an IPAM config with subnet + gateway", () => {
const result = apiCreateNetwork.safeParse({
name: "net",
ipam: {
driver: "default",
config: [{ subnet: "172.28.0.0/16", gateway: "172.28.0.1" }],
},
});
expect(result.success).toBe(true);
});

it("accepts an IPAM config with only ipRange", () => {
const result = apiCreateNetwork.safeParse({
name: "net",
ipam: { config: [{ ipRange: "172.28.5.0/24" }] },
});
expect(result.success).toBe(true);
});

it("allows explicit serverId as optional", () => {
const result = apiCreateNetwork.safeParse({
name: "net",
serverId: "srv_123",
});
expect(result.success).toBe(true);
});

it("allows serverId to be null", () => {
const result = apiCreateNetwork.safeParse({
name: "net",
serverId: null,
});
expect(result.success).toBe(true);
});

it("accepts boolean flags (internal, attachable, enableIPv6)", () => {
const result = apiCreateNetwork.safeParse({
name: "net",
internal: true,
attachable: true,
enableIPv6: true,
});
expect(result.success).toBe(true);
});
});

describe("apiFindOneNetwork / apiRemoveNetwork", () => {
it("requires a non-empty networkId on lookup", () => {
expect(apiFindOneNetwork.safeParse({ networkId: "" }).success).toBe(false);
expect(apiFindOneNetwork.safeParse({ networkId: "n_1" }).success).toBe(
true,
);
});

it("requires a non-empty networkId on remove", () => {
expect(apiRemoveNetwork.safeParse({ networkId: "" }).success).toBe(false);
expect(apiRemoveNetwork.safeParse({ networkId: "n_1" }).success).toBe(true);
});
});
182 changes: 182 additions & 0 deletions apps/dokploy/__test__/network/network-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Swap the remote-docker helper for a controllable fake before importing the
// service under test. The in-memory `fakeDocker` is mutated per test.
const fakeDocker = {
createNetwork: vi.fn(),
listNetworks: vi.fn<() => Promise<Array<{ Name: string; Id: string }>>>(),
getNetwork: vi.fn<(id: string) => { remove: () => Promise<void> }>(),
};

vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
getRemoteDocker: vi.fn(async () => fakeDocker),
}));

// Silence the IS_CLOUD branch — our tests cover non-cloud behavior.
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>;
return { ...actual, IS_CLOUD: false };
});

import {
assertNetworkIdsAttachableToResource,
findNetworkById,
getNetworkErrorMessage,
isDuplicateNetworkNameError,
removeNetworkById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";

// Tests here target error paths that don't require a live DB transaction.
// The global DB mock in __test__/setup.ts already makes `db.query.network`
// return `undefined` for findFirst, which is exactly the not-found path.
describe("findNetworkById", () => {
it("throws NOT_FOUND when the row doesn't exist", async () => {
await expect(findNetworkById("missing")).rejects.toMatchObject({
code: "NOT_FOUND",
});
});
});

describe("removeNetworkById", () => {
beforeEach(() => {
fakeDocker.createNetwork.mockReset();
fakeDocker.listNetworks.mockReset();
fakeDocker.getNetwork.mockReset();
});

it("throws NOT_FOUND when the row doesn't exist for the organization", async () => {
await expect(removeNetworkById("missing", "org_1")).rejects.toMatchObject({
code: "NOT_FOUND",
});
// Docker should never be touched when the DB row is missing.
expect(fakeDocker.listNetworks).not.toHaveBeenCalled();
});

it("rejects attached networks before touching Docker", async () => {
const target = {
networkId: "net_1",
name: "shared-net",
serverId: "srv_1",
organizationId: "org_1",
};
const orgEnvSubquery = {
from: () => ({
innerJoin: () => ({
where: () => ({}),
}),
}),
};
const usageRows = (rows: Array<{ id: string; name: string }>) => ({
from: () => ({
where: () => Promise.resolve(rows),
}),
});

vi.mocked(db.query.network.findFirst)
.mockResolvedValueOnce(target as never)
.mockResolvedValueOnce(target as never);
vi.mocked(db.select)
.mockReturnValueOnce(orgEnvSubquery as never)
.mockReturnValueOnce(
usageRows([{ id: "app_1", name: "Alpha" }]) as never,
);
for (let i = 0; i < 6; i++) {
vi.mocked(db.select).mockReturnValueOnce(usageRows([]) as never);
}

await expect(removeNetworkById("net_1", "org_1")).rejects.toMatchObject({
code: "CONFLICT",
});
expect(fakeDocker.listNetworks).not.toHaveBeenCalled();
});
});

describe("assertNetworkIdsAttachableToResource", () => {
it("allows empty attachment lists without querying", async () => {
await expect(
assertNetworkIdsAttachableToResource([], "org_1", "srv_1"),
).resolves.toEqual([]);
});

it("returns unique network IDs after successful validation", async () => {
vi.mocked(db.select).mockReturnValueOnce({
from: () => ({
where: () =>
Promise.resolve([
{ id: "net_1", serverId: "srv_1", driver: "overlay" },
{ id: "net_2", serverId: "srv_1", driver: "overlay" },
]),
}),
} as never);

await expect(
assertNetworkIdsAttachableToResource(
["net_1", "net_1", "net_2"],
"org_1",
"srv_1",
),
).resolves.toEqual(["net_1", "net_2"]);
});

it("rejects network IDs that cannot be resolved for the organization", async () => {
await expect(
assertNetworkIdsAttachableToResource(["net_missing"], "org_1", "srv_1"),
).rejects.toMatchObject({
code: "BAD_REQUEST",
});
});
});

describe("error classification", () => {
it("recognises nested Docker duplicate-name errors", () => {
const dockerodeError = {
statusCode: 403,
json: {
message:
"network with name codex-e2e-test already exists on this server",
},
};
const wrapped = new TRPCError({
code: "BAD_REQUEST",
message: "Docker rejected network creation",
cause: dockerodeError,
});

expect(getNetworkErrorMessage(wrapped)).toContain(
"network with name codex-e2e-test already exists",
);
expect(isDuplicateNetworkNameError(wrapped)).toBe(true);
});

it("recognises DB unique constraint errors", () => {
expect(
isDuplicateNetworkNameError({
code: "23505",
constraint: "network_name_serverId_idx",
message: "duplicate key value violates unique constraint",
}),
).toBe(true);
});

it("recognises Docker 'in use' errors as CONFLICT (regex shape)", () => {
// Mirrors the runtime check in removeNetworkById — if this pattern ever
// diverges from Docker's wording we'll notice via this guardrail.
const patterns = [
"network foo has active endpoints",
"Error response from daemon: network is in use",
];
for (const msg of patterns) {
expect(/has active endpoints|is in use/i.test(msg)).toBe(true);
}
});

it("is a TRPCError constructable with CONFLICT", () => {
const err = new TRPCError({
code: "CONFLICT",
message: "Network in use",
});
expect(err.code).toBe("CONFLICT");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const ENTERPRISE_RESOURCES = [
"destination",
"notification",
"tag",
"network",
"logs",
"monitoring",
"auditLog",
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/__test__/traefik/traefik.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const baseApp: ApplicationNested = {
rollbackActive: false,
applicationId: "",
previewLabels: [],
networkIds: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
Expand Down
Loading
Loading