Files
pad/internal/urlimport/openapi_test.go
T
xarmian 8bb4616a3d fix(urlimport): marshal OpenAPI examples with libopenapi's own yaml package (TASK-3422) (#1818)
libopenapi 0.41 moved its Node type to github.com/pb33f/go-yaml. Marshalling
a MediaType example with go.yaml.in/yaml/v4 saw a plain struct, and the
example block no longer rendered the author's YAML (TestConvertOpenAPI_Petstore:
markdown missing "name: Rex"). The petstore markdown is byte-identical to
libopenapi 0.40's output with this change. The test now pins the whole
example block.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 20:14:17 -04:00

293 lines
7.5 KiB
Go

package urlimport
import (
"strings"
"testing"
)
func TestConvertOpenAPI_Petstore(t *testing.T) {
spec := loadFixture(t, "petstore-openapi.yaml")
res, err := ConvertOpenAPI(spec, "https://petstore.example.com/openapi.yaml")
if err != nil {
t.Fatalf("ConvertOpenAPI: %v", err)
}
if res.Title != "Swagger Petstore" {
t.Errorf("Title = %q, want 'Swagger Petstore'", res.Title)
}
md := res.Markdown
wantSubstrings := []string{
// Info block
"# Swagger Petstore",
"**Version:** `1.0.0`",
"**Contact:**",
"petstore@example.com",
"**License:**",
"MIT",
// Servers
"## Servers",
"https://petstore.example.com/v1",
"Production server",
// Endpoints
"## Endpoints",
"### pets",
"### store",
"`GET /pets`",
"`POST /pets`",
"`GET /pets/{petId}`",
"`POST /store/orders`",
// Operation details
"List pets",
"Create a pet",
"**Parameters**",
"| `limit` | query | no |",
"| `petId` | path | yes |",
// Request body
"**Request body** *(required)*",
"Content-Type: `application/json`",
// Example body, rendered as the author's YAML (TASK-3422: a
// marshaller from the wrong yaml package drops it)
"name: Rex",
" ```\n id: 42\n name: Rex\n status: available\n ```",
// Responses
"**Responses**",
"| `200` |",
"| `400` |",
"| `404` |",
"| `201` |",
// Deprecation marker
"⚠ **Deprecated.**",
// Operation IDs
"**Operation ID:** `listPets`",
// Schemas
"## Schemas",
"### `Order`",
"### `Pet`",
"A pet for sale.",
"| `id` |",
"| `name` |",
"`integer (int64)`",
}
for _, want := range wantSubstrings {
if !strings.Contains(md, want) {
t.Errorf("markdown missing %q\n--- markdown ---\n%s\n---", want, md)
}
}
}
func TestConvertOpenAPI_RejectsSwagger2(t *testing.T) {
// libopenapi accepts both v2 and v3 documents. ConvertOpenAPI is
// limited to 3.x; Swagger 2.0 should produce a clear error so the
// caller can fall back to generic conversion.
swagger2 := []byte(`swagger: "2.0"
info:
title: Old API
version: "1.0"
paths: {}
`)
_, err := ConvertOpenAPI(swagger2, "")
if err == nil {
t.Fatal("ConvertOpenAPI: expected error for Swagger 2.0, got nil")
}
if !strings.Contains(err.Error(), "only 3.x") {
t.Fatalf("error = %v, want to mention 'only 3.x'", err)
}
}
func TestConvertOpenAPI_RejectsGarbage(t *testing.T) {
_, err := ConvertOpenAPI([]byte("not an openapi spec"), "")
if err == nil {
t.Fatal("expected error for non-OpenAPI input, got nil")
}
}
func TestConvertOpenAPI_MinimalSpec(t *testing.T) {
// Smallest valid v3 spec — just info and an empty paths object.
minimal := []byte(`openapi: 3.0.0
info:
title: Tiny API
version: "0.1"
paths: {}
`)
res, err := ConvertOpenAPI(minimal, "")
if err != nil {
t.Fatalf("ConvertOpenAPI: %v", err)
}
if !strings.Contains(res.Markdown, "# Tiny API") {
t.Errorf("markdown missing title heading\n%s", res.Markdown)
}
if strings.Contains(res.Markdown, "## Endpoints") {
t.Errorf("markdown should not have Endpoints section for empty paths\n%s", res.Markdown)
}
}
func TestConvertOpenAPI_PathLevelParametersMerged(t *testing.T) {
// Path-level parameters apply to every operation on that path.
// Operation-level parameters override path-level ones on a
// (name, in) match. Both rules must hold.
spec := []byte(`openapi: 3.0.0
info:
title: Param Merge Test
version: "1.0"
paths:
/widgets/{widgetId}:
parameters:
- name: widgetId
in: path
required: true
description: Path-level widget id
schema:
type: string
- name: trace
in: header
required: false
description: Trace header from path-level
schema:
type: string
get:
summary: Get a widget
parameters:
- name: trace
in: header
required: true
description: Operation-level trace (overrides path-level)
schema:
type: string
- name: fields
in: query
required: false
description: Op-only filter
schema:
type: string
responses:
'200':
description: ok
`)
res, err := ConvertOpenAPI(spec, "")
if err != nil {
t.Fatalf("ConvertOpenAPI: %v", err)
}
md := res.Markdown
// Path-level parameter survives merge.
if !strings.Contains(md, "| `widgetId` | path | yes |") {
t.Errorf("missing path-level widgetId row in:\n%s", md)
}
// Operation-level override wins on (name, in) collision: trace
// goes from required=false (path) to required=yes (op).
if !strings.Contains(md, "| `trace` | header | yes |") {
t.Errorf("op-level trace override missing or didn't win in:\n%s", md)
}
if strings.Contains(md, "| `trace` | header | no |") {
t.Errorf("path-level trace row leaked in; op-level should override:\n%s", md)
}
// Op-only parameter present.
if !strings.Contains(md, "| `fields` | query |") {
t.Errorf("missing op-only fields row in:\n%s", md)
}
}
func TestMergeParameters_EmptyInputs(t *testing.T) {
if got := mergeParameters(nil, nil); got != nil {
t.Errorf("mergeParameters(nil, nil) = %v, want nil", got)
}
}
func TestConvertOpenAPI_ArrayOfRefTypeCell(t *testing.T) {
// Schema properties that are arrays of component refs must render
// as a single balanced backtick-wrapped type cell. The previous
// schemaTypeBrief→codeOrBlank pipeline could double-wrap and emit
// "`array of `Pet``" which breaks the table.
spec := []byte(`openapi: 3.0.0
info:
title: Array Of Ref Test
version: "1.0"
paths: {}
components:
schemas:
Pet:
type: object
properties:
name:
type: string
Litter:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
`)
res, err := ConvertOpenAPI(spec, "")
if err != nil {
t.Fatalf("ConvertOpenAPI: %v", err)
}
want := "| `pets` | `array of Pet` |"
if !strings.Contains(res.Markdown, want) {
t.Errorf("missing %q in:\n%s", want, res.Markdown)
}
// Negative: no double-backticked nor stray-backticked variants.
bad := []string{
"`array of `Pet``",
"``array",
"Pet``",
}
for _, b := range bad {
if strings.Contains(res.Markdown, b) {
t.Errorf("found malformed type cell substring %q in:\n%s", b, res.Markdown)
}
}
}
func TestCodeOrBlank(t *testing.T) {
tests := []struct{ in, want string }{
{"", ""},
{"string", "`string`"},
{" string ", "`string`"},
{"array of Pet", "`array of Pet`"},
{"`already`", "`already`"},
{"with`backtick`inside", "`withbacktickinside`"},
{"`", ""},
}
for _, tc := range tests {
if got := codeOrBlank(tc.in); got != tc.want {
t.Errorf("codeOrBlank(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestSchemaTypeBrief_Nil(t *testing.T) {
if got := schemaTypeBrief(nil); got != "" {
t.Errorf("schemaTypeBrief(nil) = %q, want empty", got)
}
}
func TestEscapeTableCell(t *testing.T) {
tests := []struct{ in, want string }{
{`plain text`, `plain text`},
{`with | pipe`, `with \| pipe`},
{`back\slash`, `back\\slash`},
{`both | and \`, `both \| and \\`},
{``, ``},
}
for _, tc := range tests {
if got := escapeTableCell(tc.in); got != tc.want {
t.Errorf("escapeTableCell(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestSingleLine(t *testing.T) {
tests := []struct{ in, want string }{
{"single line", "single line"},
{"multi\nline\ntext", "multi line text"},
{" whitespace collapsed ", "whitespace collapsed"},
{"with | pipe", "with \\| pipe"},
{"", ""},
}
for _, tc := range tests {
if got := singleLine(tc.in); got != tc.want {
t.Errorf("singleLine(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}