package main import ( "encoding/json" "reflect" "testing" ) func decode(t *testing.T, raw string) map[string]any { t.Helper() var doc map[string]any if err := json.Unmarshal([]byte(raw), &doc); err != nil { t.Fatal(err) } return doc } // TestUnionRewrite proves the two opaque cabana types become the documented // unions (so openapi-typescript emits exact TS types) and that every other // component is left untouched. func TestUnionRewrite(t *testing.T) { doc := decode(t, `{ "swagger": "2.0", "definitions": { "cabana.jsonScalar": {"type": "object"}, "cabana.fieldContext": {"type": "object"}, "cabana.FormField": { "type": "object", "properties": { "default": {"$ref": "#/definitions/cabana.jsonScalar"}, "context": {"$ref": "#/definitions/cabana.fieldContext"}, "name": {"type": "string"} } }, "acme.Other": {"type": "object", "properties": {"x": {"type": "integer"}}} } }`) untouchedOther := decode(t, `{"type": "object", "properties": {"x": {"type": "integer"}}}`) out := swagger2openapi(doc) schemas := out["components"].(map[string]any)["schemas"].(map[string]any) wantScalar := decode(t, `{"nullable": true, "oneOf": [{"type": "string"}, {"type": "number"}, {"type": "boolean"}]}`) if got := schemas["cabana.jsonScalar"]; !reflect.DeepEqual(got, wantScalar) { t.Fatalf("jsonScalar = %#v", got) } wantContext := decode(t, `{"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}`) if got := schemas["cabana.fieldContext"]; !reflect.DeepEqual(got, wantContext) { t.Fatalf("fieldContext = %#v", got) } if got := schemas["acme.Other"]; !reflect.DeepEqual(got, untouchedOther) { t.Fatalf("an unrelated component changed: %#v", got) } props := schemas["cabana.FormField"].(map[string]any)["properties"].(map[string]any) if ref := props["default"].(map[string]any)["$ref"]; ref != "#/components/schemas/cabana.jsonScalar" { t.Fatalf("ref not rewritten: %v", ref) } if props["name"].(map[string]any)["type"] != "string" { t.Fatalf("plain property changed: %v", props["name"]) } // Documents without the opaque types are not given them. bare := swagger2openapi(decode(t, `{"definitions": {"acme.Other": {"type": "object"}}}`)) if _, ok := bare["components"].(map[string]any)["schemas"].(map[string]any)["cabana.jsonScalar"]; ok { t.Fatal("rewriteOpaque added a union the document did not declare") } } func TestConvertOperations(t *testing.T) { doc := decode(t, `{ "swagger": "2.0", "info": {"title": "Admin", "version": "1"}, "tags": [{"name": "admin"}], "basePath": "/", "paths": { "/{vendor}/items": { "parameters": [], "get": { "parameters": [ {"name": "vendor", "in": "path", "required": true, "type": "string"}, {"name": "page", "in": "query", "type": "integer", "minimum": 1}, {"name": "filter", "in": "query", "type": "object"} ], "responses": {"200": {"description": "OK", "schema": {"$ref": "#/definitions/acme.List"}}, "204": {"description": "empty"}} }, "post": { "consumes": ["application/json"], "produces": ["application/json"], "parameters": [{"name": "body", "in": "body", "required": true, "description": "record", "schema": {"$ref": "#/definitions/acme.Rec"}}], "responses": {"201": {"description": "Created", "schema": {"type": "object"}}} }, "x-note": "kept" } }, "definitions": {"acme.List": {"type": "object"}, "acme.Rec": {"type": "object"}}, "securityDefinitions": { "bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}, "oauth": {"type": "oauth2", "flow": "accessCode", "authorizationUrl": "https://a", "tokenUrl": "https://t", "scopes": {"read": "r"}} }, "security": [{"bearer": []}] }`) out := swagger2openapi(doc) if out["openapi"] != "3.0.3" || out["info"].(map[string]any)["title"] != "Admin" || out["tags"] == nil || out["security"] == nil { t.Fatalf("top level = %#v", out) } if _, ok := out["servers"]; ok { t.Fatal("a root basePath without host produced servers") } item := out["paths"].(map[string]any)["/{vendor}/items"].(map[string]any) if item["x-note"] != "kept" { t.Fatal("extension key dropped") } get := item["get"].(map[string]any) params := get["parameters"].([]any) page := params[1].(map[string]any) if page["schema"].(map[string]any)["minimum"] != float64(1) || page["minimum"] != nil { t.Fatalf("schema keys not moved: %#v", page) } filter := params[2].(map[string]any) if filter["style"] != "deepObject" || filter["explode"] != true || !reflect.DeepEqual(filter["schema"], map[string]any{"type": "object", "additionalProperties": map[string]any{"type": "string"}}) { t.Fatalf("object query param = %#v", filter) } ok200 := get["responses"].(map[string]any)["200"].(map[string]any) ref := ok200["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)["$ref"] if ref != "#/components/schemas/acme.List" { t.Fatalf("response ref = %v", ref) } if _, ok := get["responses"].(map[string]any)["204"].(map[string]any)["content"]; ok { t.Fatal("a response without schema got content") } post := item["post"].(map[string]any) body := post["requestBody"].(map[string]any) if body["required"] != true || body["description"] != "record" || post["parameters"] != nil { t.Fatalf("request body = %#v params=%v", body, post["parameters"]) } flows := out["components"].(map[string]any)["securitySchemes"].(map[string]any)["oauth"].(map[string]any)["flows"].(map[string]any) code := flows["authorizationCode"].(map[string]any) if code["tokenUrl"] != "https://t" || code["authorizationUrl"] != "https://a" { t.Fatalf("oauth flows = %#v", flows) } } func TestConvertServers(t *testing.T) { cases := []struct { doc string want []any }{ {`{}`, nil}, {`{"host": "api.test"}`, []any{map[string]any{"url": "https://api.test"}}}, {`{"host": "api.test", "basePath": "/v1", "schemes": ["http", "https"]}`, []any{ map[string]any{"url": "http://api.test/v1"}, map[string]any{"url": "https://api.test/v1"}, }}, {`{"basePath": "/v1"}`, []any{map[string]any{"url": "/v1"}}}, } for _, tc := range cases { if got := convertServers(decode(t, tc.doc)); !reflect.DeepEqual(got, tc.want) { t.Fatalf("%s servers = %#v, want %#v", tc.doc, got, tc.want) } } } func TestOAuthFlowNames(t *testing.T) { for flow, want := range map[string]string{ "implicit": "implicit", "password": "password", "application": "clientCredentials", "accessCode": "authorizationCode", } { out := convertSecurity(map[string]any{"o": map[string]any{"type": "oauth2", "flow": flow, "scopes": map[string]any{}}}) flows := out["o"].(map[string]any)["flows"].(map[string]any) if _, ok := flows[want]; !ok || len(flows) != 1 { t.Fatalf("%s -> %#v", flow, flows) } } if out := convertSecurity(map[string]any{"raw": "x"}); out["raw"] != "x" { t.Fatal("non-object security scheme not kept") } } func TestMalformedShapesPassThrough(t *testing.T) { paths := convertPaths(map[string]any{"/a": "raw", "/b": map[string]any{"get": "raw", "produces": []any{"text/plain", 1, ""}}}) if paths["/a"] != "raw" || paths["/b"].(map[string]any)["get"] != "raw" { t.Fatalf("malformed paths = %#v", paths) } op := convertOperation(map[string]any{"responses": "raw", "parameters": "raw", "summary": "s"}, nil, nil) if op["responses"] != "raw" || op["summary"] != "s" { t.Fatalf("malformed op = %#v", op) } params, body := splitParameters([]any{"loose"}, []string{"application/json"}) if len(params) != 1 || body != nil { t.Fatalf("loose parameter = %#v %#v", params, body) } if res := convertResponses(map[string]any{"200": "raw"}, nil); res["200"] != "raw" { t.Fatal("raw response not kept") } if got := stringList([]any{"a", 2, "", "b"}); !reflect.DeepEqual(got, []string{"a", "b"}) { t.Fatalf("stringList = %v", got) } if got := rewriteRefs(map[string]any{"$ref": "#/other/x"}); got.(map[string]any)["$ref"] != "#/other/x" { t.Fatalf("foreign ref rewritten: %v", got) } } func TestConvertFormData(t *testing.T) { op := decode(t, `{ "consumes": ["multipart/form-data"], "parameters": [ {"name": "id", "in": "path", "required": true, "type": "integer"}, {"name": "file_data", "in": "formData", "required": true, "type": "file", "description": "The file"}, {"name": "note", "in": "formData", "type": "string"} ], "responses": {"201": {"description": "Created"}} }`) out := convertOperation(op, nil, nil) params := out["parameters"].([]any) if len(params) != 1 || params[0].(map[string]any)["name"] != "id" { t.Fatalf("parameters = %#v", params) } body := out["requestBody"].(map[string]any) if body["required"] != true { t.Fatalf("request body = %#v", body) } schema := body["content"].(map[string]any)["multipart/form-data"].(map[string]any)["schema"].(map[string]any) want := map[string]any{ "type": "object", "properties": map[string]any{ "file_data": map[string]any{"type": "string", "format": "binary", "description": "The file"}, "note": map[string]any{"type": "string"}, }, "required": []any{"file_data"}, } if !reflect.DeepEqual(schema, want) { t.Fatalf("form schema = %#v", schema) } }