generate-operation-ids.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import fs from 'fs/promises';
  2. import { tmpdir } from 'os';
  3. import path from 'path';
  4. import type { OpenAPI3 } from 'openapi-typescript';
  5. import { describe, expect, it } from 'vitest';
  6. import { generateOperationIds } from './generate-operation-ids';
  7. async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
  8. const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
  9. const filePath = path.join(tempDir, 'openapi.json');
  10. await fs.writeFile(filePath, JSON.stringify(spec));
  11. return filePath;
  12. }
  13. async function cleanup(filePath: string): Promise<void> {
  14. try {
  15. await fs.unlink(filePath);
  16. await fs.rmdir(path.dirname(filePath));
  17. }
  18. catch (err) {
  19. // eslint-disable-next-line no-console
  20. console.error('Cleanup failed:', err);
  21. }
  22. }
  23. describe('generateOperationIds', () => {
  24. it('should generate correct operationId for simple paths', async() => {
  25. const spec: OpenAPI3 = {
  26. openapi: '3.0.0',
  27. info: { title: 'Test API', version: '1.0.0' },
  28. paths: {
  29. '/foo': {
  30. get: {},
  31. post: {},
  32. },
  33. },
  34. };
  35. const filePath = await createTempOpenAPIFile(spec);
  36. try {
  37. const result = await generateOperationIds(filePath);
  38. const parsed = JSON.parse(result);
  39. expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
  40. expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
  41. }
  42. finally {
  43. await cleanup(filePath);
  44. }
  45. });
  46. it('should generate correct operationId for paths with parameters', async() => {
  47. const spec: OpenAPI3 = {
  48. openapi: '3.0.0',
  49. info: { title: 'Test API', version: '1.0.0' },
  50. paths: {
  51. '/foo/{id}': {
  52. get: {},
  53. },
  54. '/foo/{id}/bar/{page}': {
  55. get: {},
  56. },
  57. },
  58. };
  59. const filePath = await createTempOpenAPIFile(spec);
  60. try {
  61. const result = await generateOperationIds(filePath);
  62. const parsed = JSON.parse(result);
  63. expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
  64. expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe('getBarByPageByIdForFoo');
  65. }
  66. finally {
  67. await cleanup(filePath);
  68. }
  69. });
  70. it('should generate correct operationId for nested resources', async() => {
  71. const spec: OpenAPI3 = {
  72. openapi: '3.0.0',
  73. info: { title: 'Test API', version: '1.0.0' },
  74. paths: {
  75. '/foo/bar': {
  76. get: {},
  77. },
  78. },
  79. };
  80. const filePath = await createTempOpenAPIFile(spec);
  81. try {
  82. const result = await generateOperationIds(filePath);
  83. const parsed = JSON.parse(result);
  84. expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
  85. }
  86. finally {
  87. await cleanup(filePath);
  88. }
  89. });
  90. it('should preserve existing operationId when overwriteExisting is false', async() => {
  91. const existingOperationId = 'existingOperation';
  92. const spec: OpenAPI3 = {
  93. openapi: '3.0.0',
  94. info: { title: 'Test API', version: '1.0.0' },
  95. paths: {
  96. '/foo': {
  97. get: {
  98. operationId: existingOperationId,
  99. },
  100. },
  101. },
  102. };
  103. const filePath = await createTempOpenAPIFile(spec);
  104. try {
  105. const result = await generateOperationIds(filePath, { overwriteExisting: false });
  106. const parsed = JSON.parse(result);
  107. expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
  108. }
  109. finally {
  110. await cleanup(filePath);
  111. }
  112. });
  113. it('should overwrite existing operationId when overwriteExisting is true', async() => {
  114. const spec: OpenAPI3 = {
  115. openapi: '3.0.0',
  116. info: { title: 'Test API', version: '1.0.0' },
  117. paths: {
  118. '/foo': {
  119. get: {
  120. operationId: 'existingOperation',
  121. },
  122. },
  123. },
  124. };
  125. const filePath = await createTempOpenAPIFile(spec);
  126. try {
  127. const result = await generateOperationIds(filePath, { overwriteExisting: true });
  128. const parsed = JSON.parse(result);
  129. expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
  130. }
  131. finally {
  132. await cleanup(filePath);
  133. }
  134. });
  135. it('should generate correct operationId for root path', async() => {
  136. const spec: OpenAPI3 = {
  137. openapi: '3.0.0',
  138. info: { title: 'Test API', version: '1.0.0' },
  139. paths: {
  140. '/': {
  141. get: {},
  142. },
  143. },
  144. };
  145. const filePath = await createTempOpenAPIFile(spec);
  146. try {
  147. const result = await generateOperationIds(filePath);
  148. const parsed = JSON.parse(result);
  149. expect(parsed.paths['/'].get.operationId).toBe('getRoot');
  150. }
  151. finally {
  152. await cleanup(filePath);
  153. }
  154. });
  155. it('should generate operationId for all HTTP methods', async() => {
  156. const spec: OpenAPI3 = {
  157. openapi: '3.0.0',
  158. info: { title: 'Test API', version: '1.0.0' },
  159. paths: {
  160. '/foo': {
  161. get: {},
  162. post: {},
  163. put: {},
  164. delete: {},
  165. patch: {},
  166. options: {},
  167. head: {},
  168. trace: {},
  169. },
  170. },
  171. };
  172. const filePath = await createTempOpenAPIFile(spec);
  173. try {
  174. const result = await generateOperationIds(filePath);
  175. const parsed = JSON.parse(result);
  176. expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
  177. expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
  178. expect(parsed.paths['/foo'].put.operationId).toBe('putFoo');
  179. expect(parsed.paths['/foo'].delete.operationId).toBe('deleteFoo');
  180. expect(parsed.paths['/foo'].patch.operationId).toBe('patchFoo');
  181. expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
  182. expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
  183. expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
  184. }
  185. finally {
  186. await cleanup(filePath);
  187. }
  188. });
  189. it('should throw error for non-existent file', async() => {
  190. await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
  191. });
  192. });