servcraft 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ export function integrationTestTemplate(
2
+ name: string,
3
+ pascalName: string,
4
+ camelName: string
5
+ ): string {
6
+ return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
7
+ import { build } from '../../app.js';
8
+ import { FastifyInstance } from 'fastify';
9
+ import { prisma } from '../../lib/prisma.js';
10
+
11
+ describe('${pascalName} Integration Tests', () => {
12
+ let app: FastifyInstance;
13
+
14
+ beforeAll(async () => {
15
+ app = await build();
16
+ await app.ready();
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await app.close();
21
+ await prisma.$disconnect();
22
+ });
23
+
24
+ beforeEach(async () => {
25
+ // Clean up test data
26
+ // await prisma.${camelName}.deleteMany();
27
+ });
28
+
29
+ describe('Full CRUD workflow', () => {
30
+ it('should create, read, update, and delete a ${camelName}', async () => {
31
+ // Create
32
+ const createResponse = await app.inject({
33
+ method: 'POST',
34
+ url: '/${name}',
35
+ payload: {
36
+ // TODO: Add required fields
37
+ },
38
+ });
39
+
40
+ expect(createResponse.statusCode).toBe(201);
41
+ const created = createResponse.json().data;
42
+ expect(created.id).toBeDefined();
43
+
44
+ // Read
45
+ const readResponse = await app.inject({
46
+ method: 'GET',
47
+ url: \`/${name}/\${created.id}\`,
48
+ });
49
+
50
+ expect(readResponse.statusCode).toBe(200);
51
+ const read = readResponse.json().data;
52
+ expect(read.id).toBe(created.id);
53
+
54
+ // Update
55
+ const updateResponse = await app.inject({
56
+ method: 'PUT',
57
+ url: \`/${name}/\${created.id}\`,
58
+ payload: {
59
+ // TODO: Add fields to update
60
+ },
61
+ });
62
+
63
+ expect(updateResponse.statusCode).toBe(200);
64
+ const updated = updateResponse.json().data;
65
+ expect(updated.id).toBe(created.id);
66
+
67
+ // Delete
68
+ const deleteResponse = await app.inject({
69
+ method: 'DELETE',
70
+ url: \`/${name}/\${created.id}\`,
71
+ });
72
+
73
+ expect(deleteResponse.statusCode).toBe(204);
74
+
75
+ // Verify deletion
76
+ const verifyResponse = await app.inject({
77
+ method: 'GET',
78
+ url: \`/${name}/\${created.id}\`,
79
+ });
80
+
81
+ expect(verifyResponse.statusCode).toBe(404);
82
+ });
83
+ });
84
+
85
+ describe('List and pagination', () => {
86
+ it('should list ${name} with pagination', async () => {
87
+ // Create multiple ${name}
88
+ const count = 5;
89
+ for (let i = 0; i < count; i++) {
90
+ await app.inject({
91
+ method: 'POST',
92
+ url: '/${name}',
93
+ payload: {
94
+ // TODO: Add required fields
95
+ },
96
+ });
97
+ }
98
+
99
+ // Test pagination
100
+ const response = await app.inject({
101
+ method: 'GET',
102
+ url: '/${name}?page=1&limit=3',
103
+ });
104
+
105
+ expect(response.statusCode).toBe(200);
106
+ const result = response.json();
107
+ expect(result.data).toBeDefined();
108
+ expect(result.data.length).toBeLessThanOrEqual(3);
109
+ expect(result.total).toBeGreaterThanOrEqual(count);
110
+ });
111
+ });
112
+
113
+ describe('Validation', () => {
114
+ it('should validate required fields on create', async () => {
115
+ const response = await app.inject({
116
+ method: 'POST',
117
+ url: '/${name}',
118
+ payload: {},
119
+ });
120
+
121
+ expect(response.statusCode).toBe(400);
122
+ expect(response.json()).toHaveProperty('error');
123
+ });
124
+
125
+ it('should validate data types', async () => {
126
+ const response = await app.inject({
127
+ method: 'POST',
128
+ url: '/${name}',
129
+ payload: {
130
+ // TODO: Add invalid field types
131
+ },
132
+ });
133
+
134
+ expect(response.statusCode).toBe(400);
135
+ });
136
+ });
137
+ });
138
+ `;
139
+ }
@@ -0,0 +1,100 @@
1
+ export function serviceTestTemplate(name: string, pascalName: string, camelName: string): string {
2
+ return `import { describe, it, expect, beforeEach } from 'vitest';
3
+ import { ${pascalName}Service } from '../${name}.service.js';
4
+
5
+ describe('${pascalName}Service', () => {
6
+ let service: ${pascalName}Service;
7
+
8
+ beforeEach(() => {
9
+ service = new ${pascalName}Service();
10
+ });
11
+
12
+ describe('getAll', () => {
13
+ it('should return all ${name}', async () => {
14
+ const result = await service.getAll();
15
+
16
+ expect(result).toBeDefined();
17
+ expect(Array.isArray(result)).toBe(true);
18
+ });
19
+
20
+ it('should apply pagination', async () => {
21
+ const result = await service.getAll({ page: 1, limit: 10 });
22
+
23
+ expect(result).toBeDefined();
24
+ expect(result.length).toBeLessThanOrEqual(10);
25
+ });
26
+ });
27
+
28
+ describe('getById', () => {
29
+ it('should return a ${camelName} by id', async () => {
30
+ // TODO: Create test ${camelName} first
31
+ const id = '1';
32
+ const result = await service.getById(id);
33
+
34
+ expect(result).toBeDefined();
35
+ expect(result.id).toBe(id);
36
+ });
37
+
38
+ it('should return null for non-existent id', async () => {
39
+ const result = await service.getById('999999');
40
+
41
+ expect(result).toBeNull();
42
+ });
43
+ });
44
+
45
+ describe('create', () => {
46
+ it('should create a new ${camelName}', async () => {
47
+ const data = {
48
+ // TODO: Add required fields
49
+ };
50
+
51
+ const result = await service.create(data);
52
+
53
+ expect(result).toBeDefined();
54
+ expect(result.id).toBeDefined();
55
+ });
56
+
57
+ it('should throw error for invalid data', async () => {
58
+ await expect(service.create({} as any)).rejects.toThrow();
59
+ });
60
+ });
61
+
62
+ describe('update', () => {
63
+ it('should update a ${camelName}', async () => {
64
+ // TODO: Create test ${camelName} first
65
+ const id = '1';
66
+ const updates = {
67
+ // TODO: Add fields to update
68
+ };
69
+
70
+ const result = await service.update(id, updates);
71
+
72
+ expect(result).toBeDefined();
73
+ expect(result.id).toBe(id);
74
+ });
75
+
76
+ it('should return null for non-existent id', async () => {
77
+ const result = await service.update('999999', {});
78
+
79
+ expect(result).toBeNull();
80
+ });
81
+ });
82
+
83
+ describe('delete', () => {
84
+ it('should delete a ${camelName}', async () => {
85
+ // TODO: Create test ${camelName} first
86
+ const id = '1';
87
+ const result = await service.delete(id);
88
+
89
+ expect(result).toBe(true);
90
+ });
91
+
92
+ it('should return false for non-existent id', async () => {
93
+ const result = await service.delete('999999');
94
+
95
+ expect(result).toBe(false);
96
+ });
97
+ });
98
+ });
99
+ `;
100
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft add', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should show error when not in a servcraft project', () => {
8
+ try {
9
+ execSync(`node ${cliPath} add auth`, { encoding: 'utf-8', stdio: 'pipe' });
10
+ } catch (err) {
11
+ const error = err as { stdout?: string; stderr?: string };
12
+ const output = error.stdout || error.stderr || '';
13
+ expect(output).toContain('Failed to validate project');
14
+ }
15
+ });
16
+
17
+ it('should show error for invalid module name', () => {
18
+ try {
19
+ execSync(`node ${cliPath} add invalid-module-xyz`, { encoding: 'utf-8', stdio: 'pipe' });
20
+ } catch (err) {
21
+ const error = err as { stdout?: string; stderr?: string };
22
+ const output = error.stdout || error.stderr || '';
23
+ expect(output).toContain('Module "invalid-module-xyz" not found');
24
+ }
25
+ });
26
+
27
+ it('should show help with --help flag', () => {
28
+ const output = execSync(`node ${cliPath} add --help`, { encoding: 'utf-8' });
29
+ expect(output).toContain('Add a pre-built module');
30
+ expect(output).toContain('--dry-run');
31
+ });
32
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft completion', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should generate bash completion script', () => {
8
+ const output = execSync(`node ${cliPath} completion bash`, { encoding: 'utf-8' });
9
+ expect(output).toContain('_servcraft_completions');
10
+ expect(output).toContain('complete -F _servcraft_completions servcraft');
11
+ });
12
+
13
+ it('should generate zsh completion script', () => {
14
+ const output = execSync(`node ${cliPath} completion zsh`, { encoding: 'utf-8' });
15
+ expect(output).toContain('#compdef servcraft');
16
+ expect(output).toContain('_servcraft');
17
+ });
18
+
19
+ it('should show error for unsupported shell', () => {
20
+ try {
21
+ execSync(`node ${cliPath} completion fish`, { encoding: 'utf-8', stdio: 'pipe' });
22
+ } catch (err) {
23
+ const error = err as { stdout?: string; stderr?: string };
24
+ const output = error.stdout || error.stderr || '';
25
+ expect(output).toContain('Unsupported shell: fish');
26
+ expect(output).toContain('Supported shells: bash, zsh');
27
+ }
28
+ });
29
+
30
+ it('should show help with --help flag', () => {
31
+ const output = execSync(`node ${cliPath} completion --help`, { encoding: 'utf-8' });
32
+ expect(output).toContain('Generate shell completion scripts');
33
+ expect(output).toContain('Shell type (bash or zsh)');
34
+ });
35
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft doctor', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should run diagnostics', () => {
8
+ const output = execSync(`node ${cliPath} doctor`, { encoding: 'utf-8' });
9
+ expect(output).toContain('ServCraft Doctor');
10
+ expect(output).toContain('Node.js');
11
+ expect(output).toContain('package.json');
12
+ });
13
+
14
+ it('should check Node.js version', () => {
15
+ const output = execSync(`node ${cliPath} doctor`, { encoding: 'utf-8' });
16
+ expect(output).toMatch(/Node\.js.*v\d+\.\d+\.\d+/);
17
+ });
18
+
19
+ it('should display summary', () => {
20
+ const output = execSync(`node ${cliPath} doctor`, { encoding: 'utf-8' });
21
+ expect(output).toMatch(/\d+ passed/);
22
+ });
23
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect, afterAll } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ describe('--dry-run option', () => {
7
+ const cliPath = './dist/cli/index.js';
8
+ const testDir = 'dry-test-' + Date.now();
9
+
10
+ afterAll(() => {
11
+ if (fs.existsSync(testDir)) {
12
+ fs.rmSync(testDir, { recursive: true, force: true });
13
+ }
14
+ });
15
+
16
+ it('should preview init without creating files', () => {
17
+ const output = execSync(`node ${cliPath} init ${testDir} --yes --dry-run`, {
18
+ encoding: 'utf-8',
19
+ });
20
+
21
+ expect(output).toContain('DRY RUN MODE');
22
+ expect(output).toContain('Dry Run - Preview of changes');
23
+ expect(output).toContain('package.json');
24
+ expect(output).toContain('Total operations');
25
+
26
+ // Check that package.json was not actually created
27
+ const pkgPath = path.join(testDir, 'package.json');
28
+ expect(fs.existsSync(pkgPath)).toBe(false);
29
+ });
30
+
31
+ it('should show file count in dry-run', () => {
32
+ const testName = 'test-app-' + Date.now();
33
+ const output = execSync(`node ${cliPath} init ${testName} --yes --dry-run`, {
34
+ encoding: 'utf-8',
35
+ });
36
+ expect(output).toMatch(/Total operations: \d+/);
37
+ expect(output).toMatch(/\d+ create/);
38
+ });
39
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('error handling', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should show helpful error for invalid module', () => {
8
+ try {
9
+ execSync(`node ${cliPath} add invalid-module-xyz`, { encoding: 'utf-8', stdio: 'pipe' });
10
+ } catch (err) {
11
+ const error = err as { stdout?: string; stderr?: string };
12
+ const output = error.stdout || error.stderr || '';
13
+ expect(output).toContain('Module "invalid-module-xyz" not found');
14
+ expect(output).toContain('Suggestions');
15
+ expect(output).toContain('servcraft list');
16
+ }
17
+ });
18
+
19
+ it('should show error with documentation link', () => {
20
+ try {
21
+ execSync(`node ${cliPath} add nonexistent`, { encoding: 'utf-8', stdio: 'pipe' });
22
+ } catch (err) {
23
+ const error = err as { stdout?: string; stderr?: string };
24
+ const output = error.stdout || error.stderr || '';
25
+ expect(output).toContain('Documentation:');
26
+ expect(output).toContain('github.com');
27
+ }
28
+ });
29
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft generate', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should generate controller with dry-run', () => {
8
+ const uniqueName = 'testuser' + Date.now();
9
+ const output = execSync(`node ${cliPath} generate controller ${uniqueName} --dry-run`, {
10
+ encoding: 'utf-8',
11
+ });
12
+
13
+ expect(output).toContain('DRY RUN MODE');
14
+ expect(output).toContain(`${uniqueName}.controller.ts`);
15
+ expect(output).toContain('Total operations');
16
+ });
17
+
18
+ it('should generate service with dry-run', () => {
19
+ const uniqueName = 'testproduct' + Date.now();
20
+ const output = execSync(`node ${cliPath} g s ${uniqueName} --dry-run`, {
21
+ encoding: 'utf-8',
22
+ });
23
+
24
+ expect(output).toContain('DRY RUN MODE');
25
+ expect(output).toContain(`${uniqueName}.service.ts`);
26
+ });
27
+
28
+ it('should generate module with dry-run', () => {
29
+ const uniqueName = 'testorder' + Date.now();
30
+ const output = execSync(`node ${cliPath} g m ${uniqueName} --dry-run`, {
31
+ encoding: 'utf-8',
32
+ });
33
+
34
+ expect(output).toContain('DRY RUN MODE');
35
+ expect(output).toContain(`${uniqueName}.controller.ts`);
36
+ expect(output).toContain(`${uniqueName}.service.ts`);
37
+ expect(output).toContain(`${uniqueName}.routes.ts`);
38
+ });
39
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+
5
+ describe('servcraft init', () => {
6
+ const cliPath = './dist/cli/index.js';
7
+ const testProjects: string[] = [];
8
+
9
+ afterEach(() => {
10
+ testProjects.forEach((dir) => {
11
+ if (fs.existsSync(dir)) {
12
+ fs.rmSync(dir, { recursive: true, force: true });
13
+ }
14
+ });
15
+ testProjects.length = 0;
16
+ });
17
+
18
+ it('should create project structure with --dry-run', () => {
19
+ const projectName = 'test-init-' + Date.now();
20
+
21
+ const output = execSync(`node ${cliPath} init ${projectName} --yes --dry-run`, {
22
+ encoding: 'utf-8',
23
+ });
24
+
25
+ expect(output).toContain('Servcraft Project Generator');
26
+ expect(output).toContain('package.json');
27
+ expect(output).toContain('src/');
28
+ expect(output).toContain('DRY RUN MODE');
29
+ });
30
+
31
+ it('should support --js flag with dry-run', () => {
32
+ const projectName = 'test-js-' + Date.now();
33
+
34
+ const output = execSync(`node ${cliPath} init ${projectName} --yes --js --dry-run`, {
35
+ encoding: 'utf-8',
36
+ });
37
+
38
+ expect(output).toContain('package.json');
39
+ expect(output).toContain('DRY RUN MODE');
40
+ });
41
+
42
+ it('should support --cjs flag with dry-run', () => {
43
+ const projectName = 'test-cjs-' + Date.now();
44
+
45
+ const output = execSync(`node ${cliPath} init ${projectName} --yes --cjs --dry-run`, {
46
+ encoding: 'utf-8',
47
+ });
48
+
49
+ expect(output).toContain('package.json');
50
+ expect(output).toContain('DRY RUN MODE');
51
+ });
52
+
53
+ it('should support --esm flag with dry-run', () => {
54
+ const projectName = 'test-esm-' + Date.now();
55
+
56
+ const output = execSync(`node ${cliPath} init ${projectName} --yes --esm --dry-run`, {
57
+ encoding: 'utf-8',
58
+ });
59
+
60
+ expect(output).toContain('package.json');
61
+ expect(output).toContain('module');
62
+ });
63
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft list', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should display available modules', () => {
8
+ const output = execSync(`node ${cliPath} list`, { encoding: 'utf-8' });
9
+ expect(output).toContain('Available Modules');
10
+ expect(output).toContain('Core');
11
+ expect(output).toContain('Security');
12
+ });
13
+
14
+ it('should support --json output', () => {
15
+ const output = execSync(`node ${cliPath} list --json`, { encoding: 'utf-8' });
16
+ const data = JSON.parse(output);
17
+ expect(data).toHaveProperty('available');
18
+ expect(Array.isArray(data.available)).toBe(true);
19
+ });
20
+
21
+ it('should work with alias ls', () => {
22
+ const output = execSync(`node ${cliPath} ls`, { encoding: 'utf-8' });
23
+ expect(output).toContain('Available Modules');
24
+ });
25
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft remove', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should show error when not in a servcraft project', () => {
8
+ try {
9
+ execSync(`node ${cliPath} remove auth`, { encoding: 'utf-8', stdio: 'pipe' });
10
+ } catch (err) {
11
+ const error = err as { stdout?: string; stderr?: string };
12
+ const output = error.stdout || error.stderr || '';
13
+ expect(output).toContain('Failed to validate project');
14
+ }
15
+ });
16
+
17
+ it('should show help with --help flag', () => {
18
+ const output = execSync(`node ${cliPath} remove --help`, { encoding: 'utf-8' });
19
+ expect(output).toContain('Remove an installed module');
20
+ expect(output).toContain('--yes');
21
+ expect(output).toContain('--keep-env');
22
+ });
23
+
24
+ it('should work with alias rm', () => {
25
+ const output = execSync(`node ${cliPath} rm --help`, { encoding: 'utf-8' });
26
+ expect(output).toContain('Remove an installed module');
27
+ });
28
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+
4
+ describe('servcraft update', () => {
5
+ const cliPath = './dist/cli/index.js';
6
+
7
+ it('should show error when not in a servcraft project', () => {
8
+ try {
9
+ execSync(`node ${cliPath} update`, { encoding: 'utf-8', stdio: 'pipe' });
10
+ } catch (err) {
11
+ const error = err as { stdout?: string; stderr?: string };
12
+ const output = error.stdout || error.stderr || '';
13
+ expect(output).toContain('Failed to validate project');
14
+ }
15
+ });
16
+
17
+ it('should show help with --help flag', () => {
18
+ const output = execSync(`node ${cliPath} update --help`, { encoding: 'utf-8' });
19
+ expect(output).toContain('Update installed modules');
20
+ expect(output).toContain('--check');
21
+ expect(output).toContain('--yes');
22
+ });
23
+
24
+ it('should support --check flag', () => {
25
+ try {
26
+ execSync(`node ${cliPath} update --check`, { encoding: 'utf-8', stdio: 'pipe' });
27
+ } catch (err) {
28
+ const error = err as { stdout?: string; stderr?: string };
29
+ const output = error.stdout || error.stderr || '';
30
+ // Either shows error (not in project) or check output
31
+ expect(output.length).toBeGreaterThan(0);
32
+ }
33
+ });
34
+ });