@apidiff/core 1.0.0

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.
Files changed (249) hide show
  1. package/dist/ast/hash.d.ts +2 -0
  2. package/dist/ast/hash.d.ts.map +1 -0
  3. package/dist/ast/index.d.ts +3 -0
  4. package/dist/ast/index.d.ts.map +1 -0
  5. package/dist/ast/traverse.d.ts +13 -0
  6. package/dist/ast/traverse.d.ts.map +1 -0
  7. package/dist/config/index.d.ts +9 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/diff/auth-differ.d.ts +4 -0
  10. package/dist/diff/auth-differ.d.ts.map +1 -0
  11. package/dist/diff/endpoint-differ.d.ts +3 -0
  12. package/dist/diff/endpoint-differ.d.ts.map +1 -0
  13. package/dist/diff/index.d.ts +3 -0
  14. package/dist/diff/index.d.ts.map +1 -0
  15. package/dist/diff/schema-differ.d.ts +3 -0
  16. package/dist/diff/schema-differ.d.ts.map +1 -0
  17. package/dist/diff/server-differ.d.ts +3 -0
  18. package/dist/diff/server-differ.d.ts.map +1 -0
  19. package/dist/index.cjs +2902 -0
  20. package/dist/index.d.ts +15 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2855 -0
  23. package/dist/loader/file-loader.d.ts +5 -0
  24. package/dist/loader/file-loader.d.ts.map +1 -0
  25. package/dist/loader/git-loader.d.ts +5 -0
  26. package/dist/loader/git-loader.d.ts.map +1 -0
  27. package/dist/loader/index.d.ts +7 -0
  28. package/dist/loader/index.d.ts.map +1 -0
  29. package/dist/loader/url-loader.d.ts +5 -0
  30. package/dist/loader/url-loader.d.ts.map +1 -0
  31. package/dist/output/html.d.ts +3 -0
  32. package/dist/output/html.d.ts.map +1 -0
  33. package/dist/output/index.d.ts +3 -0
  34. package/dist/output/index.d.ts.map +1 -0
  35. package/dist/output/json.d.ts +3 -0
  36. package/dist/output/json.d.ts.map +1 -0
  37. package/dist/output/markdown.d.ts +3 -0
  38. package/dist/output/markdown.d.ts.map +1 -0
  39. package/dist/output/terminal.d.ts +3 -0
  40. package/dist/output/terminal.d.ts.map +1 -0
  41. package/dist/parsers/base.d.ts +8 -0
  42. package/dist/parsers/base.d.ts.map +1 -0
  43. package/dist/parsers/graphql/index.d.ts +13 -0
  44. package/dist/parsers/graphql/index.d.ts.map +1 -0
  45. package/dist/parsers/index.d.ts +7 -0
  46. package/dist/parsers/index.d.ts.map +1 -0
  47. package/dist/parsers/openapi2/index.d.ts +9 -0
  48. package/dist/parsers/openapi2/index.d.ts.map +1 -0
  49. package/dist/parsers/openapi3/index.d.ts +9 -0
  50. package/dist/parsers/openapi3/index.d.ts.map +1 -0
  51. package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
  52. package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
  53. package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
  54. package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
  55. package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
  56. package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
  57. package/dist/parsers/protobuf/index.d.ts +14 -0
  58. package/dist/parsers/protobuf/index.d.ts.map +1 -0
  59. package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
  60. package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
  61. package/dist/rules/auth/security-added.d.ts +9 -0
  62. package/dist/rules/auth/security-added.d.ts.map +1 -0
  63. package/dist/rules/auth/security-removed.d.ts +9 -0
  64. package/dist/rules/auth/security-removed.d.ts.map +1 -0
  65. package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
  66. package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
  67. package/dist/rules/base.d.ts +19 -0
  68. package/dist/rules/base.d.ts.map +1 -0
  69. package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
  70. package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
  71. package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
  72. package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
  73. package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
  74. package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
  75. package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
  76. package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
  77. package/dist/rules/endpoint/path-changed.d.ts +9 -0
  78. package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
  79. package/dist/rules/index.d.ts +4 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/meta/server-removed.d.ts +9 -0
  82. package/dist/rules/meta/server-removed.d.ts.map +1 -0
  83. package/dist/rules/param/param-added.d.ts +9 -0
  84. package/dist/rules/param/param-added.d.ts.map +1 -0
  85. package/dist/rules/param/param-deprecated.d.ts +9 -0
  86. package/dist/rules/param/param-deprecated.d.ts.map +1 -0
  87. package/dist/rules/param/param-enum-value-added.d.ts +9 -0
  88. package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
  89. package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
  90. package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
  91. package/dist/rules/param/param-location-changed.d.ts +9 -0
  92. package/dist/rules/param/param-location-changed.d.ts.map +1 -0
  93. package/dist/rules/param/param-removed.d.ts +9 -0
  94. package/dist/rules/param/param-removed.d.ts.map +1 -0
  95. package/dist/rules/param/param-required-added.d.ts +9 -0
  96. package/dist/rules/param/param-required-added.d.ts.map +1 -0
  97. package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
  98. package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
  99. package/dist/rules/param/param-type-changed.d.ts +9 -0
  100. package/dist/rules/param/param-type-changed.d.ts.map +1 -0
  101. package/dist/rules/request/request-body-added-required.d.ts +9 -0
  102. package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
  103. package/dist/rules/request/request-body-removed.d.ts +9 -0
  104. package/dist/rules/request/request-body-removed.d.ts.map +1 -0
  105. package/dist/rules/request/request-content-type-added.d.ts +9 -0
  106. package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
  107. package/dist/rules/request/request-content-type-removed.d.ts +9 -0
  108. package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
  109. package/dist/rules/request/request-field-added-required.d.ts +9 -0
  110. package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
  111. package/dist/rules/request/request-field-removed.d.ts +9 -0
  112. package/dist/rules/request/request-field-removed.d.ts.map +1 -0
  113. package/dist/rules/request/request-field-type-changed.d.ts +9 -0
  114. package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
  115. package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
  116. package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
  117. package/dist/rules/response/response-field-added.d.ts +9 -0
  118. package/dist/rules/response/response-field-added.d.ts.map +1 -0
  119. package/dist/rules/response/response-field-removed.d.ts +9 -0
  120. package/dist/rules/response/response-field-removed.d.ts.map +1 -0
  121. package/dist/rules/response/response-field-type-changed.d.ts +9 -0
  122. package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
  123. package/dist/rules/response/response-header-added-required.d.ts +9 -0
  124. package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
  125. package/dist/rules/response/response-header-removed.d.ts +9 -0
  126. package/dist/rules/response/response-header-removed.d.ts.map +1 -0
  127. package/dist/rules/response/response-media-type-added.d.ts +9 -0
  128. package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
  129. package/dist/rules/response/response-media-type-removed.d.ts +9 -0
  130. package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
  131. package/dist/rules/response/response-status-added.d.ts +9 -0
  132. package/dist/rules/response/response-status-added.d.ts.map +1 -0
  133. package/dist/rules/response/response-status-removed.d.ts +9 -0
  134. package/dist/rules/response/response-status-removed.d.ts.map +1 -0
  135. package/dist/types/ast.d.ts +140 -0
  136. package/dist/types/ast.d.ts.map +1 -0
  137. package/dist/types/config.d.ts +17 -0
  138. package/dist/types/config.d.ts.map +1 -0
  139. package/dist/types/diff.d.ts +43 -0
  140. package/dist/types/diff.d.ts.map +1 -0
  141. package/dist/types/errors.d.ts +21 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +6 -0
  144. package/dist/types/index.d.ts.map +1 -0
  145. package/dist/types/semantic.d.ts +45 -0
  146. package/dist/types/semantic.d.ts.map +1 -0
  147. package/package.json +31 -0
  148. package/src/ast/hash.ts +26 -0
  149. package/src/ast/index.ts +2 -0
  150. package/src/ast/traverse.ts +59 -0
  151. package/src/config/index.ts +44 -0
  152. package/src/diff/auth-differ.ts +72 -0
  153. package/src/diff/endpoint-differ.ts +144 -0
  154. package/src/diff/index.ts +13 -0
  155. package/src/diff/schema-differ.ts +70 -0
  156. package/src/diff/server-differ.ts +22 -0
  157. package/src/index.ts +92 -0
  158. package/src/loader/file-loader.ts +19 -0
  159. package/src/loader/git-loader.ts +22 -0
  160. package/src/loader/index.ts +32 -0
  161. package/src/loader/url-loader.ts +25 -0
  162. package/src/output/html.ts +177 -0
  163. package/src/output/index.ts +16 -0
  164. package/src/output/json.ts +5 -0
  165. package/src/output/markdown.ts +29 -0
  166. package/src/output/terminal.ts +37 -0
  167. package/src/parsers/base.ts +8 -0
  168. package/src/parsers/graphql/index.ts +181 -0
  169. package/src/parsers/index.ts +61 -0
  170. package/src/parsers/openapi2/index.ts +218 -0
  171. package/src/parsers/openapi3/index.ts +223 -0
  172. package/src/parsers/openapi3/ref-resolver.ts +101 -0
  173. package/src/parsers/openapi3/schema-normalizer.ts +52 -0
  174. package/src/parsers/openapi3/security-normalizer.ts +18 -0
  175. package/src/parsers/protobuf/index.ts +208 -0
  176. package/src/rules/auth/oauth-scope-removed.ts +34 -0
  177. package/src/rules/auth/security-added.ts +32 -0
  178. package/src/rules/auth/security-removed.ts +47 -0
  179. package/src/rules/auth/security-scheme-type-changed.ts +30 -0
  180. package/src/rules/base.ts +29 -0
  181. package/src/rules/endpoint/endpoint-added.ts +26 -0
  182. package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
  183. package/src/rules/endpoint/endpoint-removed.ts +26 -0
  184. package/src/rules/endpoint/http-method-changed.ts +29 -0
  185. package/src/rules/endpoint/path-changed.ts +29 -0
  186. package/src/rules/index.ts +83 -0
  187. package/src/rules/meta/server-removed.ts +26 -0
  188. package/src/rules/param/param-added.ts +52 -0
  189. package/src/rules/param/param-deprecated.ts +34 -0
  190. package/src/rules/param/param-enum-value-added.ts +32 -0
  191. package/src/rules/param/param-enum-value-removed.ts +32 -0
  192. package/src/rules/param/param-location-changed.ts +43 -0
  193. package/src/rules/param/param-removed.ts +52 -0
  194. package/src/rules/param/param-required-added.ts +34 -0
  195. package/src/rules/param/param-required-true-to-false.ts +34 -0
  196. package/src/rules/param/param-type-changed.ts +32 -0
  197. package/src/rules/request/request-body-added-required.ts +33 -0
  198. package/src/rules/request/request-body-removed.ts +30 -0
  199. package/src/rules/request/request-content-type-added.ts +31 -0
  200. package/src/rules/request/request-content-type-removed.ts +31 -0
  201. package/src/rules/request/request-field-added-required.ts +41 -0
  202. package/src/rules/request/request-field-removed.ts +35 -0
  203. package/src/rules/request/request-field-type-changed.ts +37 -0
  204. package/src/rules/request/request-required-false-to-true.ts +56 -0
  205. package/src/rules/response/response-field-added.ts +34 -0
  206. package/src/rules/response/response-field-removed.ts +34 -0
  207. package/src/rules/response/response-field-type-changed.ts +37 -0
  208. package/src/rules/response/response-header-added-required.ts +47 -0
  209. package/src/rules/response/response-header-removed.ts +32 -0
  210. package/src/rules/response/response-media-type-added.ts +32 -0
  211. package/src/rules/response/response-media-type-removed.ts +32 -0
  212. package/src/rules/response/response-status-added.ts +31 -0
  213. package/src/rules/response/response-status-removed.ts +31 -0
  214. package/src/types/ast.ts +164 -0
  215. package/src/types/config.ts +31 -0
  216. package/src/types/diff.ts +49 -0
  217. package/src/types/errors.ts +26 -0
  218. package/src/types/index.ts +5 -0
  219. package/src/types/semantic.ts +60 -0
  220. package/test/integration/stripe-v1.yaml +36 -0
  221. package/test/integration/stripe-v2.yaml +35 -0
  222. package/tests/ast.test.ts +60 -0
  223. package/tests/config.test.ts +43 -0
  224. package/tests/diff/auth-differ.test.ts +55 -0
  225. package/tests/diff/endpoint-differ.test.ts +42 -0
  226. package/tests/diff/schema-differ.test.ts +46 -0
  227. package/tests/diff/server-differ.test.ts +23 -0
  228. package/tests/diff.test.ts +116 -0
  229. package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
  230. package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
  231. package/tests/integration.test.ts +21 -0
  232. package/tests/loader-more.test.ts +75 -0
  233. package/tests/loader.test.ts +61 -0
  234. package/tests/output.test.ts +58 -0
  235. package/tests/parsers/openapi3-coverage.test.ts +77 -0
  236. package/tests/parsers/openapi3.test.ts +41 -0
  237. package/tests/parsers/parsers-other.test.ts +135 -0
  238. package/tests/parsers/ref-resolver.test.ts +78 -0
  239. package/tests/parsers/schema-normalizer.test.ts +30 -0
  240. package/tests/rules/auth-meta.test.ts +87 -0
  241. package/tests/rules/base.test.ts +44 -0
  242. package/tests/rules/endpoint.test.ts +122 -0
  243. package/tests/rules/param.test.ts +242 -0
  244. package/tests/rules/request.test.ts +147 -0
  245. package/tests/rules/response.test.ts +161 -0
  246. package/tests/rules/rules-coverage.test.ts +64 -0
  247. package/tests/types-errors.test.ts +22 -0
  248. package/tsconfig.json +8 -0
  249. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,36 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Stripe Example API
4
+ version: "2020-08-27"
5
+ paths:
6
+ /v1/charges:
7
+ post:
8
+ summary: Create a charge
9
+ requestBody:
10
+ required: true
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ required: [amount, currency]
16
+ properties:
17
+ amount:
18
+ type: integer
19
+ currency:
20
+ type: string
21
+ source:
22
+ type: string
23
+ responses:
24
+ "200":
25
+ description: OK
26
+ content:
27
+ application/json:
28
+ schema:
29
+ type: object
30
+ properties:
31
+ id:
32
+ type: string
33
+ amount:
34
+ type: integer
35
+ currency:
36
+ type: string
@@ -0,0 +1,35 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Stripe Example API
4
+ version: "2022-11-15"
5
+ paths:
6
+ /v1/charges:
7
+ post:
8
+ summary: Create a charge
9
+ requestBody:
10
+ required: true
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ required: [amount, currency]
16
+ properties:
17
+ amount:
18
+ type: integer
19
+ currency:
20
+ type: string
21
+ source:
22
+ type: string
23
+ responses:
24
+ "200":
25
+ description: OK
26
+ content:
27
+ application/json:
28
+ schema:
29
+ type: object
30
+ properties:
31
+ id:
32
+ type: string
33
+ amount:
34
+ type: integer
35
+ # currency property removed!
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseSpec } from '../src/parsers/index.js';
3
+ import { semanticHash } from '../src/ast/hash.js';
4
+
5
+ describe('Phase 1: AST and Parsing', () => {
6
+ it('should parse and hash basic OpenAPI spec', async () => {
7
+ const raw = {
8
+ content: JSON.stringify({
9
+ openapi: '3.0.0',
10
+ info: { title: 'Test API', version: '1.0' },
11
+ paths: {
12
+ '/test': {
13
+ get: {
14
+ operationId: 'getTest',
15
+ responses: {
16
+ '200': { description: 'OK' }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }),
22
+ format: 'openapi3' as const
23
+ };
24
+
25
+ const ast = await parseSpec(raw);
26
+ expect(ast.meta.title).toBe('Test API');
27
+ expect(ast.endpoints).toHaveLength(1);
28
+ expect(ast.endpoints[0].id).toBe('GET:/test');
29
+
30
+ const hash = semanticHash(ast);
31
+ expect(typeof hash).toBe('string');
32
+ expect(hash.length).toBeGreaterThan(0);
33
+ });
34
+
35
+ it('hashes consistently', () => {
36
+ const a = semanticHash({ a: 1, b: 2 });
37
+ const b = semanticHash({ b: 2, a: 1 });
38
+ expect(a).toBe(b);
39
+ });
40
+
41
+ it('handles null and undefined', () => {
42
+ const a = semanticHash(null);
43
+ const b = semanticHash(undefined);
44
+ expect(a).toBe(b); // both should return semantic hash of null
45
+ });
46
+
47
+ it('handles arrays', () => {
48
+ const a = semanticHash([1, 2, 3]);
49
+ const b = semanticHash([1, 2, 3]);
50
+ expect(a).toBe(b);
51
+ const c = semanticHash([3, 2, 1]);
52
+ expect(a).not.toBe(c);
53
+ });
54
+
55
+ it('handles primitive values', () => {
56
+ const a = semanticHash('test');
57
+ const b = semanticHash('test');
58
+ expect(a).toBe(b);
59
+ });
60
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { loadConfig, DEFAULT_CONFIG } from '../src/config/index.js';
3
+ import * as fs from 'node:fs';
4
+
5
+ vi.mock('node:fs', () => ({
6
+ readFileSync: vi.fn(),
7
+ existsSync: vi.fn()
8
+ }));
9
+
10
+ describe('Config Loader', () => {
11
+ it('returns default config', async () => {
12
+ const config = await loadConfig({});
13
+ expect(config).toEqual(DEFAULT_CONFIG);
14
+ });
15
+
16
+ it('loads from file', async () => {
17
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
18
+ vi.mocked(fs.readFileSync).mockReturnValueOnce('{"failOn":"warning","output":{"format":"json"}}');
19
+ const config = await loadConfig({ config: 'apidiff.json' });
20
+ expect(config.failOn).toBe('warning');
21
+ expect(config.output.format).toBe('json');
22
+ });
23
+
24
+ it('overrides with cli options', async () => {
25
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
26
+ vi.mocked(fs.readFileSync).mockReturnValueOnce('{"failOn":"warning","output":{"format":"json"}}');
27
+ const config = await loadConfig({ config: 'apidiff.json', format: 'markdown', failOn: 'info', ignorePath: ['/test'] });
28
+ expect(config.failOn).toBe('info');
29
+ expect(config.output.format).toBe('markdown');
30
+ expect(config.ignorePaths).toContain('/test');
31
+ });
32
+
33
+ it('throws on not found', async () => {
34
+ vi.mocked(fs.existsSync).mockReturnValueOnce(false);
35
+ await expect(loadConfig({ config: 'does-not-exist.json' })).rejects.toThrow('Config file not found');
36
+ });
37
+
38
+ it('throws on invalid json', async () => {
39
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
40
+ vi.mocked(fs.readFileSync).mockReturnValueOnce('invalid');
41
+ await expect(loadConfig({ config: 'invalid.json' })).rejects.toThrow('Failed to parse config file');
42
+ });
43
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { diffSecuritySchemes, diffSecurityRequirements } from '../../src/diff/auth-differ.js';
3
+ import type { SecurityScheme, SecurityRequirement, FieldChange } from '../../src/types/index.js';
4
+
5
+ describe('Auth Differ', () => {
6
+ it('diffSecuritySchemes detects added schemes', () => {
7
+ const oldSchemes: SecurityScheme[] = [];
8
+ const newSchemes: SecurityScheme[] = [{ id: 'ApiKey', type: 'apiKey', in: 'header', name: 'X-API-Key' }];
9
+ const diffs = diffSecuritySchemes(oldSchemes, newSchemes);
10
+ expect(diffs).toHaveLength(1);
11
+ expect(diffs[0].changeType).toBe('added');
12
+ expect(diffs[0].schemeId).toBe('ApiKey');
13
+ });
14
+
15
+ it('diffSecuritySchemes detects removed schemes', () => {
16
+ const oldSchemes: SecurityScheme[] = [{ id: 'ApiKey', type: 'apiKey', in: 'header', name: 'X-API-Key' }];
17
+ const newSchemes: SecurityScheme[] = [];
18
+ const diffs = diffSecuritySchemes(oldSchemes, newSchemes);
19
+ expect(diffs).toHaveLength(1);
20
+ expect(diffs[0].changeType).toBe('removed');
21
+ expect(diffs[0].schemeId).toBe('ApiKey');
22
+ });
23
+
24
+ it('diffSecuritySchemes detects changed schemes', () => {
25
+ const oldSchemes: SecurityScheme[] = [{ id: 'Auth', type: 'http', scheme: 'basic' }];
26
+ const newSchemes: SecurityScheme[] = [{ id: 'Auth', type: 'http', scheme: 'bearer' }];
27
+ const diffs = diffSecuritySchemes(oldSchemes, newSchemes);
28
+ expect(diffs).toHaveLength(1);
29
+ expect(diffs[0].changeType).toBe('changed');
30
+ expect(diffs[0].fieldChanges).toHaveLength(1);
31
+ expect(diffs[0].fieldChanges[0].fieldPath).toEqual(['scheme']);
32
+ expect(diffs[0].fieldChanges[0].oldValue).toBe('basic');
33
+ expect(diffs[0].fieldChanges[0].newValue).toBe('bearer');
34
+ });
35
+
36
+ it('diffSecurityRequirements detects added/removed/changed scopes', () => {
37
+ const oldReqs: SecurityRequirement[] = [{ schemeId: 'OAuth', scopes: ['read'] }];
38
+ const newReqs: SecurityRequirement[] = [{ schemeId: 'OAuth', scopes: ['read', 'write'] }, { schemeId: 'ApiKey', scopes: [] }];
39
+ const changes: FieldChange[] = [];
40
+
41
+ diffSecurityRequirements(oldReqs, newReqs, ['security'], changes);
42
+
43
+ expect(changes).toContainEqual({ fieldPath: ['security', 'ApiKey'], changeType: 'added', newValue: 'ApiKey' });
44
+ expect(changes).toContainEqual({ fieldPath: ['security', 'OAuth', 'scopes', 'write'], changeType: 'added', newValue: 'write' });
45
+ });
46
+
47
+ it('diffSecurityRequirements detects removed requirements', () => {
48
+ const oldReqs: SecurityRequirement[] = [{ schemeId: 'ApiKey', scopes: [] }];
49
+ const newReqs: SecurityRequirement[] = [];
50
+ const changes: FieldChange[] = [];
51
+
52
+ diffSecurityRequirements(oldReqs, newReqs, ['security'], changes);
53
+ expect(changes).toContainEqual({ fieldPath: ['security', 'ApiKey'], changeType: 'removed', oldValue: 'ApiKey' });
54
+ });
55
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { diffEndpoints } from '../../src/diff/endpoint-differ.js';
3
+ import type { Endpoint } from '../../src/types/index.js';
4
+
5
+ describe('Endpoint Differ', () => {
6
+ it('detects added endpoint', () => {
7
+ const oldEndpoints: Endpoint[] = [];
8
+ const newEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: '', description: '', tags: [], deprecated: false, security: [], parameters: [], responses: [] }];
9
+ const diffs = diffEndpoints(oldEndpoints, newEndpoints);
10
+ expect(diffs).toHaveLength(1);
11
+ expect(diffs[0].type).toBe('added');
12
+ expect(diffs[0].endpointId).toBe('GET:/');
13
+ });
14
+
15
+ it('detects removed endpoint', () => {
16
+ const oldEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: '', description: '', tags: [], deprecated: false, security: [], parameters: [], responses: [] }];
17
+ const newEndpoints: Endpoint[] = [];
18
+ const diffs = diffEndpoints(oldEndpoints, newEndpoints);
19
+ expect(diffs).toHaveLength(1);
20
+ expect(diffs[0].type).toBe('removed');
21
+ });
22
+
23
+ it('detects changed endpoint fields', () => {
24
+ const oldEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: '', description: '', tags: [], deprecated: false, security: [], parameters: [], responses: [] }];
25
+ const newEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: 'new', description: '', tags: [], deprecated: true, security: [], parameters: [], responses: [] }];
26
+ const diffs = diffEndpoints(oldEndpoints, newEndpoints);
27
+ expect(diffs).toHaveLength(1);
28
+ expect(diffs[0].type).toBe('changed');
29
+ expect(diffs[0].fieldChanges).toContainEqual({ fieldPath: ['deprecated'], changeType: 'changed', oldValue: false, newValue: true });
30
+ });
31
+
32
+ it('diffs parameters', () => {
33
+ const oldEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: '', description: '', tags: [], deprecated: false, security: [], parameters: [
34
+ { name: 'p1', in: 'query', required: false }
35
+ ], responses: [] }];
36
+ const newEndpoints: Endpoint[] = [{ id: 'GET:/', path: '/', method: 'GET', summary: '', description: '', tags: [], deprecated: false, security: [], parameters: [
37
+ { name: 'p1', in: 'query', required: true }
38
+ ], responses: [] }];
39
+ const diffs = diffEndpoints(oldEndpoints, newEndpoints);
40
+ expect(diffs[0].fieldChanges).toContainEqual({ fieldPath: ['parameters', 'p1:query', 'required'], changeType: 'changed', oldValue: false, newValue: true });
41
+ });
42
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { diffSchema } from '../../src/diff/schema-differ.js';
3
+ import type { Schema, FieldChange } from '../../src/types/index.js';
4
+
5
+ describe('Schema Differ', () => {
6
+ it('detects type change', () => {
7
+ const oldSchema: Schema = { type: 'string' };
8
+ const newSchema: Schema = { type: 'number' };
9
+ const changes: FieldChange[] = [];
10
+ diffSchema(oldSchema, newSchema, ['schema'], changes);
11
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'type'], changeType: 'changed', oldValue: 'string', newValue: 'number' });
12
+ });
13
+
14
+ it('detects added property', () => {
15
+ const oldSchema: Schema = { type: 'object', properties: {} };
16
+ const newSchema: Schema = { type: 'object', properties: { newProp: { type: 'string' } } };
17
+ const changes: FieldChange[] = [];
18
+ diffSchema(oldSchema, newSchema, ['schema'], changes);
19
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'properties', 'newProp'], changeType: 'added', newValue: { type: 'string' } });
20
+ });
21
+
22
+ it('detects removed property', () => {
23
+ const oldSchema: Schema = { type: 'object', properties: { oldProp: { type: 'string' } } };
24
+ const newSchema: Schema = { type: 'object', properties: {} };
25
+ const changes: FieldChange[] = [];
26
+ diffSchema(oldSchema, newSchema, ['schema'], changes);
27
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'properties', 'oldProp'], changeType: 'removed', oldValue: { type: 'string' } });
28
+ });
29
+
30
+ it('detects required added/removed', () => {
31
+ const oldSchema: Schema = { type: 'object', required: ['oldReq'] };
32
+ const newSchema: Schema = { type: 'object', required: ['newReq'] };
33
+ const changes: FieldChange[] = [];
34
+ diffSchema(oldSchema, newSchema, ['schema'], changes);
35
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'required', 'newReq'], changeType: 'added', newValue: 'newReq' });
36
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'required', 'oldReq'], changeType: 'removed', oldValue: 'oldReq' });
37
+ });
38
+
39
+ it('detects nested schema differences', () => {
40
+ const oldSchema: Schema = { type: 'object', properties: { nested: { type: 'object', properties: { field: { type: 'string' } } } } };
41
+ const newSchema: Schema = { type: 'object', properties: { nested: { type: 'object', properties: { field: { type: 'number' } } } } };
42
+ const changes: FieldChange[] = [];
43
+ diffSchema(oldSchema, newSchema, ['schema'], changes);
44
+ expect(changes).toContainEqual({ fieldPath: ['schema', 'properties', 'nested', 'properties', 'field', 'type'], changeType: 'changed', oldValue: 'string', newValue: 'number' });
45
+ });
46
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { diffServers } from '../../src/diff/server-differ.js';
3
+ import type { Server } from '../../src/types/index.js';
4
+
5
+ describe('Server Differ', () => {
6
+ it('detects added servers', () => {
7
+ const oldServers: Server[] = [];
8
+ const newServers: Server[] = [{ url: 'http://test.com', description: '' }];
9
+ const diffs = diffServers(oldServers, newServers);
10
+ expect(diffs).toHaveLength(1);
11
+ expect(diffs[0].changeType).toBe('added');
12
+ expect(diffs[0].newServer?.url).toBe('http://test.com');
13
+ });
14
+
15
+ it('detects removed servers', () => {
16
+ const oldServers: Server[] = [{ url: 'http://test.com', description: '' }];
17
+ const newServers: Server[] = [];
18
+ const diffs = diffServers(oldServers, newServers);
19
+ expect(diffs).toHaveLength(1);
20
+ expect(diffs[0].changeType).toBe('removed');
21
+ expect(diffs[0].oldServer?.url).toBe('http://test.com');
22
+ });
23
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { diffEndpoints } from '../src/diff/endpoint-differ.js';
3
+ import { diffServers } from '../src/diff/server-differ.js';
4
+ import { diffSchema } from '../src/diff/schema-differ.js';
5
+ import type { Endpoint, Server, Schema, FieldChange } from '../src/types/index.js';
6
+
7
+ describe('Differ Coverage', () => {
8
+ it('diffEndpoints - coverage', () => {
9
+ const oldE: Endpoint = {
10
+ id: 'test',
11
+ method: 'GET',
12
+ path: '/test',
13
+ security: [],
14
+ parameters: [{ name: 'p', in: 'query', schema: {}, required: true, deprecated: false }],
15
+ requestBody: {
16
+ required: true,
17
+ content: { 'app/json': { schema: {} } }
18
+ },
19
+ responses: [
20
+ {
21
+ statusCode: '200',
22
+ description: 'ok',
23
+ content: { 'app/json': { schema: {} } },
24
+ headers: { 'X-RateLimit': { schema: {}, required: true, description: '' } }
25
+ }
26
+ ],
27
+ tags: [],
28
+ deprecated: false,
29
+ extensions: {}
30
+ };
31
+
32
+ const newE: Endpoint = {
33
+ id: 'test',
34
+ method: 'POST',
35
+ path: '/test2',
36
+ security: [],
37
+ parameters: [{ name: 'p', in: 'query', schema: {}, required: false, deprecated: true }],
38
+ requestBody: {
39
+ required: false,
40
+ content: { 'text/plain': { schema: {} } }
41
+ },
42
+ responses: [
43
+ {
44
+ statusCode: '200',
45
+ description: 'ok',
46
+ content: { 'text/plain': { schema: {} } },
47
+ headers: { 'X-RateLimit': { schema: {}, required: false, description: '' } }
48
+ }
49
+ ],
50
+ tags: [],
51
+ deprecated: true,
52
+ extensions: {}
53
+ };
54
+
55
+ const diffs = diffEndpoints([oldE], [newE]);
56
+ expect(diffs).toHaveLength(1);
57
+ expect(diffs[0].type).toBe('changed');
58
+
59
+ // Check old missing request body to new request body
60
+ const diffs2 = diffEndpoints(
61
+ [{ ...oldE, requestBody: undefined }],
62
+ [{ ...newE, requestBody: { required: false, content: {} } }]
63
+ );
64
+ expect(diffs2).toBeDefined();
65
+
66
+ // Check old request body to missing
67
+ const diffs3 = diffEndpoints(
68
+ [{ ...oldE, requestBody: { required: false, content: {} } }],
69
+ [{ ...newE, requestBody: undefined }]
70
+ );
71
+ expect(diffs3).toBeDefined();
72
+ });
73
+
74
+ it('diffServers - coverage', () => {
75
+ const changes = diffServers(
76
+ [{ url: 'http://old' }],
77
+ [{ url: 'http://new' }]
78
+ );
79
+ expect(changes).toBeDefined();
80
+
81
+ const changes2 = diffServers(
82
+ [{ url: 'http://same', description: 'old' }],
83
+ [{ url: 'http://same', description: 'new' }]
84
+ );
85
+ expect(changes2).toBeDefined();
86
+ });
87
+
88
+ it('diffSchema - coverage', () => {
89
+ const changes: FieldChange[] = [];
90
+ diffSchema(undefined, { type: 'string' }, ['root'], changes);
91
+ diffSchema({ type: 'string' }, undefined, ['root'], changes);
92
+
93
+ const oldS: Schema = {
94
+ type: 'object',
95
+ properties: { a: { type: 'string' }, b: { type: 'number' } },
96
+ required: ['a'],
97
+ enum: ['x'],
98
+ allOf: [{ type: 'object' }],
99
+ oneOf: [{ type: 'object' }],
100
+ anyOf: [{ type: 'object' }]
101
+ };
102
+
103
+ const newS: Schema = {
104
+ type: 'object',
105
+ properties: { b: { type: 'string' }, c: { type: 'number' } },
106
+ required: ['b'],
107
+ enum: ['y'],
108
+ allOf: [{ type: 'string' }],
109
+ oneOf: [{ type: 'string' }],
110
+ anyOf: [{ type: 'string' }]
111
+ };
112
+
113
+ diffSchema(oldS, newS, ['schema'], changes);
114
+ expect(changes.length).toBeGreaterThan(0);
115
+ });
116
+ });
@@ -0,0 +1,11 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test API
4
+ version: 1.0.0
5
+ paths:
6
+ /users:
7
+ get:
8
+ summary: Get users
9
+ responses:
10
+ '200':
11
+ description: OK
@@ -0,0 +1,18 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test API
4
+ version: 2.0.0
5
+ components:
6
+ securitySchemes:
7
+ bearerAuth:
8
+ type: http
9
+ scheme: bearer
10
+ paths:
11
+ /users:
12
+ get:
13
+ summary: Get users
14
+ security:
15
+ - bearerAuth: []
16
+ responses:
17
+ '200':
18
+ description: OK
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolve } from 'node:path';
3
+ import { run } from '../src/index.js';
4
+
5
+ describe('Integration Tests: Real World Scenarios', () => {
6
+ it('Stripe API Versioning (v1 to v2)', async () => {
7
+ const oldPath = resolve(__dirname, '../test/integration/stripe-v1.yaml');
8
+ const newPath = resolve(__dirname, '../test/integration/stripe-v2.yaml');
9
+
10
+ const result = await run(oldPath, newPath);
11
+
12
+ expect(result.stats.total).toBeGreaterThan(0);
13
+ expect(result.stats.breaking).toBeGreaterThan(0);
14
+
15
+ const changes = result.changes;
16
+ // We expect RESPONSE_FIELD_REMOVED for currency
17
+ const fieldRemoved = changes.find(c => c.ruleId === 'RESPONSE_FIELD_REMOVED' && c.message.includes('currency'));
18
+ expect(fieldRemoved).toBeDefined();
19
+ expect(fieldRemoved?.severity).toBe('breaking');
20
+ });
21
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { loadGit } from '../src/loader/git-loader.js';
3
+ import { loadUrl } from '../src/loader/url-loader.js';
4
+ import { loadSpec } from '../src/loader/index.js';
5
+ import * as child_process from 'node:child_process';
6
+
7
+ vi.mock('node:child_process', () => ({
8
+ execSync: vi.fn()
9
+ }));
10
+
11
+ global.fetch = vi.fn();
12
+
13
+ describe('More Loaders', () => {
14
+ describe('git-loader', () => {
15
+ it('throws on invalid source', () => {
16
+ expect(() => loadGit('git:invalid')).toThrow('invalid git source format');
17
+ });
18
+
19
+ it('loads from git', () => {
20
+ vi.mocked(child_process.execSync).mockReturnValueOnce('mock_content');
21
+ const doc = loadGit('git:HEAD:file.yaml');
22
+ expect(doc.content).toBe('mock_content');
23
+ });
24
+
25
+ it('throws on git error', () => {
26
+ vi.mocked(child_process.execSync).mockImplementationOnce(() => {
27
+ const err: any = new Error('Command failed');
28
+ err.stderr = 'Not a valid object name';
29
+ throw err;
30
+ });
31
+ expect(() => loadGit('git:HEAD:not-found.yaml')).toThrow('git ref or path not found');
32
+ });
33
+ });
34
+
35
+ describe('url-loader', () => {
36
+ it('loads from url', async () => {
37
+ vi.mocked(fetch).mockResolvedValueOnce({
38
+ ok: true,
39
+ text: async () => 'mock_content'
40
+ } as Response);
41
+ const doc = await loadUrl('https://example.com/api.yaml');
42
+ expect(doc.content).toBe('mock_content');
43
+ });
44
+
45
+ it('throws on http error', async () => {
46
+ vi.mocked(fetch).mockResolvedValueOnce({
47
+ ok: false,
48
+ status: 404
49
+ } as Response);
50
+ await expect(loadUrl('https://example.com/not-found.yaml')).rejects.toThrow('failed to fetch url');
51
+ });
52
+
53
+ it('throws on network error', async () => {
54
+ vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
55
+ await expect(loadUrl('https://example.com/api.yaml')).rejects.toThrow('network error fetching url');
56
+ });
57
+ });
58
+
59
+ describe('index', () => {
60
+ it('routes url', async () => {
61
+ vi.mocked(fetch).mockResolvedValueOnce({
62
+ ok: true,
63
+ text: async () => 'mock_content'
64
+ } as Response);
65
+ const doc = await loadSpec('https://example.com/api.yaml');
66
+ expect(doc.content).toBe('mock_content');
67
+ });
68
+
69
+ it('routes git', async () => {
70
+ vi.mocked(child_process.execSync).mockReturnValueOnce('mock_content');
71
+ const doc = await loadSpec('git:HEAD:file.yaml');
72
+ expect(doc.content).toBe('mock_content');
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { loadSpec } from '../src/loader/index.js';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ vi.mock('node:fs', async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import('node:fs')>();
12
+ return {
13
+ ...actual,
14
+ readFileSync: vi.fn(actual.readFileSync)
15
+ };
16
+ });
17
+
18
+ describe('Loader', () => {
19
+ it('loads valid file', async () => {
20
+ // Assuming integration.test.ts has valid files we can load
21
+ const doc = await loadSpec(path.join(__dirname, '../test/integration/stripe-v1.yaml'));
22
+ expect(doc).toBeDefined();
23
+ });
24
+
25
+ it('throws on invalid file', async () => {
26
+ await expect(loadSpec('does-not-exist.yaml')).rejects.toThrow();
27
+ });
28
+
29
+ it('throws EACCES on permission denied', async () => {
30
+ vi.mocked(fs.readFileSync).mockImplementationOnce(() => {
31
+ const err = new Error('EACCES');
32
+ (err as any).code = 'EACCES';
33
+ throw err;
34
+ });
35
+
36
+ await expect(loadSpec('dummy.yaml')).rejects.toThrow('permission denied');
37
+ });
38
+
39
+ it('throws on generic file error', async () => {
40
+ vi.mocked(fs.readFileSync).mockImplementationOnce(() => {
41
+ const err = new Error('Generic error');
42
+ throw err;
43
+ });
44
+
45
+ await expect(loadSpec('dummy.yaml')).rejects.toThrow('failed to read file');
46
+ });
47
+
48
+ it('reads from stdin', async () => {
49
+ vi.mocked(fs.readFileSync).mockReturnValueOnce('mock_stdin_content');
50
+ const doc = await loadSpec('-');
51
+ expect(doc.content).toBe('mock_stdin_content');
52
+ expect(fs.readFileSync).toHaveBeenCalledWith(0, 'utf-8');
53
+ });
54
+
55
+ it('throws on stdin error', async () => {
56
+ vi.mocked(fs.readFileSync).mockImplementationOnce(() => {
57
+ throw new Error('stdin error');
58
+ });
59
+ await expect(loadSpec('-')).rejects.toThrow('failed to read from stdin');
60
+ });
61
+ });