openapi_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package openapi
  14. import (
  15. "fmt"
  16. "net/http"
  17. "testing"
  18. "github.com/emicklei/go-restful"
  19. "github.com/emicklei/go-restful/swagger"
  20. "github.com/go-openapi/spec"
  21. "github.com/stretchr/testify/assert"
  22. "sort"
  23. )
  24. // setUp is a convenience function for setting up for (most) tests.
  25. func setUp(t *testing.T, fullMethods bool) (openAPI, *assert.Assertions) {
  26. assert := assert.New(t)
  27. config := Config{
  28. SwaggerConfig: getSwaggerConfig(fullMethods),
  29. Info: &spec.Info{
  30. InfoProps: spec.InfoProps{
  31. Title: "TestAPI",
  32. Description: "Test API",
  33. },
  34. },
  35. }
  36. return openAPI{config: &config}, assert
  37. }
  38. func noOp(request *restful.Request, response *restful.Response) {}
  39. type TestInput struct {
  40. Name string `json:"name,omitempty"`
  41. ID int `json:"id,omitempty"`
  42. Tags []string `json:"tags,omitempty"`
  43. }
  44. type TestOutput struct {
  45. Name string `json:"name,omitempty"`
  46. Count int `json:"count,omitempty"`
  47. }
  48. func (t TestInput) SwaggerDoc() map[string]string {
  49. return map[string]string{
  50. "": "Test input",
  51. "name": "Name of the input",
  52. "id": "ID of the input",
  53. }
  54. }
  55. func (t TestOutput) SwaggerDoc() map[string]string {
  56. return map[string]string{
  57. "": "Test output",
  58. "name": "Name of the output",
  59. "count": "Number of outputs",
  60. }
  61. }
  62. func getTestRoute(ws *restful.WebService, method string, additionalParams bool) *restful.RouteBuilder {
  63. ret := ws.Method(method).
  64. Path("/test/{path:*}").
  65. Doc(fmt.Sprintf("%s test input", method)).
  66. Operation(fmt.Sprintf("%sTestInput", method)).
  67. Produces(restful.MIME_JSON).
  68. Consumes(restful.MIME_JSON).
  69. Param(ws.PathParameter("path", "path to the resource").DataType("string")).
  70. Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
  71. Reads(TestInput{}).
  72. Returns(200, "OK", TestOutput{}).
  73. Writes(TestOutput{}).
  74. To(noOp)
  75. if additionalParams {
  76. ret.Param(ws.HeaderParameter("hparam", "a test head parameter").DataType("integer"))
  77. ret.Param(ws.FormParameter("fparam", "a test form parameter").DataType("number"))
  78. }
  79. return ret
  80. }
  81. func getSwaggerConfig(fullMethods bool) *swagger.Config {
  82. mux := http.NewServeMux()
  83. container := restful.NewContainer()
  84. container.ServeMux = mux
  85. ws := new(restful.WebService)
  86. ws.Path("/foo")
  87. ws.Route(getTestRoute(ws, "get", true))
  88. if fullMethods {
  89. ws.Route(getTestRoute(ws, "post", false)).
  90. Route(getTestRoute(ws, "put", false)).
  91. Route(getTestRoute(ws, "head", false)).
  92. Route(getTestRoute(ws, "patch", false)).
  93. Route(getTestRoute(ws, "options", false)).
  94. Route(getTestRoute(ws, "delete", false))
  95. }
  96. ws.Path("/bar")
  97. ws.Route(getTestRoute(ws, "get", true))
  98. if fullMethods {
  99. ws.Route(getTestRoute(ws, "post", false)).
  100. Route(getTestRoute(ws, "put", false)).
  101. Route(getTestRoute(ws, "head", false)).
  102. Route(getTestRoute(ws, "patch", false)).
  103. Route(getTestRoute(ws, "options", false)).
  104. Route(getTestRoute(ws, "delete", false))
  105. }
  106. container.Add(ws)
  107. return &swagger.Config{
  108. WebServicesUrl: "https://test-server",
  109. WebServices: container.RegisteredWebServices(),
  110. }
  111. }
  112. func getTestOperation(method string) *spec.Operation {
  113. return &spec.Operation{
  114. OperationProps: spec.OperationProps{
  115. Description: fmt.Sprintf("%s test input", method),
  116. Consumes: []string{"application/json"},
  117. Produces: []string{"application/json"},
  118. Schemes: []string{"https"},
  119. Parameters: []spec.Parameter{},
  120. Responses: getTestResponses(),
  121. },
  122. }
  123. }
  124. func getTestPathItem(allMethods bool) spec.PathItem {
  125. ret := spec.PathItem{
  126. PathItemProps: spec.PathItemProps{
  127. Get: getTestOperation("get"),
  128. Parameters: getTestCommonParameters(),
  129. },
  130. }
  131. ret.Get.Parameters = getAdditionalTestParameters()
  132. if allMethods {
  133. ret.PathItemProps.Put = getTestOperation("put")
  134. ret.PathItemProps.Post = getTestOperation("post")
  135. ret.PathItemProps.Head = getTestOperation("head")
  136. ret.PathItemProps.Patch = getTestOperation("patch")
  137. ret.PathItemProps.Delete = getTestOperation("delete")
  138. ret.PathItemProps.Options = getTestOperation("options")
  139. }
  140. return ret
  141. }
  142. func getRefSchema(ref string) *spec.Schema {
  143. return &spec.Schema{
  144. SchemaProps: spec.SchemaProps{
  145. Ref: spec.MustCreateRef(ref),
  146. },
  147. }
  148. }
  149. func getTestResponses() *spec.Responses {
  150. ret := spec.Responses{
  151. ResponsesProps: spec.ResponsesProps{
  152. StatusCodeResponses: map[int]spec.Response{},
  153. },
  154. }
  155. ret.StatusCodeResponses[200] = spec.Response{
  156. ResponseProps: spec.ResponseProps{
  157. Description: "OK",
  158. Schema: getRefSchema("#/definitions/openapi.TestOutput"),
  159. },
  160. }
  161. return &ret
  162. }
  163. func getTestCommonParameters() []spec.Parameter {
  164. ret := make([]spec.Parameter, 3)
  165. ret[0] = spec.Parameter{
  166. ParamProps: spec.ParamProps{
  167. Name: "body",
  168. In: "body",
  169. Required: true,
  170. Schema: getRefSchema("#/definitions/openapi.TestInput"),
  171. },
  172. }
  173. ret[1] = spec.Parameter{
  174. SimpleSchema: spec.SimpleSchema{
  175. Type: "string",
  176. },
  177. ParamProps: spec.ParamProps{
  178. Description: "path to the resource",
  179. Name: "path",
  180. In: "path",
  181. Required: true,
  182. },
  183. CommonValidations: spec.CommonValidations{
  184. UniqueItems: true,
  185. },
  186. }
  187. ret[2] = spec.Parameter{
  188. SimpleSchema: spec.SimpleSchema{
  189. Type: "string",
  190. },
  191. ParamProps: spec.ParamProps{
  192. Description: "If 'true', then the output is pretty printed.",
  193. Name: "pretty",
  194. In: "query",
  195. },
  196. CommonValidations: spec.CommonValidations{
  197. UniqueItems: true,
  198. },
  199. }
  200. return ret
  201. }
  202. func getAdditionalTestParameters() []spec.Parameter {
  203. ret := make([]spec.Parameter, 2)
  204. ret[0] = spec.Parameter{
  205. ParamProps: spec.ParamProps{
  206. Name: "fparam",
  207. Description: "a test form parameter",
  208. In: "form",
  209. },
  210. SimpleSchema: spec.SimpleSchema{
  211. Type: "number",
  212. },
  213. CommonValidations: spec.CommonValidations{
  214. UniqueItems: true,
  215. },
  216. }
  217. ret[1] = spec.Parameter{
  218. SimpleSchema: spec.SimpleSchema{
  219. Type: "integer",
  220. },
  221. ParamProps: spec.ParamProps{
  222. Description: "a test head parameter",
  223. Name: "hparam",
  224. In: "header",
  225. },
  226. CommonValidations: spec.CommonValidations{
  227. UniqueItems: true,
  228. },
  229. }
  230. return ret
  231. }
  232. type Parameters []spec.Parameter
  233. func (s Parameters) Len() int { return len(s) }
  234. func (s Parameters) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
  235. type ByName struct {
  236. Parameters
  237. }
  238. func (s ByName) Less(i, j int) bool {
  239. return s.Parameters[i].Name < s.Parameters[j].Name
  240. }
  241. // TODO(mehdy): Consider sort parameters in actual spec generation for more predictable spec generation
  242. func sortParameters(s *spec.Swagger) *spec.Swagger {
  243. for k, p := range s.Paths.Paths {
  244. sort.Sort(ByName{p.Parameters})
  245. sort.Sort(ByName{p.Get.Parameters})
  246. sort.Sort(ByName{p.Put.Parameters})
  247. sort.Sort(ByName{p.Post.Parameters})
  248. sort.Sort(ByName{p.Head.Parameters})
  249. sort.Sort(ByName{p.Delete.Parameters})
  250. sort.Sort(ByName{p.Options.Parameters})
  251. sort.Sort(ByName{p.Patch.Parameters})
  252. s.Paths.Paths[k] = p // Unnecessary?! Magic!!!
  253. }
  254. return s
  255. }
  256. func getTestInputDefinition() spec.Schema {
  257. return spec.Schema{
  258. SchemaProps: spec.SchemaProps{
  259. Description: "Test input",
  260. Required: []string{},
  261. Properties: map[string]spec.Schema{
  262. "id": {
  263. SchemaProps: spec.SchemaProps{
  264. Description: "ID of the input",
  265. Type: spec.StringOrArray{"integer"},
  266. Format: "int32",
  267. Enum: []interface{}{},
  268. },
  269. },
  270. "name": {
  271. SchemaProps: spec.SchemaProps{
  272. Description: "Name of the input",
  273. Type: spec.StringOrArray{"string"},
  274. Enum: []interface{}{},
  275. },
  276. },
  277. "tags": {
  278. SchemaProps: spec.SchemaProps{
  279. Type: spec.StringOrArray{"array"},
  280. Enum: []interface{}{},
  281. Items: &spec.SchemaOrArray{
  282. Schema: &spec.Schema{
  283. SchemaProps: spec.SchemaProps{
  284. Type: spec.StringOrArray{"string"},
  285. },
  286. },
  287. },
  288. },
  289. },
  290. },
  291. },
  292. }
  293. }
  294. func getTestOutputDefinition() spec.Schema {
  295. return spec.Schema{
  296. SchemaProps: spec.SchemaProps{
  297. Description: "Test output",
  298. Required: []string{},
  299. Properties: map[string]spec.Schema{
  300. "count": {
  301. SchemaProps: spec.SchemaProps{
  302. Description: "Number of outputs",
  303. Type: spec.StringOrArray{"integer"},
  304. Format: "int32",
  305. Enum: []interface{}{},
  306. },
  307. },
  308. "name": {
  309. SchemaProps: spec.SchemaProps{
  310. Description: "Name of the output",
  311. Type: spec.StringOrArray{"string"},
  312. Enum: []interface{}{},
  313. },
  314. },
  315. },
  316. },
  317. }
  318. }
  319. func TestBuildSwaggerSpec(t *testing.T) {
  320. o, assert := setUp(t, true)
  321. expected := &spec.Swagger{
  322. SwaggerProps: spec.SwaggerProps{
  323. Info: &spec.Info{
  324. InfoProps: spec.InfoProps{
  325. Title: "TestAPI",
  326. Description: "Test API",
  327. },
  328. },
  329. Swagger: "2.0",
  330. Paths: &spec.Paths{
  331. Paths: map[string]spec.PathItem{
  332. "/foo/test/{path}": getTestPathItem(true),
  333. "/bar/test/{path}": getTestPathItem(true),
  334. },
  335. },
  336. Definitions: spec.Definitions{
  337. "openapi.TestInput": getTestInputDefinition(),
  338. "openapi.TestOutput": getTestOutputDefinition(),
  339. },
  340. },
  341. }
  342. err := o.buildSwaggerSpec()
  343. if assert.NoError(err) {
  344. sortParameters(expected)
  345. sortParameters(o.swagger)
  346. assert.Equal(expected, o.swagger)
  347. }
  348. }
  349. func TestBuildSwaggerSpecTwice(t *testing.T) {
  350. o, assert := setUp(t, true)
  351. err := o.buildSwaggerSpec()
  352. if assert.NoError(err) {
  353. assert.Error(o.buildSwaggerSpec(), "Swagger spec is already built. Duplicate call to buildSwaggerSpec is not allowed.")
  354. }
  355. }
  356. func TestBuildDefinitions(t *testing.T) {
  357. o, assert := setUp(t, true)
  358. expected := spec.Definitions{
  359. "openapi.TestInput": getTestInputDefinition(),
  360. "openapi.TestOutput": getTestOutputDefinition(),
  361. }
  362. def, err := o.buildDefinitions()
  363. if assert.NoError(err) {
  364. assert.Equal(expected, def)
  365. }
  366. }
  367. func TestBuildProtocolList(t *testing.T) {
  368. assert := assert.New(t)
  369. o := openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "https://something"}}}
  370. p, err := o.buildProtocolList()
  371. if assert.NoError(err) {
  372. assert.Equal([]string{"https"}, p)
  373. }
  374. o = openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "http://something"}}}
  375. p, err = o.buildProtocolList()
  376. if assert.NoError(err) {
  377. assert.Equal([]string{"http"}, p)
  378. }
  379. o = openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "something"}}}
  380. p, err = o.buildProtocolList()
  381. if assert.NoError(err) {
  382. assert.Equal([]string{"http"}, p)
  383. }
  384. }