Skip to content

Commit 1013da3

Browse files
authored
Fix OpenAPI 3 validation: operationId must be unique (#504)
1 parent dfd16a7 commit 1013da3

File tree

3 files changed

+105
-10
lines changed

3 files changed

+105
-10
lines changed

openapi3/paths.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ func (value Paths) Validate(ctx context.Context) error {
8282
return err
8383
}
8484
}
85+
86+
if err := value.validateUniqueOperationIDs(); err != nil {
87+
return err
88+
}
89+
8590
return nil
8691
}
8792

@@ -114,6 +119,30 @@ func (paths Paths) Find(key string) *PathItem {
114119
return nil
115120
}
116121

122+
func (value Paths) validateUniqueOperationIDs() error {
123+
operationIDs := make(map[string]string)
124+
for urlPath, pathItem := range value {
125+
if pathItem == nil {
126+
continue
127+
}
128+
for httpMethod, operation := range pathItem.Operations() {
129+
if operation == nil || operation.OperationID == "" {
130+
continue
131+
}
132+
endpoint := httpMethod + " " + urlPath
133+
if endpointDup, ok := operationIDs[operation.OperationID]; ok {
134+
if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
135+
endpoint, endpointDup = endpointDup, endpoint
136+
}
137+
return fmt.Errorf("operations %q and %q have the same operation id %q",
138+
endpoint, endpointDup, operation.OperationID)
139+
}
140+
operationIDs[operation.OperationID] = endpoint
141+
}
142+
}
143+
return nil
144+
}
145+
117146
func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
118147
if strings.IndexByte(path, '{') < 0 {
119148
return path, 0, nil

openapi3/paths_test.go

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,88 @@ import (
77
"github.com/stretchr/testify/require"
88
)
99

10-
var emptyPathSpec = `
10+
func TestPathsValidate(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
spec string
14+
wantErr string
15+
}{
16+
{
17+
name: "ok, empty paths",
18+
spec: `
1119
openapi: "3.0.0"
1220
info:
1321
version: 1.0.0
1422
title: Swagger Petstore
1523
license:
1624
name: MIT
17-
servers:
18-
- url: http://petstore.swagger.io/v1
1925
paths:
2026
/pets:
21-
`
27+
`,
28+
},
29+
{
30+
name: "operation ids are not unique, same path",
31+
spec: `
32+
openapi: "3.0.0"
33+
info:
34+
version: 1.0.0
35+
title: Swagger Petstore
36+
license:
37+
name: MIT
38+
paths:
39+
/pets:
40+
post:
41+
operationId: createPet
42+
responses:
43+
201:
44+
description: "entity created"
45+
delete:
46+
operationId: createPet
47+
responses:
48+
204:
49+
description: "entity deleted"
50+
`,
51+
wantErr: `operations "DELETE /pets" and "POST /pets" have the same operation id "createPet"`,
52+
},
53+
{
54+
name: "operation ids are not unique, different paths",
55+
spec: `
56+
openapi: "3.0.0"
57+
info:
58+
version: 1.0.0
59+
title: Swagger Petstore
60+
license:
61+
name: MIT
62+
paths:
63+
/pets:
64+
post:
65+
operationId: createPet
66+
responses:
67+
201:
68+
description: "entity created"
69+
/users:
70+
post:
71+
operationId: createPet
72+
responses:
73+
201:
74+
description: "entity created"
75+
`,
76+
wantErr: `operations "POST /pets" and "POST /users" have the same operation id "createPet"`,
77+
},
78+
}
79+
80+
for i := range tests {
81+
tt := tests[i]
82+
t.Run(tt.name, func(t *testing.T) {
83+
doc, err := NewLoader().LoadFromData([]byte(tt.spec))
84+
require.NoError(t, err)
2285

23-
func TestPathValidate(t *testing.T) {
24-
doc, err := NewLoader().LoadFromData([]byte(emptyPathSpec))
25-
require.NoError(t, err)
26-
err = doc.Paths.Validate(context.Background())
27-
require.NoError(t, err)
86+
err = doc.Paths.Validate(context.Background())
87+
if tt.wantErr == "" {
88+
require.NoError(t, err)
89+
return
90+
}
91+
require.Equal(t, tt.wantErr, err.Error())
92+
})
93+
}
2894
}

openapi3filter/fixtures/petstore.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
],
122122
"summary": "Add a new pet to the store",
123123
"description": "",
124-
"operationId": "addPet",
124+
"operationId": "addPet2",
125125
"responses": {
126126
"405": {
127127
"description": "Invalid input"

0 commit comments

Comments
 (0)