File: //proc/thread-self/root/opt/go/pkg/mod/github.com/go-openapi/
[email protected]/expander_test.go
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package spec
import (
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
crossFileRefFixture = "fixtures/expansion/crossFileRef.json"
withoutSchemaID = "removed"
withSchemaID = "schema"
pathItemsFixture = "fixtures/expansion/pathItems.json"
extraRefFixture = "fixtures/expansion/extraRef.json"
)
var (
// PetStoreJSONMessage json raw message for Petstore20
PetStoreJSONMessage = json.RawMessage([]byte(PetStore20))
specs = filepath.Join("fixtures", "specs")
)
func TestExpand_Issue148(t *testing.T) {
fp := filepath.Join("fixtures", "bugs", "schema-148.json")
b, err := jsonDoc(fp)
require.NoError(t, err)
assertAdditionalProps := func(sp Swagger) func(*testing.T) {
return func(t *testing.T) {
require.Len(t, sp.Definitions, 2)
require.Contains(t, sp.Definitions, "empty")
empty := sp.Definitions["empty"]
require.NotNil(t, empty.AdditionalProperties)
require.NotNil(t, empty.AdditionalProperties.Schema)
require.True(t, empty.AdditionalProperties.Allows)
require.Contains(t, sp.Definitions, "false")
additionalIsFalse := sp.Definitions["false"]
require.NotNil(t, additionalIsFalse.AdditionalProperties)
require.Nil(t, additionalIsFalse.AdditionalProperties.Schema)
require.False(t, additionalIsFalse.AdditionalProperties.Allows)
}
}
var sp Swagger
require.NoError(t, json.Unmarshal(b, &sp))
t.Run("check additional properties", assertAdditionalProps(sp))
require.NoError(t, ExpandSpec(&sp, nil))
t.Run("check additional properties after expansion", assertAdditionalProps(sp))
}
func TestExpand_KnownRef(t *testing.T) {
// json schema draft 4 meta schema is embedded by default: it resolves without setup
schema := RefProperty("http://json-schema.org/draft-04/schema#")
require.NoError(t, ExpandSchema(schema, nil, nil))
assert.Equal(t, "Core schema meta-schema", schema.Description)
// from the expanded schema, verify that all remaining $ref actually resolve
jazon := asJSON(t, schema)
assertRefResolve(t, jazon, "", schema)
}
func TestExpand_ResponseSchema(t *testing.T) {
fp := filepath.Join("fixtures", "local_expansion", "spec.json")
b, err := jsonDoc(fp)
require.NoError(t, err)
var spec Swagger
require.NoError(t, json.Unmarshal(b, &spec))
require.NoError(t, ExpandSpec(&spec, &ExpandOptions{RelativeBase: fp}))
// verify that the document is full expanded
jazon := asJSON(t, spec)
assertNoRef(t, jazon)
sch := spec.Paths.Paths["/item"].Get.Responses.StatusCodeResponses[200].Schema
require.NotNil(t, sch)
assert.Empty(t, sch.Ref.String())
assert.Contains(t, sch.Type, "object")
assert.Len(t, sch.Properties, 2)
}
func TestExpand_EmptySpec(t *testing.T) {
spec := new(Swagger)
// expansion of an empty spec
require.NoError(t, ExpandSpec(spec, nil))
// expansion of a nil schema
var schema *Schema
require.NoError(t, ExpandSchemaWithBasePath(schema, nil, nil))
// expansion of a nil paths
var paths *PathItem
resolver := defaultSchemaLoader(spec, nil, nil, nil)
require.NoError(t, expandPathItem(paths, resolver, ""))
// expansion of a nil Parameter
var param *Parameter
require.NoError(t, expandParameterOrResponse(param, resolver, ""))
}
func TestExpand_Spec(t *testing.T) {
// expansion of a rich spec
specPath := filepath.Join("fixtures", "expansion", "all-the-things.json")
specDoc, err := jsonDoc(specPath)
require.NoError(t, err)
opts := &ExpandOptions{
RelativeBase: specPath,
}
spec := new(Swagger)
require.NoError(t, json.Unmarshal(specDoc, spec))
// verify the resulting unmarshaled structure
pet := spec.Definitions["pet"]
errorModel := spec.Definitions["errorModel"]
petResponse := spec.Responses["petResponse"]
petResponse.Schema = &pet
stringResponse := spec.Responses["stringResponse"]
tagParam := spec.Parameters["tag"]
idParam := spec.Parameters["idParam"]
require.NoError(t, ExpandSpec(spec, opts))
// verify that the spec is fully expanded
assertNoRef(t, asJSON(t, spec))
assert.Equal(t, tagParam, spec.Parameters["query"])
assert.Equal(t, petResponse, spec.Responses["petResponse"])
assert.Equal(t, petResponse, spec.Responses["anotherPet"])
assert.Equal(t, pet, *spec.Responses["petResponse"].Schema)
assert.Equal(t, stringResponse, *spec.Paths.Paths["/"].Get.Responses.Default)
assert.Equal(t, petResponse, spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200])
assert.Equal(t, pet, *spec.Paths.Paths["/pets"].Get.Responses.StatusCodeResponses[200].Schema.Items.Schema)
assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Get.Responses.Default.Schema)
assert.Equal(t, pet, spec.Definitions["petInput"].AllOf[0])
assert.Equal(t, spec.Definitions["petInput"], *spec.Paths.Paths["/pets"].Post.Parameters[0].Schema)
assert.Equal(t, petResponse, spec.Paths.Paths["/pets"].Post.Responses.StatusCodeResponses[200])
assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Post.Responses.Default.Schema)
pi := spec.Paths.Paths["/pets/{id}"]
assert.Equal(t, idParam, pi.Get.Parameters[0])
assert.Equal(t, petResponse, pi.Get.Responses.StatusCodeResponses[200])
assert.Equal(t, errorModel, *pi.Get.Responses.Default.Schema)
assert.Equal(t, idParam, pi.Delete.Parameters[0])
assert.Equal(t, errorModel, *pi.Delete.Responses.Default.Schema)
}
func TestExpand_InternalResponse(t *testing.T) {
basePath := normalizeBase(filepath.Join("fixtures", "expansion", "all-the-things.json"))
specDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(specDoc, spec))
resolver := defaultSchemaLoader(spec, nil, nil, nil)
expectedPet := spec.Responses["petResponse"]
require.NoError(t, expandParameterOrResponse(&expectedPet, resolver, basePath))
jazon := asJSON(t, expectedPet)
assert.JSONEq(t, `{
"description": "pet response",
"schema": {
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}`, jazon)
// response pointing to the same target: result is unchanged
another := spec.Responses["anotherPet"]
require.NoError(t, expandParameterOrResponse(&another, resolver, basePath))
assert.Equal(t, expectedPet, another)
defaultResponse := spec.Paths.Paths["/"].Get.Responses.Default
require.NoError(t, expandParameterOrResponse(defaultResponse, resolver, basePath))
expectedString := spec.Responses["stringResponse"]
assert.Equal(t, expectedString, *defaultResponse)
// cascading ref
successResponse := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200]
jazon = asJSON(t, successResponse)
assert.JSONEq(t, `{
"$ref": "#/responses/anotherPet"
}`, jazon)
require.NoError(t, expandParameterOrResponse(&successResponse, resolver, basePath))
assert.Equal(t, expectedPet, successResponse)
}
func TestExpand_Response(t *testing.T) {
basePath := filepath.Join("fixtures", "expansion", "all-the-things.json")
specDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(specDoc, spec))
resp := spec.Responses["anotherPet"]
expected := spec.Responses["petResponse"]
require.NoError(t, ExpandResponse(&expected, basePath))
require.NoError(t, ExpandResponse(&resp, basePath))
assert.Equal(t, expected, resp)
resp2 := spec.Paths.Paths["/"].Get.Responses.Default
expected = spec.Responses["stringResponse"]
require.NoError(t, ExpandResponse(resp2, basePath))
assert.Equal(t, expected, *resp2)
resp = spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200]
expected = spec.Responses["petResponse"]
require.NoError(t, ExpandResponse(&resp, basePath))
assert.Equal(t, expected, resp)
}
func TestExpand_ResponseAndParamWithRoot(t *testing.T) {
specDoc, err := jsonDoc("fixtures/bugs/1614/gitea.json")
require.NoError(t, err)
var spec Swagger
err = json.Unmarshal(specDoc, &spec)
require.NoError(t, err)
// check responses with $ref
resp := spec.Paths.Paths["/admin/users"].Post.Responses.StatusCodeResponses[201]
require.NoError(t, ExpandResponseWithRoot(&resp, spec, nil))
jazon := asJSON(t, resp)
assertNoRef(t, jazon)
resp = spec.Paths.Paths["/admin/users"].Post.Responses.StatusCodeResponses[403]
require.NoError(t, ExpandResponseWithRoot(&resp, spec, nil))
jazon = asJSON(t, resp)
assertNoRef(t, jazon)
// check param with $ref
param := spec.Paths.Paths["/admin/users"].Post.Parameters[0]
require.NoError(t, ExpandParameterWithRoot(¶m, spec, nil))
jazon = asJSON(t, param)
assertNoRef(t, jazon)
}
func TestExpand_InternalParameter(t *testing.T) {
basePath := normalizeBase(filepath.Join("fixtures", "expansion", "params.json"))
paramDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(paramDoc, spec))
resolver := defaultSchemaLoader(spec, nil, nil, nil)
param := spec.Parameters["query"]
expected := spec.Parameters["tag"]
require.NoError(t, expandParameterOrResponse(¶m, resolver, basePath))
assert.Equal(t, expected, param)
param = spec.Paths.Paths["/cars/{id}"].Parameters[0]
expected = spec.Parameters["id"]
require.NoError(t, expandParameterOrResponse(¶m, resolver, basePath))
assert.Equal(t, expected, param)
}
func TestExpand_Parameter(t *testing.T) {
basePath := filepath.Join("fixtures", "expansion", "params.json")
paramDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(paramDoc, spec))
param := spec.Parameters["query"]
expected := spec.Parameters["tag"]
require.NoError(t, ExpandParameter(¶m, basePath))
assert.Equal(t, expected, param)
param = spec.Paths.Paths["/cars/{id}"].Parameters[0]
expected = spec.Parameters["id"]
require.NoError(t, ExpandParameter(¶m, basePath))
assert.Equal(t, expected, param)
}
func TestExpand_JSONSchemaDraft4(t *testing.T) {
fixturePath := filepath.Join("schemas", "jsonschema-draft-04.json")
jazon, sch := expandThisSchemaOrDieTrying(t, fixturePath)
// expansion leaves some circular $ref behind: let's assert those
// assert all $ref match
// "$ref": "http://json-schema.org/draft-04/something"
//
// Case: Circular $refs against schema ID
assertRefInJSONRegexp(t, jazon, `^(http://json-schema.org/draft-04/)|(#/definitions/schemaArray)`)
// verify that all remaining $ref may be expanded in the new root schema
assertRefExpand(t, jazon, "", sch)
}
func TestExpand_SwaggerSchema(t *testing.T) {
fixturePath := filepath.Join("schemas", "v2", "schema.json")
jazon, sch := expandThisSchemaOrDieTrying(t, fixturePath)
// expansion leaves some circular $ref behind: let's assert those
// assert all $ref match
// "$ref": "#/definitions/something"
assertRefInJSON(t, jazon, "#/definitions/")
// verify that all $ref resolve in the new root schema
assertRefExpand(t, jazon, "", sch)
}
func TestExpand_ContinueOnError(t *testing.T) {
specPath := filepath.Join("fixtures", "expansion", "missingRef.json")
defer log.SetOutput(os.Stdout)
log.SetOutput(io.Discard)
// missing $ref in spec
missingRefDoc, err := jsonDoc(specPath)
require.NoError(t, err)
testCase := struct {
Input *Swagger `json:"input"`
Expected *Swagger `json:"expected"`
}{}
require.NoError(t, json.Unmarshal(missingRefDoc, &testCase))
opts := &ExpandOptions{
ContinueOnError: true,
RelativeBase: specPath,
}
require.NoError(t, ExpandSpec(testCase.Input, opts))
assert.Equal(t, testCase.Expected, testCase.Input, "Should continue expanding spec when a definition can't be found.")
// missing $ref in items
doc, err := jsonDoc("fixtures/expansion/missingItemRef.json")
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(doc, spec))
assert.NotPanics(t, func() {
require.NoError(t, ExpandSpec(spec, opts))
}, "Array of missing refs should not cause a panic, and continue to expand spec.")
}
func TestExpand_InternalSchemas2(t *testing.T) {
basePath := normalizeBase(filepath.Join("fixtures", "expansion", "schemas2.json"))
carsDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(carsDoc, spec))
resolver := defaultSchemaLoader(spec, nil, nil, nil)
// verify unmarshaled structure
schema := spec.Definitions["car"]
oldBrand := schema.Properties["brand"]
require.NotEmpty(t, oldBrand.Items.Schema.Ref.String()) // this is a $ref
require.NotEqual(t, spec.Definitions["brand"], oldBrand)
_, err = expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath)
require.NoError(t, err)
// verify expanded schema for Car, in the document passed
newBrand := schema.Properties["brand"]
assert.Empty(t, newBrand.Items.Schema.Ref.String())
assert.Equal(t, spec.Definitions["brand"], *newBrand.Items.Schema)
// verify expanded schema for Truck, in the returned schema
schema = spec.Definitions["truck"]
require.NotEmpty(t, schema.Items.Schema.Ref.String())
s, err := expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Items.Schema.Ref.String()) // no more a $ref
assert.False(t, schema.Items.Schema.Ref.IsRoot()) // no more a $ref
assert.Equal(t, spec.Definitions["car"], *schema.Items.Schema)
sch := new(Schema)
_, err = expandSchema(*sch, []string{""}, resolver, basePath)
require.NoError(t, err)
// verify expanded schema for Batch, in the returned schema
schema = spec.Definitions["batch"]
s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Items.Schema.Items.Schema.Ref.String())
assert.Equal(t, *schema.Items.Schema.Items.Schema, spec.Definitions["brand"])
// verify expanded schema for Batch2, in the returned schema
schema = spec.Definitions["batch2"]
s, err = expandSchema(schema, []string{"#/definitions/batch2"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Items.Schemas[0].Items.Schema.Ref.String())
assert.Empty(t, schema.Items.Schemas[1].Items.Schema.Ref.String())
assert.Equal(t, *schema.Items.Schemas[0].Items.Schema, spec.Definitions["brand"])
assert.Equal(t, *schema.Items.Schemas[1].Items.Schema, spec.Definitions["tag"])
// verify expanded schema for AllOfBoth, in the returned schema [expand allOf]
schema = spec.Definitions["allofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AllOf[0].Items.Schema.Ref.String())
assert.Empty(t, schema.AllOf[1].Items.Schema.Ref.String())
assert.Equal(t, *schema.AllOf[0].Items.Schema, spec.Definitions["brand"])
assert.Equal(t, *schema.AllOf[1].Items.Schema, spec.Definitions["tag"])
// verify expanded schema for AnyOfBoth, in the returned schema [expand anyOf]
schema = spec.Definitions["anyofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AnyOf[0].Items.Schema.Ref.String())
assert.Empty(t, schema.AnyOf[1].Items.Schema.Ref.String())
assert.Equal(t, *schema.AnyOf[0].Items.Schema, spec.Definitions["brand"])
assert.Equal(t, *schema.AnyOf[1].Items.Schema, spec.Definitions["tag"])
// verify expanded schema for OneOfBoth, in the returned schema [expand oneOf]
schema = spec.Definitions["oneofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.OneOf[0].Items.Schema.Ref.String())
assert.Empty(t, schema.OneOf[1].Items.Schema.Ref.String())
assert.Equal(t, *schema.OneOf[0].Items.Schema, spec.Definitions["brand"])
assert.Equal(t, *schema.OneOf[1].Items.Schema, spec.Definitions["tag"])
// verify expanded schema for NotSomething, in the returned schema [expand not]
schema = spec.Definitions["notSomething"]
s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Not.Items.Schema.Ref.String())
assert.Equal(t, *schema.Not.Items.Schema, spec.Definitions["tag"])
// verify expanded schema for WithAdditional, in the returned schema [expand additionalProperties]
schema = spec.Definitions["withAdditional"]
s, err = expandSchema(schema, []string{"#/definitions/withAdditional"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AdditionalProperties.Schema.Items.Schema.Ref.String())
assert.Equal(t, *schema.AdditionalProperties.Schema.Items.Schema, spec.Definitions["tag"])
// verify expanded schema for WithAdditionalItems, in the returned schema [expand additionalItems]
schema = spec.Definitions["withAdditionalItems"]
s, err = expandSchema(schema, []string{"#/definitions/withAdditionalItems"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AdditionalItems.Schema.Items.Schema.Ref.String())
assert.Equal(t, *schema.AdditionalItems.Schema.Items.Schema, spec.Definitions["tag"])
// verify expanded schema for WithPattern, in the returned schema [expand PatternProperties]
schema = spec.Definitions["withPattern"]
s, err = expandSchema(schema, []string{"#/definitions/withPattern"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop := schema.PatternProperties["^x-ab"]
assert.Empty(t, prop.Items.Schema.Ref.String())
assert.Equal(t, *prop.Items.Schema, spec.Definitions["tag"])
// verify expanded schema for Deps, in the returned schema [expand dependencies]
schema = spec.Definitions["deps"]
s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop2 := schema.Dependencies["something"]
assert.Empty(t, prop2.Schema.Items.Schema.Ref.String())
assert.Equal(t, *prop2.Schema.Items.Schema, spec.Definitions["tag"])
// verify expanded schema for Defined, in the returned schema [expand nested definitions]
schema = spec.Definitions["defined"]
s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop = schema.Definitions["something"]
assert.Empty(t, prop.Items.Schema.Ref.String())
assert.Equal(t, *prop.Items.Schema, spec.Definitions["tag"])
}
func TestExpand_InternalSchemas1(t *testing.T) {
basePath := normalizeBase(filepath.Join("fixtures", "expansion", "schemas1.json"))
carsDoc, err := jsonDoc(basePath)
require.NoError(t, err)
spec := new(Swagger)
require.NoError(t, json.Unmarshal(carsDoc, spec))
resolver := defaultSchemaLoader(spec, nil, nil, nil)
schema := spec.Definitions["car"]
oldBrand := schema.Properties["brand"]
assert.NotEmpty(t, oldBrand.Ref.String())
assert.NotEqual(t, spec.Definitions["brand"], oldBrand)
s, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
newBrand := schema.Properties["brand"]
assert.Empty(t, newBrand.Ref.String())
assert.Equal(t, spec.Definitions["brand"], newBrand)
schema = spec.Definitions["truck"]
assert.NotEmpty(t, schema.Ref.String())
s, err = expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Ref.String())
assert.Equal(t, spec.Definitions["car"], schema)
sch := new(Schema)
_, err = expandSchema(*sch, []string{""}, resolver, basePath)
require.NoError(t, err)
schema = spec.Definitions["batch"]
s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Items.Schema.Ref.String())
assert.Equal(t, *schema.Items.Schema, spec.Definitions["brand"])
schema = spec.Definitions["batch2"]
s, err = expandSchema(schema, []string{"#/definitions/batch2"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Items.Schemas[0].Ref.String())
assert.Empty(t, schema.Items.Schemas[1].Ref.String())
assert.Equal(t, schema.Items.Schemas[0], spec.Definitions["brand"])
assert.Equal(t, schema.Items.Schemas[1], spec.Definitions["tag"])
schema = spec.Definitions["allofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AllOf[0].Ref.String())
assert.Empty(t, schema.AllOf[1].Ref.String())
assert.Equal(t, schema.AllOf[0], spec.Definitions["brand"])
assert.Equal(t, schema.AllOf[1], spec.Definitions["tag"])
schema = spec.Definitions["anyofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AnyOf[0].Ref.String())
assert.Empty(t, schema.AnyOf[1].Ref.String())
assert.Equal(t, schema.AnyOf[0], spec.Definitions["brand"])
assert.Equal(t, schema.AnyOf[1], spec.Definitions["tag"])
schema = spec.Definitions["oneofBoth"]
s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.OneOf[0].Ref.String())
assert.Empty(t, schema.OneOf[1].Ref.String())
assert.Equal(t, schema.OneOf[0], spec.Definitions["brand"])
assert.Equal(t, schema.OneOf[1], spec.Definitions["tag"])
schema = spec.Definitions["notSomething"]
s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.Not.Ref.String())
assert.Equal(t, *schema.Not, spec.Definitions["tag"])
schema = spec.Definitions["withAdditional"]
s, err = expandSchema(schema, []string{"#/definitions/withAdditional"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AdditionalProperties.Schema.Ref.String())
assert.Equal(t, *schema.AdditionalProperties.Schema, spec.Definitions["tag"])
schema = spec.Definitions["withAdditionalItems"]
s, err = expandSchema(schema, []string{"#/definitions/withAdditionalItems"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
assert.Empty(t, schema.AdditionalItems.Schema.Ref.String())
assert.Equal(t, *schema.AdditionalItems.Schema, spec.Definitions["tag"])
schema = spec.Definitions["withPattern"]
s, err = expandSchema(schema, []string{"#/definitions/withPattern"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop := schema.PatternProperties["^x-ab"]
assert.Empty(t, prop.Ref.String())
assert.Equal(t, prop, spec.Definitions["tag"])
schema = spec.Definitions["deps"]
s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop2 := schema.Dependencies["something"]
assert.Empty(t, prop2.Schema.Ref.String())
assert.Equal(t, *prop2.Schema, spec.Definitions["tag"])
schema = spec.Definitions["defined"]
s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath)
require.NoError(t, err)
require.NotNil(t, s)
schema = *s
prop = schema.Definitions["something"]
assert.Empty(t, prop.Ref.String())
assert.Equal(t, prop, spec.Definitions["tag"])
}
func TestExpand_RelativeBaseURI(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir("fixtures/remote")))
defer server.Close()
spec := new(Swagger)
require.NoError(t, ExpandSpec(spec, nil))
// this a spec on local fs...
specDoc, err := jsonDoc("fixtures/remote/all-the-things.json")
require.NoError(t, err)
spec = new(Swagger)
require.NoError(t, json.Unmarshal(specDoc, spec))
pet := spec.Definitions["pet"]
errorModel := spec.Definitions["errorModel"]
petResponse := spec.Responses["petResponse"]
petResponse.Schema = &pet
stringResponse := spec.Responses["stringResponse"]
tagParam := spec.Parameters["tag"]
idParam := spec.Parameters["idParam"]
anotherPet := spec.Responses["anotherPet"]
// ... with some $ref with http scheme
anotherPet.Ref = MustCreateRef(server.URL + "/" + anotherPet.Ref.String())
opts := &ExpandOptions{
RelativeBase: server.URL + "/all-the-things.json",
}
require.NoError(t, ExpandResponse(&anotherPet, opts.RelativeBase))
spec.Responses["anotherPet"] = anotherPet
circularA := spec.Responses["circularA"]
circularA.Ref = MustCreateRef(server.URL + "/" + circularA.Ref.String())
require.NoError(t, ExpandResponse(&circularA, opts.RelativeBase))
// circularA is self-referencing: results in an empty response: this is okay and expected
// for the expand use case. The flatten use case should however be expected to fail on this.
assert.Empty(t, circularA.Description)
assert.Empty(t, circularA.Ref)
spec.Responses["circularA"] = circularA
// expand again, no issue should arise
require.NoError(t, ExpandSpec(spec, opts))
// backRef navigates back to the root document (relative $ref)
backRef := spec.Responses["backRef"]
require.NoError(t, ExpandResponse(&backRef, opts.RelativeBase))
assert.Equal(t, "pet response", backRef.Description)
assert.NotEmpty(t, backRef.Schema)
assert.Empty(t, backRef.Ref)
assert.Equal(t, tagParam, spec.Parameters["query"])
assert.Equal(t, petResponse, spec.Responses["petResponse"])
assert.Equal(t, petResponse, spec.Responses["anotherPet"])
assert.Equal(t, petResponse, spec.Paths.Paths["/pets"].Post.Responses.StatusCodeResponses[200])
assert.Equal(t, petResponse, spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200])
assert.Equal(t, pet, *spec.Paths.Paths["/pets"].Get.Responses.StatusCodeResponses[200].Schema.Items.Schema)
assert.Equal(t, pet, *spec.Responses["petResponse"].Schema)
assert.Equal(t, pet, spec.Definitions["petInput"].AllOf[0])
assert.Equal(t, spec.Definitions["petInput"], *spec.Paths.Paths["/pets"].Post.Parameters[0].Schema)
assert.Equal(t, stringResponse, *spec.Paths.Paths["/"].Get.Responses.Default)
assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Get.Responses.Default.Schema)
assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Post.Responses.Default.Schema)
pi := spec.Paths.Paths["/pets/{id}"]
assert.Equal(t, idParam, pi.Get.Parameters[0])
assert.Equal(t, petResponse, pi.Get.Responses.StatusCodeResponses[200])
assert.Equal(t, idParam, pi.Delete.Parameters[0])
assert.Equal(t, errorModel, *pi.Get.Responses.Default.Schema)
assert.Equal(t, errorModel, *pi.Delete.Responses.Default.Schema)
}
func resolutionContextServer() *httptest.Server {
var servedAt string
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/resolution.json" {
b, _ := os.ReadFile(filepath.Join(specs, "resolution.json"))
var ctnt map[string]interface{}
_ = json.Unmarshal(b, &ctnt)
ctnt["id"] = servedAt
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
bb, _ := json.Marshal(ctnt)
_, _ = rw.Write(bb)
return
}
if req.URL.Path == "/resolution2.json" {
b, _ := os.ReadFile(filepath.Join(specs, "resolution2.json"))
var ctnt map[string]interface{}
_ = json.Unmarshal(b, &ctnt)
ctnt["id"] = servedAt
rw.Header().Set("Content-Type", "application/json")
bb, _ := json.Marshal(ctnt)
_, _ = rw.Write(bb)
return
}
if req.URL.Path == "/boolProp.json" {
rw.Header().Set("Content-Type", "application/json")
b, _ := json.Marshal(map[string]interface{}{
"type": "boolean",
})
_, _ = rw.Write(b)
return
}
if req.URL.Path == "/deeper/stringProp.json" {
rw.Header().Set("Content-Type", "application/json")
b, _ := json.Marshal(map[string]interface{}{
"type": "string",
})
_, _ = rw.Write(b)
return
}
if req.URL.Path == "/deeper/arrayProp.json" {
rw.Header().Set("Content-Type", "application/json")
b, _ := json.Marshal(map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "file",
},
})
_, _ = rw.Write(b)
return
}
rw.WriteHeader(http.StatusNotFound)
}))
servedAt = server.URL
return server
}
func TestExpand_RemoteRefWithResolutionContext(t *testing.T) {
server := resolutionContextServer()
defer server.Close()
tgt := RefSchema(server.URL + "/resolution.json#/definitions/bool")
require.NoError(t, ExpandSchema(tgt, nil, nil))
assert.Equal(t, StringOrArray([]string{"boolean"}), tgt.Type)
assert.Empty(t, tgt.Ref)
}
func TestExpandRemoteRef_WithNestedResolutionContext(t *testing.T) {
server := resolutionContextServer()
defer server.Close()
tgt := RefSchema(server.URL + "/resolution.json#/items")
require.NoError(t, ExpandSchema(tgt, nil, nil))
require.Empty(t, tgt.Ref)
require.NotNil(t, tgt.Items)
require.NotNil(t, tgt.Schema)
assert.Equal(t, "deeper/", tgt.ID) // schema id is preserved
assert.Equal(t, StringOrArray([]string{"string"}), tgt.Items.Schema.Type)
assert.Empty(t, tgt.Items.Schema.Ref)
}
/*
This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced
// For now, TestExpandRemoteRef_WithNestedResolutionContext replaces this next test
func TestExpandRemoteRef_WithNestedResolutionContext_WithParentID(t *testing.T) {
server := resolutionContextServer()
defer server.Close()
tgt := RefSchema(server.URL + "/resolution.json#/items/items")
require.NoError(t, ExpandSchema(tgt, nil, nil))
t.Logf("%s", asJSON(t, tgt))
assert.Equal(t, StringOrArray([]string{"string"}), tgt.Type)
}
*/
func TestExpand_RemoteRefWithNestedResolutionContextWithFragment(t *testing.T) {
server := resolutionContextServer()
defer server.Close()
tgt := RefSchema(server.URL + "/resolution2.json#/items")
require.NoError(t, ExpandSchema(tgt, nil, nil))
require.Empty(t, tgt.Ref)
require.NotNil(t, tgt.Items)
require.NotNil(t, tgt.Schema)
assert.Equal(t, "deeper/", tgt.ID) // schema id is preserved
assert.Equal(t, StringOrArray([]string{"file"}), tgt.Items.Schema.Type)
assert.Empty(t, tgt.Items.Schema.Ref)
}
func TestExpand_TransitiveRefs(t *testing.T) {
basePath := filepath.Join(specs, "todos.json")
rawSpec, err := os.ReadFile(basePath)
require.NoError(t, err)
var spec *Swagger
require.NoError(t, json.Unmarshal(rawSpec, &spec))
opts := &ExpandOptions{
RelativeBase: basePath,
}
require.NoError(t, ExpandSpec(spec, opts))
assert.Equal(t, "todos.stoplight.io", spec.Host) // i.e. not empty
jazon := asJSON(t, spec)
// verify that the spec has been fully expanded
assertNoRef(t, jazon)
}
func TestExpand_SchemaWithRoot(t *testing.T) {
root := new(Swagger)
require.NoError(t, json.Unmarshal(PetStoreJSONMessage, root))
// 1. remove ID from root definition
origPet := root.Definitions["Pet"]
newPet := origPet
newPet.ID = ""
root.Definitions["Pet"] = newPet
expandRootWithID(t, root, withoutSchemaID)
// 2. put back ID in Pet definition
// nested $ref should fail
root.Definitions["Pet"] = origPet
expandRootWithID(t, root, withSchemaID)
}
func expandRootWithID(t testing.TB, root *Swagger, testcase string) {
t.Logf("case: expanding $ref to schema without ID, with nested $ref with %s ID", testcase)
sch := RefSchema("#/definitions/newPet")
err := ExpandSchema(sch, root, nil)
if testcase == withSchemaID {
require.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String())
} else {
require.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String())
// verify that the spec has been fully expanded
jazon := asJSON(t, sch)
assertNoRef(t, jazon)
}
t.Log("case: expanding $ref to schema without nested $ref")
sch = RefSchema("#/definitions/Category")
require.NoErrorf(t, ExpandSchema(sch, root, nil), "expected %s to expand properly", sch.Ref.String())
t.Logf("case: expanding $ref to schema with %s ID and nested $ref", testcase)
sch = RefSchema("#/definitions/Pet")
err = ExpandSchema(sch, root, nil)
if testcase == withSchemaID {
require.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String())
} else {
require.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String())
// verify that the spec has been fully expanded
jazon := asJSON(t, sch)
assertNoRef(t, jazon)
}
}
func TestExpand_PathItem(t *testing.T) {
jazon, _ := expandThisOrDieTrying(t, pathItemsFixture)
assert.JSONEq(t, `{
"swagger": "2.0",
"info": {
"title": "PathItems refs",
"version": "1.0"
},
"paths": {
"/todos": {
"get": {
"responses": {
"200": {
"description": "List Todos",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"404": {
"description": "error"
}
}
}
}
}
}`, jazon)
}
func TestExpand_ExtraItems(t *testing.T) {
jazon, _ := expandThisOrDieTrying(t, extraRefFixture)
assert.JSONEq(t, `{
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"title": "Supported, but non Swagger 20 compliant $ref constructs",
"version": "2.1.0"
},
"host": "item.com",
"basePath": "/extraRefs",
"paths": {
"/employees": {
"get": {
"summary": "List Employee Types",
"operationId": "LIST-Employees",
"parameters": [
{
"description": "unsupported $ref in simple param",
"type": "array",
"items": {
"$ref": "#/definitions/arrayType"
},
"name": "myQueryParam",
"in": "query"
}
],
"responses": {
"200": {
"description": "unsupported $ref in header",
"schema": {
"type": "string"
},
"headers": {
"X-header": {
"type": "array",
"items": {
"$ref": "#/definitions/headerType"
}
}
}
}
}
}
}
},
"definitions": {
"arrayType": {
"type": "integer",
"format": "int32"
},
"headerType": {
"type": "string",
"format": "uuid"
}
}
}`, jazon)
}
func TestExpand_Issue145(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
pseudoRoot := normalizeBase(filepath.Join(cwd, rootBase))
// assert the internal behavior of baseForRoot()
t.Run("with nil root, empty cache", func(t *testing.T) {
cache := defaultResolutionCache()
require.Equal(t, pseudoRoot, baseForRoot(nil, cache))
t.Run("empty root is cached", func(t *testing.T) {
value, ok := cache.Get(pseudoRoot)
require.True(t, ok) // found in cache
asMap, ok := value.(map[string]interface{})
require.True(t, ok)
require.Empty(t, asMap)
})
})
t.Run("with non-nil root, empty cache", func(t *testing.T) {
cache := defaultResolutionCache()
require.Equal(t, pseudoRoot, baseForRoot(map[string]interface{}{"key": "arbitrary"}, cache))
t.Run("non-empty root is cached", func(t *testing.T) {
value, ok := cache.Get(pseudoRoot)
require.True(t, ok) // found in cache
asMap, ok := value.(map[string]interface{})
require.True(t, ok)
require.Contains(t, asMap, "key")
require.Equal(t, "arbitrary", asMap["key"])
})
t.Run("with nil root, non-empty cache", func(t *testing.T) {
require.Equal(t, pseudoRoot, baseForRoot(nil, cache))
t.Run("non-empty root is kept", func(t *testing.T) {
value, ok := cache.Get(pseudoRoot)
require.True(t, ok) // found in cache
asMap, ok := value.(map[string]interface{})
require.True(t, ok)
require.Contains(t, asMap, "key")
require.Equal(t, "arbitrary", asMap["key"])
})
})
})
}
// PetStore20 json doc for swagger 2.0 pet store
const PetStore20 = `{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"contact": {
"name": "Wordnik API Team",
"url": "http://developer.wordnik.com"
},
"license": {
"name": "Creative Commons 4.0 International",
"url": "http://creativecommons.org/licenses/by/4.0/"
}
},
"host": "petstore.swagger.wordnik.com",
"basePath": "/api",
"schemes": [
"http"
],
"paths": {
"/pets": {
"get": {
"security": [
{
"basic": []
}
],
"tags": [ "Pet Operations" ],
"operationId": "getAllPets",
"parameters": [
{
"name": "status",
"in": "query",
"description": "The status to filter by",
"type": "string"
},
{
"name": "limit",
"in": "query",
"description": "The maximum number of results to return",
"type": "integer",
"format": "int64"
}
],
"summary": "Finds all pets in the system",
"responses": {
"200": {
"description": "Pet response",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
}
},
"default": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"post": {
"security": [
{
"basic": []
}
],
"tags": [ "Pet Operations" ],
"operationId": "createPet",
"summary": "Creates a new pet",
"consumes": ["application/x-yaml"],
"produces": ["application/x-yaml"],
"parameters": [
{
"name": "pet",
"in": "body",
"description": "The Pet to create",
"required": true,
"schema": {
"$ref": "#/definitions/newPet"
}
}
],
"responses": {
"200": {
"description": "Created Pet response",
"schema": {
"$ref": "#/definitions/Pet"
}
},
"default": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/pets/{id}": {
"delete": {
"security": [
{
"apiKey": []
}
],
"description": "Deletes the Pet by id",
"operationId": "deletePet",
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of pet to delete",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"204": {
"description": "pet deleted"
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"get": {
"tags": [ "Pet Operations" ],
"operationId": "getPetById",
"summary": "Finds the pet by id",
"responses": {
"200": {
"description": "Pet response",
"schema": {
"$ref": "#/definitions/Pet"
}
},
"default": {
"description": "Unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"description": "ID of pet",
"required": true,
"type": "integer",
"format": "int64"
}
]
}
},
"definitions": {
"Category": {
"id": "Category",
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"Pet": {
"id": "Pet",
"properties": {
"category": {
"$ref": "#/definitions/Category"
},
"id": {
"description": "unique identifier for the pet",
"format": "int64",
"maximum": 100.0,
"minimum": 0.0,
"type": "integer"
},
"name": {
"type": "string"
},
"photoUrls": {
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
],
"type": "string"
},
"tags": {
"items": {
"$ref": "#/definitions/Tag"
},
"type": "array"
}
},
"required": [
"id",
"name"
]
},
"newPet": {
"anyOf": [
{
"$ref": "#/definitions/Pet"
},
{
"required": [
"name"
]
}
]
},
"Tag": {
"id": "Tag",
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
},
"consumes": [
"application/json",
"application/xml"
],
"produces": [
"application/json",
"application/xml",
"text/plain",
"text/html"
],
"securityDefinitions": {
"basic": {
"type": "basic"
},
"apiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-KEY"
}
}
}
`