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
11 changes: 11 additions & 0 deletions src/commands/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,17 @@ function registerCreateCommand(projects: Command): void {
`Created ${c.bold(filled.name)} ${c.dim(`(${result.id})`)} on ${c.bold(env.name)}`
)
);
if (filled.platforms.length === 0) {
console.log(c.dim(" No platforms enabled."));
console.log(
c.dim(" See accepted platform names with `photon projects create --help`.")
);
console.log(
c.dim(
` Enable one with \`photon spectrum platforms enable <platform-name> --project '${result.id}'\`.`
)
);
}
console.log(
c.dim(` To make this the active project: export PHOTON_PROJECT_ID='${result.id}'`)
);
Expand Down
106 changes: 104 additions & 2 deletions tests/contract/projects.contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { startMockServer, stopMockServer } from "../helpers/mock-server.ts";
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "bun:test";
import {
getMockProjectCreateRequests,
resetMockState,
startMockServer,
stopMockServer,
} from "../helpers/mock-server.ts";
import { runCommand } from "../helpers/cli-runner.ts";

let baseUrl: string;
Expand All @@ -12,6 +24,96 @@ afterAll(async () => {
await stopMockServer();
});

beforeEach(() => {
resetMockState();
});

describe("photon projects create", () => {
test("explains how to enable a platform when created without platforms", async () => {
const { stdout, exitCode } = await runCommand(
["projects", "create", "--name", "No Platform"],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);

expect(exitCode).toBe(0);
expect(getMockProjectCreateRequests()).toEqual([
{
name: "No Platform",
location: "United States",
platforms: [],
template: false,
observability: false,
},
]);
expect(stdout).toContain("No platforms enabled.");
expect(stdout).toContain("photon projects create --help");
expect(stdout).toContain(
"photon spectrum platforms enable <platform-name> --project '00000000-0000-4000-a000-000000000001'",
);
});

test("does not show the no-platform hint for an explicit platform", async () => {
const { stdout, exitCode } = await runCommand(
[
"projects",
"create",
"--name",
"iMessage Project",
"--platforms",
"imessage",
],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);

expect(exitCode).toBe(0);
expect(getMockProjectCreateRequests()).toEqual([
{
name: "iMessage Project",
location: "United States",
platforms: ["imessage"],
template: false,
observability: false,
},
]);
expect(stdout).not.toContain("No platforms enabled.");
expect(stdout).not.toContain("photon projects create --help");
expect(stdout).not.toContain("spectrum platforms enable");
});

test("keeps --json output unchanged when no platform is selected", async () => {
const { stdout, exitCode } = await runCommand(
["projects", "create", "--name", "JSON Project", "--json"],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);

expect(exitCode).toBe(0);
const parsed = JSON.parse(stdout);
expect(Object.keys(parsed)).toEqual(["id", "name", "env"]);
expect(parsed.id).toBe("00000000-0000-4000-a000-000000000001");
expect(parsed.name).toBe("JSON Project");
expect(typeof parsed.env).toBe("string");
expect(getMockProjectCreateRequests()[0]?.platforms).toEqual([]);
});
});

describe("photon projects list", () => {
test("lists project names from fixtures", async () => {
const { stdout, exitCode } = await runCommand(["projects", "list"], {
Expand Down
10 changes: 9 additions & 1 deletion tests/helpers/mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface MockState {
lineAvatarResponseFault: "missing-avatar-url" | "missing-upload-key" | null;
lineProfileRequests: MockLineProfileRequest[];
profileSyncRequests: MockProfileSyncRequest[];
projectCreateRequests: Record<string, unknown>[];
}

export interface MockLineProfileRequest {
Expand Down Expand Up @@ -70,6 +71,7 @@ const state: MockState = {
lineAvatarResponseFault: null,
lineProfileRequests: [],
profileSyncRequests: [],
projectCreateRequests: [],
};

export function setMockSubscription(sub: "free" | "active"): void {
Expand Down Expand Up @@ -102,6 +104,10 @@ export function getMockLineProfileRequests(): MockLineProfileRequest[] {
return state.lineProfileRequests.map((request) => ({ ...request }));
}

export function getMockProjectCreateRequests(): Record<string, unknown>[] {
return state.projectCreateRequests.map((request) => ({ ...request }));
}

export function resetMockState(): void {
state.subscription = subscriptionFree;
state.forceUnauthorized = false;
Expand All @@ -110,6 +116,7 @@ export function resetMockState(): void {
state.lineAvatarResponseFault = null;
state.lineProfileRequests = [];
state.profileSyncRequests = [];
state.projectCreateRequests = [];
}

function requireAuth(headers: Record<string, string | undefined>) {
Expand Down Expand Up @@ -292,9 +299,10 @@ const app = new Elysia()
if (found) return found;
return projectFixture;
})
.post("/api/projects", ({ headers }) => {
.post("/api/projects", ({ body, headers }) => {
const denied = requireAuth(headers as Record<string, string | undefined>);
if (denied) return denied;
state.projectCreateRequests.push({ ...(body as Record<string, unknown>) });
return { success: true, id: projectFixture.id };
})
.get("/api/projects/:id/subscription", ({ headers }) => {
Expand Down
Loading