Skip to content
Closed
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
33 changes: 21 additions & 12 deletions src/commands/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ function registerCreateCommand(projects: Command): void {
interface FilledCreate {
name: string;
location: string;
platforms: Platform[];
platforms: Platform[] | undefined;
template: boolean;
observability: boolean;
}
Expand All @@ -292,6 +292,17 @@ function parsePlatforms(value: string): Platform[] {
return [...new Set(parsed)] as Platform[];
}

/** Empty platform selections stay omitted so the API can apply its default. */
function normalizeOptionalPlatforms(
value: string | undefined
): Platform[] | undefined {
if (value === undefined) {
return undefined;
}
const parsed = parsePlatforms(value);
return parsed.length > 0 ? parsed : undefined;
}

async function fillCreateOpts(opts: CreateOpts): Promise<FilledCreate> {
// Non-interactive path: name is required; defaults fill the rest.
if (!isInteractive()) {
Expand All @@ -303,7 +314,7 @@ async function fillCreateOpts(opts: CreateOpts): Promise<FilledCreate> {
return {
name: opts.name.trim(),
location: opts.location ?? "United States",
platforms: opts.platforms !== undefined ? parsePlatforms(opts.platforms) : [],
platforms: normalizeOptionalPlatforms(opts.platforms),
template: opts.template ?? false,
observability: opts.observability ?? false,
};
Expand Down Expand Up @@ -337,16 +348,14 @@ async function fillCreateOpts(opts: CreateOpts): Promise<FilledCreate> {
location = answer || "United States";
}

const platforms =
opts.platforms !== undefined
? parsePlatforms(opts.platforms)
: parsePlatforms(
await promptText(
`Platforms (comma-separated: ${PLATFORMS.join(", ")})`,
undefined,
true
)
);
const platforms = normalizeOptionalPlatforms(
opts.platforms ??
(await promptText(
`Platforms (comma-separated, blank = iMessage default: ${PLATFORMS.join(", ")})`,
undefined,
true
))
);
const template = opts.template ?? (await promptBool("Use as template?", false));
const observability =
opts.observability ?? (await promptBool("Enable observability?", false));
Expand Down
112 changes: 110 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,102 @@ afterAll(async () => {
await stopMockServer();
});

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

describe("photon projects create", () => {
test("omits platforms from the request body when --platforms is absent", async () => {
const { exitCode } = await runCommand(
["projects", "create", "--name", "DX 25", "--json"],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);

expect(exitCode).toBe(0);
expect(getMockProjectCreateRequests()).toEqual([
{
name: "DX 25",
location: "United States",
template: false,
observability: false,
},
]);
});

test("omits platforms from the request body when --platforms is empty", async () => {
const emptyValues = ["", " ", ",", ",,", " , "];

for (const value of emptyValues) {
const { exitCode } = await runCommand(
[
"projects",
"create",
"--name",
"DX 25",
"--platforms",
value,
"--json",
],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);
expect(exitCode).toBe(0);
}

expect(getMockProjectCreateRequests()).toEqual(
emptyValues.map(() => ({
name: "DX 25",
location: "United States",
template: false,
observability: false,
})),
);
});

test("preserves explicit platforms in the request body", async () => {
const { exitCode } = await runCommand(
[
"projects",
"create",
"--name",
"DX 25",
"--platforms",
" imessage , voice , imessage ,",
"--json",
],
{
env: {
CI: "1",
PHOTON_TOKEN: "test-token",
PHOTON_API_HOST: baseUrl,
},
},
);

expect(exitCode).toBe(0);
expect(getMockProjectCreateRequests()).toEqual([
{
name: "DX 25",
location: "United States",
platforms: ["imessage", "voice"],
template: false,
observability: false,
},
]);
});
});

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,7 +299,8 @@ const app = new Elysia()
if (found) return found;
return projectFixture;
})
.post("/api/projects", ({ headers }) => {
.post("/api/projects", ({ body, headers }) => {
state.projectCreateRequests.push({ ...(body as Record<string, unknown>) });
const denied = requireAuth(headers as Record<string, string | undefined>);
if (denied) return denied;
return { success: true, id: projectFixture.id };
Expand Down
Loading