Yuki Takei 10 месяцев назад
Родитель
Сommit
da7a6d152e

+ 96 - 0
apps/app/bin/openapi/generate-operation-ids/cli.spec.ts

@@ -0,0 +1,96 @@
+import { writeFileSync } from 'fs';
+
+import {
+  beforeEach, describe, expect, it, vi,
+} from 'vitest';
+
+import { generateOperationIds } from './generate-operation-ids';
+
+// Mock the modules
+vi.mock('fs');
+vi.mock('./generate-operation-ids');
+
+const originalArgv = process.argv;
+
+describe('cli', () => {
+  const mockJsonStrings = '{"test": "data"}';
+
+  beforeEach(() => {
+    vi.resetModules();
+    vi.resetAllMocks();
+    process.argv = [...originalArgv]; // Reset process.argv
+    // Mock console.error to avoid actual console output during tests
+    vi.spyOn(console, 'error').mockImplementation(() => {});
+  });
+
+  it('processes input file and writes output to specified file', async() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json', '-o', 'output.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with correct arguments
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+
+    // Verify writeFileSync was called with correct arguments
+    expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
+  });
+
+  it('uses input file as output when no output file is specified', async() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with correct arguments
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+
+    // Verify writeFileSync was called with input file as output
+    expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
+  });
+
+  it('handles overwrite-existing option correctly', async() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json', '--overwrite-existing'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with overwriteExisting option
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: true });
+  });
+
+  it('handles generateOperationIds error correctly', async() => {
+    // Mock generateOperationIds to throw error
+    const error = new Error('Test error');
+    vi.mocked(generateOperationIds).mockRejectedValue(error);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify error was logged
+    // eslint-disable-next-line no-console
+    expect(console.error).toHaveBeenCalledWith(error);
+
+    // Verify writeFileSync was not called
+    expect(writeFileSync).not.toHaveBeenCalled();
+  });
+});

+ 5 - 3
apps/app/bin/openapi/generate-operation-ids/cli.ts

@@ -4,7 +4,7 @@ import { Command } from 'commander';
 
 import { generateOperationIds } from './generate-operation-ids';
 
-const main = async() => {
+export const main = async(): Promise<void> => {
   // parse command line arguments
   const program = new Command();
   program
@@ -20,8 +20,10 @@ const main = async() => {
   // eslint-disable-next-line no-console
   const jsonStrings = await generateOperationIds(inputFile, { overwriteExisting }).catch(console.error);
   if (jsonStrings != null) {
-    writeFileSync(outputFile, jsonStrings);
+    writeFileSync(outputFile ?? inputFile, jsonStrings);
   }
 };
 
-main();
+if (require.main === module) {
+  main();
+}

+ 219 - 0
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts

@@ -0,0 +1,219 @@
+import fs from 'fs/promises';
+import { tmpdir } from 'os';
+import path from 'path';
+
+import type { OpenAPI3 } from 'openapi-typescript';
+import { describe, expect, it } from 'vitest';
+
+import { generateOperationIds } from './generate-operation-ids';
+
+
+async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
+  const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
+  const filePath = path.join(tempDir, 'openapi.json');
+  await fs.writeFile(filePath, JSON.stringify(spec));
+  return filePath;
+}
+
+async function cleanup(filePath: string): Promise<void> {
+  try {
+    await fs.unlink(filePath);
+    await fs.rmdir(path.dirname(filePath));
+  }
+  catch (err) {
+    // eslint-disable-next-line no-console
+    console.error('Cleanup failed:', err);
+  }
+}
+
+describe('generateOperationIds', () => {
+  it('should generate correct operationId for simple paths', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {},
+          post: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+      expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate correct operationId for paths with parameters', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo/{id}': {
+          get: {},
+        },
+        '/foo/{id}/bar/{page}': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
+      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe('getBarByPageByIdForFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate correct operationId for nested resources', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo/bar': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should preserve existing operationId when overwriteExisting is false', async() => {
+    const existingOperationId = 'existingOperation';
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {
+            operationId: existingOperationId,
+          },
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should overwrite existing operationId when overwriteExisting is true', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {
+            operationId: 'existingOperation',
+          },
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate correct operationId for root path', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/'].get.operationId).toBe('getRoot');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate operationId for all HTTP methods', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {},
+          post: {},
+          put: {},
+          delete: {},
+          patch: {},
+          options: {},
+          head: {},
+          trace: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+      expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
+      expect(parsed.paths['/foo'].put.operationId).toBe('putFoo');
+      expect(parsed.paths['/foo'].delete.operationId).toBe('deleteFoo');
+      expect(parsed.paths['/foo'].patch.operationId).toBe('patchFoo');
+      expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
+      expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
+      expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should throw error for non-existent file', async() => {
+    await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
+  });
+});