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
230 changes: 230 additions & 0 deletions cli/shared/deployment/deploy-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
createHttpDeployControlPlane,
type DeployControlPlane,
type DeployReleaseAssetManifestBody,
type DeployReleaseFile,
} from "./control-plane.ts";
import {
assertProjectOwnership,
Expand Down Expand Up @@ -2145,3 +2146,232 @@ describe("deployment routing convergence", () => {
});
});
});

describe("unroutable hosted environment names", () => {
/**
* Live infrastructure only routes `{slug}.{preview|staging|production}.veryfront.com`.
* Any other label either has no wildcard certificate at all (TLS handshake failure)
* or resolves to a proxy that answers
* `404 {"error":"No project configured for domain: ..."}` — both of which the
* readiness poller treats as transient and retries until the timeout expires.
*/
function hostedNotFound() {
return new Response(
JSON.stringify({ error: "No project configured for domain", status: 404 }),
{ status: 404, headers: { "content-type": "application/json" } },
);
}

/** Matches on the parsed host, so a control-plane URL can never be mistaken for one. */
function isHostedEnvironmentRequest(input: string | URL | Request): boolean {
const url = input instanceof Request ? input.url : String(input);
try {
return new URL(url).hostname.endsWith(".veryfront.com");
} catch {
return false;
}
}

it("rejects a user-created environment name before creating a release", async () => {
await withDeployEnv(async () => {
const { projectDir } = await createPushedProject();
const controlPlane = new InMemoryDeployControlPlane();
controlPlane.environmentDomains = [];
try {
const error = await expectDeployError(() =>
withFetchStub(
(input) => isHostedEnvironmentRequest(input) ? hostedNotFound() : new Response("ready"),
() =>
createDeployment(controlPlane).execute({
projectDir,
environment: "development",
mode: "apply",
source: { kind: "already-pushed" },
}),
)
);

const message = (error as Error).message;
assertStringIncludes(message, "development");
assertStringIncludes(message, "preview");
assertStringIncludes(message, "staging");
assertStringIncludes(message, "production");
assertEquals(
controlPlane.createdReleases,
[],
"an unroutable environment must be rejected before any release is created",
);
assertEquals(
controlPlane.createdDeployments,
[],
"an unroutable environment must be rejected before any deployment is created",
);
} finally {
await Deno.remove(projectDir, { recursive: true });
}
});
});

it("does not spend the readiness timeout on a guaranteed failure", async () => {
await withDeployEnv(async () => {
const { projectDir } = await createPushedProject();
const controlPlane = new InMemoryDeployControlPlane();
controlPlane.environmentDomains = [];
let probes = 0;
try {
await expectDeployError(() =>
withFetchStub(
(input) => {
if (isHostedEnvironmentRequest(input)) {
probes++;
return hostedNotFound();
}
return new Response("ready");
},
() =>
createDeployment(controlPlane).execute({
projectDir,
environment: "qa",
mode: "apply",
source: { kind: "already-pushed" },
}),
)
);

assertEquals(probes, 0, "no readiness probe may be sent to an unroutable host");
} finally {
await Deno.remove(projectDir, { recursive: true });
}
});
});

it("omits the canonical companion probe for a protected custom-domain environment", async () => {
const probed: string[] = [];

await withMockFetch(
(input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init);
probed.push(request.url);
return Promise.resolve(
new Response(null, {
status: 302,
headers: { location: "https://veryfront.com/sign-in" },
}),
);
},
() =>
waitForEnvironmentReady({
projectSlug: "my-project",
environmentName: "development",
url: "https://dev.example.com",
protected: true,
apiToken: "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJ1XzEifQ.test-signature",
}, { pollIntervalMs: 1, timeoutMs: 1_000 }),
);

assertEquals(
probed,
["https://dev.example.com/"],
"the custom domain answered; no unroutable canonical host may be probed",
);
});

/**
* An API-only, agent-only or otherwise page-less project. `readinessRoute` is
* null for it, so `buildEnvironmentReadinessProbes` yields nothing and the
* deploy never asks the platform for a hosted address — which is why the name
* check must not apply to it.
*/
const SERVER_ONLY_CONTENT = "export const handler = () => new Response('ok');\n";

async function createPushedPagelessProject(): Promise<{
projectDir: string;
files: DeployReleaseFile[];
}> {
const projectDir = await Deno.makeTempDir();
await Deno.mkdir(`${projectDir}/server`, { recursive: true });
await Deno.writeTextFile(`${projectDir}/veryfront.json`, projectConfigText());
await Deno.writeTextFile(`${projectDir}/server/handler.ts`, SERVER_ONLY_CONTENT);
const commitSha = await commitProject(projectDir);
const files: DeployReleaseFile[] = [
{ path: "server/handler.ts", content: SERVER_ONLY_CONTENT },
{ path: "veryfront.json", content: projectConfigText() },
];
await writePushReceipt(projectDir, {
controlPlane: CONTROL_PLANE,
projectId: PROJECT_ID,
projectSlug: PROJECT_SLUG,
branch: "main",
commitSha,
sourceDigest: await computeSourceDigest(files),
clean: true,
});
return { projectDir, files };
}

it("still deploys a page-less project to an environment with no hosted address", async () => {
await withDeployEnv(async () => {
const { projectDir, files } = await createPushedPagelessProject();
const controlPlane = new InMemoryDeployControlPlane();
controlPlane.environmentDomains = [];
controlPlane.releaseFiles = files;
controlPlane.manifestResponses = [readyManifest({})];
let probes = 0;
try {
const outcome = await withFetchStub(
(input) => {
if (isHostedEnvironmentRequest(input)) {
probes++;
return hostedNotFound();
}
return new Response("ready");
},
() =>
createDeployment(controlPlane).execute({
projectDir,
environment: "development",
mode: "apply",
source: { kind: "already-pushed" },
}),
);

assertEquals(
outcome.kind,
"deployed",
"a deploy that never probes a hosted address does not depend on the environment name",
);
assertEquals(probes, 0, "a page-less deploy sends no readiness probe");
} finally {
await Deno.remove(projectDir, { recursive: true });
}
});
});

it("still deploys an unroutable environment name that has a custom domain", async () => {
await withDeployEnv(async () => {
const { projectDir } = await createPushedProject();
const controlPlane = new InMemoryDeployControlPlane();
controlPlane.environmentDomains = ["https://dev.example.com"];
try {
const outcome = await withFetchStub(
() => new Response("ready"),
() =>
createDeployment(controlPlane).execute({
projectDir,
environment: "development",
mode: "apply",
source: { kind: "already-pushed" },
}),
);

assertEquals(
outcome.kind,
"deployed",
"a custom domain makes the environment name irrelevant to routing",
);
} finally {
await Deno.remove(projectDir, { recursive: true });
}
});
});
});
Loading