- type: fileupload compiles the D-08 keys and binds to the model's attach.Relation at boot
- X-Session-Key (cabana.SessionKeyHeader) carries the form session key; RecordInput.SessionKey
- GET and POST .../{id}/files/{field}: list with pending uploads, multipart upload into attach.Store
- the create and update save attaches the session's pending files in its transaction
- swagger2openapi folds formData parameters into a multipart requestBody
- admin OpenAPI, TS types, conformance cases, README and forms docs
237 lines
9.0 KiB
Go
237 lines
9.0 KiB
Go
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)
|
|
}
|
|
}
|