@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.
- package/dist/ast/hash.d.ts +2 -0
- package/dist/ast/hash.d.ts.map +1 -0
- package/dist/ast/index.d.ts +3 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/traverse.d.ts +13 -0
- package/dist/ast/traverse.d.ts.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/diff/auth-differ.d.ts +4 -0
- package/dist/diff/auth-differ.d.ts.map +1 -0
- package/dist/diff/endpoint-differ.d.ts +3 -0
- package/dist/diff/endpoint-differ.d.ts.map +1 -0
- package/dist/diff/index.d.ts +3 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/schema-differ.d.ts +3 -0
- package/dist/diff/schema-differ.d.ts.map +1 -0
- package/dist/diff/server-differ.d.ts +3 -0
- package/dist/diff/server-differ.d.ts.map +1 -0
- package/dist/index.cjs +2902 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2855 -0
- package/dist/loader/file-loader.d.ts +5 -0
- package/dist/loader/file-loader.d.ts.map +1 -0
- package/dist/loader/git-loader.d.ts +5 -0
- package/dist/loader/git-loader.d.ts.map +1 -0
- package/dist/loader/index.d.ts +7 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/url-loader.d.ts +5 -0
- package/dist/loader/url-loader.d.ts.map +1 -0
- package/dist/output/html.d.ts +3 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/index.d.ts +3 -0
- package/dist/output/index.d.ts.map +1 -0
- package/dist/output/json.d.ts +3 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/markdown.d.ts +3 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/terminal.d.ts +3 -0
- package/dist/output/terminal.d.ts.map +1 -0
- package/dist/parsers/base.d.ts +8 -0
- package/dist/parsers/base.d.ts.map +1 -0
- package/dist/parsers/graphql/index.d.ts +13 -0
- package/dist/parsers/graphql/index.d.ts.map +1 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/openapi2/index.d.ts +9 -0
- package/dist/parsers/openapi2/index.d.ts.map +1 -0
- package/dist/parsers/openapi3/index.d.ts +9 -0
- package/dist/parsers/openapi3/index.d.ts.map +1 -0
- package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
- package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
- package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
- package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
- package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
- package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
- package/dist/parsers/protobuf/index.d.ts +14 -0
- package/dist/parsers/protobuf/index.d.ts.map +1 -0
- package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
- package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
- package/dist/rules/auth/security-added.d.ts +9 -0
- package/dist/rules/auth/security-added.d.ts.map +1 -0
- package/dist/rules/auth/security-removed.d.ts +9 -0
- package/dist/rules/auth/security-removed.d.ts.map +1 -0
- package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
- package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
- package/dist/rules/base.d.ts +19 -0
- package/dist/rules/base.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
- package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
- package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
- package/dist/rules/endpoint/path-changed.d.ts +9 -0
- package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/meta/server-removed.d.ts +9 -0
- package/dist/rules/meta/server-removed.d.ts.map +1 -0
- package/dist/rules/param/param-added.d.ts +9 -0
- package/dist/rules/param/param-added.d.ts.map +1 -0
- package/dist/rules/param/param-deprecated.d.ts +9 -0
- package/dist/rules/param/param-deprecated.d.ts.map +1 -0
- package/dist/rules/param/param-enum-value-added.d.ts +9 -0
- package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
- package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
- package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
- package/dist/rules/param/param-location-changed.d.ts +9 -0
- package/dist/rules/param/param-location-changed.d.ts.map +1 -0
- package/dist/rules/param/param-removed.d.ts +9 -0
- package/dist/rules/param/param-removed.d.ts.map +1 -0
- package/dist/rules/param/param-required-added.d.ts +9 -0
- package/dist/rules/param/param-required-added.d.ts.map +1 -0
- package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
- package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
- package/dist/rules/param/param-type-changed.d.ts +9 -0
- package/dist/rules/param/param-type-changed.d.ts.map +1 -0
- package/dist/rules/request/request-body-added-required.d.ts +9 -0
- package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
- package/dist/rules/request/request-body-removed.d.ts +9 -0
- package/dist/rules/request/request-body-removed.d.ts.map +1 -0
- package/dist/rules/request/request-content-type-added.d.ts +9 -0
- package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
- package/dist/rules/request/request-content-type-removed.d.ts +9 -0
- package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
- package/dist/rules/request/request-field-added-required.d.ts +9 -0
- package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
- package/dist/rules/request/request-field-removed.d.ts +9 -0
- package/dist/rules/request/request-field-removed.d.ts.map +1 -0
- package/dist/rules/request/request-field-type-changed.d.ts +9 -0
- package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
- package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
- package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
- package/dist/rules/response/response-field-added.d.ts +9 -0
- package/dist/rules/response/response-field-added.d.ts.map +1 -0
- package/dist/rules/response/response-field-removed.d.ts +9 -0
- package/dist/rules/response/response-field-removed.d.ts.map +1 -0
- package/dist/rules/response/response-field-type-changed.d.ts +9 -0
- package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
- package/dist/rules/response/response-header-added-required.d.ts +9 -0
- package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
- package/dist/rules/response/response-header-removed.d.ts +9 -0
- package/dist/rules/response/response-header-removed.d.ts.map +1 -0
- package/dist/rules/response/response-media-type-added.d.ts +9 -0
- package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
- package/dist/rules/response/response-media-type-removed.d.ts +9 -0
- package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
- package/dist/rules/response/response-status-added.d.ts +9 -0
- package/dist/rules/response/response-status-added.d.ts.map +1 -0
- package/dist/rules/response/response-status-removed.d.ts +9 -0
- package/dist/rules/response/response-status-removed.d.ts.map +1 -0
- package/dist/types/ast.d.ts +140 -0
- package/dist/types/ast.d.ts.map +1 -0
- package/dist/types/config.d.ts +17 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/diff.d.ts +43 -0
- package/dist/types/diff.d.ts.map +1 -0
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/semantic.d.ts +45 -0
- package/dist/types/semantic.d.ts.map +1 -0
- package/package.json +31 -0
- package/src/ast/hash.ts +26 -0
- package/src/ast/index.ts +2 -0
- package/src/ast/traverse.ts +59 -0
- package/src/config/index.ts +44 -0
- package/src/diff/auth-differ.ts +72 -0
- package/src/diff/endpoint-differ.ts +144 -0
- package/src/diff/index.ts +13 -0
- package/src/diff/schema-differ.ts +70 -0
- package/src/diff/server-differ.ts +22 -0
- package/src/index.ts +92 -0
- package/src/loader/file-loader.ts +19 -0
- package/src/loader/git-loader.ts +22 -0
- package/src/loader/index.ts +32 -0
- package/src/loader/url-loader.ts +25 -0
- package/src/output/html.ts +177 -0
- package/src/output/index.ts +16 -0
- package/src/output/json.ts +5 -0
- package/src/output/markdown.ts +29 -0
- package/src/output/terminal.ts +37 -0
- package/src/parsers/base.ts +8 -0
- package/src/parsers/graphql/index.ts +181 -0
- package/src/parsers/index.ts +61 -0
- package/src/parsers/openapi2/index.ts +218 -0
- package/src/parsers/openapi3/index.ts +223 -0
- package/src/parsers/openapi3/ref-resolver.ts +101 -0
- package/src/parsers/openapi3/schema-normalizer.ts +52 -0
- package/src/parsers/openapi3/security-normalizer.ts +18 -0
- package/src/parsers/protobuf/index.ts +208 -0
- package/src/rules/auth/oauth-scope-removed.ts +34 -0
- package/src/rules/auth/security-added.ts +32 -0
- package/src/rules/auth/security-removed.ts +47 -0
- package/src/rules/auth/security-scheme-type-changed.ts +30 -0
- package/src/rules/base.ts +29 -0
- package/src/rules/endpoint/endpoint-added.ts +26 -0
- package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
- package/src/rules/endpoint/endpoint-removed.ts +26 -0
- package/src/rules/endpoint/http-method-changed.ts +29 -0
- package/src/rules/endpoint/path-changed.ts +29 -0
- package/src/rules/index.ts +83 -0
- package/src/rules/meta/server-removed.ts +26 -0
- package/src/rules/param/param-added.ts +52 -0
- package/src/rules/param/param-deprecated.ts +34 -0
- package/src/rules/param/param-enum-value-added.ts +32 -0
- package/src/rules/param/param-enum-value-removed.ts +32 -0
- package/src/rules/param/param-location-changed.ts +43 -0
- package/src/rules/param/param-removed.ts +52 -0
- package/src/rules/param/param-required-added.ts +34 -0
- package/src/rules/param/param-required-true-to-false.ts +34 -0
- package/src/rules/param/param-type-changed.ts +32 -0
- package/src/rules/request/request-body-added-required.ts +33 -0
- package/src/rules/request/request-body-removed.ts +30 -0
- package/src/rules/request/request-content-type-added.ts +31 -0
- package/src/rules/request/request-content-type-removed.ts +31 -0
- package/src/rules/request/request-field-added-required.ts +41 -0
- package/src/rules/request/request-field-removed.ts +35 -0
- package/src/rules/request/request-field-type-changed.ts +37 -0
- package/src/rules/request/request-required-false-to-true.ts +56 -0
- package/src/rules/response/response-field-added.ts +34 -0
- package/src/rules/response/response-field-removed.ts +34 -0
- package/src/rules/response/response-field-type-changed.ts +37 -0
- package/src/rules/response/response-header-added-required.ts +47 -0
- package/src/rules/response/response-header-removed.ts +32 -0
- package/src/rules/response/response-media-type-added.ts +32 -0
- package/src/rules/response/response-media-type-removed.ts +32 -0
- package/src/rules/response/response-status-added.ts +31 -0
- package/src/rules/response/response-status-removed.ts +31 -0
- package/src/types/ast.ts +164 -0
- package/src/types/config.ts +31 -0
- package/src/types/diff.ts +49 -0
- package/src/types/errors.ts +26 -0
- package/src/types/index.ts +5 -0
- package/src/types/semantic.ts +60 -0
- package/test/integration/stripe-v1.yaml +36 -0
- package/test/integration/stripe-v2.yaml +35 -0
- package/tests/ast.test.ts +60 -0
- package/tests/config.test.ts +43 -0
- package/tests/diff/auth-differ.test.ts +55 -0
- package/tests/diff/endpoint-differ.test.ts +42 -0
- package/tests/diff/schema-differ.test.ts +46 -0
- package/tests/diff/server-differ.test.ts +23 -0
- package/tests/diff.test.ts +116 -0
- package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
- package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
- package/tests/integration.test.ts +21 -0
- package/tests/loader-more.test.ts +75 -0
- package/tests/loader.test.ts +61 -0
- package/tests/output.test.ts +58 -0
- package/tests/parsers/openapi3-coverage.test.ts +77 -0
- package/tests/parsers/openapi3.test.ts +41 -0
- package/tests/parsers/parsers-other.test.ts +135 -0
- package/tests/parsers/ref-resolver.test.ts +78 -0
- package/tests/parsers/schema-normalizer.test.ts +30 -0
- package/tests/rules/auth-meta.test.ts +87 -0
- package/tests/rules/base.test.ts +44 -0
- package/tests/rules/endpoint.test.ts +122 -0
- package/tests/rules/param.test.ts +242 -0
- package/tests/rules/request.test.ts +147 -0
- package/tests/rules/response.test.ts +161 -0
- package/tests/rules/rules-coverage.test.ts +64 -0
- package/tests/types-errors.test.ts +22 -0
- package/tsconfig.json +8 -0
- 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,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
|
+
});
|