Skip to content

Commit a9eebaf

Browse files
committed
test(cli-core): add unit tests for CliFs, CliDockerComposeYaml, CliService, and other core services
1 parent 2cc8b84 commit a9eebaf

File tree

15 files changed

+1015
-18
lines changed

15 files changed

+1015
-18
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {injectable} from "@tsed/di";
2+
3+
import type {CommandOptions} from "../interfaces/CommandOptions.js";
4+
import type {CommandProvider} from "../interfaces/CommandProvider.js";
5+
import {command} from "./command.js";
6+
7+
vi.mock("@tsed/di", () => ({
8+
injectable: vi.fn()
9+
}));
10+
11+
describe("command()", () => {
12+
const createBuilder = () =>
13+
({
14+
type: vi.fn().mockReturnThis(),
15+
set: vi.fn().mockReturnThis(),
16+
factory: vi.fn().mockReturnThis()
17+
}) as any;
18+
19+
beforeEach(() => {
20+
vi.clearAllMocks();
21+
});
22+
23+
it("should register a functional command via factory", () => {
24+
const builder = createBuilder();
25+
vi.mocked(injectable).mockReturnValue(builder);
26+
27+
const handler: CommandProvider["$exec"] = vi.fn();
28+
const prompt: CommandProvider["$prompt"] = vi.fn();
29+
const options: CommandOptions = {
30+
name: "test",
31+
description: "description",
32+
handler,
33+
prompt
34+
};
35+
36+
command(options);
37+
38+
expect(injectable).toHaveBeenCalledWith(Symbol.for("COMMAND_test"));
39+
expect(builder.type).toHaveBeenCalledWith("command");
40+
expect(builder.set).toHaveBeenCalledWith("command", options);
41+
expect(builder.factory).toHaveBeenCalledTimes(1);
42+
43+
const registeredFactory = builder.factory.mock.calls[0][0];
44+
const result = registeredFactory();
45+
46+
expect(result).toMatchObject({
47+
...options,
48+
$exec: handler,
49+
$prompt: prompt
50+
});
51+
expect(result.handler).toBe(handler);
52+
});
53+
54+
it("should register a class-based command with provided token", () => {
55+
const builder = createBuilder();
56+
vi.mocked(injectable).mockReturnValue(builder);
57+
58+
class TestCommand implements CommandProvider {
59+
$exec(): any {}
60+
}
61+
62+
const options: CommandOptions = {
63+
name: "test-class",
64+
description: "description",
65+
token: TestCommand
66+
};
67+
68+
command(options);
69+
70+
expect(injectable).toHaveBeenCalledWith(TestCommand);
71+
expect(builder.type).toHaveBeenCalledWith("command");
72+
expect(builder.set).toHaveBeenCalledWith("command", options);
73+
expect(builder.factory).not.toHaveBeenCalled();
74+
});
75+
});

packages/cli-core/src/fn/command.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {type FactoryTokenProvider, injectable, type TokenProvider} from "@tsed/d
44
import type {CommandOptions} from "../interfaces/CommandOptions.js";
55
import type {CommandProvider} from "../interfaces/index.js";
66

7-
export function command(options: CommandOptions) {
7+
export function command<Input>(options: CommandOptions<Input>) {
88
if (!options.token) {
9-
return injectable<FactoryTokenProvider<CommandProvider>>(Symbol.for(`COMMAND_${options.name}`) as any)
9+
return injectable<FactoryTokenProvider<CommandProvider<Input>>>(Symbol.for(`COMMAND_${options.name}`) as any)
1010
.type("command")
1111
.set("command", options)
1212
.factory(() => {
@@ -18,5 +18,5 @@ export function command(options: CommandOptions) {
1818
});
1919
}
2020

21-
return injectable<Type<CommandProvider>>(options.token).type("command").set("command", options);
21+
return injectable<Type<CommandProvider<Input>>>(options.token).type("command").set("command", options);
2222
}

packages/cli-core/src/interfaces/CommandMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {BaseCommandOptions, CommandArg, CommandOpts} from "./CommandOptions.js";
22

3-
export interface CommandMetadata extends BaseCommandOptions {
3+
export interface CommandMetadata extends BaseCommandOptions<any> {
44
/**
55
* CommandProvider arguments
66
*/

packages/cli-core/src/interfaces/CommandOptions.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {Type} from "@tsed/core";
22
import type {TokenProvider} from "@tsed/di";
3+
import type {JsonSchema} from "@tsed/schema";
4+
import type {Answers} from "inquirer";
35

4-
import type {CommandProvider} from "./CommandProvider.js";
6+
import type {CommandProvider, QuestionOptions} from "./CommandProvider.js";
7+
import type {Tasks} from "./Tasks.js";
58

69
export interface CommandArg {
710
/**
@@ -54,7 +57,7 @@ export interface CommandOpts {
5457
customParser?: (value: any) => any;
5558
}
5659

57-
export interface BaseCommandOptions {
60+
export interface BaseCommandOptions<Input> {
5861
/**
5962
* name commands
6063
*/
@@ -71,6 +74,9 @@ export interface BaseCommandOptions {
7174
args?: {
7275
[key: string]: CommandArg;
7376
};
77+
78+
inputSchema?: JsonSchema<Input> | (() => JsonSchema<Input>);
79+
7480
/**
7581
* CommandProvider options
7682
*/
@@ -85,17 +91,17 @@ export interface BaseCommandOptions {
8591
disableReadUpPkg?: boolean;
8692
}
8793

88-
interface FunctionalCommandOptions extends BaseCommandOptions {
89-
prompt?: CommandProvider["$prompt"];
90-
handler: CommandProvider["$exec"];
94+
interface FunctionalCommandOptions<Input> extends BaseCommandOptions<Input> {
95+
prompt?<T extends Answers = Answers>(initialOptions: Partial<Input>): QuestionOptions<T> | Promise<QuestionOptions<T>>;
96+
handler: (data: Input) => Tasks | Promise<Tasks> | any | Promise<any>;
9197

9298
[key: string]: any;
9399
}
94100

95-
interface ClassCommandOptions extends BaseCommandOptions {
96-
token: TokenProvider<CommandProvider>;
101+
interface ClassCommandOptions<Input> extends BaseCommandOptions<Input> {
102+
token: TokenProvider<CommandProvider<Input>>;
97103

98104
[key: string]: any;
99105
}
100106

101-
export type CommandOptions = ClassCommandOptions | FunctionalCommandOptions;
107+
export type CommandOptions<Input> = ClassCommandOptions<Input> | FunctionalCommandOptions<Input>;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// @ts-ignore
2+
import {CliPlatformTest} from "@tsed/cli-testing";
3+
4+
import {CliDockerComposeYaml} from "./CliDockerComposeYaml.js";
5+
import {CliFs} from "./CliFs.js";
6+
import {CliYaml} from "./CliYaml.js";
7+
import {ProjectPackageJson} from "./ProjectPackageJson.js";
8+
9+
describe("CliDockerComposeYaml", () => {
10+
beforeEach(() => CliPlatformTest.create());
11+
afterEach(() => CliPlatformTest.reset());
12+
13+
async function createService(deps: Partial<Record<string, any>> = {}) {
14+
const cliYaml = deps.cliYaml || {
15+
read: vi.fn().mockResolvedValue({}),
16+
write: vi.fn().mockResolvedValue(undefined)
17+
};
18+
const fs = deps.fs || {
19+
exists: vi.fn().mockReturnValue(true),
20+
findUpFile: vi.fn().mockReturnValue(undefined)
21+
};
22+
const projectPkg = deps.projectPkg || ({dir: "/project"} as ProjectPackageJson);
23+
24+
const service = await CliPlatformTest.invoke<CliDockerComposeYaml>(CliDockerComposeYaml, [
25+
{
26+
token: CliYaml,
27+
use: cliYaml
28+
},
29+
{
30+
token: CliFs,
31+
use: fs
32+
},
33+
{
34+
token: ProjectPackageJson,
35+
use: projectPkg
36+
}
37+
]);
38+
39+
return {service, cliYaml, fs, projectPkg};
40+
}
41+
42+
describe("read()", () => {
43+
it("should read docker-compose.yml from project root when it exists", async () => {
44+
const {service, cliYaml, fs} = await createService();
45+
46+
const result = await service.read();
47+
48+
expect(fs.exists).toHaveBeenCalledWith("docker-compose.yml");
49+
expect(cliYaml.read).toHaveBeenCalledWith("docker-compose.yml");
50+
expect(result).toEqual({});
51+
});
52+
53+
it("should return an empty object when no docker-compose file is found", async () => {
54+
const fs = {
55+
exists: vi.fn().mockReturnValue(false),
56+
findUpFile: vi.fn().mockReturnValue(undefined)
57+
};
58+
const {service, cliYaml} = await createService({fs});
59+
60+
const result = await service.read();
61+
62+
expect(fs.findUpFile).toHaveBeenCalled();
63+
expect(cliYaml.read).not.toHaveBeenCalled();
64+
expect(result).toEqual({});
65+
});
66+
});
67+
68+
describe("write()", () => {
69+
it("should write to the discovered docker-compose file", async () => {
70+
const cliYaml = {
71+
read: vi.fn(),
72+
write: vi.fn().mockResolvedValue(undefined)
73+
};
74+
const fs = {
75+
exists: vi.fn(),
76+
findUpFile: vi.fn().mockReturnValue("/repo/docker-compose.yml")
77+
};
78+
const {service} = await createService({cliYaml, fs});
79+
const payload = {services: {}};
80+
81+
await service.write(payload);
82+
83+
expect(cliYaml.write).toHaveBeenCalledWith("/repo/docker-compose.yml", payload);
84+
});
85+
86+
it("should fallback to project dir when docker-compose file does not exist yet", async () => {
87+
const cliYaml = {
88+
read: vi.fn(),
89+
write: vi.fn().mockResolvedValue(undefined)
90+
};
91+
const fs = {
92+
exists: vi.fn(),
93+
findUpFile: vi.fn().mockReturnValue(undefined)
94+
};
95+
const projectPkg = {dir: "/repo"} as ProjectPackageJson;
96+
const {service} = await createService({cliYaml, fs, projectPkg});
97+
const payload = {services: {}};
98+
99+
await service.write(payload);
100+
101+
expect(cliYaml.write).toHaveBeenCalledWith("/repo/docker-compose.yml", payload);
102+
});
103+
});
104+
105+
describe("addDatabaseService()", () => {
106+
it("should append a postgres service and persist the file", async () => {
107+
const cliYaml = {
108+
read: vi.fn().mockResolvedValue({services: {}}),
109+
write: vi.fn().mockResolvedValue(undefined)
110+
};
111+
const fs = {
112+
exists: vi.fn().mockReturnValue(true),
113+
findUpFile: vi.fn()
114+
};
115+
const {service} = await createService({cliYaml, fs});
116+
117+
await service.addDatabaseService("OrdersDb", "postgres");
118+
119+
expect(cliYaml.write).toHaveBeenCalledTimes(1);
120+
const [, dockerCompose] = cliYaml.write.mock.calls[0];
121+
expect(dockerCompose).toEqual({
122+
services: {
123+
orders_db: {
124+
image: "postgres:9.6.1",
125+
ports: ["5432:5432"],
126+
volumes: ["./pgdata:/var/lib/postgresql/data"],
127+
environment: {
128+
POSTGRES_USER: "test",
129+
POSTGRES_PASSWORD: "test",
130+
POSTGRES_DB: "test"
131+
}
132+
}
133+
}
134+
});
135+
});
136+
137+
it("should append a mongodb service definition when requested", async () => {
138+
const cliYaml = {
139+
read: vi.fn().mockResolvedValue({services: {}}),
140+
write: vi.fn().mockResolvedValue(undefined)
141+
};
142+
const fs = {
143+
exists: vi.fn().mockReturnValue(true),
144+
findUpFile: vi.fn()
145+
};
146+
const {service} = await createService({cliYaml, fs});
147+
148+
await service.addDatabaseService("Analytics", "mongodb");
149+
150+
expect(cliYaml.write).toHaveBeenCalledTimes(1);
151+
const [, dockerCompose] = cliYaml.write.mock.calls[0];
152+
expect(dockerCompose).toEqual({
153+
services: {
154+
analytics: {
155+
image: "mongo:5.0.8",
156+
ports: ["27017:27017"]
157+
}
158+
}
159+
});
160+
});
161+
});
162+
});

0 commit comments

Comments
 (0)