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
2 changes: 1 addition & 1 deletion scripts/lint/check-skipped-tests-baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const SCAN_ROOTS = [

// Lower this when you re-enable or delete skipped tests. Raising it means new
// dead coverage is being added — prefer fixing or deleting the test instead.
export const SKIPPED_TEST_BASELINE = 20;
export const SKIPPED_TEST_BASELINE = 18;

// Method form: it.skip( / describe.ignore( / test.skip( / Deno.test.ignore(
const METHOD_FORM = /\b(?:it|describe|test|Deno\.test)\.(?:skip|ignore)\s*\(/g;
Expand Down
27 changes: 0 additions & 27 deletions tests/_helpers/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { join } from "#veryfront/compat/path";
import { isNotFoundError, makeTempDir, mkdir, remove } from "../../src/platform/compat/fs.ts";
import { startDevServer } from "../../src/server/dev-server.ts";
import { startProductionServer } from "../../src/server/production-server.ts";
import { resetApiHandler } from "../../src/server/handlers/request/api/index.ts";
import { testDelay } from "#veryfront/testing";
import { CLEANUP_CONFIG, SERVER_CONFIG, TEST_TIMEOUTS } from "./constants.ts";
Expand Down Expand Up @@ -198,29 +197,3 @@ export async function createTestProjectDir(): Promise<string> {

return dir;
}

/**
* Create a production server with proper lifecycle management
*/
export async function createTestProductionServer(options: {
projectDir: string;
port?: number;
hostname?: string;
projectId?: string;
}): Promise<TestServer> {
const port = options.port ?? (await getFreePort());
const hostname = options.hostname ?? "127.0.0.1";
const server = await startProductionServer({
projectDir: options.projectDir,
port,
bindAddress: hostname,
defaultProjectSlug: options.projectId,
defaultProjectId: options.projectId,
});

return {
...server,
port,
hostname,
};
}
139 changes: 93 additions & 46 deletions tests/integration/core/config-loader-edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
*/

import "../../_helpers/contract-init.ts";
import { assertEquals, assertExists, assertRejects } from "#veryfront/testing/assert";
import { assert, assertEquals, assertExists, assertRejects } from "#veryfront/testing/assert";
import { assertStringIncludes } from "#veryfront/testing/assert";
import { describe, it } from "#veryfront/testing/bdd";
import { clearConfigCache, getConfig } from "#veryfront/config";
import { VeryfrontError } from "#veryfront/errors";
import { createMockAdapter } from "#veryfront/platform/adapters/mock.ts";
import { createMockAdapter, type MockRuntimeAdapter } from "#veryfront/platform/adapters/mock.ts";
import { join } from "#veryfront/compat/path";
import { makeTempDir, remove, writeTextFile } from "#veryfront/testing/deno-compat";

type SetupResult = {
projectDir: string;
adapter: any;
adapter: MockRuntimeAdapter;
cleanup: () => Promise<void>;
};

Expand Down Expand Up @@ -44,7 +44,7 @@ async function setupConfigTest(

async function withConfigTest(
configs: { content: string; filename?: string }[] | string,
fn: (ctx: { projectDir: string; adapter: any }) => Promise<void>,
fn: (ctx: { projectDir: string; adapter: MockRuntimeAdapter }) => Promise<void>,
options?: { useAdapter?: boolean },
): Promise<void> {
const { projectDir, adapter, cleanup } = await setupConfigTest(configs, options);
Expand All @@ -57,37 +57,60 @@ async function withConfigTest(
}
}

/**
* Assert the full config-validation error contract, not just a message substring.
*
* A loose `assertRejects(..., "security.cors")` passes even if the loader
* degrades to a bare `Error` with no slug or machine-readable context, so it
* cannot catch a regression in the structured half of the contract. This pins
* all three parts callers actually depend on: the registry slug, the
* human-readable `Invalid veryfront.config at <field>:` prefix, and the
* `context.field` / `context.expected` pair.
*/
async function assertConfigValidationFailure(
operation: () => Promise<unknown>,
field: string,
expectedIncludes: readonly string[],
): Promise<void> {
const error = await assertRejects(operation);
assert(error instanceof VeryfrontError, "Expected config validation to use VeryfrontError");

assertEquals(error.slug, "config-validation-failed");
assertStringIncludes(error.message, `Invalid veryfront.config at ${field}:`);

assert(typeof error.context === "object" && error.context !== null);
const contextField = Reflect.get(error.context, "field");
const contextExpected = Reflect.get(error.context, "expected");
assertEquals(contextField, field);
assertEquals(typeof contextExpected, "string");
assert(typeof contextExpected === "string");
for (const expected of expectedIncludes) {
assertStringIncludes(contextExpected, expected);
}
}

describe("Config Loader - Edge Cases and Error Handling", () => {
describe("Invalid config structure", () => {
it("should reject non-object config exports", async () => {
await withConfigTest(`export default "not an object";`, async ({ projectDir, adapter }) => {
await assertRejects(
() => getConfig(projectDir, adapter),
Error,
"expected object, received string",
);
});
});

it("should reject null config export", async () => {
await withConfigTest("export default null;", async ({ projectDir, adapter }) => {
await assertRejects(
() => getConfig(projectDir, adapter),
Error,
"expected object, received null",
);
});
});

it("should reject undefined config export", async () => {
await withConfigTest("export default undefined;", async ({ projectDir, adapter }) => {
await assertRejects(
() => getConfig(projectDir, adapter),
Error,
"expected object, received undefined",
);
for (
const { name, source, received } of [
{ name: "string", source: `export default "not an object";`, received: "string" },
{ name: "empty string", source: `export default "";`, received: "string" },
{ name: "null", source: "export default null;", received: "null" },
{ name: "undefined", source: "export default undefined;", received: "undefined" },
{ name: "false", source: "export default false;", received: "boolean" },
{ name: "zero", source: "export default 0;", received: "number" },
] as const
) {
it(`should reject ${name} config export`, async () => {
await withConfigTest(source, async ({ projectDir, adapter }) => {
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"<root>",
["expected object", `received ${received}`],
);
});
});
});
}

it("should reject config with syntax errors", async () => {
await withConfigTest(
Expand Down Expand Up @@ -115,7 +138,7 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
});
});

describe("Invalid CORS configuration", () => {
describe("CORS configuration", () => {
it("should reject invalid cors.origin type", async () => {
await withConfigTest(
`
Expand All @@ -128,7 +151,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "security.cors");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"security.cors",
["Expected boolean or a CORS object"],
);
},
);
});
Expand Down Expand Up @@ -165,7 +192,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "security.cors");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"security.cors",
["Expected boolean or a CORS object"],
);
},
);
});
Expand All @@ -188,7 +219,7 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
);
});

it("should handle cors as array (invalid)", async () => {
it("should reject a top-level cors array", async () => {
await withConfigTest(
`
export default {
Expand All @@ -198,7 +229,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "Invalid input");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"security.cors",
["Expected boolean or a CORS object"],
);
},
);
});
Expand Down Expand Up @@ -232,7 +267,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "Unrecognized keys");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"<root>",
["unknownKey1", "unknownKey2", "validKey"],
);
},
);
});
Expand All @@ -246,7 +285,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "Unrecognized keys");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"<root>",
["unknownKey1", "unknownKey2"],
);
},
);
});
Expand Down Expand Up @@ -481,10 +524,10 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
export default config;
`,
async ({ projectDir, adapter }) => {
await assertRejects(
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
Error,
'Unrecognized key: "self"',
"<root>",
["self"],
);
},
);
Expand All @@ -502,7 +545,11 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
};
`,
async ({ projectDir, adapter }) => {
await assertRejects(() => getConfig(projectDir, adapter), Error, "Unrecognized keys");
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
"<root>",
["onBuild", "plugins"],
);
},
);
});
Expand Down Expand Up @@ -581,10 +628,10 @@ describe("Config Loader - Edge Cases and Error Handling", () => {
},
],
async ({ projectDir, adapter }) => {
await assertRejects(
await assertConfigValidationFailure(
() => getConfig(projectDir, adapter),
Error,
'Unrecognized key: "port"',
"<root>",
["port"],
);
},
);
Expand Down
14 changes: 10 additions & 4 deletions tests/integration/core/config-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { assertRejects } from "#veryfront/testing/assert";
import { assert, assertEquals, assertRejects } from "#veryfront/testing/assert";
import { join } from "#veryfront/compat/path";
import { describe, it } from "#veryfront/testing/bdd";
import { remove, writeTextFile } from "#veryfront/testing/deno-compat";
import { getAdapter } from "#veryfront/platform";
import { clearConfigCache, getConfig } from "#veryfront/config";
import { VeryfrontError } from "#veryfront/errors";
import { withTestContext } from "../../_helpers/context.ts";

async function setupConfig(
Expand Down Expand Up @@ -48,11 +49,16 @@ describe("Config validation", () => {
} as const`,
);

await assertRejects(
const error = await assertRejects(
() => getConfig(context.projectDir, adapter),
Error,
'Unrecognized key: "notARealKey"',
VeryfrontError,
);
assert(error instanceof VeryfrontError);
assertEquals(error.slug, "config-validation-failed");
assertEquals(error.context, {
field: "<root>",
expected: 'Unrecognized key: "notARealKey"',
});

clearConfigCache();
});
Expand Down
19 changes: 14 additions & 5 deletions tests/integration/core/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Disable LRU intervals during testing to prevent resource leaks
(globalThis as Record<string, unknown>).__vfDisableLruInterval = true;

import { assertEquals, assertRejects } from "#veryfront/testing/assert";
import { assert, assertEquals, assertRejects } from "#veryfront/testing/assert";
import { describe, it } from "#veryfront/testing/bdd";
import { getAdapter } from "#veryfront/platform";
import { clearConfigCache, getConfig, type VeryfrontConfig } from "#veryfront/config";
import { VeryfrontError } from "#veryfront/errors";
import { remove, writeTextFile } from "#veryfront/testing/deno-compat";
import { withTestContext } from "../../_helpers/context.ts";

Expand Down Expand Up @@ -117,7 +118,10 @@ describe("config/loader", () => {
);

clearConfigCache();
await expectConfigError(context.projectDir, ["Invalid veryfront.config at security.cors"]);
await expectConfigError(context.projectDir, [
"Invalid veryfront.config at security.cors:",
"Expected boolean or a CORS object",
]);
});
});

Expand All @@ -133,11 +137,16 @@ describe("config/loader", () => {
clearConfigCache();
const adapter = await getAdapter();

await assertRejects(
const error = await assertRejects(
() => getConfig(context.projectDir, adapter),
Error,
'Unrecognized keys: "unknownKey", "anotherUnknown"',
VeryfrontError,
);
assert(error instanceof VeryfrontError);
assertEquals(error.slug, "config-validation-failed");
assertEquals(error.context, {
field: "<root>",
expected: 'Unrecognized keys: "unknownKey", "anotherUnknown"',
});
});
});

Expand Down
Loading