Skip to content

Commit

Permalink
fix(resolve): fix critial bug in OpenAPI 3.1.0 resolution (#3050)
Browse files Browse the repository at this point in the history
This bug was further upstream also in ApiDOM,
which was unable to resolve local references
from fragments as well.

Refs swagger-api/apidom#2934
  • Loading branch information
char0n committed Jul 14, 2023
1 parent c797417 commit 3697242
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 183 deletions.
260 changes: 87 additions & 173 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@
},
"dependencies": {
"@babel/runtime-corejs3": "^7.20.13",
"@swagger-api/apidom-core": ">=0.70.1 <1.0.0",
"@swagger-api/apidom-json-pointer": ">=0.70.1 <1.0.0",
"@swagger-api/apidom-ns-openapi-3-1": ">=0.70.2 <1.0.0",
"@swagger-api/apidom-reference": ">=0.70.2 <1.0.0",
"@swagger-api/apidom-core": ">=0.71.0 <1.0.0",
"@swagger-api/apidom-json-pointer": ">=0.71.0 <1.0.0",
"@swagger-api/apidom-ns-openapi-3-1": ">=0.71.0 <1.0.0",
"@swagger-api/apidom-reference": ">=0.71.1 <1.0.0",
"cookie": "~0.5.0",
"cross-fetch": "^3.1.5",
"deepmerge": "~4.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
}

const reference = await this.toReference(referenceElement.$ref.toValue());
const retrievalURI = reference.uri;
const { uri: retrievalURI } = reference;
const $refBaseURI = url.resolve(retrievalURI, referenceElement.$ref.toValue());

this.indirections.push(referenceElement);
Expand Down Expand Up @@ -230,7 +230,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
}

const reference = await this.toReference(pathItemElement.$ref.toValue());
const retrievalURI = reference.uri;
const { uri: retrievalURI } = reference;
const $refBaseURI = url.resolve(retrievalURI, pathItemElement.$ref.toValue());

this.indirections.push(pathItemElement);
Expand Down Expand Up @@ -366,7 +366,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
}

// compute baseURI using rules around $id and $ref keywords
let { reference } = this;
let reference = await this.toReference(url.unsanitize(this.reference.uri));
let { uri: retrievalURI } = reference;
const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,41 @@ describe('dereference', () => {
describe('strategies', () => {
describe('openapi-3-1-swagger-client', () => {
describe('Path Item Object', () => {
describe('given single PathItemElement passed to dereferenceApiDOM', () => {
describe('given single PathItemElement passed to dereferenceApiDOM with internal references', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'internal-only', 'root.json');

test('should dereference', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const jsonPointer = compile(['paths', '/path1']);
const pathItemElement = evaluate(jsonPointer, parseResult.api);
const dereferenced = await dereferenceApiDOM(pathItemElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: { baseURI: `${fixturePath}#${jsonPointer}` },
});

expect(isPathItemElement(dereferenced)).toBe(true);
});

test('should dereference and contain metadata about origin', async () => {
const jsonPointer = compile(['paths', '/path1']);
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const pathItemElement = evaluate(jsonPointer, parseResult.api);
const dereferenced = await dereferenceApiDOM(pathItemElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: { baseURI: `${fixturePath}#${jsonPointer}` },
});

expect(dereferenced.meta.get('ref-origin').toValue()).toEqual(
expect.stringMatching(/internal-only\/root\.json$/)
);
});
});

describe('given single PathItemElement passed to dereferenceApiDOM with external references', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'external-only', 'root.json');

test('should dereference', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,90 @@ describe('dereference', () => {
describe('strategies', () => {
describe('openapi-3-1-swagger-client', () => {
describe('Reference Object', () => {
describe('given single ReferenceElement passed to dereferenceApiDOM', () => {
describe('given single ReferenceElement passed to dereferenceApiDOM with internal references', () => {
describe('given dereferencing using local file system', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'internal-only', 'root.json');

test('should dereference', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const referenceElement = evaluate('/components/parameters/userId', parseResult.api);
const dereferenced = await dereferenceApiDOM(referenceElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: {
baseURI: `${fixturePath}#/components/parameters/userId`,
},
});

expect(isParameterElement(dereferenced)).toBe(true);
});

test('should dereference and contain metadata about origin', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const referenceElement = evaluate('/components/parameters/userId', parseResult.api);
const dereferenced = await dereferenceApiDOM(referenceElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: { baseURI: `${fixturePath}#/components/parameters/userId` },
});

expect(dereferenced.meta.get('ref-origin').toValue()).toEqual(
expect.stringMatching(/internal-only\/root\.json$/)
);
});
});

describe('given dereferencing using HTTP protocol', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'internal-only', 'root.json');
const httpPort = 8123;
let httpServer;

beforeEach(() => {
const cwd = path.join(__dirname, '__fixtures__', 'internal-only');
httpServer = globalThis.createHTTPServer({ port: httpPort, cwd });
});

afterEach(async () => {
await httpServer.terminate();
});

test('should dereference', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const referenceElement = evaluate('/components/parameters/userId', parseResult.api);
const dereferenced = await dereferenceApiDOM(referenceElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: {
baseURI: `http://localhost:${httpPort}/root.json#/components/parameters/userId`,
},
});

expect(isParameterElement(dereferenced)).toBe(true);
});

test('should dereference and contain metadata about origin', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const referenceElement = evaluate('/components/parameters/userId', parseResult.api);
const dereferenced = await dereferenceApiDOM(referenceElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: {
baseURI: `http://localhost:${httpPort}/root.json#/components/parameters/userId`,
},
});

expect(dereferenced.meta.get('ref-origin').toValue()).toEqual(
expect.stringMatching(/\/root\.json$/)
);
});
});
});

describe('given single ReferenceElement passed to dereferenceApiDOM with external references', () => {
describe('given dereferencing using local file system', () => {
const fixturePath = path.join(rootFixturePath, 'external-only', 'root.json');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[
{
"openapi": "3.1.0",
"components": {
"schemas": {
"User": {
"$id": "/components/schemas/user",
"type": "object",
"properties": {
"login": {
"type": "string"
},
"password": {
"type": "string"
},
"profile": {
"$id": "/components/schemas/user-profile",
"$anchor": "user-profile",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
}
},
"UserProfile": {
"$id": "/components/schemas/user-profile",
"$anchor": "user-profile",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"openapi": "3.1.0",
"components": {
"schemas": {
"User": {
"$id": "/components/schemas/user",
"type": "object",
"properties": {
"login": {
"type": "string"
},
"password": {
"type": "string"
},
"profile": {
"$ref": "/components/schemas/user-profile#user-profile"
}
}
},
"UserProfile": {
"$id": "/components/schemas/user-profile",
"$anchor": "user-profile",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"openapi": "3.1.0",
"components": {
"schemas": {
"User": {
"type": "object"
},
"UserProfile": {
"$id": "/components/schemas/user-profile",
"type": "object",
"properties": {
"user": {
"type": "object"
}
}
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"openapi": "3.1.0",
"components": {
"schemas": {
"User": {
"type": "object"
},
"UserProfile": {
"$id": "/components/schemas/user-profile",
"type": "object",
"properties": {
"user": {
"$ref": "/#/components/schemas/User"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,45 @@ describe('dereference', () => {
describe('strategies', () => {
describe('openapi-3-1-swagger-client', () => {
describe('Schema Object', () => {
describe('given single SchemaElement passed to dereferenceApiDOM', () => {
describe('given single SchemaElement passed to dereferenceApiDOM with internal references', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'internal-only', 'root.json');

test('should dereference', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const schemaElement = evaluate(
'/components/schemas/User/properties/profile',
parseResult.api
);
const dereferenced = await dereferenceApiDOM(schemaElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: { baseURI: `${fixturePath}#/components/schemas/User/properties/profile` },
});

expect(isSchemaElement(dereferenced)).toBe(true);
});

test('should dereference and contain metadata about origin', async () => {
const parseResult = await parse(fixturePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const schemaElement = evaluate(
'/components/schemas/User/properties/profile',
parseResult.api
);
const dereferenced = await dereferenceApiDOM(schemaElement, {
parse: { mediaType: mediaTypes.latest('json') },
resolve: { baseURI: `${fixturePath}#/components/schemas/User/properties/profile` },
});

expect(dereferenced.meta.get('ref-origin').toValue()).toEqual(
expect.stringMatching(/internal-only\/root\.json$/)
);
});
});

describe('given single SchemaElement passed to dereferenceApiDOM with external references', () => {
const fixturePath = path.join(__dirname, '__fixtures__', 'external-only', 'root.json');

test('should dereference', async () => {
Expand Down
Loading

0 comments on commit 3697242

Please sign in to comment.