mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
fix(databases): allow clearing external port to revoke internet exposure
This commit is contained in:
parent
1572008cdf
commit
85ac164d4a
@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { externalPortFormSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
|
||||
const parse = (input: unknown) =>
|
||||
externalPortFormSchema.safeParse({ externalPort: input });
|
||||
|
||||
const getPort = (data: unknown): unknown =>
|
||||
(data as { externalPort?: unknown } | null)?.externalPort;
|
||||
|
||||
describe("externalPort client preprocess (G7-G12)", () => {
|
||||
it("G7: empty string '' -> null (clearing revokes exposure, no client parse error)", () => {
|
||||
const result = parse("");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("G8: undefined -> null (DB row started with null, form submitted undefined)", () => {
|
||||
const result = parse(undefined);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("G8b: null -> null (passed through)", () => {
|
||||
const result = parse(null);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("G9 + G10: numeric string '3306' -> 3306 (unchanged numeric value re-saves)", () => {
|
||||
const result = parse("3306");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(3306);
|
||||
}
|
||||
});
|
||||
|
||||
it("G9b: number 3306 -> 3306 (reset numeric value re-saves without z.string().parse(number) throw)", () => {
|
||||
const result = parse(3306);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(3306);
|
||||
}
|
||||
});
|
||||
|
||||
it("boundaries: 0 -> 0 (valid, lowest port)", () => {
|
||||
const result = parse(0);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("boundaries: '0' -> 0", () => {
|
||||
const result = parse("0");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("boundaries: 65535 -> 65535 (valid, highest port)", () => {
|
||||
const result = parse(65535);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(65535);
|
||||
}
|
||||
});
|
||||
|
||||
it("G11: 65536 -> rejected with 'Range must be 0 - 65535'", () => {
|
||||
const result = parse(65536);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0]?.message).toContain(
|
||||
"Range must be 0 - 65535",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("G11b: -1 -> rejected with 'Range must be 0 - 65535'", () => {
|
||||
const result = parse(-1);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0]?.message).toContain(
|
||||
"Range must be 0 - 65535",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("G11c: '99999' -> rejected", () => {
|
||||
const result = parse("99999");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("G12: non-numeric garbage 'abc' -> NaN maps to null (existing behavior preserved)", () => {
|
||||
const result = parse("abc");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("whitespace string ' 12 ' -> NaN -> null (parseInt(' 12 ')=12 but String then parseInt handles trim)", () => {
|
||||
const result = parse(" 12 ");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getPort(result.data)).toBe(12);
|
||||
}
|
||||
});
|
||||
});
|
||||
169
apps/dokploy/__test__/db/build-db-ports.test.ts
Normal file
169
apps/dokploy/__test__/db/build-db-ports.test.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import type { MariadbNested } from "@dokploy/server/utils/databases/mariadb";
|
||||
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
||||
import type { MongoNested } from "@dokploy/server/utils/databases/mongo";
|
||||
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
||||
import type { MysqlNested } from "@dokploy/server/utils/databases/mysql";
|
||||
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
||||
import type { PostgresNested } from "@dokploy/server/utils/databases/postgres";
|
||||
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
||||
import type { RedisNested } from "@dokploy/server/utils/databases/redis";
|
||||
import { buildRedis } from "@dokploy/server/utils/databases/redis";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type MockSettings = {
|
||||
EndpointSpec?: {
|
||||
Mode?: string;
|
||||
Ports?: Array<Record<string, unknown>>;
|
||||
} | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
|
||||
vi.hoisted(() => {
|
||||
const inspect = vi.fn<() => Promise<never>>();
|
||||
const getService = vi.fn(() => ({ inspect }));
|
||||
const createService = vi.fn<(opts: MockSettings) => Promise<void>>(
|
||||
async () => undefined,
|
||||
);
|
||||
const getRemoteDocker = vi.fn(async () => ({
|
||||
getService,
|
||||
createService,
|
||||
}));
|
||||
return {
|
||||
inspectMock: inspect,
|
||||
getServiceMock: getService,
|
||||
createServiceMock: createService,
|
||||
getRemoteDockerMock: getRemoteDocker,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||
getRemoteDocker: getRemoteDockerMock,
|
||||
}));
|
||||
|
||||
const baseFixture = {
|
||||
appName: "test-db",
|
||||
env: null,
|
||||
dockerImage: "image:latest",
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
cpuLimit: null,
|
||||
cpuReservation: null,
|
||||
command: null,
|
||||
args: null,
|
||||
mounts: [],
|
||||
databaseName: "db",
|
||||
databaseUser: "user",
|
||||
databasePassword: "pass",
|
||||
databaseRootPassword: "rootpass",
|
||||
externalPort: null as number | null,
|
||||
serverId: "server-id",
|
||||
environment: {
|
||||
project: { env: null },
|
||||
env: null,
|
||||
},
|
||||
updateConfigSwarm: null,
|
||||
endpointSpecSwarm: null,
|
||||
networkIds: [],
|
||||
detachDokployNetwork: false,
|
||||
};
|
||||
|
||||
type Case = {
|
||||
name: string;
|
||||
build: (input: unknown) => Promise<unknown>;
|
||||
targetPort: number;
|
||||
extras?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const cases: Case[] = [
|
||||
{
|
||||
name: "mariadb",
|
||||
build: (x) => buildMariadb(x as MariadbNested),
|
||||
targetPort: 3306,
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
build: (x) => buildMysql(x as MysqlNested),
|
||||
targetPort: 3306,
|
||||
},
|
||||
{
|
||||
name: "postgres",
|
||||
build: (x) => buildPostgres(x as PostgresNested),
|
||||
targetPort: 5432,
|
||||
},
|
||||
{
|
||||
name: "mongo",
|
||||
build: (x) => buildMongo(x as MongoNested),
|
||||
targetPort: 27017,
|
||||
extras: { replicaSets: false },
|
||||
},
|
||||
{
|
||||
name: "redis",
|
||||
build: (x) => buildRedis(x as RedisNested),
|
||||
targetPort: 6379,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
inspectMock.mockReset();
|
||||
inspectMock.mockImplementation(() => {
|
||||
throw new Error("service not found");
|
||||
});
|
||||
getServiceMock.mockClear();
|
||||
createServiceMock.mockClear();
|
||||
getRemoteDockerMock.mockClear();
|
||||
getRemoteDockerMock.mockResolvedValue({
|
||||
getService: getServiceMock,
|
||||
createService: createServiceMock,
|
||||
});
|
||||
});
|
||||
|
||||
const lastSettings = (): MockSettings => {
|
||||
const call = createServiceMock.mock.calls[0] as [MockSettings] | undefined;
|
||||
if (!call) throw new Error("createService was not called");
|
||||
return call[0];
|
||||
};
|
||||
|
||||
describe.each(cases)(
|
||||
"build $name EndpointSpec.Ports (G13/G16)",
|
||||
({ name, build, targetPort, extras }) => {
|
||||
it("G13: externalPort null deploys with empty Ports (no host-published port = not internet-reachable)", async () => {
|
||||
await build({ ...baseFixture, ...extras, externalPort: null });
|
||||
|
||||
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(lastSettings().EndpointSpec?.Ports).toEqual([]);
|
||||
});
|
||||
|
||||
it("G16: externalPort set deploys with a single host-published port", async () => {
|
||||
await build({ ...baseFixture, ...extras, externalPort: 13306 });
|
||||
|
||||
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||
const ports = lastSettings().EndpointSpec?.Ports;
|
||||
expect(ports).toEqual([
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: targetPort,
|
||||
PublishedPort: 13306,
|
||||
PublishMode: "host",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("G13b: externalPort 0 (falsy) deploys with empty Ports", async () => {
|
||||
await build({ ...baseFixture, ...extras, externalPort: 0 });
|
||||
|
||||
expect(lastSettings().EndpointSpec?.Ports).toEqual([]);
|
||||
expect(lastSettings().EndpointSpec?.Mode).toBe("dnsrr");
|
||||
});
|
||||
|
||||
it("sanity: target port matches the expected $name default", async () => {
|
||||
await build({ ...baseFixture, ...extras, externalPort: 13306 });
|
||||
expect(lastSettings().EndpointSpec?.Ports?.[0]?.TargetPort).toBe(
|
||||
targetPort,
|
||||
);
|
||||
if (name === "redis") {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
169
apps/dokploy/__test__/db/external-port-schema.test.ts
Normal file
169
apps/dokploy/__test__/db/external-port-schema.test.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import {
|
||||
apiSaveExternalPortMariaDB,
|
||||
apiSaveExternalPortMongo,
|
||||
apiSaveExternalPortMySql,
|
||||
apiSaveExternalPortPostgres,
|
||||
apiSaveExternalPortRedis,
|
||||
apiUpdateMariaDB,
|
||||
apiUpdateMongo,
|
||||
apiUpdateMySql,
|
||||
apiUpdatePostgres,
|
||||
apiUpdateRedis,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { z } from "zod";
|
||||
|
||||
const getExternalPort = (data: unknown): unknown =>
|
||||
(data as { externalPort?: unknown } | null)?.externalPort;
|
||||
|
||||
type SchemaCase = {
|
||||
name: string;
|
||||
schema: z.ZodTypeAny;
|
||||
updateSchema: z.ZodTypeAny;
|
||||
idField: string;
|
||||
idValue: string;
|
||||
};
|
||||
|
||||
const cases: SchemaCase[] = [
|
||||
{
|
||||
name: "mariadb",
|
||||
schema: apiSaveExternalPortMariaDB,
|
||||
updateSchema: apiUpdateMariaDB,
|
||||
idField: "mariadbId",
|
||||
idValue: "mariadb-1",
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
schema: apiSaveExternalPortMySql,
|
||||
updateSchema: apiUpdateMySql,
|
||||
idField: "mysqlId",
|
||||
idValue: "mysql-1",
|
||||
},
|
||||
{
|
||||
name: "postgres",
|
||||
schema: apiSaveExternalPortPostgres,
|
||||
updateSchema: apiUpdatePostgres,
|
||||
idField: "postgresId",
|
||||
idValue: "postgres-1",
|
||||
},
|
||||
{
|
||||
name: "mongo",
|
||||
schema: apiSaveExternalPortMongo,
|
||||
updateSchema: apiUpdateMongo,
|
||||
idField: "mongoId",
|
||||
idValue: "mongo-1",
|
||||
},
|
||||
{
|
||||
name: "redis",
|
||||
schema: apiSaveExternalPortRedis,
|
||||
updateSchema: apiUpdateRedis,
|
||||
idField: "redisId",
|
||||
idValue: "redis-1",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(cases)(
|
||||
"saveExternalPort schema for $name accepts null to revoke exposure",
|
||||
({ schema, updateSchema, idField, idValue }) => {
|
||||
it("accepts a valid published port number", () => {
|
||||
const result = schema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: 3306,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
[idField]: idValue,
|
||||
externalPort: 3306,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts null and preserves it (clearing revokes external exposure)", () => {
|
||||
const result = schema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: null,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getExternalPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a string port (client must coerce before sending)", () => {
|
||||
const result = schema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: "3306",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a missing/undefined port (field must be sent explicitly as null)", () => {
|
||||
const result = schema.safeParse({ [idField]: idValue });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("apiUpdate accepts input without externalPort (partial) (G6)", () => {
|
||||
const result = updateSchema.safeParse({ [idField]: idValue });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getExternalPort(result.data)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("apiUpdate accepts externalPort: null to clear via the update flow (G6)", () => {
|
||||
const result = updateSchema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: null,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getExternalPort(result.data)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("apiUpdate accepts a valid externalPort number (G6 no-regression)", () => {
|
||||
const result = updateSchema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: 5432,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getExternalPort(result.data)).toBe(5432);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("cross-DB lockstep: all 5 saveExternalPort schemas behave identically (H)", () => {
|
||||
it("all accept { externalPort: null } and parse to null", () => {
|
||||
for (const { schema, idField, idValue } of cases) {
|
||||
const result = schema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: null,
|
||||
});
|
||||
expect(result.success, `${idField} null`).toBe(true);
|
||||
if (result.success) {
|
||||
expect(
|
||||
getExternalPort(result.data),
|
||||
`${idField} null value`,
|
||||
).toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("all accept { externalPort: 5432 } and parse to 5432", () => {
|
||||
for (const { schema, idField, idValue } of cases) {
|
||||
const result = schema.safeParse({
|
||||
[idField]: idValue,
|
||||
externalPort: 5432,
|
||||
});
|
||||
expect(result.success, `${idField} 5432`).toBe(true);
|
||||
if (result.success) {
|
||||
expect(getExternalPort(result.data), `${idField} 5432 value`).toBe(
|
||||
5432,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
129
apps/dokploy/__test__/db/save-external-port.handler.test.ts
Normal file
129
apps/dokploy/__test__/db/save-external-port.handler.test.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockFind = vi.hoisted(() => vi.fn());
|
||||
const mockUpdate = vi.hoisted(() => vi.fn());
|
||||
const mockDeploy = vi.hoisted(() => vi.fn());
|
||||
const mockCheckPort = vi.hoisted(() => vi.fn());
|
||||
const mockCheckPerm = vi.hoisted(() => vi.fn());
|
||||
const mockAudit = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@dokploy/server", () => ({
|
||||
findMariadbById: mockFind,
|
||||
updateMariadbById: mockUpdate,
|
||||
deployMariadb: mockDeploy,
|
||||
checkPortInUse: mockCheckPort,
|
||||
IS_CLOUD: false,
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/permission", () => ({
|
||||
checkServicePermissionAndAccess: mockCheckPerm,
|
||||
checkServiceAccess: vi.fn(),
|
||||
checkPermission: vi.fn(),
|
||||
findMemberByUserId: vi.fn(),
|
||||
addNewService: vi.fn(),
|
||||
hasPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/server/api/utils/audit", () => ({ audit: mockAudit }));
|
||||
|
||||
const { mariadbRouter } = await import("@/server/api/routers/mariadb");
|
||||
|
||||
const mariadbFixture = {
|
||||
mariadbId: "mariadb-1",
|
||||
appName: "test-mariadb",
|
||||
serverId: null,
|
||||
externalPort: 3306,
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
user: { id: "user-1", email: "u@test.com", role: "owner" },
|
||||
session: { activeOrganizationId: "org-1" },
|
||||
} as unknown as Parameters<typeof mariadbRouter.createCaller>[0];
|
||||
|
||||
const call = () => mariadbRouter.createCaller(ctx);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCheckPerm.mockResolvedValue(undefined);
|
||||
mockFind.mockResolvedValue({ ...mariadbFixture });
|
||||
mockUpdate.mockResolvedValue({ ...mariadbFixture });
|
||||
mockDeploy.mockResolvedValue(undefined);
|
||||
mockAudit.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("mariadb.saveExternalPort handler (G13-G17)", () => {
|
||||
it("G13+G14: externalPort null clears the port — update called with null, deploy called, checkPortInUse NOT called", async () => {
|
||||
await call().saveExternalPort({
|
||||
mariadbId: "mariadb-1",
|
||||
externalPort: null,
|
||||
});
|
||||
|
||||
expect(mockCheckPerm).toHaveBeenCalledWith(ctx, "mariadb-1", {
|
||||
service: ["create"],
|
||||
});
|
||||
expect(mockCheckPort).not.toHaveBeenCalled();
|
||||
expect(mockUpdate).toHaveBeenCalledWith("mariadb-1", {
|
||||
externalPort: null,
|
||||
});
|
||||
expect(mockDeploy).toHaveBeenCalledWith("mariadb-1");
|
||||
expect(mockAudit).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("G16: externalPort number (free port) runs checkPortInUse and deploys with the port", async () => {
|
||||
mockCheckPort.mockResolvedValue({ isInUse: false });
|
||||
|
||||
await call().saveExternalPort({
|
||||
mariadbId: "mariadb-1",
|
||||
externalPort: 3306,
|
||||
});
|
||||
|
||||
expect(mockCheckPort).toHaveBeenCalledWith(3306, undefined);
|
||||
expect(mockUpdate).toHaveBeenCalledWith("mariadb-1", {
|
||||
externalPort: 3306,
|
||||
});
|
||||
expect(mockDeploy).toHaveBeenCalledWith("mariadb-1");
|
||||
});
|
||||
|
||||
it("G15: externalPort number on a busy port throws CONFLICT and does NOT update/deploy", async () => {
|
||||
mockCheckPort.mockResolvedValue({
|
||||
isInUse: true,
|
||||
conflictingContainer: "some-container",
|
||||
});
|
||||
|
||||
await expect(
|
||||
call().saveExternalPort({ mariadbId: "mariadb-1", externalPort: 3306 }),
|
||||
).rejects.toMatchObject({
|
||||
code: "CONFLICT",
|
||||
message: "Port 3306 is already in use by some-container",
|
||||
});
|
||||
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
expect(mockDeploy).not.toHaveBeenCalled();
|
||||
expect(mockAudit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("G15b: checkPortInUse receives the serverId from the mariadb row when present", async () => {
|
||||
mockFind.mockResolvedValue({ ...mariadbFixture, serverId: "server-9" });
|
||||
mockCheckPort.mockResolvedValue({ isInUse: false });
|
||||
|
||||
await call().saveExternalPort({
|
||||
mariadbId: "mariadb-1",
|
||||
externalPort: 3306,
|
||||
});
|
||||
|
||||
expect(mockCheckPort).toHaveBeenCalledWith(3306, "server-9");
|
||||
});
|
||||
|
||||
it("G17: unauthorized caller is rejected before any service call", async () => {
|
||||
mockCheckPerm.mockRejectedValue(new TRPCError({ code: "UNAUTHORIZED" }));
|
||||
|
||||
await expect(
|
||||
call().saveExternalPort({ mariadbId: "mariadb-1", externalPort: null }),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
|
||||
expect(mockFind).not.toHaveBeenCalled();
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
expect(mockDeploy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -3,7 +3,8 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { externalPortFormSchema as DockerProviderSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -26,20 +27,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a !== null) {
|
||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -3,7 +3,8 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { externalPortFormSchema as DockerProviderSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -26,20 +27,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a !== null) {
|
||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -3,7 +3,8 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { externalPortFormSchema as DockerProviderSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -26,20 +27,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a !== null) {
|
||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -3,7 +3,8 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { externalPortFormSchema as DockerProviderSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -26,20 +27,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a !== null) {
|
||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -3,7 +3,8 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { externalPortFormSchema as DockerProviderSchema } from "@/components/dashboard/shared/external-port-schema";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -26,20 +27,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a !== null) {
|
||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const externalPortFormSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a === null || a === undefined || a === "") return null;
|
||||
const parsed = Number.parseInt(String(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
export type ExternalPortForm = z.infer<typeof externalPortFormSchema>;
|
||||
@ -145,7 +145,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalPort: z.number().nullable(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
|
||||
@ -135,7 +135,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalPort: z.number().nullable(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
replicaSets: z.boolean().default(false),
|
||||
|
||||
@ -142,7 +142,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalPort: z.number().nullable(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
|
||||
@ -136,7 +136,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalPort: z.number().nullable(),
|
||||
createdAt: z.string(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
|
||||
@ -126,7 +126,7 @@ const createSchema = createInsertSchema(redis, {
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalPort: z.number().nullable(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user