1// Copyright 2015 go-swagger maintainers
2//
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//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package loads
16
17import (
18	"encoding/json"
19	"regexp"
20	"strconv"
21	"strings"
22	"testing"
23
24	"github.com/stretchr/testify/assert"
25)
26
27func TestUnknownSpecVersion(t *testing.T) {
28	_, err := Analyzed([]byte{}, "0.9")
29	assert.Error(t, err)
30}
31
32func TestDefaultsTo20(t *testing.T) {
33	d, err := Analyzed(PetStoreJSONMessage, "")
34
35	assert.NoError(t, err)
36	assert.NotNil(t, d)
37	assert.Equal(t, "2.0", d.Version())
38	// assert.Equal(t, "2.0", d.data["swagger"].(string))
39	assert.Equal(t, "/api", d.BasePath())
40}
41
42func TestLoadsYAMLContent(t *testing.T) {
43	d, err := Analyzed(json.RawMessage([]byte(YAMLSpec)), "")
44	if assert.NoError(t, err) {
45		if assert.NotNil(t, d) {
46			sw := d.Spec()
47			assert.Equal(t, "1.0.0", sw.Info.Version)
48		}
49	}
50}
51
52// for issue 11
53func TestRegressionExpand(t *testing.T) {
54	swaggerFile := "fixtures/yaml/swagger/1/2/3/4/swagger.yaml"
55	document, err := Spec(swaggerFile)
56	assert.NoError(t, err)
57	assert.NotNil(t, document)
58	d, err := document.Expanded()
59	assert.NoError(t, err)
60	assert.NotNil(t, d)
61	b, _ := d.Spec().MarshalJSON()
62	assert.JSONEq(t, expectedExpanded, string(b))
63}
64
65func TestCascadingRefExpand(t *testing.T) {
66	swaggerFile := "fixtures/yaml/swagger/spec.yml"
67	document, err := Spec(swaggerFile)
68	assert.NoError(t, err)
69	assert.NotNil(t, document)
70	d, err := document.Expanded()
71	assert.NoError(t, err)
72	assert.NotNil(t, d)
73	b, _ := d.Spec().MarshalJSON()
74	assert.JSONEq(t, cascadeRefExpanded, string(b))
75}
76
77func TestFailsInvalidJSON(t *testing.T) {
78	_, err := Analyzed(json.RawMessage([]byte("{]")), "")
79
80	assert.Error(t, err)
81}
82
83// issue go-swagger/go-swagger#1816 (regression when cloning original spec)
84func TestIssue1846(t *testing.T) {
85	swaggerFile := "fixtures/bugs/1816/fixture-1816.yaml"
86	document, err := Spec(swaggerFile)
87	assert.NoError(t, err)
88	assert.NotNil(t, document)
89
90	sp, err := cloneSpec(document.Spec())
91	assert.NoError(t, err)
92	jazon, _ := json.MarshalIndent(sp, "", " ")
93	//t.Logf("%s", string(jazon))
94	rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
95	m := rex.FindAllStringSubmatch(string(jazon), -1)
96	if assert.NotNil(t, m) {
97		for _, matched := range m {
98			subMatch := matched[1]
99			if !assert.True(t, strings.HasPrefix(subMatch, "#/definitions") || strings.HasPrefix(subMatch, "#/responses"),
100				"expected $ref to point either to definitions or responses section, got: %s", matched[0]) {
101				t.FailNow()
102			}
103		}
104	}
105}
106
107func BenchmarkAnalyzed(b *testing.B) {
108	d := []byte(`{
109  "swagger": "2.0",
110  "info": {
111    "version": "1.0.0",
112    "title": "Swagger Petstore",
113    "contact": {
114      "name": "Wordnik API Team",
115      "url": "http://developer.wordnik.com"
116    },
117    "license": {
118      "name": "Creative Commons 4.0 International",
119      "url": "http://creativecommons.org/licenses/by/4.0/"
120    }
121  },
122  "host": "petstore.swagger.wordnik.com",
123  "basePath": "/api",
124  "schemes": [
125    "http"
126  ],
127  "paths": {
128    "/pets": {
129      "get": {
130        "security": [
131          {
132            "basic": []
133          }
134        ],
135        "tags": [ "Pet Operations" ],
136        "operationId": "getAllPets",
137        "parameters": [
138          {
139            "name": "status",
140            "in": "query",
141            "description": "The status to filter by",
142            "type": "string"
143          },
144          {
145            "name": "limit",
146            "in": "query",
147            "description": "The maximum number of results to return",
148            "type": "integer",
149						"format": "int64"
150          }
151        ],
152        "summary": "Finds all pets in the system",
153        "responses": {
154          "200": {
155            "description": "Pet response",
156            "schema": {
157              "type": "array",
158              "items": {
159                "$ref": "#/definitions/Pet"
160              }
161            }
162          },
163          "default": {
164            "description": "Unexpected error",
165            "schema": {
166              "$ref": "#/definitions/Error"
167            }
168          }
169        }
170      },
171      "post": {
172        "security": [
173          {
174            "basic": []
175          }
176        ],
177        "tags": [ "Pet Operations" ],
178        "operationId": "createPet",
179        "summary": "Creates a new pet",
180        "consumes": ["application/x-yaml"],
181        "produces": ["application/x-yaml"],
182        "parameters": [
183          {
184            "name": "pet",
185            "in": "body",
186            "description": "The Pet to create",
187            "required": true,
188            "schema": {
189              "$ref": "#/definitions/newPet"
190            }
191          }
192        ],
193        "responses": {
194          "200": {
195            "description": "Created Pet response",
196            "schema": {
197              "$ref": "#/definitions/Pet"
198            }
199          },
200          "default": {
201            "description": "Unexpected error",
202            "schema": {
203              "$ref": "#/definitions/Error"
204            }
205          }
206        }
207      }
208    }`)
209
210	for i := 0; i < 1000; i++ {
211		d = append(d, []byte(`,
212    "/pets/`)...)
213		d = strconv.AppendInt(d, int64(i), 10)
214		d = append(d, []byte(`": {
215      "delete": {
216        "security": [
217          {
218            "apiKey": []
219          }
220        ],
221        "description": "Deletes the Pet by id",
222        "operationId": "deletePet",
223        "parameters": [
224          {
225            "name": "id",
226            "in": "path",
227            "description": "ID of pet to delete",
228            "required": true,
229            "type": "integer",
230            "format": "int64"
231          }
232        ],
233        "responses": {
234          "204": {
235            "description": "pet deleted"
236          },
237          "default": {
238            "description": "unexpected error",
239            "schema": {
240              "$ref": "#/definitions/Error"
241            }
242          }
243        }
244      },
245      "get": {
246        "tags": [ "Pet Operations" ],
247        "operationId": "getPetById",
248        "summary": "Finds the pet by id",
249        "responses": {
250          "200": {
251            "description": "Pet response",
252            "schema": {
253              "$ref": "#/definitions/Pet"
254            }
255          },
256          "default": {
257            "description": "Unexpected error",
258            "schema": {
259              "$ref": "#/definitions/Error"
260            }
261          }
262        }
263      },
264      "parameters": [
265        {
266          "name": "id",
267          "in": "path",
268          "description": "ID of pet",
269          "required": true,
270          "type": "integer",
271          "format": "int64"
272        }
273      ]
274    }`)...)
275	}
276
277	d = append(d, []byte(`
278  },
279  "definitions": {
280    "Category": {
281      "id": "Category",
282      "properties": {
283        "id": {
284          "format": "int64",
285          "type": "integer"
286        },
287        "name": {
288          "type": "string"
289        }
290      }
291    },
292    "Pet": {
293      "id": "Pet",
294      "properties": {
295        "category": {
296          "$ref": "#/definitions/Category"
297        },
298        "id": {
299          "description": "unique identifier for the pet",
300          "format": "int64",
301          "maximum": 100.0,
302          "minimum": 0.0,
303          "type": "integer"
304        },
305        "name": {
306          "type": "string"
307        },
308        "photoUrls": {
309          "items": {
310            "type": "string"
311          },
312          "type": "array"
313        },
314        "status": {
315          "description": "pet status in the store",
316          "enum": [
317            "available",
318            "pending",
319            "sold"
320          ],
321          "type": "string"
322        },
323        "tags": {
324          "items": {
325            "$ref": "#/definitions/Tag"
326          },
327          "type": "array"
328        }
329      },
330      "required": [
331        "id",
332        "name"
333      ]
334    },
335    "newPet": {
336      "anyOf": [
337        {
338          "$ref": "#/definitions/Pet"
339        },
340        {
341          "required": [
342            "name"
343          ]
344        }
345      ]
346    },
347    "Tag": {
348      "id": "Tag",
349      "properties": {
350        "id": {
351          "format": "int64",
352          "type": "integer"
353        },
354        "name": {
355          "type": "string"
356        }
357      }
358    },
359    "Error": {
360      "required": [
361        "code",
362        "message"
363      ],
364      "properties": {
365        "code": {
366          "type": "integer",
367          "format": "int32"
368        },
369        "message": {
370          "type": "string"
371        }
372      }
373    }
374  },
375  "consumes": [
376    "application/json",
377    "application/xml"
378  ],
379  "produces": [
380    "application/json",
381    "application/xml",
382    "text/plain",
383    "text/html"
384  ],
385  "securityDefinitions": {
386    "basic": {
387      "type": "basic"
388    },
389    "apiKey": {
390      "type": "apiKey",
391      "in": "header",
392      "name": "X-API-KEY"
393    }
394  }
395}
396`)...)
397	rm := json.RawMessage(d)
398	b.ResetTimer()
399	for i := 0; i < b.N; i++ {
400		_, err := Analyzed(rm, "")
401		if err != nil {
402			b.Fatal(err)
403		}
404	}
405}
406
407const YAMLSpec = `swagger: '2.0'
408
409info:
410  version: "1.0.0"
411  title: Simple Search API
412  description: |
413    A very simple api description that makes a x-www-form-urlencoded only API to submit searches.
414
415produces:
416  - application/json
417
418consumes:
419  - application/json
420
421paths:
422  /search:
423    post:
424      operationId: search
425      summary: searches tasks
426      description: searches the task titles and descriptions for a match
427      consumes:
428        - application/x-www-form-urlencoded
429      parameters:
430        - name: q
431          in: formData
432          type: string
433          description: the search string
434          required: true
435  /tasks:
436    get:
437      operationId: getTasks
438      summary: Gets Task objects.
439      description: |
440        Optional query param of **size** determines
441        size of returned array
442      tags:
443        - tasks
444      parameters:
445        - name: size
446          in: query
447          description: Size of task list
448          type: integer
449          format: int32
450          default: 20
451        - name: completed
452          in: query
453          description: when true shows completed tasks
454          type: boolean
455
456      responses:
457        default:
458          description: Generic Error
459        200:
460          description: Successful response
461          headers:
462            X-Rate-Limit:
463              type: integer
464              format: int32
465            X-Rate-Limit-Remaining:
466              type: integer
467              format: int32
468              default: 42
469            X-Rate-Limit-Reset:
470              type: integer
471              format: int32
472              default: "1449875311"
473            X-Rate-Limit-Reset-Human:
474              type: string
475              default: 3 days
476            X-Rate-Limit-Reset-Human-Number:
477              type: string
478              default: 3
479            Access-Control-Allow-Origin:
480              type: string
481              default: "*"
482          schema:
483            type: array
484            items:
485              $ref: "#/definitions/Task"
486    post:
487      operationId: createTask
488      summary: Creates a 'Task' object.
489      description: |
490        Validates the content property for length etc.
491      parameters:
492        - name: body
493          in: body
494          schema:
495            $ref: "#/definitions/Task"
496      tags:
497        - tasks
498      responses:
499        default:
500          description: Generic Error
501        201:
502          description: Task Created
503
504  /tasks/{id}:
505    parameters:
506      - name: id
507        in: path
508        type: integer
509        format: int32
510        description: The id of the task
511        required: true
512        minimum: 1
513    put:
514      operationId: updateTask
515      summary: updates a task.
516      description: |
517        Validates the content property for length etc.
518      tags:
519        - tasks
520      parameters:
521        - name: body
522          in: body
523          description: the updated task
524          schema:
525            $ref: "#/definitions/Task"
526      responses:
527        default:
528          description: Generic Error
529        200:
530          description: Task updated
531          schema:
532            $ref: "#/definitions/Task"
533    delete:
534      operationId: deleteTask
535      summary: deletes a task
536      description: |
537        Deleting a task is irrevocable.
538      tags:
539        - tasks
540      responses:
541        default:
542          description: Generic Error
543        204:
544          description: Task Deleted
545
546
547definitions:
548  Task:
549    title: A Task object
550    description: |
551      This describes a task. Tasks require a content property to be set.
552    required:
553      - content
554    type: object
555    properties:
556      id:
557        title: the unique id of the task
558        description: |
559          This id property is autogenerated when a task is created.
560        type: integer
561        format: int64
562        readOnly: true
563      content:
564        title: The content of the task
565        description: |
566          Task content can contain [GFM](https://help.github.com/articles/github-flavored-markdown/).
567        type: string
568        minLength: 5
569      completed:
570        title: when true this task is completed
571        type: boolean
572      creditcard:
573        title: the credit card format usage
574        type: string
575        format: creditcard
576      createdAt:
577        title: task creation time
578        type: string
579        format: date-time
580        readOnly: true
581`
582
583// PetStoreJSONMessage json raw message for Petstore20
584var PetStoreJSONMessage = json.RawMessage([]byte(PetStore20))
585
586// PetStore20 json doc for swagger 2.0 pet store
587const PetStore20 = `{
588  "swagger": "2.0",
589  "info": {
590    "version": "1.0.0",
591    "title": "Swagger Petstore",
592    "contact": {
593      "name": "Wordnik API Team",
594      "url": "http://developer.wordnik.com"
595    },
596    "license": {
597      "name": "Creative Commons 4.0 International",
598      "url": "http://creativecommons.org/licenses/by/4.0/"
599    }
600  },
601  "host": "petstore.swagger.wordnik.com",
602  "basePath": "/api",
603  "schemes": [
604    "http"
605  ],
606  "paths": {
607    "/pets": {
608      "get": {
609        "security": [
610          {
611            "basic": []
612          }
613        ],
614        "tags": [ "Pet Operations" ],
615        "operationId": "getAllPets",
616        "parameters": [
617          {
618            "name": "status",
619            "in": "query",
620            "description": "The status to filter by",
621            "type": "string"
622          },
623          {
624            "name": "limit",
625            "in": "query",
626            "description": "The maximum number of results to return",
627            "type": "integer",
628						"format": "int64"
629          }
630        ],
631        "summary": "Finds all pets in the system",
632        "responses": {
633          "200": {
634            "description": "Pet response",
635            "schema": {
636              "type": "array",
637              "items": {
638                "$ref": "#/definitions/Pet"
639              }
640            }
641          },
642          "default": {
643            "description": "Unexpected error",
644            "schema": {
645              "$ref": "#/definitions/Error"
646            }
647          }
648        }
649      },
650      "post": {
651        "security": [
652          {
653            "basic": []
654          }
655        ],
656        "tags": [ "Pet Operations" ],
657        "operationId": "createPet",
658        "summary": "Creates a new pet",
659        "consumes": ["application/x-yaml"],
660        "produces": ["application/x-yaml"],
661        "parameters": [
662          {
663            "name": "pet",
664            "in": "body",
665            "description": "The Pet to create",
666            "required": true,
667            "schema": {
668              "$ref": "#/definitions/newPet"
669            }
670          }
671        ],
672        "responses": {
673          "200": {
674            "description": "Created Pet response",
675            "schema": {
676              "$ref": "#/definitions/Pet"
677            }
678          },
679          "default": {
680            "description": "Unexpected error",
681            "schema": {
682              "$ref": "#/definitions/Error"
683            }
684          }
685        }
686      }
687    },
688    "/pets/{id}": {
689      "delete": {
690        "security": [
691          {
692            "apiKey": []
693          }
694        ],
695        "description": "Deletes the Pet by id",
696        "operationId": "deletePet",
697        "parameters": [
698          {
699            "name": "id",
700            "in": "path",
701            "description": "ID of pet to delete",
702            "required": true,
703            "type": "integer",
704            "format": "int64"
705          }
706        ],
707        "responses": {
708          "204": {
709            "description": "pet deleted"
710          },
711          "default": {
712            "description": "unexpected error",
713            "schema": {
714              "$ref": "#/definitions/Error"
715            }
716          }
717        }
718      },
719      "get": {
720        "tags": [ "Pet Operations" ],
721        "operationId": "getPetById",
722        "summary": "Finds the pet by id",
723        "responses": {
724          "200": {
725            "description": "Pet response",
726            "schema": {
727              "$ref": "#/definitions/Pet"
728            }
729          },
730          "default": {
731            "description": "Unexpected error",
732            "schema": {
733              "$ref": "#/definitions/Error"
734            }
735          }
736        }
737      },
738      "parameters": [
739        {
740          "name": "id",
741          "in": "path",
742          "description": "ID of pet",
743          "required": true,
744          "type": "integer",
745          "format": "int64"
746        }
747      ]
748    }
749  },
750  "definitions": {
751    "Category": {
752      "id": "Category",
753      "properties": {
754        "id": {
755          "format": "int64",
756          "type": "integer"
757        },
758        "name": {
759          "type": "string"
760        }
761      }
762    },
763    "Pet": {
764      "id": "Pet",
765      "properties": {
766        "category": {
767          "$ref": "#/definitions/Category"
768        },
769        "id": {
770          "description": "unique identifier for the pet",
771          "format": "int64",
772          "maximum": 100.0,
773          "minimum": 0.0,
774          "type": "integer"
775        },
776        "name": {
777          "type": "string"
778        },
779        "photoUrls": {
780          "items": {
781            "type": "string"
782          },
783          "type": "array"
784        },
785        "status": {
786          "description": "pet status in the store",
787          "enum": [
788            "available",
789            "pending",
790            "sold"
791          ],
792          "type": "string"
793        },
794        "tags": {
795          "items": {
796            "$ref": "#/definitions/Tag"
797          },
798          "type": "array"
799        }
800      },
801      "required": [
802        "id",
803        "name"
804      ]
805    },
806    "newPet": {
807      "anyOf": [
808        {
809          "$ref": "#/definitions/Pet"
810        },
811        {
812          "required": [
813            "name"
814          ]
815        }
816      ]
817    },
818    "Tag": {
819      "id": "Tag",
820      "properties": {
821        "id": {
822          "format": "int64",
823          "type": "integer"
824        },
825        "name": {
826          "type": "string"
827        }
828      }
829    },
830    "Error": {
831      "required": [
832        "code",
833        "message"
834      ],
835      "properties": {
836        "code": {
837          "type": "integer",
838          "format": "int32"
839        },
840        "message": {
841          "type": "string"
842        }
843      }
844    }
845  },
846  "consumes": [
847    "application/json",
848    "application/xml"
849  ],
850  "produces": [
851    "application/json",
852    "application/xml",
853    "text/plain",
854    "text/html"
855  ],
856  "securityDefinitions": {
857    "basic": {
858      "type": "basic"
859    },
860    "apiKey": {
861      "type": "apiKey",
862      "in": "header",
863      "name": "X-API-KEY"
864    }
865  }
866}
867`
868
869const expectedExpanded = `
870{
871   "produces":[
872      "application/json",
873      "plain/text"
874   ],
875   "schemes":[
876      "https",
877      "http"
878   ],
879   "swagger":"2.0",
880   "info":{
881      "description":"Something",
882      "title":"Something",
883      "contact":{
884         "name":"Somebody",
885         "url":"https://url.com",
886         "email":"email@url.com"
887      },
888      "version":"v1"
889   },
890   "host":"security.sonusnet.com",
891   "basePath":"/api",
892   "paths":{
893      "/whatnot":{
894         "get":{
895            "description":"Get something",
896            "responses":{
897               "200":{
898                  "description":"The something",
899                  "schema":{
900                     "description":"A collection of service events",
901                     "type":"object",
902                     "properties":{
903                        "page":{
904                           "description":"A description of a paged result",
905                           "type":"object",
906                           "properties":{
907                              "page":{
908                                 "description":"the page that was requested",
909                                 "type":"integer"
910                              },
911                              "page_items":{
912                                 "description":"the number of items per page requested",
913                                 "type":"integer"
914                              },
915                              "pages":{
916                                 "description":"the total number of pages available",
917                                 "type":"integer"
918                              },
919                              "total_items":{
920                                 "description":"the total number of items available",
921                                 "type":"integer",
922                                 "format":"int64"
923                              }
924                           }
925                        },
926                        "something":{
927                           "description":"Something",
928                           "type":"object",
929                           "properties":{
930                              "p1":{
931                                 "description":"A string",
932                                 "type":"string"
933                              },
934                              "p2":{
935                                 "description":"An integer",
936                                 "type":"integer"
937                              }
938                           }
939                        }
940                     }
941                  }
942               },
943               "500":{
944                  "description":"Oops"
945               }
946            }
947         }
948      }
949   },
950   "definitions":{
951      "Something":{
952         "description":"A collection of service events",
953         "type":"object",
954         "properties":{
955            "page":{
956               "description":"A description of a paged result",
957               "type":"object",
958               "properties":{
959                  "page":{
960                     "description":"the page that was requested",
961                     "type":"integer"
962                  },
963                  "page_items":{
964                     "description":"the number of items per page requested",
965                     "type":"integer"
966                  },
967                  "pages":{
968                     "description":"the total number of pages available",
969                     "type":"integer"
970                  },
971                  "total_items":{
972                     "description":"the total number of items available",
973                     "type":"integer",
974                     "format":"int64"
975                  }
976               }
977            },
978            "something":{
979               "description":"Something",
980               "type":"object",
981               "properties":{
982                  "p1":{
983                     "description":"A string",
984                     "type":"string"
985                  },
986                  "p2":{
987                     "description":"An integer",
988                     "type":"integer"
989                  }
990               }
991            }
992         }
993      }
994   }
995}
996`
997
998const cascadeRefExpanded = `
999{
1000  "swagger": "2.0",
1001  "consumes":[
1002     "application/json"
1003  ],
1004  "produces":[
1005     "application/json"
1006  ],
1007  "schemes":[
1008     "http"
1009  ],
1010  "info":{
1011     "description":"recursively following JSON references",
1012     "title":"test 1",
1013     "contact":{
1014        "name":"Fred"
1015     },
1016     "version":"0.1.1"
1017  },
1018  "paths":{
1019     "/getAll":{
1020        "get":{
1021           "operationId":"getAll",
1022           "parameters":[
1023              {
1024                 "description":"max number of results",
1025                 "name":"a",
1026                 "in":"body",
1027                 "schema":{
1028                    "type":"string"
1029                 }
1030              }
1031           ],
1032           "responses":{
1033              "200":{
1034                 "description":"Success",
1035                 "schema":{
1036                    "type":"array",
1037                    "items":{
1038                       "type":"string"
1039                    }
1040                 }
1041              }
1042           }
1043        }
1044     }
1045  },
1046  "definitions":{
1047     "a":{
1048        "type":"string"
1049     },
1050     "b":{
1051        "type":"array",
1052        "items":{
1053           "type":"string"
1054        }
1055     }
1056  }
1057}
1058`
1059