Skip to content
Merged
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
40 changes: 40 additions & 0 deletions apps/dokploy/__test__/api/openapi-document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { dockerDiskUsageDetailLimitSchema } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { describe, expect, it } from "vitest";
import { appRouter } from "@/server/api/root";

const generate = () =>
generateOpenApiDocument(appRouter, {
title: "Dokploy API",
version: "test",
baseUrl: "http://localhost/api",
});

describe("OpenAPI document", () => {
// One procedure the generator can't describe throws for the whole router,
// and /swagger then renders an empty spec instead of the API.
it("generates for every procedure in the app router", () => {
const document = generate();

expect(document.openapi).toMatch(/^3\./);
const getDiskUsage = document.paths?.["/dockerDiskUsage.getDiskUsage"]
?.get as { parameters?: { name: string }[] } | undefined;
expect(getDiskUsage?.parameters?.map(({ name }) => name)).toContain(
"detailLimit",
);
});
});

describe("dockerDiskUsageDetailLimitSchema", () => {
it("accepts the allowed limits from tRPC and query strings", () => {
expect(dockerDiskUsageDetailLimitSchema.parse(5)).toBe(5);
expect(dockerDiskUsageDetailLimitSchema.parse("15")).toBe(15);
expect(dockerDiskUsageDetailLimitSchema.parse(null)).toBeNull();
expect(dockerDiskUsageDetailLimitSchema.parse(undefined)).toBeUndefined();
});

it("rejects other limits", () => {
expect(() => dockerDiskUsageDetailLimitSchema.parse(7)).toThrow();
expect(() => dockerDiskUsageDetailLimitSchema.parse("all")).toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const telegramBodies = () =>

describe("super password alerts over the Telegram Bot API", () => {
beforeEach(() => {
// "Time left" is floored to minutes, so a real clock ticking between
// building expiresAt and formatting it renders 23 h 59 min.
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-10-07T12:00:00Z"));
fake.reset();
mocks.fetch.mockReset();
mocks.fetch.mockResolvedValue(new Response("{}", { status: 200 }));
Expand Down Expand Up @@ -71,6 +75,7 @@ describe("super password alerts over the Telegram Bot API", () => {
});

afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ const mocks = vi.hoisted(() => ({

const redactWebServerSettings = <T>(settings: T) => settings;

vi.mock("@dokploy/server", () => ({
vi.mock("@dokploy/server", async () => ({
CLEANUP_CRON_JOB: "0 0 * * *",
DEFAULT_UPDATE_DATA: {},
dockerDiskUsageDetailLimitSchema: (await import("zod")).z.any(),
DOKPLOY_KEEP_IMAGES_MAX: 5,
DOKPLOY_KEEP_IMAGES_MIN: 3,
getDokployImageKeepCount: vi.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ const mocks = vi.hoisted(() => ({
writeTraefikSetup: vi.fn(),
}));

vi.mock("@dokploy/server", () => ({
vi.mock("@dokploy/server", async () => ({
CLEANUP_CRON_JOB: "0 0 * * *",
DEFAULT_UPDATE_DATA: {},
dockerDiskUsageDetailLimitSchema: (await import("zod")).z.any(),
DOKPLOY_KEEP_IMAGES_MAX: 5,
DOKPLOY_KEEP_IMAGES_MIN: 3,
getDokployImageKeepCount: vi.fn(),
Expand Down
6 changes: 2 additions & 4 deletions apps/dokploy/server/api/routers/docker-disk-usage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
cleanupBuilders,
dockerDiskUsageDetailLimitSchema,
findServerById,
getBuildCache,
getDockerDiskUsage,
Expand All @@ -14,10 +15,7 @@ export const dockerDiskUsageRouter = createTRPCRouter({
getDiskUsage: withPermission("docker", "read")
.input(
z.object({
detailLimit: z
.union([z.literal(5), z.literal(10), z.literal(15)])
.nullable()
.optional(),
detailLimit: dockerDiskUsageDetailLimitSchema,
serverId: z.string().optional(),
}),
)
Expand Down
10 changes: 10 additions & 0 deletions apps/dokploy/server/api/routers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const GITHUB_APP_SETUP_STATE_TTL_MS = 60 * 60 * 1000;

export const githubRouter = createTRPCRouter({
appSetupState: withPermission("gitProviders", "create")
// The generator can't express a discriminated-union query input, and this
// state only drives the in-panel GitHub App setup redirect.
.meta({
openapi: {
path: "/github-app-setup-state",
method: "GET",
override: true,
enabled: false,
},
})
.input(apiGithubAppSetupState)
.query(async ({ input, ctx }) => {
if (
Expand Down
6 changes: 2 additions & 4 deletions apps/dokploy/server/api/routers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DEFAULT_UPDATE_DATA,
DOKPLOY_KEEP_IMAGES_MAX,
DOKPLOY_KEEP_IMAGES_MIN,
dockerDiskUsageDetailLimitSchema,
filterProtectedTraefikEntries,
findServerById,
getAccessibleServerIds,
Expand Down Expand Up @@ -333,10 +334,7 @@ export const settingsRouter = createTRPCRouter({
.input(
z
.object({
detailLimit: z
.union([z.literal(5), z.literal(10), z.literal(15)])
.nullable()
.optional(),
detailLimit: dockerDiskUsageDetailLimitSchema,
})
.optional(),
)
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/utils/docker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Compose } from "@dokploy/server/services/compose";
import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import { quote } from "shell-quote";
import { z } from "zod";
import type { ApplicationNested } from "../builders";
import type { LibsqlNested } from "../databases/libsql";
import type { MariadbNested } from "../databases/mariadb";
Expand Down Expand Up @@ -311,6 +312,15 @@ export interface DockerDiskUsageVerboseDetails {

export type DockerDiskUsageDetailLimit = 5 | 10 | 15 | null;

// The OpenAPI generator rejects numeric literals in query inputs, which takes
// down the whole /swagger document; a coerced number piped into the literals
// passes its check and also accepts "?detailLimit=5" from REST callers.
export const dockerDiskUsageDetailLimitSchema = z.coerce
.number()
.pipe(z.union([z.literal(5), z.literal(10), z.literal(15)]))
.nullable()
.optional();

export const resolveDockerDiskUsageDetailLimit = (
detailLimit?: DockerDiskUsageDetailLimit,
): DockerDiskUsageDetailLimit => (detailLimit === undefined ? 10 : detailLimit);
Expand Down
Loading