diff --git a/compilers/openapi/allof_visibility_test.go b/compilers/openapi/allof_visibility_test.go index ab0c5db3..eead8ca0 100644 --- a/compilers/openapi/allof_visibility_test.go +++ b/compilers/openapi/allof_visibility_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -109,7 +110,7 @@ func TestAllOfVisibilityMerge_ReadOnlyOnRedeclarationSurvives(t *testing.T) { assertNoErrorDiags(t, diags) assert.Equal(t, wantReadOnlyVisibility, got) - assert.False(t, hasDiagCode(diags, diag.ConflictingRedecl), + assert.False(t, openapitest.HasDiag(diags, diag.ConflictingRedecl), "a redeclaration adding readOnly to an unrestricted property is not a disagreement") } @@ -150,8 +151,8 @@ func TestAllOfVisibilityMerge_DisjointRestrictionsAreInvisibleNotAConflict(t *te assertNoErrorDiags(t, diags) assert.Equal(t, ir.Visibility{None: true}, got) - assert.True(t, hasDiagCode(diags, diag.DisjointVisibility), + assert.True(t, openapitest.HasDiag(diags, diag.DisjointVisibility), "an allOf that leaves a field visible nowhere is reported, not merged in silence") - assert.False(t, hasDiagCode(diags, diag.ConflictingRedecl), + assert.False(t, openapitest.HasDiag(diags, diag.ConflictingRedecl), "disjoint readOnly/writeOnly branches intersect to an exact empty set, not an unrepresentable conflict") } diff --git a/compilers/openapi/annotations_test.go b/compilers/openapi/annotations_test.go index 38fbdec1..a1f27a1c 100644 --- a/compilers/openapi/annotations_test.go +++ b/compilers/openapi/annotations_test.go @@ -16,6 +16,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/internal/harness" "github.com/dexpace/morphic/ir" ) @@ -863,7 +864,7 @@ components: assert.Equal(t, ir.ReasonValidationOnly, raw.Reason) }, assertDiags: func(t *testing.T, diags []ir.Diagnostic) { - assert.True(t, hasDiagCode(diags, "openapi/validation-only-keyword"), + assert.True(t, openapitest.HasDiag(diags, "openapi/validation-only-keyword"), "expected a validation-only-keyword info diagnostic") }, } @@ -894,7 +895,7 @@ components: assert.False(t, leaked, "if/then on the declaration must not leak onto the shared primitive") }, assertDiags: func(t *testing.T, diags []ir.Diagnostic) { - assert.True(t, hasDiagCode(diags, "openapi/validation-only-keyword"), + assert.True(t, openapitest.HasDiag(diags, "openapi/validation-only-keyword"), "expected a validation-only-keyword info diagnostic") }, } @@ -933,7 +934,7 @@ components: "a reference-site keyword must not attach to the referent") }, assertDiags: func(t *testing.T, diags []ir.Diagnostic) { - assert.True(t, hasDiagCode(diags, "openapi/validation-only-keyword"), + assert.True(t, openapitest.HasDiag(diags, "openapi/validation-only-keyword"), "expected a validation-only-keyword info diagnostic") }, } @@ -1080,17 +1081,6 @@ func primitiveNode(t *testing.T, doc *ir.Document, id ir.TypeID) ir.TypeDef { return td } -// hasDiagCode reports whether diags contains a diagnostic with the given -// stable code. -func hasDiagCode(diags []ir.Diagnostic, code string) bool { - for _, d := range diags { - if d.Code == code { - return true - } - } - return false -} - // declShape is one declaration shape the SiteKind axis does not name // individually. body holds the keywords that give component S that shape, and // value a literal legal for it, reused for both `example` and `default`. diff --git a/compilers/openapi/conformance_test.go b/compilers/openapi/conformance_test.go index a883418f..12d593ae 100644 --- a/compilers/openapi/conformance_test.go +++ b/compilers/openapi/conformance_test.go @@ -19,6 +19,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" "github.com/dexpace/morphic/ir/irtest" ) @@ -252,15 +253,6 @@ func namedID(name string) ir.TypeID { return ir.TypeID("t/openapi/components/schemas/" + name) } -// propsByWire indexes a model's properties by wire name. -func propsByWire(props []ir.Property) map[string]ir.Property { - out := make(map[string]ir.Property, len(props)) - for _, p := range props { - out[p.WireName] = p - } - return out -} - // allOperations flattens every operation across a document's service groups. func allOperations(doc *ir.Document) []ir.Operation { var out []ir.Operation @@ -930,7 +922,7 @@ func assertNullabilityFourStates(t *testing.T, doc *ir.Document, _ []ir.Diagnost m, ok := doc.Types[namedID("S")].(*ir.Model) require.True(t, ok) require.Len(t, m.Properties, 4) - states := propsByWire(m.Properties) + states := openapitest.PropsByWire(m.Properties) assert.True(t, states["reqPlain"].Required) assert.False(t, states["reqPlain"].Type.Nullable) assert.True(t, states["reqNull"].Required) @@ -952,7 +944,7 @@ func assertNullable31Ref(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { m, ok := doc.Types[namedID("Owner")].(*ir.Model) require.True(t, ok) require.Len(t, m.Properties, 2) - byName := propsByWire(m.Properties) + byName := openapitest.PropsByWire(m.Properties) assert.True(t, byName["p"].Type.Nullable, "3.1's type-array null spelling normalizes to the same IR bit at a $ref site") diff --git a/compilers/openapi/constraints_internal_test.go b/compilers/openapi/constraints_internal_test.go index 779a65b4..d2ffa1fa 100644 --- a/compilers/openapi/constraints_internal_test.go +++ b/compilers/openapi/constraints_internal_test.go @@ -7,12 +7,13 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) func TestConstraints_ExclusiveBoolean30(t *testing.T) { t.Parallel() - spec := componentSpecVer("3.0.3", ` S: + spec := openapitest.ComponentSpecVer("3.0.3", ` S: type: object properties: n: @@ -27,7 +28,7 @@ func TestConstraints_ExclusiveBoolean30(t *testing.T) { // to own, load suppresses that false positive, so a valid 3.0 boolean exclusive // bound lowers cleanly with the flag set and no error diagnostic. doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) c := propConstraints(t, doc, "S", "n") assert.True(t, c.ExclusiveMin) assert.True(t, c.ExclusiveMax) @@ -37,7 +38,7 @@ func TestConstraints_ExclusiveBoolean30(t *testing.T) { func TestConstraints_ExclusiveNumeric31(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: n: @@ -46,7 +47,7 @@ func TestConstraints_ExclusiveNumeric31(t *testing.T) { exclusiveMaximum: 9.5 `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) c := propConstraints(t, doc, "S", "n") assert.True(t, c.ExclusiveMin) assert.True(t, c.ExclusiveMax) @@ -58,7 +59,7 @@ func TestConstraints_ExclusiveNumeric31(t *testing.T) { func TestConstraints_MalformedNumericLiterals(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: a: {type: number, minimum: .inf} @@ -78,7 +79,7 @@ func TestConstraints_MalformedNumericLiterals(t *testing.T) { func TestConstraints_NumericPrecisionSurvives(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: ratio: @@ -88,7 +89,7 @@ func TestConstraints_NumericPrecisionSurvives(t *testing.T) { multipleOf: 0.1 `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) c := m.Properties[0].Constraints require.NotNil(t, c) @@ -116,11 +117,11 @@ func TestConstraints_LosslessNumericLiterals(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - spec := componentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: " + tc.literal + "}\n") + spec := openapitest.ComponentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: " + tc.literal + "}\n") doc, diags := lowerSpec(t, spec) // A valid number, however spelled, is accepted with no error: the // library's float64/JSON complaint is not surfaced. - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) c := propConstraints(t, doc, "S", "n") require.NotNil(t, c.Min) assert.Equal(t, tc.want, *c.Min) @@ -139,10 +140,10 @@ func TestConstraints_LosslessNumericLiterals(t *testing.T) { // reddens this test — it then reports the identical error twice. func TestConstraints_HoistedSubSchemaBadBoundSingleError(t *testing.T) { t.Parallel() - spec := componentSpec(" Foo:\n type: object\n properties:\n bar: {type: number, minimum: hello}\n" + + spec := openapitest.ComponentSpec(" Foo:\n type: object\n properties:\n bar: {type: number, minimum: hello}\n" + " User:\n type: object\n properties:\n b: {$ref: '#/components/schemas/Foo/properties/bar'}\n") _, diags := lowerSpec(t, spec) - assert.Equal(t, 1, countDiagsAt(diags, diag.NumericPrecision, ir.SeverityError), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.NumericPrecision, ir.SeverityError), "one error for the shared bad bound, got: %+v", diags) } @@ -162,11 +163,11 @@ func TestConstraints_ExclusiveWrongDialectForm(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - spec := componentSpecVer(tc.version, + spec := openapitest.ComponentSpecVer(tc.version, " S:\n type: object\n properties:\n n: {type: number, exclusiveMinimum: "+tc.value+"}\n") doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.Equal(t, 1, countDiagsAt(diags, diag.ExclusiveBoundForm, ir.SeverityError), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.ExclusiveBoundForm, ir.SeverityError), "one dialect-form error, got: %+v", diags) // The degenerate bound is dropped, not recorded. m, ok := typeByName(doc, "S").(*ir.Model) @@ -182,7 +183,7 @@ func TestConstraints_ExclusiveWrongDialectForm(t *testing.T) { func TestConstraints_TypeWrongBoundYieldsSingleError(t *testing.T) { t.Parallel() - spec := componentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: hello}\n") + spec := openapitest.ComponentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: hello}\n") _, diags := lowerSpec(t, spec) // Exactly one diagnostic: Morphic's error with the schema's own provenance. // The library emits two redundant float64 type-mismatch findings on the same @@ -195,7 +196,7 @@ func TestConstraints_TypeWrongBoundYieldsSingleError(t *testing.T) { func TestConstraints_NonNumericMinimumErrors(t *testing.T) { t.Parallel() - spec := componentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: hello}\n") + spec := openapitest.ComponentSpec(" S:\n type: object\n properties:\n n: {type: number, minimum: hello}\n") doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) // A genuinely non-numeric bound is never dropped silently: Morphic owns the @@ -230,7 +231,7 @@ func propConstraints(t *testing.T, doc *ir.Document, model, wire string) *ir.Con // numeric-precision error stamped with the component's own pointer. func TestComponentConstraints_DiagnosticProvenance(t *testing.T) { t.Parallel() - spec := componentSpec(" BadN: {type: number, minimum: hello}\n") + spec := openapitest.ComponentSpec(" BadN: {type: number, minimum: hello}\n") _, diags := lowerSpec(t, spec) var found bool for _, d := range diags { diff --git a/compilers/openapi/cycles_test.go b/compilers/openapi/cycles_test.go index 5579da92..10e12ce2 100644 --- a/compilers/openapi/cycles_test.go +++ b/compilers/openapi/cycles_test.go @@ -11,6 +11,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/scan" "github.com/dexpace/morphic/ir" ) @@ -265,7 +266,7 @@ func TestCompile_MergeChainPastBoundStillCompiles(t *testing.T) { compilers.Options{}) require.NoError(t, err) require.NotNil(t, doc, "a legal document is still compiled") - assertHasCode(t, diags, diag.CycleScanFailed, ir.SeverityWarning) + openapitest.AssertHasCode(t, diags, diag.CycleScanFailed, ir.SeverityWarning) for _, d := range diags { assert.NotEqual(t, ir.SeverityError, d.Severity, "no diagnostic refuses the source") } diff --git a/compilers/openapi/helpers_test.go b/compilers/openapi/helpers_test.go index f1b32f18..ad8b7c32 100644 --- a/compilers/openapi/helpers_test.go +++ b/compilers/openapi/helpers_test.go @@ -1,7 +1,6 @@ package openapi import ( - "context" "testing" soa "github.com/speakeasy-api/openapi/openapi" @@ -12,22 +11,23 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/auth" "github.com/dexpace/morphic/compilers/openapi/internal/load" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/operation" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/compilers/openapi/internal/schema" "github.com/dexpace/morphic/ir" ) -// sourceOf wraps a spec string as a compilers.Source. -func sourceOf(src string) compilers.Source { - return compilers.Source{Path: "spec.yaml", Data: []byte(src)} -} - // parseFull runs the whole public compiler pipeline over src. +// +// It is one of the four copies openapitest cannot hold: a helper that drives the +// compiler has to import it, and this package's own internal tests could then +// not import openapitest at all. The four are written identically so a reader +// comparing them finds no difference to account for. func parseFull(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { t.Helper() - doc, diags, err := New().Compile(context.Background(), - []compilers.Source{{Path: "spec.yaml", Data: []byte(src)}}, compilers.Options{}) + doc, diags, err := New().Compile(t.Context(), + []compilers.Source{openapitest.SourceOf(src)}, compilers.Options{}) require.NoError(t, err) require.NotNil(t, doc) return doc, diags @@ -55,14 +55,6 @@ func lowerSpec(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { return l.out, append(diags, l.diags.List()...) } -// requireNoErrorDiags fails the test if any diagnostic has error severity, -// reporting the first offending diagnostic. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - // lowerServiceSpec lowers components and the service layer of src. func lowerServiceSpec(t *testing.T, src string) (*ir.Document, ir.Service, []ir.Diagnostic) { t.Helper() @@ -85,44 +77,6 @@ func lowerServiceSpec(t *testing.T, src string) (*ir.Document, ir.Service, []ir. return doc, doc.Services[0], diags } -// componentSpec wraps a components/schemas block in a minimal 3.1 document. -func componentSpec(schemas string) string { - return componentSpecVer("3.1.0", schemas) -} - -// componentSpecVer wraps a components/schemas block in a minimal document of the -// given OpenAPI version. -func componentSpecVer(version, schemas string) string { - return "openapi: " + version + "\n" + - "info: {title: T, version: \"1\"}\n" + - "paths: {}\n" + - "components:\n schemas:\n" + schemas -} - -// pathsSpec wraps a paths block in a minimal 3.1 document with no components. -func pathsSpec(paths string) string { - return pathsSpecVer("3.1.0", paths) -} - -// pathsSpecVer wraps a paths block in a minimal document of the given OpenAPI -// version, with no components. -func pathsSpecVer(version, paths string) string { - return "openapi: " + version + "\n" + - "info: {title: T, version: \"1\"}\n" + - "paths:\n" + paths -} - -// componentID is the stable TypeID of a components-named schema, or of a -// sub-schema beneath one ("Holder/properties/inner"). -func componentID(name string) ir.TypeID { - return ir.TypeID("t/openapi/components/schemas/" + name) -} - -// typeByName returns the named component schema's lowered TypeDef. -func typeByName(doc *ir.Document, name string) ir.TypeDef { - return doc.Types[componentID(name)] -} - // lowerer is test scaffolding, not a compiler type. The compiler has no such // struct: every lowering is a function of the context, the registry and the // memos, and run owns the document being built (#177). Tests that drive one @@ -137,101 +91,54 @@ type lowerer struct { operationIDs map[string]string } -// newLowerer builds the fixture over one loaded document, as run would. There -// is no srcIndex parameter: every test drives a single source, and the one the -// compiler varies lives in run's caller now. -func newLowerer(doc *load.Document, opts Options) *lowerer { +// lowererOver is the only place the fixture's fields are initialised. Both +// entry points below build on it, so a field added to lowerer cannot reach one +// of them and miss the other — which is what newRawLowerer, hand-constructing +// the struct beside newLowerer, used to allow. +func lowererOver(ctx lowering.Ctx) *lowerer { types := compile.NewTypes(0) return &lowerer{ - ctx: loweringCtx(doc, opts), + ctx: ctx, out: &ir.Document{Types: types.Registry()}, types: types, operationIDs: make(map[string]string), } } +// newLowerer builds the fixture over one loaded document, as run would. There +// is no srcIndex parameter: every test drives a single source, and the one the +// compiler varies lives in run's caller now. +func newLowerer(doc *load.Document, opts Options) *lowerer { + return lowererOver(loweringCtx(doc, opts)) +} + // newRawLowerer builds a lowerer over a hand-constructed document, bypassing the // parser so nil slice/map entries (which the parser panics on) can be exercised. func newRawLowerer(doc *soa.OpenAPI) *lowerer { - rawTypes := compile.NewTypes(0) - l := &lowerer{ - ctx: lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{}), - out: &ir.Document{Types: rawTypes.Registry()}, - types: rawTypes, - operationIDs: make(map[string]string), - } - return l -} - -// assertHasErrorCode requires diags to carry an error-severity diagnostic with -// the given code. -func assertHasErrorCode(t *testing.T, diags []ir.Diagnostic, code string) { - t.Helper() - assertHasCode(t, diags, code, ir.SeverityError) -} - -// assertHasCode requires diags to carry a diagnostic with the given code at the -// given severity. -func assertHasCode(t *testing.T, diags []ir.Diagnostic, code string, sev ir.Severity) { - t.Helper() - for _, d := range diags { - if d.Code == code && d.Severity == sev { - return - } - } - t.Fatalf("expected a %v diagnostic with code %q, got %+v", sev, code, diags) + return lowererOver(lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{})) } -// hasDiag reports whether diags contains a diagnostic with the exact code, at -// any severity. It is the existential half of the vocabulary: use it where a -// test only needs to know a diagnostic fired, not how many or at what -// severity. -func hasDiag(diags []ir.Diagnostic, code string) bool { - for _, d := range diags { - if d.Code == code { - return true - } - } - return false +// componentID is the stable TypeID of a components-named schema, or of a +// sub-schema beneath one ("Holder/properties/inner"). +// +// This one stays a per-package copy where the rest of the vocabulary moved to +// openapitest: internal/archtest's ID-grammar sweep permits a spelled-out ID in +// a test file and refuses one in a production file, and openapitest's files are +// production files. Restating the grammar independently of the compiler is what +// makes these lookups an oracle rather than a tautology, so deriving it through +// compile to satisfy the sweep would be the wrong way out. +func componentID(name string) ir.TypeID { + return ir.TypeID("t/openapi/components/schemas/" + name) } -// countDiagsAt counts the diagnostics in diags matching code and sev exactly. -// code is an exact match with no wildcard: countDiagsAt(diags, "", -// ir.SeverityError) matches only diagnostics whose code is literally empty — -// it is not a way to spell "every error," and reads dangerously like one, so -// callers who want that must filter on severity alone instead. -func countDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { - var n int - for _, d := range diags { - if d.Code == code && d.Severity == sev { - n++ - } - } - return n +// typeByName returns the named component schema's lowered TypeDef. +func typeByName(doc *ir.Document, name string) ir.TypeDef { + return doc.Types[componentID(name)] } -// firstOp returns the operation at svc.Groups[0].Operations[0], requiring both -// to be non-empty first rather than letting a malformed fixture fail with a -// bare index-out-of-range panic. -func firstOp(t *testing.T, svc ir.Service) ir.Operation { +// assertHasErrorCode requires diags to carry an error-severity diagnostic with +// the given code. +func assertHasErrorCode(t *testing.T, diags []ir.Diagnostic, code string) { t.Helper() - require.NotEmpty(t, svc.Groups, "service has no operation groups") - require.NotEmpty(t, svc.Groups[0].Operations, "first group has no operations") - return svc.Groups[0].Operations[0] -} - -// indexBy builds a lookup keyed by key(item), the shape behind every -// hand-rolled "m := map[K]T{}; for _, x := range xs { m[key(x)] = x }" loop -// this suite used to repeat per test. -func indexBy[T any, K comparable](items []T, key func(T) K) map[K]T { - out := make(map[K]T, len(items)) - for _, item := range items { - out[key(item)] = item - } - return out -} - -// propsByWire indexes a model's properties by wire name. -func propsByWire(props []ir.Property) map[string]ir.Property { - return indexBy(props, func(p ir.Property) string { return p.WireName }) + openapitest.AssertHasCode(t, diags, code, ir.SeverityError) } diff --git a/compilers/openapi/internal/annotation/annotation_internal_test.go b/compilers/openapi/internal/annotation/annotation_internal_test.go index 45a67a97..5e65e015 100644 --- a/compilers/openapi/internal/annotation/annotation_internal_test.go +++ b/compilers/openapi/internal/annotation/annotation_internal_test.go @@ -11,6 +11,7 @@ import ( yaml "gopkg.in/yaml.v3" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -50,7 +51,7 @@ func TestAnnotations_ReadsEverySiteLocalAspect(t *testing.T) { node := &oas3.Schema{ Description: new("D"), XML: &oas3.XML{Name: new("Q")}, - Example: yamlNode(t, "hello"), + Example: openapitest.YAMLNode(t, "hello"), } got, diags := Read(Site{Kind: Declaration, Node: node}, "/components/schemas/S", 0) @@ -199,15 +200,6 @@ func TestNoIRHomeAt_ModelSetWithoutRawSourceRecordsNothing(t *testing.T) { assert.Empty(t, diags, "and nothing is announced, so the two channels agree") } -// yamlNode parses src as a single YAML document and returns its root node. -func yamlNode(t *testing.T, src string) *yaml.Node { - t.Helper() - var doc yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) - require.Len(t, doc.Content, 1, "expected a single document node") - return doc.Content[0] -} - // TestKind_String covers both named values and the default case, so an // assertion failure or test diff over a Kind prints a name instead of a bare // int. diff --git a/compilers/openapi/internal/annotation/rawjson_internal_test.go b/compilers/openapi/internal/annotation/rawjson_internal_test.go index 45fc5638..a92dde9f 100644 --- a/compilers/openapi/internal/annotation/rawjson_internal_test.go +++ b/compilers/openapi/internal/annotation/rawjson_internal_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" + + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" ) // decodeAndMarshal is the conversion RawFromNode used before GitHub #32: decode @@ -68,7 +70,7 @@ func TestRawFromNode_KeepsNumericLiteralsExact(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := RawFromNode(yamlNode(t, tc.yaml)) + got, err := RawFromNode(openapitest.YAMLNode(t, tc.yaml)) require.NoError(t, err) assert.Equal(t, tc.want, string(got)) }) @@ -94,7 +96,7 @@ func TestRawFromNode_ResolvesYAMLIntegerBases(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := RawFromNode(yamlNode(t, tc.yaml)) + got, err := RawFromNode(openapitest.YAMLNode(t, tc.yaml)) require.NoError(t, err) assert.Equal(t, tc.want, string(got)) assert.True(t, json.Valid(got), "every rendered number is JSON-valid") @@ -141,7 +143,7 @@ func TestRawFromNode_RendersEveryScalarTag(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := RawFromNode(yamlNode(t, tc.yaml)) + got, err := RawFromNode(openapitest.YAMLNode(t, tc.yaml)) require.NoError(t, err) assert.Equal(t, tc.want, string(got)) }) @@ -171,7 +173,7 @@ func TestRawFromNode_PreservesMergeAndOrderingSemantics(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := RawFromNode(yamlNode(t, tc.yaml)) + got, err := RawFromNode(openapitest.YAMLNode(t, tc.yaml)) require.NoError(t, err) assert.Equal(t, tc.want, string(got)) }) @@ -216,7 +218,7 @@ func TestRawFromNode_RefusesWhatJSONCannotName(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := RawFromNode(yamlNode(t, tc.yaml)) + got, err := RawFromNode(openapitest.YAMLNode(t, tc.yaml)) require.Error(t, err) assert.Nil(t, got, "a refusal writes nothing") assert.Contains(t, err.Error(), tc.wantErr) @@ -398,7 +400,7 @@ func TestRawFromNode_DiffersFromTheOldDecodeOnlyWhereRecorded(t *testing.T) { for _, src := range rawEquivalenceCorpus { t.Run(src, func(t *testing.T) { t.Parallel() - node := yamlNode(t, src) + node := openapitest.YAMLNode(t, src) want, oldErr := decodeAndMarshal(node) got, newErr := RawFromNode(node) @@ -465,14 +467,14 @@ func TestRawConv_RefusesNodesNoCallerShouldPass(t *testing.T) { assert.Nil(t, got) assert.Contains(t, err.Error(), "nil yaml node") - err = c.mappingInto(map[string]json.RawMessage{}, yamlNode(t, "[1]"), 0) + err = c.mappingInto(map[string]json.RawMessage{}, openapitest.YAMLNode(t, "[1]"), 0) require.Error(t, err, "filling a mapping from a sequence is a caller bug") assert.Contains(t, err.Error(), "expected a mapping") // scalar routes only !!timestamp and !!binary into verbatimTagged, so no // input reaches this arm. It answers rather than falling through to the // base64 check, which is what a third tag added to that case would hit. - got, err = c.verbatimTagged(yamlNode(t, "plain")) + got, err = c.verbatimTagged(openapitest.YAMLNode(t, "plain")) require.Error(t, err, "a tag scalar does not route here is a caller bug") assert.Nil(t, got) assert.Contains(t, err.Error(), `scalar tag "!!str" is not kept verbatim`) diff --git a/compilers/openapi/internal/annotation/readers_internal_test.go b/compilers/openapi/internal/annotation/readers_internal_test.go index b617b99d..4d9e75ec 100644 --- a/compilers/openapi/internal/annotation/readers_internal_test.go +++ b/compilers/openapi/internal/annotation/readers_internal_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -428,15 +429,15 @@ func TestRawFromNode_DistinguishesAbsentFromUnconvertible(t *testing.T) { require.NoError(t, err) assert.Nil(t, got, "an absent node is not a failure") - got, err = RawFromNode(yamlNode(t, "{a: 1}")) + got, err = RawFromNode(openapitest.YAMLNode(t, "{a: 1}")) require.NoError(t, err) assert.JSONEq(t, `{"a":1}`, string(got)) - got, err = RawFromNode(yamlNode(t, ".nan")) + got, err = RawFromNode(openapitest.YAMLNode(t, ".nan")) require.Error(t, err, "a value that decodes but does not marshal is a failure") assert.Nil(t, got) - got, err = RawFromNode(yamlNode(t, "{? [1, 2]\n: v}")) + got, err = RawFromNode(openapitest.YAMLNode(t, "{? [1, 2]\n: v}")) require.Error(t, err, "a mapping with a non-string key does not decode into the JSON model") assert.Nil(t, got) } @@ -457,8 +458,8 @@ func TestRawChildNode_ReadsOnlyAMappingChild(t *testing.T) { assert.Nil(t, RawChildNode(nil, "a")) assert.Nil(t, RawChildNode(&doc, "absent")) - assert.Nil(t, RawChildNode(yamlNode(t, "[1, 2]"), "a"), "a sequence has no keyed children") - assert.Nil(t, RawChildNode(yamlNode(t, "plain"), "a"), "nor does a scalar") + assert.Nil(t, RawChildNode(openapitest.YAMLNode(t, "[1, 2]"), "a"), "a sequence has no keyed children") + assert.Nil(t, RawChildNode(openapitest.YAMLNode(t, "plain"), "a"), "nor does a scalar") assert.Nil(t, RawChildNode(&yaml.Node{Kind: yaml.DocumentNode}, "a"), "nor an empty document") } @@ -517,13 +518,13 @@ func TestPreserveNodeInto_ReportsWhichOfThreeOutcomesHappened(t *testing.T) { assert.Empty(t, diags) assert.Nil(t, p) - kept, diags = PreserveNodeInto(&p, "openapi:x", yamlNode(t, ".nan"), ir.ReasonNoIRHome, "/x", 0) + kept, diags = PreserveNodeInto(&p, "openapi:x", openapitest.YAMLNode(t, ".nan"), ir.ReasonNoIRHome, "/x", 0) assert.False(t, kept) require.Len(t, diags, 1) assert.Equal(t, ir.SeverityError, diags[0].Severity) assert.Nil(t, p, "an unconvertible node writes no entry") - kept, diags = PreserveNodeInto(&p, "openapi:x", yamlNode(t, "{a: 1}"), ir.ReasonNoIRHome, "/x", 0) + kept, diags = PreserveNodeInto(&p, "openapi:x", openapitest.YAMLNode(t, "{a: 1}"), ir.ReasonNoIRHome, "/x", 0) assert.True(t, kept) assert.Empty(t, diags) assert.JSONEq(t, `{"a":1}`, string(p["openapi:x"].Value)) @@ -534,7 +535,7 @@ func TestPreserveNodeInto_ReportsWhichOfThreeOutcomesHappened(t *testing.T) { // leaves no trace at all, which is a losslessness failure. func TestUnpreservableDiag_IsAnErrorNotADegradation(t *testing.T) { t.Parallel() - _, err := RawFromNode(yamlNode(t, ".nan")) + _, err := RawFromNode(openapitest.YAMLNode(t, ".nan")) require.Error(t, err) got := UnpreservableDiag("openapi:not", "/components/schemas/S/not", 2, err) diff --git a/compilers/openapi/internal/auth/auth_test.go b/compilers/openapi/internal/auth/auth_test.go index 8e1491a2..947d23bf 100644 --- a/compilers/openapi/internal/auth/auth_test.go +++ b/compilers/openapi/internal/auth/auth_test.go @@ -16,6 +16,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -107,7 +108,7 @@ func TestAuth_SchemeKinds(t *testing.T) { " securitySchemes:\n" + " s: " + tc.scheme + "\n" doc, _, diags := serviceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) s, ok := doc.Auth[ids.Auth("s")] require.True(t, ok) tc.check(t, s) @@ -136,7 +137,7 @@ components: scopes: {read: r, write: w} ` doc, svc, diags := serviceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) keyID := ids.Auth("key") scheme, ok := doc.Auth[keyID] @@ -174,7 +175,7 @@ components: s: {type: apiKey, in: header, name: X-Key, x-bad: {1: intkey}} ` doc, _, diags := serviceSpec(t, spec) - assert.True(t, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, + assert.True(t, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, "an entirely unserializable extension still warns even though the scheme's own Unmodeled ends up empty") scheme, ok := doc.Auth[ids.Auth("s")] require.True(t, ok) @@ -226,7 +227,7 @@ func TestAuth_AllSchemeKinds(t *testing.T) { oauth := byKind[ir.AuthKindOAuth2] assert.NotEmpty(t, oauth.Unmodeled, "oauth x-* extension") - kinds := indexBy(oauth.Flows, func(f ir.OAuthFlow) string { return f.Kind }) + kinds := openapitest.IndexBy(oauth.Flows, func(f ir.OAuthFlow) string { return f.Kind }) assert.Len(t, oauth.Flows, 5) assert.Equal(t, "https://r", kinds["authorization_code"].RefreshURL) assert.NotEmpty(t, kinds["authorization_code"].Scopes) @@ -245,7 +246,7 @@ func TestAuth_AllSchemeKinds(t *testing.T) { func TestAuth_OAuthNoFlowsUnknownTypeAndGhostRef(t *testing.T) { t.Parallel() - spec := pathsSpec(` /x: + spec := openapitest.PathsSpec(` /x: get: {operationId: x, responses: {"200": {description: ok}}} components: securitySchemes: @@ -561,24 +562,6 @@ func serviceSpec(t *testing.T, src string) (*ir.Document, ir.Service, []ir.Diagn return doc, doc.Services[0], diags } -// requireNoErrorDiags fails the test if any diagnostic has error severity. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - -// countDiagsAt counts the diagnostics matching code and sev exactly. -func countDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { - var n int - for _, d := range diags { - if d.Code == code && d.Severity == sev { - n++ - } - } - return n -} - // firstDiagAt returns the first diagnostic carrying code, so a test can assert // on its provenance pointer. func firstDiagAt(diags []ir.Diagnostic, code string) (ir.Diagnostic, bool) { @@ -641,15 +624,6 @@ func operationsByDeclaration(svc ir.Service) map[string]ir.Operation { return out } -// indexBy builds a lookup keyed by key(item). -func indexBy[T any, K comparable](items []T, key func(T) K) map[K]T { - out := make(map[K]T, len(items)) - for _, item := range items { - out[key(item)] = item - } - return out -} - // TestLowerSecuritySchemes_KeptFieldsAndExtensionsShareOneMap pins that the two // writers of a scheme's Unmodeled map do not overwrite each other. The x-* // extensions used to be the only one and were assigned over the whole map, so a @@ -774,13 +748,6 @@ func unmodeledKeys(u ir.Unmodeled) []string { return out } -// pathsSpec wraps a paths block in a minimal 3.1 document with no components. -func pathsSpec(paths string) string { - return "openapi: 3.1.0\n" + - "info: {title: T, version: \"1\"}\n" + - "paths:\n" + paths -} - // TestSecurityRequirement_OneUndeclaredMemberDropsTheWholeOption pins the // refusal issue #14 exists for, corrected per issue #41. A requirement may name // any string; only a name the document declares has an AuthID behind it, and @@ -803,7 +770,7 @@ components: `) assert.Nil(t, svc.Auth, "the sole option named an AND of ghost+key; ghost failing to resolve drops it whole, key included") - assert.Equal(t, 1, countDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError), "the drop is reported exactly once: %+v", diags) } @@ -917,7 +884,7 @@ components: require.Len(t, svc.Auth[0].Schemes, 1) assert.Equal(t, ids.Auth("key"), svc.Auth[0].Schemes[0].Scheme, "the first option survives in place") assert.Empty(t, svc.Auth[1].Schemes, "the trailing empty option still means no-auth-is-fine") - assert.Equal(t, 1, countDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError)) + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError)) d, ok := firstDiagAt(diags, diag.UnresolvedRef) require.True(t, ok, "an unresolved-ref diagnostic: %+v", diags) assert.Equal(t, "/security/1", d.Provenance.Pointer, diff --git a/compilers/openapi/internal/load/entry_internal_test.go b/compilers/openapi/internal/load/entry_internal_test.go index 0e39055c..858e8446 100644 --- a/compilers/openapi/internal/load/entry_internal_test.go +++ b/compilers/openapi/internal/load/entry_internal_test.go @@ -11,14 +11,10 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" ) -// sourceOf wraps src as the single source a load call takes. -func sourceOf(src string) compilers.Source { - return compilers.Source{Path: "spec.yaml", Data: []byte(src)} -} - // TestLoad_DegenerateCycleIsRefusedBeforeParsing pins the first gate in the // load path. A document whose references close a cycle is refused with a // diagnostic rather than handed to a parser that would fault on it, so the @@ -42,7 +38,7 @@ func TestLoad_DegenerateCycleIsRefusedBeforeParsing(t *testing.T) { // TestUnmarshal_RecoversParserPanic covers.) func TestLoad_UnparseableSourceIsAGoError(t *testing.T) { t.Parallel() - doc, diags, err := Load(t.Context(), 3, sourceOf("\tnot: yaml\n"), Options{}) + doc, diags, err := Load(t.Context(), 3, openapitest.SourceOf("\tnot: yaml\n"), Options{}) require.Error(t, err) assert.Contains(t, err.Error(), "source 3", "the failing source is named") @@ -78,7 +74,7 @@ components: // document-version run, which is what makes it an artifact rather than a defect. func TestLoad_32KeywordIsNotAnInvalidSchema(t *testing.T) { t.Parallel() - doc, diags, err := Load(t.Context(), 0, sourceOf(defaultMapping32Spec), Options{}) + doc, diags, err := Load(t.Context(), 0, openapitest.SourceOf(defaultMapping32Spec), Options{}) require.NoError(t, err) require.NotNil(t, doc) @@ -129,7 +125,7 @@ func TestMetaSchemaVersionArtifacts_AFindingBothRunsRaiseIsKept(t *testing.T) { // an unresolved reference, so the fault never escapes as a Go error or a crash. func TestLoad_ResolverFaultBecomesADiagnostic(t *testing.T) { t.Parallel() - doc, diags, err := Load(t.Context(), 0, sourceOf(resolverPanicSpec), Options{}) + doc, diags, err := Load(t.Context(), 0, openapitest.SourceOf(resolverPanicSpec), Options{}) require.NoError(t, err, "a resolver fault is a spec problem, not a Go error") assert.NotNil(t, doc, "resolution failure does not stop the document being lowered") @@ -149,7 +145,7 @@ info: {title: T, version: "1"} paths: {} components: {schemas: {S: {type: number, minimum: .5}}} ` - _, diags, err := Load(t.Context(), 0, sourceOf(spec), Options{}) + _, diags, err := Load(t.Context(), 0, openapitest.SourceOf(spec), Options{}) require.NoError(t, err) assert.Zero(t, countErrorsAt(diags, @@ -209,7 +205,7 @@ components: schemas: Pet: {type: object} ` - got, diags, err := Load(t.Context(), 0, sourceOf(spec), + got, diags, err := Load(t.Context(), 0, openapitest.SourceOf(spec), overlayOptions(" - target: $.components.schemas\n update:\n Owner: {type: object}\n")) require.NoError(t, err) @@ -227,7 +223,7 @@ components: // an IR describing no document anyone has. func TestLoad_RefusesToLowerAfterAnOverlayFails(t *testing.T) { t.Parallel() - got, diags, err := Load(t.Context(), 0, sourceOf(minimal31), + got, diags, err := Load(t.Context(), 0, openapitest.SourceOf(minimal31), overlayOptions(" - target: $.paths['/nope']\n update: {description: x}\n")) require.NoError(t, err, "a bad overlay is a document problem, not a Go error") @@ -250,11 +246,11 @@ components: A: {$ref: '#/components/schemas/B'} B: {type: string} ` - clean, _, err := Load(t.Context(), 0, sourceOf(acyclic), Options{}) + clean, _, err := Load(t.Context(), 0, openapitest.SourceOf(acyclic), Options{}) require.NoError(t, err) require.NotNil(t, clean, "the source alone gives the scan nothing to find") - got, diags, err := Load(t.Context(), 0, sourceOf(acyclic), + got, diags, err := Load(t.Context(), 0, openapitest.SourceOf(acyclic), overlayOptions(" - target: $.components.schemas.B\n update: {$ref: '#/components/schemas/A'}\n")) require.NoError(t, err) @@ -268,7 +264,7 @@ components: // are not YAML at all get, reached one step later. func TestLoad_ADocumentThatFailsToBuildIsAGoError(t *testing.T) { t.Parallel() - doc, diags, err := Load(t.Context(), 5, sourceOf(" "), Options{}) + doc, diags, err := Load(t.Context(), 5, openapitest.SourceOf(" "), Options{}) require.Error(t, err) assert.ErrorIs(t, err, errParse) @@ -292,13 +288,13 @@ func TestLoad_RejectsAnOverlaySharingTheSourceIndex(t *testing.T) { opts := overlayOptions(" - target: $.info\n update: {description: d}\n") opts.OverlaySrcIndex = 2 - refused, _, err := Load(t.Context(), 2, sourceOf(minimal31), opts) + refused, _, err := Load(t.Context(), 2, openapitest.SourceOf(minimal31), opts) require.Error(t, err, "the overlay may not share source 2's index") assert.Contains(t, err.Error(), "overlay source index 2", "the collision is named") assert.Nil(t, refused) opts.OverlaySrcIndex = 3 - got, diags, err := Load(t.Context(), 2, sourceOf(minimal31), opts) + got, diags, err := Load(t.Context(), 2, openapitest.SourceOf(minimal31), opts) require.NoError(t, err, "an index of its own is fine: %+v", diags) assert.True(t, got.Overlay.Applied()) } diff --git a/compilers/openapi/internal/load/load_internal_test.go b/compilers/openapi/internal/load/load_internal_test.go index 03a4bf71..028278cb 100644 --- a/compilers/openapi/internal/load/load_internal_test.go +++ b/compilers/openapi/internal/load/load_internal_test.go @@ -14,6 +14,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -312,19 +313,19 @@ func TestInvalidSyntaxOnValidNumbers_Candidacy(t *testing.T) { scalars []*yaml.Node want bool }{ - {"leading dot", []*yaml.Node{scalarNode("!!float", ".5")}, true}, - {"octal", []*yaml.Node{scalarNode("!!int", "0644")}, true}, - {"separators", []*yaml.Node{scalarNode("!!int", "1_000")}, true}, + {"leading dot", []*yaml.Node{openapitest.ScalarNode("!!float", ".5")}, true}, + {"octal", []*yaml.Node{openapitest.ScalarNode("!!int", "0644")}, true}, + {"separators", []*yaml.Node{openapitest.ScalarNode("!!int", "1_000")}, true}, {"recoverable beside a json-valid literal", - []*yaml.Node{scalarNode("!!float", ".5"), scalarNode("!!int", "42")}, true}, + []*yaml.Node{openapitest.ScalarNode("!!float", ".5"), openapitest.ScalarNode("!!int", "42")}, true}, - {"nothing to recover", []*yaml.Node{scalarNode("!!int", "42")}, false}, - {"infinity", []*yaml.Node{scalarNode("!!float", ".inf")}, false}, + {"nothing to recover", []*yaml.Node{openapitest.ScalarNode("!!int", "42")}, false}, + {"infinity", []*yaml.Node{openapitest.ScalarNode("!!float", ".inf")}, false}, {"recoverable beside an unrecoverable literal", - []*yaml.Node{scalarNode("!!float", ".5"), scalarNode("!!float", ".inf")}, false}, + []*yaml.Node{openapitest.ScalarNode("!!float", ".5"), openapitest.ScalarNode("!!float", ".inf")}, false}, // JSON accepts "-0", so normalizing it to "0" is not evidence that it // provoked anything. - {"negative zero", []*yaml.Node{scalarNode("!!int", "-0")}, false}, + {"negative zero", []*yaml.Node{openapitest.ScalarNode("!!int", "-0")}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -383,11 +384,6 @@ func countErrorsAt(diags []ir.Diagnostic, code string) int { return n } -// scalarNode builds a bare scalar yaml.Node with the given tag and value. -func scalarNode(tag, val string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: val} -} - // TestUnmarshal_RejectsADocumentNodeHoldingMoreThanOneRoot pins the model // build's other failure exit: the library refuses a document node that does not // wrap exactly one root, and that refusal is a Go error rather than a validation diff --git a/compilers/openapi/internal/lowering/lowering_test.go b/compilers/openapi/internal/lowering/lowering_test.go index 707ac74f..8b88bb4e 100644 --- a/compilers/openapi/internal/lowering/lowering_test.go +++ b/compilers/openapi/internal/lowering/lowering_test.go @@ -4,31 +4,18 @@ import ( "reflect" "testing" - oas3 "github.com/speakeasy-api/openapi/jsonschema/oas3" soa "github.com/speakeasy-api/openapi/openapi" - "github.com/speakeasy-api/openapi/sequencedmap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/ir" ) -// docDeclaring builds a document declaring the named component schemas, with no -// parser and no fixture — which is the point of deriving the index from the -// document rather than from a lowering pass. -func docDeclaring(names ...string) *soa.OpenAPI { - elems := make([]*sequencedmap.Element[string, *oas3.JSONSchema[oas3.Referenceable]], 0, len(names)) - for _, n := range names { - elems = append(elems, sequencedmap.NewElem(n, - oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{}))) - } - return &soa.OpenAPI{Components: &soa.Components{Schemas: sequencedmap.New(elems...)}} -} - // TestCtx_HasNoExportedMap is the guard that makes "immutable by value" true // rather than conventional. A struct copy shares a map rather than copying it, // so an exported map field would be the one part of the context a callee could @@ -72,7 +59,7 @@ func TestNew_DerivesTheDeclaredSchemaNames(t *testing.T) { denies []string }{ { - name: "every declared component", doc: docDeclaring("User", "Order", "A~B"), + name: "every declared component", doc: openapitest.DocDeclaring("User", "Order", "A~B"), declares: []string{"User", "Order", "A~B"}, denies: []string{"Missing", ""}, }, { @@ -84,7 +71,7 @@ func TestNew_DerivesTheDeclaredSchemaNames(t *testing.T) { denies: []string{"User"}, }, { - name: "the empty name is a name like any other", doc: docDeclaring(""), + name: "the empty name is a name like any other", doc: openapitest.DocDeclaring(""), declares: []string{""}, denies: []string{"User"}, }, { @@ -120,7 +107,7 @@ func TestDeclaresSchema_TheZeroContextDeclaresNothing(t *testing.T) { // Provenance the compile stamps is built from the last two. func TestNew_KeepsTheDocumentItWasGiven(t *testing.T) { t.Parallel() - doc := docDeclaring("User") + doc := openapitest.DocDeclaring("User") src := ir.SourceInfo{Format: "openapi@3.1", Path: "spec.yaml", Hash: "abc"} c := lowering.New(7, doc, src, lowering.GroupByPathPrefix, overlay.Origin{}) @@ -138,7 +125,7 @@ func TestNew_KeepsTheDocumentItWasGiven(t *testing.T) { // above the security-scheme phase able to see them. func TestWithAuth_ExtendsACopy(t *testing.T) { t.Parallel() - doc := docDeclaring("User") + doc := openapitest.DocDeclaring("User") src := ir.SourceInfo{Format: "openapi@3.1", Path: "spec.yaml", Hash: "abc"} before := lowering.New(7, doc, src, lowering.GroupByPathPrefix, overlay.Origin{}) schemes := map[ir.AuthID]ir.AuthScheme{"a/apiKey": {ID: "a/apiKey"}} @@ -215,7 +202,7 @@ func TestExclusiveBoundIsBoolean_FollowsTheDialect(t *testing.T) { // decides whether an internal pointer names anything. func TestRefScope_IsTheContextSeenAsAScope(t *testing.T) { t.Parallel() - c := lowering.New(0, docDeclaring("User"), ir.SourceInfo{Path: "spec.yaml"}, "", overlay.Origin{}) + c := lowering.New(0, openapitest.DocDeclaring("User"), ir.SourceInfo{Path: "spec.yaml"}, "", overlay.Origin{}) scope := c.RefScope() @@ -281,7 +268,7 @@ func TestSources_ListsTheOverlayAfterTheSourceItPatched(t *testing.T) { "overlay: 1.0.0\ninfo: {title: O, version: \"1\"}\nactions:\n"+ " - target: $.info\n update: {description: d}\n") - c := lowering.New(0, docDeclaring(), src, "", origin) + c := lowering.New(0, openapitest.DocDeclaring(), src, "", origin) require.Len(t, c.Sources(), 2) assert.Equal(t, src, c.Sources()[0], "the source being lowered comes first") @@ -296,7 +283,7 @@ func TestSources_ListsOnlyTheSourceWhenNoOverlayApplied(t *testing.T) { t.Parallel() src := ir.SourceInfo{Format: "openapi@3.1", Path: "spec.yaml"} - c := lowering.New(0, docDeclaring(), src, "", overlay.Origin{}) + c := lowering.New(0, openapitest.DocDeclaring(), src, "", overlay.Origin{}) assert.Equal(t, []ir.SourceInfo{src}, c.Sources()) } @@ -312,7 +299,7 @@ func TestProvenanceAt_NamesTheOverlayForThePositionsItIntroduced(t *testing.T) { "overlay: 1.0.0\ninfo: {title: O, version: \"1\"}\nactions:\n"+ " - target: $.info\n update: {description: d}\n") - c := lowering.New(0, docDeclaring(), ir.SourceInfo{}, "", origin) + c := lowering.New(0, openapitest.DocDeclaring(), ir.SourceInfo{}, "", origin) assert.Equal(t, ir.Provenance{Source: 1, Pointer: "/info/description"}, c.ProvenanceAt("/info/description"), "the overlay introduced this position") diff --git a/compilers/openapi/internal/openapitest/doc.go b/compilers/openapi/internal/openapitest/doc.go new file mode 100644 index 00000000..5af5afa8 --- /dev/null +++ b/compilers/openapi/internal/openapitest/doc.go @@ -0,0 +1,55 @@ +// Package openapitest holds the test scaffolding every test package under +// compilers/openapi would otherwise carry as its own copy. +// +// Go test files cannot share unexported helpers across packages, so the split +// into one package per lowering stage duplicated the scaffolding instead of +// retiring it: the same sourceOf, requireNoErrorDiags and componentSpec ended +// up defined once per package, byte for byte. A helper that exists once cannot +// drift; five copies held in step only by review can. +// +// It sits here rather than in the repo-root internal/testspec, which holds the +// fixture spec strings the engine and CLI tests share: that package cannot +// import compilers/openapi/internal/..., and this one needs diag for the +// diagnostic codes it matches on. +// +// # What may live here +// +// A helper belongs here only if it is reachable from every test package under +// compilers/openapi, internal and external alike. That bounds the imports to +// ir, compilers, diag and third-party libraries — packages no test package +// under compilers/openapi sits inside. Reaching any further would make this +// package unimportable from the internal tests of whatever it reached, because +// an internal test file may not import a package that imports its own. +// +// Two families of scaffolding are therefore absent by necessity, not oversight: +// +// - parseFull, which drives the whole compiler, needs compilers/openapi, and +// that package's own internal tests could then not import this one. +// - The lowerer fixture needs lowering.Ctx and schema.AnchorIndex, which would +// shut out the internal tests of load, resolve, annotation, schema and +// everything else beneath them. +// +// Both stay with the packages that drive them, each built on a single +// field-initialising constructor so the fixture cannot drift within a package +// even where it must be repeated across them. +// +// A third family is absent for a different reason. componentID and typeByName +// spell a type ID out as a string, and internal/archtest's ID-grammar sweep +// permits that in a test file while refusing it in a production file — which is +// what these are. Deriving the ID through compile instead would satisfy the +// sweep by making the lookup agree with the compiler by construction, and a +// lookup that cannot disagree is no longer an oracle, so those two stay in the +// test files that spell them. +package openapitest + +// TB is the subset of *testing.T these helpers need. +// +// It is an interface rather than *testing.T so this package's own tests can +// drive the failure branches with a recording stub, the way ir/irtest does: +// a helper that aborts a real test cannot have its abort path covered. +type TB interface { + Helper() + Errorf(format string, args ...any) + FailNow() + Fatalf(format string, args ...any) +} diff --git a/compilers/openapi/internal/openapitest/result.go b/compilers/openapi/internal/openapitest/result.go new file mode 100644 index 00000000..f5f444b9 --- /dev/null +++ b/compilers/openapi/internal/openapitest/result.go @@ -0,0 +1,174 @@ +package openapitest + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/ir" +) + +// FindOp returns the operation whose source name matches. +func FindOp(t TB, doc *ir.Document, source string) ir.Operation { + t.Helper() + for _, g := range doc.Services[0].Groups { + for _, op := range g.Operations { + if op.Name.Source == source { + return op + } + } + } + t.Fatalf("operation %q not found", source) + return ir.Operation{} +} + +// FirstOp returns the operation at svc.Groups[0].Operations[0], requiring both +// to be non-empty first rather than letting a malformed fixture fail with a bare +// index-out-of-range panic. +func FirstOp(t TB, svc ir.Service) ir.Operation { + t.Helper() + require.NotEmpty(t, svc.Groups, "service has no operation groups") + require.NotEmpty(t, svc.Groups[0].Operations, "first group has no operations") + return svc.Groups[0].Operations[0] +} + +// IndexBy builds a lookup keyed by key(item), the shape behind every +// hand-rolled "m := map[K]T{}; for _, x := range xs { m[key(x)] = x }" loop +// these suites used to repeat per test. +func IndexBy[T any, K comparable](items []T, key func(T) K) map[K]T { + out := make(map[K]T, len(items)) + for _, item := range items { + out[key(item)] = item + } + return out +} + +// PropsByWire indexes a model's properties by wire name. +func PropsByWire(props []ir.Property) map[string]ir.Property { + return IndexBy(props, func(p ir.Property) string { return p.WireName }) +} + +// RequireNoErrorDiags fails the test if any diagnostic has error severity, +// reporting the first offending diagnostic. +func RequireNoErrorDiags(t TB, diags []ir.Diagnostic) { + t.Helper() + d, ok := ir.FirstError(diags) + require.False(t, ok, "unexpected error diagnostic: %+v", d) +} + +// AssertHasCode requires diags to carry a diagnostic with the given code at the +// given severity. +func AssertHasCode(t TB, diags []ir.Diagnostic, code string, sev ir.Severity) { + t.Helper() + for _, d := range diags { + if d.Code == code && d.Severity == sev { + return + } + } + t.Fatalf("expected a %v diagnostic with code %q, got %+v", sev, code, diags) +} + +// HasDiag reports whether diags contains a diagnostic with the exact code, at +// any severity. It is the existential half of the vocabulary: use it where a +// test only needs to know a diagnostic fired, not how many or at what severity. +func HasDiag(diags []ir.Diagnostic, code string) bool { + for _, d := range diags { + if d.Code == code { + return true + } + } + return false +} + +// HasDiagAt reports whether diags contains a diagnostic with the exact code at +// the exact severity. +func HasDiagAt(diags []ir.Diagnostic, code string, sev ir.Severity) bool { + return CountDiagsAt(diags, code, sev) > 0 +} + +// HasDiagCodeAt reports whether diags carries code at exactly pointer. +func HasDiagCodeAt(diags []ir.Diagnostic, code, pointer string) bool { + for _, d := range diags { + if d.Code == code && d.Provenance.Pointer == pointer { + return true + } + } + return false +} + +// CountDiagsAt counts the diagnostics in diags matching code and sev exactly. +// code is an exact match with no wildcard: CountDiagsAt(diags, "", +// ir.SeverityError) matches only diagnostics whose code is literally empty — it +// is not a way to spell "every error," and reads dangerously like one, so +// callers who want that must filter on severity alone instead. +func CountDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { + var n int + for _, d := range diags { + if d.Code == code && d.Severity == sev { + n++ + } + } + return n +} + +// DiagMessageAt returns the message of the single diagnostic matching code, +// severity and provenance pointer. Tests that only compare a diagnostic's code +// cannot tell two lowerings apart when both report the same code with different +// reasons, so the reason itself needs an assertable handle. +func DiagMessageAt(t TB, diags []ir.Diagnostic, code string, sev ir.Severity, pointer string) string { + t.Helper() + var found []string + for _, d := range diags { + if d.Code == code && d.Severity == sev && d.Provenance.Pointer == pointer { + found = append(found, d.Message) + } + } + require.Len(t, found, 1, "want exactly one %v %q at %q, got %+v", sev, code, pointer, diags) + return found[0] +} + +// FirstDegradedWarning returns the first diag.DegradedConstruct warning in +// diags, and whether one was found — the pointer/message inspection counterpart +// to HasDiagAt/CountDiagsAt. +func FirstDegradedWarning(diags []ir.Diagnostic) (ir.Diagnostic, bool) { + for _, d := range diags { + if d.Code == diag.DegradedConstruct && d.Severity == ir.SeverityWarning { + return d, true + } + } + return ir.Diagnostic{}, false +} + +// AssertInfoDiagAt requires one info diagnostic stamped at pointer. +func AssertInfoDiagAt(t TB, diags []ir.Diagnostic, pointer string) { + t.Helper() + for _, d := range diags { + if d.Severity == ir.SeverityInfo && d.Provenance.Pointer == pointer { + return + } + } + assert.Fail(t, "nothing announced this", "no info diagnostic at %q; got %+v", pointer, diags) +} + +// AssertProbeDocsKept checks all three documentation keywords InlineProbeBody +// writes reached d, wherever the position's home turned out to be. +func AssertProbeDocsKept(t TB, d ir.Docs) { + t.Helper() + assert.Equal(t, "SUM", d.Summary, "title") + assert.Equal(t, "DOC", d.Description, "description") + if assert.Len(t, d.ExternalDocs, 1, "externalDocs") { + assert.Equal(t, "https://e.example", d.ExternalDocs[0].URL) + assert.Equal(t, "ED", d.ExternalDocs[0].Description) + } +} + +// AssertProbeExample checks the single example InlineProbeBody writes reached +// the home under test with its value intact. +func AssertProbeExample(t TB, examples []ir.Example) { + t.Helper() + if !assert.Len(t, examples, 1, "examples") { + return + } + require.NotNil(t, examples[0].Value) + assert.Equal(t, "abc", examples[0].Value.Str) +} diff --git a/compilers/openapi/internal/openapitest/result_test.go b/compilers/openapi/internal/openapitest/result_test.go new file mode 100644 index 00000000..c138e74f --- /dev/null +++ b/compilers/openapi/internal/openapitest/result_test.go @@ -0,0 +1,218 @@ +package openapitest_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" + "github.com/dexpace/morphic/ir" +) + +// recorder is a TB that records failures instead of aborting the run. +// +// Fatalf and FailNow deliberately return rather than calling runtime.Goexit: +// the statements a helper writes after them are unreachable under a real +// *testing.T, so a stub that aborted would leave them uncovered — and they are +// the return values the compiler demands, not dead code. +type recorder struct { + helpers int + errorf []string + fatalf []string + failNows int +} + +func (r *recorder) Helper() { r.helpers++ } + +func (r *recorder) Errorf(format string, args ...any) { + r.errorf = append(r.errorf, fmt.Sprintf(format, args...)) +} + +func (r *recorder) FailNow() { r.failNows++ } + +func (r *recorder) Fatalf(format string, args ...any) { + r.fatalf = append(r.fatalf, fmt.Sprintf(format, args...)) +} + +// failed reports whether the helper under test complained through any channel. +func (r *recorder) failed() bool { + return len(r.errorf) > 0 || len(r.fatalf) > 0 +} + +// diagAt builds one diagnostic with the code, severity and pointer a case needs. +func diagAt(code string, sev ir.Severity, pointer string) ir.Diagnostic { + return ir.Diagnostic{ + Severity: sev, + Code: code, + Message: "m/" + code, + Provenance: ir.Provenance{Pointer: pointer}, + } +} + +// opNamed builds a service holding one group with the named operations. +func opNamed(names ...string) ir.Service { + ops := make([]ir.Operation, 0, len(names)) + for _, n := range names { + ops = append(ops, ir.Operation{Name: ir.Naming{Source: n}}) + } + return ir.Service{Groups: []ir.OperationGroup{{Operations: ops}}} +} + +// TestFindOp_FindsTheOperationBySourceName covers both outcomes: the match, and +// the miss that must fail the test rather than return a zero operation quietly. +func TestFindOp_FindsTheOperationBySourceName(t *testing.T) { + t.Parallel() + doc := &ir.Document{Services: []ir.Service{opNamed("getA", "getB")}} + assert.Equal(t, "getB", openapitest.FindOp(t, doc, "getB").Name.Source) + + r := &recorder{} + assert.Equal(t, ir.Operation{}, openapitest.FindOp(r, doc, "absent")) + require.Len(t, r.fatalf, 1) + assert.Contains(t, r.fatalf[0], `operation "absent" not found`) +} + +// TestFirstOp_ReturnsTheFirstOperationOfTheFirstGroup pins the shorthand the +// single-operation fixtures use. +func TestFirstOp_ReturnsTheFirstOperationOfTheFirstGroup(t *testing.T) { + t.Parallel() + assert.Equal(t, "getA", openapitest.FirstOp(t, opNamed("getA", "getB")).Name.Source) +} + +// TestIndexBy_KeysEveryItem covers the generic lookup and PropsByWire, the one +// keying it is used for often enough to have a name. +func TestIndexBy_KeysEveryItem(t *testing.T) { + t.Parallel() + assert.Equal(t, map[int]string{1: "a", 2: "bb"}, + openapitest.IndexBy([]string{"a", "bb"}, func(s string) int { return len(s) })) + + props := []ir.Property{{WireName: "x"}, {WireName: "y"}} + byWire := openapitest.PropsByWire(props) + assert.Equal(t, props[1], byWire["y"]) + assert.Len(t, byWire, 2) +} + +// TestRequireNoErrorDiags_FailsOnlyOnAnErrorSeverity pins that warnings pass +// and an error does not, which is the whole contract the suites lean on. +func TestRequireNoErrorDiags_FailsOnlyOnAnErrorSeverity(t *testing.T) { + t.Parallel() + openapitest.RequireNoErrorDiags(t, []ir.Diagnostic{diagAt("a", ir.SeverityWarning, "")}) + + r := &recorder{} + openapitest.RequireNoErrorDiags(r, []ir.Diagnostic{diagAt("boom", ir.SeverityError, "")}) + assert.True(t, r.failed(), "an error diagnostic must fail the test") +} + +// TestAssertHasCode_RequiresCodeAndSeverityTogether pins that neither half +// matches on its own — a code at the wrong severity is a miss. +func TestAssertHasCode_RequiresCodeAndSeverityTogether(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{diagAt("a", ir.SeverityWarning, "/p")} + openapitest.AssertHasCode(t, diags, "a", ir.SeverityWarning) + + r := &recorder{} + openapitest.AssertHasCode(r, diags, "a", ir.SeverityError) + require.Len(t, r.fatalf, 1) + assert.Contains(t, r.fatalf[0], `code "a"`) +} + +// TestHasDiag_MatchesCodeAtAnySeverity separates the existential predicate from +// the severity-qualified one beside it. +func TestHasDiag_MatchesCodeAtAnySeverity(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{diagAt("a", ir.SeverityInfo, "/p")} + assert.True(t, openapitest.HasDiag(diags, "a")) + assert.False(t, openapitest.HasDiag(diags, "b")) + assert.True(t, openapitest.HasDiagAt(diags, "a", ir.SeverityInfo)) + assert.False(t, openapitest.HasDiagAt(diags, "a", ir.SeverityError)) +} + +// TestHasDiagCodeAt_RequiresTheExactPointer pins that the pointer is part of +// the match, not decoration. +func TestHasDiagCodeAt_RequiresTheExactPointer(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{diagAt("a", ir.SeverityInfo, "/p")} + assert.True(t, openapitest.HasDiagCodeAt(diags, "a", "/p")) + assert.False(t, openapitest.HasDiagCodeAt(diags, "a", "/q")) +} + +// TestCountDiagsAt_CountsExactMatchesOnly guards the documented sharp edge: the +// empty code is a literal, not a wildcard. +func TestCountDiagsAt_CountsExactMatchesOnly(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{ + diagAt("a", ir.SeverityWarning, "/p"), + diagAt("a", ir.SeverityWarning, "/q"), + diagAt("a", ir.SeverityError, "/r"), + } + assert.Equal(t, 2, openapitest.CountDiagsAt(diags, "a", ir.SeverityWarning)) + assert.Equal(t, 0, openapitest.CountDiagsAt(diags, "", ir.SeverityWarning)) +} + +// TestDiagMessageAt_ReturnsTheSingleMatchingMessage pins that the message is +// reachable by code, severity and pointer together. +func TestDiagMessageAt_ReturnsTheSingleMatchingMessage(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{ + diagAt("a", ir.SeverityWarning, "/p"), + diagAt("a", ir.SeverityWarning, "/q"), + } + assert.Equal(t, "m/a", openapitest.DiagMessageAt(t, diags, "a", ir.SeverityWarning, "/q")) +} + +// TestFirstDegradedWarning_MatchesTheDegradedCodeAtWarning pins both halves of +// the match: the code it is named for, at warning severity. +func TestFirstDegradedWarning_MatchesTheDegradedCodeAtWarning(t *testing.T) { + t.Parallel() + want := diagAt(diag.DegradedConstruct, ir.SeverityWarning, "/p") + got, ok := openapitest.FirstDegradedWarning([]ir.Diagnostic{ + diagAt(diag.DegradedConstruct, ir.SeverityInfo, "/x"), want, + }) + require.True(t, ok) + assert.Equal(t, want, got) + + _, ok = openapitest.FirstDegradedWarning([]ir.Diagnostic{diagAt("other", ir.SeverityWarning, "/p")}) + assert.False(t, ok) +} + +// TestAssertInfoDiagAt_MatchesOnSeverityAndPointer pins that this one ignores +// the code — it asks only whether something was announced at that position. +func TestAssertInfoDiagAt_MatchesOnSeverityAndPointer(t *testing.T) { + t.Parallel() + diags := []ir.Diagnostic{diagAt("any", ir.SeverityInfo, "/p")} + openapitest.AssertInfoDiagAt(t, diags, "/p") + + r := &recorder{} + openapitest.AssertInfoDiagAt(r, diags, "/q") + assert.True(t, r.failed(), "no info diagnostic at /q") +} + +// TestAssertProbeDocsKept_ChecksAllThreeDocumentationKeywords covers the kept +// case and the missing-externalDocs case, since the link fields are only read +// when the length assertion holds. +func TestAssertProbeDocsKept_ChecksAllThreeDocumentationKeywords(t *testing.T) { + t.Parallel() + kept := ir.Docs{ + Summary: "SUM", + Description: "DOC", + ExternalDocs: []ir.Link{{URL: "https://e.example", Description: "ED"}}, + } + openapitest.AssertProbeDocsKept(t, kept) + + r := &recorder{} + openapitest.AssertProbeDocsKept(r, ir.Docs{Summary: "SUM", Description: "DOC"}) + assert.True(t, r.failed(), "a dropped externalDocs must be reported") +} + +// TestAssertProbeExample_ChecksTheSingleExampleValue covers the kept case and +// the dropped case, which returns before reading the value. +func TestAssertProbeExample_ChecksTheSingleExampleValue(t *testing.T) { + t.Parallel() + openapitest.AssertProbeExample(t, []ir.Example{{Value: &ir.Value{Kind: ir.ValueString, Str: "abc"}}}) + + r := &recorder{} + openapitest.AssertProbeExample(r, nil) + assert.True(t, r.failed(), "a dropped example must be reported") +} diff --git a/compilers/openapi/internal/openapitest/spec.go b/compilers/openapi/internal/openapitest/spec.go new file mode 100644 index 00000000..c4a6c7b3 --- /dev/null +++ b/compilers/openapi/internal/openapitest/spec.go @@ -0,0 +1,93 @@ +package openapitest + +import ( + oas3 "github.com/speakeasy-api/openapi/jsonschema/oas3" + soa "github.com/speakeasy-api/openapi/openapi" + "github.com/speakeasy-api/openapi/sequencedmap" + "github.com/stretchr/testify/require" + yaml "gopkg.in/yaml.v3" + + "github.com/dexpace/morphic/compilers" +) + +// SourceOf wraps a spec string as a compilers.Source. +func SourceOf(src string) compilers.Source { + return compilers.Source{Path: "spec.yaml", Data: []byte(src)} +} + +// ComponentSpec wraps a components/schemas block in a minimal 3.1 document. +func ComponentSpec(schemas string) string { + return ComponentSpecVer("3.1.0", schemas) +} + +// ComponentSpecVer wraps a components/schemas block in a minimal document of +// the given OpenAPI version. +func ComponentSpecVer(version, schemas string) string { + return "openapi: " + version + "\n" + + "info: {title: T, version: \"1\"}\n" + + "paths: {}\n" + + "components:\n schemas:\n" + schemas +} + +// PathsSpec wraps a paths block in a minimal 3.1 document with no components. +func PathsSpec(paths string) string { + return PathsSpecVer("3.1.0", paths) +} + +// PathsSpecVer wraps a paths block in a minimal document of the given OpenAPI +// version, with no components. +func PathsSpecVer(version, paths string) string { + return "openapi: " + version + "\n" + + "info: {title: T, version: \"1\"}\n" + + "paths:\n" + paths +} + +// DocDeclaring builds a document declaring the named component schemas, with no +// parser and no fixture — the shape a test wants when what it needs from the +// document is only which components it declares. +func DocDeclaring(names ...string) *soa.OpenAPI { + elems := make([]*sequencedmap.Element[string, *oas3.JSONSchema[oas3.Referenceable]], 0, len(names)) + for _, n := range names { + elems = append(elems, sequencedmap.NewElem(n, + oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{}))) + } + return &soa.OpenAPI{Components: &soa.Components{Schemas: sequencedmap.New(elems...)}} +} + +// EmptyEitherSchema is a JSONSchema whose either-value has neither a Left schema +// nor a Right bool set: IsSchema() is true (IsLeft defaults true) yet +// GetSchema() is nil. The parser never produces this, so it drives the +// nil-schema guards. +func EmptyEitherSchema() *oas3.JSONSchema[oas3.Referenceable] { + return oas3.NewJSONSchemaFromSchema[oas3.Referenceable](nil) +} + +// YAMLNode parses a YAML snippet and returns its root value node (the document +// node's single content child), matching what schema fields expose. +func YAMLNode(t TB, src string) *yaml.Node { + t.Helper() + var doc yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) + require.Len(t, doc.Content, 1, "expected a single document node") + return doc.Content[0] +} + +// StrNode builds a bare string-scalar yaml.Node. +func StrNode(val string) *yaml.Node { + return ScalarNode("!!str", val) +} + +// ScalarNode builds a bare scalar yaml.Node with the given tag and value. +func ScalarNode(tag, val string) *yaml.Node { + return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: val} +} + +// InlineProbeBody is the body every inline-position case writes: one annotation +// of each kind the declared-annotation reader takes, one validation-only +// keyword, and one value constraint — all of them position-scoped, so a position +// that lowers this to the shared string primitive loses every one. All three +// documentation keywords are here because a home that keeps only the description +// passes a probe that writes only a description. +const InlineProbeBody = `{type: string, title: SUM, description: DOC, ` + + `externalDocs: {url: 'https://e.example', description: ED}, deprecated: true, ` + + `example: abc, x-vendor: V, xml: {name: X}, not: {const: N}, maxLength: 3}` diff --git a/compilers/openapi/internal/openapitest/spec_test.go b/compilers/openapi/internal/openapitest/spec_test.go new file mode 100644 index 00000000..eebf40ed --- /dev/null +++ b/compilers/openapi/internal/openapitest/spec_test.go @@ -0,0 +1,129 @@ +package openapitest_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + yaml "gopkg.in/yaml.v3" + + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" +) + +// TestSourceOf_CarriesTheSpecBytes pins that a spec string arrives as source +// bytes under the fixed path the suites' diagnostics are stamped against. +func TestSourceOf_CarriesTheSpecBytes(t *testing.T) { + t.Parallel() + src := openapitest.SourceOf("openapi: 3.1.0\n") + assert.Equal(t, "spec.yaml", src.Path) + assert.Equal(t, "openapi: 3.1.0\n", string(src.Data)) +} + +// TestComponentSpec_WrapsSchemasInAMinimalDocument checks the wrapper produces +// a document the compiler accepts and that the version is settable, since the +// two entry points differ only in that. +// +// The empty paths key is asserted because 3.0 requires it and 3.1 does not, and +// callers pass both versions: dropping it leaves every 3.1 case green and turns +// the 3.0 ones into a different document than they were written against. +func TestComponentSpec_WrapsSchemasInAMinimalDocument(t *testing.T) { + t.Parallel() + got := openapitest.ComponentSpec(" A: {type: string}\n") + assert.True(t, strings.HasPrefix(got, "openapi: 3.1.0\n"), got) + assert.Contains(t, got, "info: {title: T, version: \"1\"}\n") + assert.Contains(t, got, "paths: {}\n") + assert.Contains(t, got, "components:\n schemas:\n A: {type: string}\n") + assert.True(t, strings.HasPrefix(openapitest.ComponentSpecVer("3.0.3", ""), "openapi: 3.0.3\n")) +} + +// TestPathsSpec_WrapsPathsInAMinimalDocument is the paths counterpart: no +// components block, and the same settable version. +func TestPathsSpec_WrapsPathsInAMinimalDocument(t *testing.T) { + t.Parallel() + got := openapitest.PathsSpec(" /a:\n get: {responses: {\"200\": {description: ok}}}\n") + assert.True(t, strings.HasPrefix(got, "openapi: 3.1.0\n"), got) + assert.Contains(t, got, "info: {title: T, version: \"1\"}\n") + assert.NotContains(t, got, "components:") + assert.Contains(t, got, "paths:\n /a:\n") + assert.True(t, strings.HasPrefix(openapitest.PathsSpecVer("3.0.3", ""), "openapi: 3.0.3\n")) +} + +// TestBothWrappers_ParseAsYAML guards the property every caller depends on and +// no string assertion above establishes: the wrappers emit well-formed YAML. +func TestBothWrappers_ParseAsYAML(t *testing.T) { + t.Parallel() + for name, src := range map[string]string{ + "components": openapitest.ComponentSpec(" A: {type: string}\n"), + "paths": openapitest.PathsSpec(" /a: {}\n"), + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + var doc map[string]any + require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) + assert.Equal(t, "3.1.0", doc["openapi"]) + }) + } +} + +// TestDocDeclaring_DeclaresEveryNamedComponent pins that each name reaches the +// document as its own schema entry, in the order given. +func TestDocDeclaring_DeclaresEveryNamedComponent(t *testing.T) { + t.Parallel() + doc := openapitest.DocDeclaring("A", "B") + require.NotNil(t, doc.Components) + require.NotNil(t, doc.Components.Schemas) + + var names []string + for name := range doc.Components.Schemas.All() { + names = append(names, name) + } + assert.Equal(t, []string{"A", "B"}, names) +} + +// TestEmptyEitherSchema_IsASchemaWithNoSchema pins the shape the nil-schema +// guards are driven with: the either-value says it holds a schema, and holds +// none. +func TestEmptyEitherSchema_IsASchemaWithNoSchema(t *testing.T) { + t.Parallel() + js := openapitest.EmptyEitherSchema() + require.NotNil(t, js) + assert.True(t, js.IsSchema()) + assert.Nil(t, js.GetSchema()) +} + +// TestYAMLNode_ReturnsTheRootValueNode checks the document node is unwrapped, +// which is what makes the result comparable to what a parsed schema field +// exposes. +func TestYAMLNode_ReturnsTheRootValueNode(t *testing.T) { + t.Parallel() + node := openapitest.YAMLNode(t, "{a: 1}") + require.NotNil(t, node) + assert.Equal(t, yaml.MappingNode, node.Kind) + require.Len(t, node.Content, 2) + assert.Equal(t, "a", node.Content[0].Value) +} + +// TestScalarNode_BuildsABareScalar covers both builders, StrNode being the +// string-tagged case of ScalarNode. +func TestScalarNode_BuildsABareScalar(t *testing.T) { + t.Parallel() + assert.Equal(t, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: "7"}, + openapitest.ScalarNode("!!int", "7")) + assert.Equal(t, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "x"}, + openapitest.StrNode("x")) +} + +// TestInlineProbeBody_WritesEveryKeywordTheProbeAssertsOn keeps the constant +// and the assertions that read it in step: AssertProbeDocsKept and +// AssertProbeExample look for exactly these, and a keyword dropped here would +// silently weaken every inline-position case in two packages. +func TestInlineProbeBody_WritesEveryKeywordTheProbeAssertsOn(t *testing.T) { + t.Parallel() + for _, want := range []string{ + "title: SUM", "description: DOC", "externalDocs", "description: ED", + "deprecated: true", "example: abc", "x-vendor: V", "xml:", "not:", "maxLength: 3", + } { + assert.Contains(t, openapitest.InlineProbeBody, want) + } +} diff --git a/compilers/openapi/internal/operation/content_internal_test.go b/compilers/openapi/internal/operation/content_internal_test.go index 966050a7..56ca06fe 100644 --- a/compilers/openapi/internal/operation/content_internal_test.go +++ b/compilers/openapi/internal/operation/content_internal_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -120,8 +121,8 @@ func TestPositionalEncoding_WithoutRootNode(t *testing.T) { diags := fillSequential(l.ctx, l.types, &l.anchors, content, media, "/mp", "h") assert.Nil(t, content.ItemEncoding, "prefixes still block the every-item lowering") assert.Nil(t, content.Unmodeled, "a media type with no source node has nothing verbatim to keep") - assertHasCode(t, diags, diag.UnpreservableConstruct, ir.SeverityError) - assert.False(t, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo) > 0, + openapitest.AssertHasCode(t, diags, diag.UnpreservableConstruct, ir.SeverityError) + assert.False(t, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo) > 0, "nothing was kept, so nothing announces that it was") } diff --git a/compilers/openapi/internal/operation/content_test.go b/compilers/openapi/internal/operation/content_test.go index 4273d5d7..7fcd04f8 100644 --- a/compilers/openapi/internal/operation/content_test.go +++ b/compilers/openapi/internal/operation/content_test.go @@ -9,12 +9,13 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) func TestContent_AllMediaTypesKeptInOrder(t *testing.T) { t.Parallel() - spec := pathsSpec(` /docs: + spec := openapitest.PathsSpec(` /docs: post: operationId: createDoc requestBody: @@ -25,8 +26,8 @@ func TestContent_AllMediaTypesKeptInOrder(t *testing.T) { responses: {"201": {description: created}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.NotNil(t, op.Request) require.Len(t, op.Request.Contents, 2, "no primary-content selection in the IR") assert.Equal(t, "application/json", op.Request.Contents[0].MediaType) @@ -37,7 +38,7 @@ func TestContent_AllMediaTypesKeptInOrder(t *testing.T) { func TestContent_MultipartPartEncoding(t *testing.T) { t.Parallel() - spec := pathsSpec(` /upload: + spec := openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -56,8 +57,8 @@ func TestContent_MultipartPartEncoding(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] metaProp := ir.PropID("p/openapi" + ids.Ptr("paths", "/upload", "post", "requestBody", "content", "multipart/form-data", "schema", "properties", "meta")) enc, ok := content.Encoding[metaProp] require.True(t, ok, "encoding keyed by the part property's PropID; got keys %v", content.Encoding) @@ -73,7 +74,7 @@ func TestContent_MultipartPartEncoding(t *testing.T) { func TestContent_BinaryOctetStreamBody(t *testing.T) { t.Parallel() - spec := pathsSpec(` /raw: + spec := openapitest.PathsSpec(` /raw: post: operationId: putRaw requestBody: @@ -84,8 +85,8 @@ func TestContent_BinaryOctetStreamBody(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.NotNil(t, op.Request) require.Len(t, op.Request.Contents, 1) content := op.Request.Contents[0] @@ -98,7 +99,7 @@ func TestContent_BinaryRefBodyDetectedAsFile(t *testing.T) { t.Parallel() // A binary body referenced via $ref must still be detected as a File body, // exactly like the inline string+binary form. - spec := pathsSpec(` /raw: + spec := openapitest.PathsSpec(` /raw: post: operationId: putRaw requestBody: @@ -112,8 +113,8 @@ components: Blob: {type: string, format: binary} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] require.NotNil(t, content.File, "binary body behind a $ref lowers to a FileInfo") assert.False(t, content.File.IsText) assert.Equal(t, ir.TypeID("t/prim/bytes"), content.Type.Target) @@ -123,7 +124,7 @@ func TestContent_MultipartRefBodyKeepsEncoding(t *testing.T) { t.Parallel() // A multipart body referenced via $ref must keep its per-part encoding, keyed // by the RESOLVED model's property IDs (under the ref target's pointer). - spec := pathsSpec(` /upload: + spec := openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -143,8 +144,8 @@ components: file: {type: string, format: binary} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] require.NotNil(t, content.Encoding, "referenced multipart body keeps per-part encoding") metaProp := ir.PropID("p/openapi" + ids.Ptr("components", "schemas", "Form", "properties", "meta")) @@ -162,7 +163,7 @@ components: // mediaSchema, over a Form component reachable both directly and through a // second component that is a bare $ref to it. func aliasedMultipartSpec(mediaSchema string) string { - return pathsSpec(` /upload: + return openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -207,19 +208,19 @@ func TestContent_MultipartAliasBodyKeyedByAliasedModel(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, svc, diags := lowerServiceSpec(t, aliasedMultipartSpec(tc.mediaSchema)) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] form, isModel := typeByName(doc, "Form").(*ir.Model) require.True(t, isModel, "the body model is the component at the end of the chain") - declared := indexBy(form.Properties, func(p ir.Property) ir.PropID { return p.ID }) + declared := openapitest.IndexBy(form.Properties, func(p ir.Property) ir.PropID { return p.ID }) require.NotEmpty(t, content.Encoding, "an aliased multipart body still carries per-part encoding") for key := range content.Encoding { assert.Contains(t, declared, key, "every encoding key addresses a property the aliased model declares") } - byWire := propsByWire(form.Properties) + byWire := openapitest.PropsByWire(form.Properties) enc, ok := content.Encoding[byWire["meta"].ID] require.True(t, ok, "the declared encoding entry keys the aliased model's property") assert.Equal(t, []string{"application/json"}, enc.ContentTypes) @@ -257,7 +258,7 @@ func TestContent_MultipartBodyStandingForNoModelKeepsItsOwnPointer(t *testing.T) for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - spec := pathsSpec(` /upload: + spec := openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -270,8 +271,8 @@ components: NotAModel: {enum: [x], properties: {file: {type: string, format: binary}}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] require.Contains(t, content.Encoding, tc.want, "the key falls back to the schema's own position; got %v", content.Encoding) }) @@ -280,7 +281,7 @@ components: func TestContent_NonRequiredRequestBody(t *testing.T) { t.Parallel() - spec := pathsSpec(` /maybe: + spec := openapitest.PathsSpec(` /maybe: post: operationId: maybe requestBody: @@ -289,8 +290,8 @@ func TestContent_NonRequiredRequestBody(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.NotNil(t, op.Request, "a non-required body still lowers to a present Payload") raw, ok := op.Request.Unmodeled["openapi:required"] require.True(t, ok, "body optionality kept under Unmodeled") @@ -307,7 +308,7 @@ func TestContent_NonRequiredRequestBody(t *testing.T) { func TestContent_ArrayMultipartPartMulti(t *testing.T) { t.Parallel() - spec := pathsSpec(` /bulk: + spec := openapitest.PathsSpec(` /bulk: post: operationId: bulk requestBody: @@ -320,8 +321,8 @@ func TestContent_ArrayMultipartPartMulti(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - content := firstOp(t, svc).Request.Contents[0] + openapitest.RequireNoErrorDiags(t, diags) + content := openapitest.FirstOp(t, svc).Request.Contents[0] tagsProp := ir.PropID("p/openapi" + ids.Ptr("paths", "/bulk", "post", "requestBody", "content", "multipart/form-data", "schema", "properties", "tags")) enc, ok := content.Encoding[tagsProp] require.True(t, ok, "array part gets a synthesized PartEncoding; got keys %v", content.Encoding) @@ -402,7 +403,7 @@ paths: func TestContent_FullPipeline(t *testing.T) { t.Parallel() doc, diags := parseFull(t, contentSpec) - upload := findOp(t, doc, "upload") + upload := openapitest.FindOp(t, doc, "upload") // Non-required body preserved as present with optionality under Unmodeled. require.NotNil(t, upload.Request) @@ -434,7 +435,7 @@ func TestContent_FullPipeline(t *testing.T) { _, hasLinks := resp.Unmodeled["openapi:links"] assert.True(t, hasLinks) - assert.True(t, hasDiag(diags, diag.DegradedConstruct)) + assert.True(t, openapitest.HasDiag(diags, diag.DegradedConstruct)) } func TestContent_OctetAndErrorMulti(t *testing.T) { @@ -460,7 +461,7 @@ func TestContent_OctetAndErrorMulti(t *testing.T) { func TestContent_SequentialAndEmptyBody(t *testing.T) { t.Parallel() doc, _ := parseFull(t, contentSpec) - stream := findOp(t, doc, "stream") + stream := openapitest.FindOp(t, doc, "stream") resp := stream.Responses[0] require.NotNil(t, resp.Payload) c := resp.Payload.Contents[0] @@ -470,7 +471,7 @@ func TestContent_SequentialAndEmptyBody(t *testing.T) { assert.True(t, c.ItemEncoding.Multi, "itemEncoding governs a repeated tail") // Empty request-body content yields no Request payload. - empty := findOp(t, doc, "emptyBody") + empty := openapitest.FindOp(t, doc, "emptyBody") assert.Nil(t, empty.Request) } @@ -549,7 +550,7 @@ func TestContent_MultipartEncodingVariants(t *testing.T) { t.Parallel() doc, _ := parseFull(t, multipartVariantsSpec) for _, name := range []string{"noSchema", "noProps", "plainProps"} { - op := findOp(t, doc, name) + op := openapitest.FindOp(t, doc, name) require.NotNil(t, op.Request, "%s has a request", name) for _, c := range op.Request.Contents { assert.Empty(t, c.Encoding, "%s multipart yields no per-part encoding", name) @@ -560,13 +561,13 @@ func TestContent_MultipartEncodingVariants(t *testing.T) { func TestContent_ExampleWithoutValueSkipped(t *testing.T) { t.Parallel() doc, diags := parseFull(t, multipartVariantsSpec) - op := findOp(t, doc, "exGet") + op := openapitest.FindOp(t, doc, "exGet") c := op.Responses[0].Payload.Contents[0] assert.Empty(t, c.Examples, "an example carrying no example is skipped") // Skipped, but not in silence: the entry declares neither a value nor the // externalValue that would have given it a home. - d, ok := firstDegradedWarning(diags) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok, "the skipped entry is reported") assert.Equal(t, "/paths/~1examples/get/responses/200/content/application~1json/examples/empty", d.Provenance.Pointer) @@ -578,7 +579,7 @@ func TestContent_UnconvertibleExamplesDiagnosed(t *testing.T) { // 3.1-style `examples` map — each carries a custom, structurally // unconvertible tag, and each conversion failure must be diagnosed rather // than discarded silently. - spec := pathsSpec(` /items: + spec := openapitest.PathsSpec(` /items: get: operationId: getItem responses: @@ -592,12 +593,12 @@ func TestContent_UnconvertibleExamplesDiagnosed(t *testing.T) { one: {value: !foo baz} `) _, svc, diags := lowerServiceSpec(t, spec) - op := firstOp(t, svc) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Responses, 1) c := op.Responses[0].Payload.Contents[0] assert.Empty(t, c.Examples, "both unconvertible examples are skipped, not appended") - require.Equal(t, 2, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) + require.Equal(t, 2, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) pointers := map[string]bool{} for _, d := range diags { if d.Code == diag.DegradedConstruct && d.Severity == ir.SeverityWarning { @@ -638,14 +639,14 @@ components: Bad: {value: !foo baz} ` _, svc, diags := lowerServiceSpec(t, spec) - op := firstOp(t, svc) + op := openapitest.FirstOp(t, svc) c := op.Responses[0].Payload.Contents[0] require.Len(t, c.Examples, 1, "the convertible $ref'd example still lowers") require.NotNil(t, c.Examples[0].Value) assert.Equal(t, ir.Value{Kind: ir.ValueString, Str: "fine"}, *c.Examples[0].Value) - require.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) - d, ok := firstDegradedWarning(diags) + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok) assert.Equal(t, "/paths/~1items/get/responses/200/content/application~1json/examples/bad", d.Provenance.Pointer, "the reference site, not a /value the source never had") @@ -676,7 +677,7 @@ paths: func TestContent_PositionalPrefixEncodingIsPreserved(t *testing.T) { t.Parallel() doc, diags := parseFull(t, positionalEncodingSpec) - c := findOp(t, doc, "mixed").Responses[0].Payload.Contents[0] + c := openapitest.FindOp(t, doc, "mixed").Responses[0].Payload.Contents[0] assert.Nil(t, c.ItemEncoding, "positional prefixes rule out an every-item encoding") prefix, ok := c.Unmodeled["openapi:prefixEncoding"] @@ -687,21 +688,21 @@ func TestContent_PositionalPrefixEncodingIsPreserved(t *testing.T) { item, ok := c.Unmodeled["openapi:itemEncoding"] require.True(t, ok, "the tail encoding is kept beside the prefixes it follows") assert.JSONEq(t, `{"contentType": "text/plain"}`, string(item.Value)) - assertHasCode(t, diags, diag.DegradedConstruct, ir.SeverityInfo) + openapitest.AssertHasCode(t, diags, diag.DegradedConstruct, ir.SeverityInfo) } func TestFillSequential_PrefixEncodingWithoutItemEncoding(t *testing.T) { t.Parallel() spec := strings.ReplaceAll(positionalEncodingSpec, " itemEncoding: {contentType: text/plain}\n", "") doc, diags := parseFull(t, spec) - c := findOp(t, doc, "mixed").Responses[0].Payload.Contents[0] + c := openapitest.FindOp(t, doc, "mixed").Responses[0].Payload.Contents[0] assert.Nil(t, c.ItemEncoding) _, ok := c.Unmodeled["openapi:prefixEncoding"] assert.True(t, ok, "prefixEncoding alone is still reported rather than dropped") _, ok = c.Unmodeled["openapi:itemEncoding"] assert.False(t, ok, "no itemEncoding was declared, so none is recorded") - assertHasCode(t, diags, diag.DegradedConstruct, ir.SeverityInfo) + openapitest.AssertHasCode(t, diags, diag.DegradedConstruct, ir.SeverityInfo) } const componentBodyRefSpec = `openapi: 3.1.0 @@ -732,9 +733,9 @@ components: func TestContent_RequestBodyRefSharedAcrossOperationsInternsOnce(t *testing.T) { t.Parallel() doc, diags := parseFull(t, componentBodyRefSpec) - requireNoErrorDiags(t, diags) - postA := findOp(t, doc, "postA") - postB := findOp(t, doc, "postB") + openapitest.RequireNoErrorDiags(t, diags) + postA := openapitest.FindOp(t, doc, "postA") + postB := openapitest.FindOp(t, doc, "postB") require.NotNil(t, postA.Request) require.NotNil(t, postB.Request) @@ -774,8 +775,8 @@ components: func TestContent_RefdResponseHeaderNestedRefInternsSchemaOnce(t *testing.T) { t.Parallel() doc, diags := parseFull(t, refdResponseNestedHeaderSpec) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "getA") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "getA") require.Len(t, op.Responses, 1) require.Len(t, op.Responses[0].Headers, 1) @@ -810,10 +811,10 @@ components: func TestContent_HeaderMapEntriesSharingComponentGetDistinctIDs(t *testing.T) { t.Parallel() doc, diags := parseFull(t, headerIdentitySpec) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "getA") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "getA") require.Len(t, op.Responses[0].Headers, 2) - byWire := indexBy(op.Responses[0].Headers, func(p ir.Property) string { return p.WireName }) + byWire := openapitest.IndexBy(op.Responses[0].Headers, func(p ir.Property) string { return p.WireName }) rate, limit := byWire["X-Rate"], byWire["X-Limit"] assert.NotEqual(t, rate.ID, limit.ID, "distinct map keys keep distinct PropIDs") @@ -833,7 +834,7 @@ func TestContent_HeaderMapEntriesSharingComponentGetDistinctIDs(t *testing.T) { func TestContent_SharedComponentSchemaTakesItsDeclarationHint(t *testing.T) { t.Parallel() doc, diags := parseFull(t, componentBodyRefSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) bodyID := ir.TypeID("t/anon/components/requestBodies/Body/content/application~1json/schema") body, ok := doc.Types[bodyID] require.True(t, ok) @@ -842,7 +843,7 @@ func TestContent_SharedComponentSchemaTakesItsDeclarationHint(t *testing.T) { "the shared body schema is hinted from its component, not from postA or postB") hdrDoc, hdrDiags := parseFull(t, headerIdentitySpec) - requireNoErrorDiags(t, hdrDiags) + openapitest.RequireNoErrorDiags(t, hdrDiags) hdr, ok := hdrDoc.Types[ir.TypeID("t/anon/components/headers/Rate/schema")] require.True(t, ok) assert.Equal(t, "Rate", hdr.Common().Name.Hint, @@ -855,7 +856,7 @@ func TestContent_SharedComponentSchemaTakesItsDeclarationHint(t *testing.T) { // available and must survive. func TestContent_InlineSchemaKeepsItsUseSiteHint(t *testing.T) { t.Parallel() - spec := pathsSpec(` /a: + spec := openapitest.PathsSpec(` /a: post: operationId: postA requestBody: @@ -865,7 +866,7 @@ func TestContent_InlineSchemaKeepsItsUseSiteHint(t *testing.T) { responses: {"200": {description: ok}} `) doc, diags := parseFull(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) td, ok := doc.Types[ir.TypeID("t/anon/paths/~1a/post/requestBody/content/application~1json/schema")] require.True(t, ok) assert.Equal(t, "postA_request", td.Common().Name.Hint) @@ -901,8 +902,8 @@ components: func TestContent_EncodingHeaderRefInternsAtDeclaration(t *testing.T) { t.Parallel() doc, diags := parseFull(t, refdEncodingHeaderSpec) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "upload") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "upload") require.NotNil(t, op.Request) require.Len(t, op.Request.Contents, 1) enc := op.Request.Contents[0].Encoding @@ -929,16 +930,16 @@ func TestContent_EncodingHeaderRefInternsAtDeclaration(t *testing.T) { // field for each (GitHub #116). func TestHeaders_SchemaDetailReachesTheProperty(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ - " X-H: {schema: "+inlineProbeBody+"}\n")) - requireNoErrorDiags(t, diags) + " X-H: {schema: "+openapitest.InlineProbeBody+"}\n")) + openapitest.RequireNoErrorDiags(t, diags) - h := firstOp(t, svc).Responses[0].Headers[0] - assertProbeDocsKept(t, h.Docs) + h := openapitest.FirstOp(t, svc).Responses[0].Headers[0] + openapitest.AssertProbeDocsKept(t, h.Docs) assert.NotNil(t, h.Deprecation) - assertProbeExample(t, h.Examples) + openapitest.AssertProbeExample(t, h.Examples) require.NotNil(t, h.XML) assert.Equal(t, "X", h.XML.Name) assert.Contains(t, h.Unmodeled, "openapi:x-vendor") @@ -959,15 +960,15 @@ func TestHeaders_SchemaDetailReachesTheProperty(t *testing.T) { // overlay order, so the assertion over it passes whichever side wins. func TestHeaders_OwnAnnotationsOverrideTheSchema(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " X-H:\n description: HEADER\n example: HEADER\n"+ " x-scope: header\n schema:\n"+ " {type: string, description: SCHEMA, example: SCHEMA, x-scope: schema}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - h := firstOp(t, svc).Responses[0].Headers[0] + h := openapitest.FirstOp(t, svc).Responses[0].Headers[0] assert.Equal(t, "HEADER", h.Docs.Description, "the header's own description wins") require.Len(t, h.Examples, 1, "and its own example replaces the schema's rather than joining it") require.NotNil(t, h.Examples[0].Value) @@ -991,14 +992,14 @@ func TestHeaders_DeprecationUnionsWithTheSchema(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " X-H:\n deprecated: "+tc.header+"\n"+ " schema: {type: string, deprecated: "+tc.schema+"}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - h := firstOp(t, svc).Responses[0].Headers[0] + h := openapitest.FirstOp(t, svc).Responses[0].Headers[0] assert.NotNil(t, h.Deprecation, "either side alone deprecates the header") }) } @@ -1018,7 +1019,7 @@ func TestHeaders_SerializationKeywordsKept(t *testing.T) { for _, tc := range []struct{ name, spec, at string }{ { name: "response header", - spec: pathsSpec(" /x:\n get:\n operationId: g\n responses:\n" + + spec: openapitest.PathsSpec(" /x:\n get:\n operationId: g\n responses:\n" + " \"200\":\n description: ok\n headers:\n" + " X-H:\n style: simple\n explode: true\n" + " schema: {type: array, items: {type: string}}\n"), @@ -1026,7 +1027,7 @@ func TestHeaders_SerializationKeywordsKept(t *testing.T) { }, { name: "multipart encoding header", - spec: pathsSpec(" /x:\n post:\n operationId: g\n requestBody:\n" + + spec: openapitest.PathsSpec(" /x:\n post:\n operationId: g\n requestBody:\n" + " content:\n multipart/form-data:\n" + " schema: {type: object, properties: {file: {type: string}}}\n" + " encoding:\n file:\n headers:\n" + @@ -1040,8 +1041,8 @@ func TestHeaders_SerializationKeywordsKept(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, tc.spec) - requireNoErrorDiags(t, diags) - assertHeaderSerializationKept(t, headerAt(t, firstOp(t, svc)), diags, tc.at) + openapitest.RequireNoErrorDiags(t, diags) + assertHeaderSerializationKept(t, headerAt(t, openapitest.FirstOp(t, svc)), diags, tc.at) }) } } @@ -1073,7 +1074,7 @@ func assertHeaderSerializationKept(t *testing.T, h ir.Property, diags []ir.Diagn assert.Equal(t, ir.ReasonNoIRHome, entry.Reason) assert.JSONEq(t, want, string(entry.Value)) assert.Equal(t, at+"/"+key, entry.Provenance.Pointer) - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) } } @@ -1083,13 +1084,13 @@ func assertHeaderSerializationKept(t *testing.T, h ir.Property, diags []ir.Diagn // defaults the accessors would hand back and calling them source facts. func TestHeaders_SerializationKeywordsAbsentRecordNothing(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " X-H: {schema: {type: string}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - h := firstOp(t, svc).Responses[0].Headers[0] + h := openapitest.FirstOp(t, svc).Responses[0].Headers[0] assert.NotContains(t, h.Unmodeled, "openapi:style") assert.NotContains(t, h.Unmodeled, "openapi:explode") assert.Empty(t, diags, "and nothing is announced about keywords the header never wrote") @@ -1125,16 +1126,16 @@ func TestHeaders_ReservedContentTypeEntryIsReported(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " "+tc.header+": {schema: {type: string}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - headers := firstOp(t, svc).Responses[0].Headers + headers := openapitest.FirstOp(t, svc).Responses[0].Headers require.Len(t, headers, 1, "the header lowers either way; nothing is dropped") assert.Equal(t, tc.header, headers[0].WireName) - assert.Equal(t, tc.reported, hasDiagCodeAt(diags, diag.ReservedHeaderName, tc.at), + assert.Equal(t, tc.reported, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, tc.at), "reported at the map entry's own pointer") }) } @@ -1145,7 +1146,7 @@ func TestHeaders_ReservedContentTypeEntryIsReported(t *testing.T) { // describes Content-Type separately and SHALL ignore an entry for it. func TestHeaders_ReservedContentTypeInEncodingIsReported(t *testing.T) { t.Parallel() - _, _, diags := lowerServiceSpec(t, pathsSpec( + _, _, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n post:\n operationId: g\n requestBody:\n"+ " content:\n multipart/form-data:\n"+ " schema: {type: object, properties: {file: {type: string}}}\n"+ @@ -1153,13 +1154,13 @@ func TestHeaders_ReservedContentTypeInEncodingIsReported(t *testing.T) { " Content-Type: {schema: {type: string}}\n"+ " X-Other: {schema: {type: string}}\n"+ " responses: {\"200\": {description: ok}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) base := "/paths/~1x/post/requestBody/content/multipart~1form-data/encoding/file/headers/" - assert.True(t, hasDiagCodeAt(diags, diag.ReservedHeaderName, base+"Content-Type")) - assert.False(t, hasDiagCodeAt(diags, diag.ReservedHeaderName, base+"X-Other"), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, base+"Content-Type")) + assert.False(t, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, base+"X-Other"), "the entry beside it is ordinary and says nothing") - assertHasCode(t, diags, diag.ReservedHeaderName, ir.SeverityWarning) + openapitest.AssertHasCode(t, diags, diag.ReservedHeaderName, ir.SeverityWarning) } // TestHeaders_ReservedNameIsTheKeyNotTheDeclaration pins which of two pointers @@ -1185,14 +1186,14 @@ paths: Content-Type: {$ref: '#/components/headers/Shared'} X-Other: {$ref: '#/components/headers/Shared'} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) base := "/paths/~1x/get/responses/200/headers/" - assert.True(t, hasDiagCodeAt(diags, diag.ReservedHeaderName, base+"Content-Type"), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, base+"Content-Type"), "the reserved key is reported at its own use site") - assert.False(t, hasDiagCodeAt(diags, diag.ReservedHeaderName, base+"X-Other"), + assert.False(t, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, base+"X-Other"), "the other key sharing that declaration is not") - assert.False(t, hasDiagCodeAt(diags, diag.ReservedHeaderName, "/components/headers/Shared"), + assert.False(t, openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, "/components/headers/Shared"), "and nothing is reported at the declaration they share") } @@ -1210,7 +1211,7 @@ func TestSingleContentEntry_ReportsExtraMediaTypes(t *testing.T) { }{ { name: "response header", - spec: pathsSpec(" /x:\n get:\n responses:\n" + + spec: openapitest.PathsSpec(" /x:\n get:\n responses:\n" + " \"200\":\n description: ok\n headers:\n" + " H:\n content:\n" + " application/xml: {schema: {type: string}}\n" + @@ -1219,7 +1220,7 @@ func TestSingleContentEntry_ReportsExtraMediaTypes(t *testing.T) { }, { name: "operation parameter", - spec: pathsSpec(" /x:\n get:\n parameters:\n" + + spec: openapitest.PathsSpec(" /x:\n get:\n parameters:\n" + " - name: p\n in: query\n content:\n" + " application/xml: {schema: {type: string}}\n" + " application/json: {schema: {type: integer}}\n" + @@ -1231,9 +1232,9 @@ func TestSingleContentEntry_ReportsExtraMediaTypes(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, diags := parseFull(t, tc.spec) - assert.True(t, hasDiagCodeAt(diags, diag.DegradedConstruct, tc.at), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.DegradedConstruct, tc.at), "the ignored media types are named at the content map: %+v", diags) - assert.Contains(t, diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityWarning, tc.at), + assert.Contains(t, openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityWarning, tc.at), "application/json", "the message names what was ignored, not only that something was") }) } @@ -1243,11 +1244,11 @@ func TestSingleContentEntry_ReportsExtraMediaTypes(t *testing.T) { // spelling must not warn, or every content-style header and parameter would. func TestSingleContentEntry_OneEntryIsSilent(t *testing.T) { t.Parallel() - _, diags := parseFull(t, pathsSpec(" /x:\n get:\n responses:\n"+ + _, diags := parseFull(t, openapitest.PathsSpec(" /x:\n get:\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " H:\n content:\n"+ " application/xml: {schema: {type: string}}\n")) - assert.Equal(t, 0, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), + assert.Equal(t, 0, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "one media type is the legal spelling: %+v", diags) } @@ -1256,12 +1257,12 @@ func TestSingleContentEntry_OneEntryIsSilent(t *testing.T) { // type without reporting a loss, since nothing was written to lose. func TestHeaderSchema_NeitherSpelling(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(" /x:\n get:\n operationId: untypedHeader\n responses:\n"+ + doc, diags := parseFull(t, openapitest.PathsSpec(" /x:\n get:\n operationId: untypedHeader\n responses:\n"+ " \"200\":\n description: ok\n headers:\n"+ " H: {description: untyped}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - headers := findOp(t, doc, "untypedHeader").Responses[0].Headers + headers := openapitest.FindOp(t, doc, "untypedHeader").Responses[0].Headers require.Len(t, headers, 1) assert.Equal(t, ir.TypeID("t/prim/any"), headers[0].Type.Target, "a header declaring no type lowers to the top type") @@ -1275,7 +1276,7 @@ func TestHeaderSchema_NeitherSpelling(t *testing.T) { // them — and its key must be the ID that mixin declares. func TestPartEncodings_MixinPartIsKeyedOnTheMixin(t *testing.T) { t.Parallel() - spec := pathsSpec(` /upload: + spec := openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -1298,8 +1299,8 @@ components: properties: {note: {type: string}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - enc := firstOp(t, svc).Request.Contents[0].Encoding + openapitest.RequireNoErrorDiags(t, diags) + enc := openapitest.FirstOp(t, svc).Request.Contents[0].Encoding want := ir.PropID("p/openapi" + ids.Ptr("components", "schemas", "Extra", "properties", "note")) pe, ok := enc[want] @@ -1312,7 +1313,7 @@ components: // last. The encoding entry describes one part on the wire either way. func TestBodyParts_RedeclaredNameIsOnePart(t *testing.T) { t.Parallel() - spec := pathsSpec(` /upload: + spec := openapitest.PathsSpec(` /upload: post: operationId: upload requestBody: @@ -1327,8 +1328,8 @@ func TestBodyParts_RedeclaredNameIsOnePart(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - enc := firstOp(t, svc).Request.Contents[0].Encoding + openapitest.RequireNoErrorDiags(t, diags) + enc := openapitest.FirstOp(t, svc).Request.Contents[0].Encoding assert.Len(t, enc, 1, "one wire part is one encoding entry; got %v", enc) } @@ -1360,9 +1361,9 @@ components: type: object properties: {file: {type: string, format: binary}} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - body := findOp(t, doc, "upload").Request + body := openapitest.FindOp(t, doc, "upload").Request require.NotNil(t, body) require.Len(t, body.Contents, 1) content := body.Contents[0] @@ -1377,7 +1378,7 @@ components: require.NotNil(t, composed.Base, "a sole $ref branch becomes the composed model's base") inherited, isModel := doc.Types[composed.Base.Target].(*ir.Model) require.True(t, isModel, "and the base is the referenced model") - want := propsByWire(inherited.Properties)["file"].ID + want := openapitest.PropsByWire(inherited.Properties)["file"].ID require.NotEmpty(t, want, "the base declares the part this encoding names") assert.Contains(t, content.Encoding, want, @@ -1391,7 +1392,7 @@ components: // like any other, and only an example declaring neither is reported. func TestExample_ExternalValueOnlyIsCarried(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /a: + doc, diags := parseFull(t, openapitest.PathsSpec(` /a: get: operationId: getA responses: @@ -1403,9 +1404,9 @@ func TestExample_ExternalValueOnlyIsCarried(t *testing.T) { examples: remote: {externalValue: 'https://e.example/one.json', summary: s} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - resp := findOp(t, doc, "getA").Responses + resp := openapitest.FindOp(t, doc, "getA").Responses require.NotEmpty(t, resp) require.NotEmpty(t, resp[0].Payload.Contents) examples := resp[0].Payload.Contents[0].Examples diff --git a/compilers/openapi/internal/operation/helpers_internal_test.go b/compilers/openapi/internal/operation/helpers_internal_test.go index 75bc475a..3f502f50 100644 --- a/compilers/openapi/internal/operation/helpers_internal_test.go +++ b/compilers/openapi/internal/operation/helpers_internal_test.go @@ -3,15 +3,14 @@ package operation import ( "testing" - oas3 "github.com/speakeasy-api/openapi/jsonschema/oas3" soa "github.com/speakeasy-api/openapi/openapi" "github.com/stretchr/testify/require" - "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/compile" "github.com/dexpace/morphic/compilers/openapi/internal/auth" "github.com/dexpace/morphic/compilers/openapi/internal/load" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/compilers/openapi/internal/schema" "github.com/dexpace/morphic/ir" @@ -34,55 +33,34 @@ type lowerer struct { operationIDs map[string]string } +// lowererOver is the only place the fixture's fields are initialised. Both +// entry points below build on it, so a field added to lowerer cannot reach one +// of them and miss the other. +func lowererOver(ctx lowering.Ctx) *lowerer { + types := compile.NewTypes(0) + return &lowerer{ + ctx: ctx, + out: &ir.Document{Types: types.Registry()}, + types: types, + operationIDs: make(map[string]string), + } +} + // loweredFor loads src and returns the fixture over it with nothing lowered // yet, plus the load diagnostics. func loweredFor(t *testing.T, src string) (*lowerer, []ir.Diagnostic) { t.Helper() - loadedDoc, diags, err := load.Load(t.Context(), 0, - compilers.Source{Path: "spec.yaml", Data: []byte(src)}, load.Options{}) + loadedDoc, diags, err := load.Load(t.Context(), 0, openapitest.SourceOf(src), load.Options{}) require.NoError(t, err) require.NotNil(t, loadedDoc, "load returned no document: %+v", diags) - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, loadedDoc.Doc, loadedDoc.Source, lowering.GroupByTags, overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - operationIDs: make(map[string]string), - }, diags + return lowererOver(lowering.New(0, loadedDoc.Doc, loadedDoc.Source, + lowering.GroupByTags, overlay.Origin{})), diags } // newRawLowerer builds a fixture over a hand-constructed document, bypassing // the parser so nil slice/map entries can be exercised. func newRawLowerer(doc *soa.OpenAPI) *lowerer { - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - operationIDs: make(map[string]string), - } -} - -// componentSpec wraps a components/schemas block in a minimal 3.1 document. -func componentSpec(schemas string) string { - return "openapi: 3.1.0\n" + - "info: {title: T, version: \"1\"}\n" + - "paths: {}\n" + - "components:\n schemas:\n" + schemas -} - -// pathsSpec wraps a paths block in a minimal 3.1 document with no components. -func pathsSpec(paths string) string { - return "openapi: 3.1.0\n" + - "info: {title: T, version: \"1\"}\n" + - "paths:\n" + paths -} - -// requireNoErrorDiags fails the test if any diagnostic has error severity. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) + return lowererOver(lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{})) } // lowerServiceSpec loads src and runs the phases the service walk needs beneath @@ -104,42 +82,3 @@ func lowerServiceSpec(t *testing.T, src string) (ir.Service, []ir.Diagnostic) { l.diags.AppendAll(svcDiags) return svc, append(loadDiags, l.diags.List()...) } - -// emptyEitherSchema is a JSONSchema whose either-value has neither a Left schema -// nor a Right bool set: IsSchema() is true yet GetSchema() is nil. The parser -// never produces this, so it drives the nil-schema guards. -func emptyEitherSchema() *oas3.JSONSchema[oas3.Referenceable] { - return oas3.NewJSONSchemaFromSchema[oas3.Referenceable](nil) -} - -// assertHasCode requires diags to carry a diagnostic with the given code at the -// given severity. -func assertHasCode(t *testing.T, diags []ir.Diagnostic, code string, sev ir.Severity) { - t.Helper() - for _, d := range diags { - if d.Code == code && d.Severity == sev { - return - } - } - t.Fatalf("expected a %v diagnostic with code %q, got %+v", sev, code, diags) -} - -// countDiagsAt counts the diagnostics matching code and sev exactly. -func countDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { - var n int - for _, d := range diags { - if d.Code == code && d.Severity == sev { - n++ - } - } - return n -} - -// indexBy builds a lookup keyed by key(item). -func indexBy[T any, K comparable](items []T, key func(T) K) map[K]T { - out := make(map[K]T, len(items)) - for _, item := range items { - out[key(item)] = item - } - return out -} diff --git a/compilers/openapi/internal/operation/helpers_test.go b/compilers/openapi/internal/operation/helpers_test.go index b8a0254c..097628d3 100644 --- a/compilers/openapi/internal/operation/helpers_test.go +++ b/compilers/openapi/internal/operation/helpers_test.go @@ -3,16 +3,14 @@ package operation_test import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v3" "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/compile" "github.com/dexpace/morphic/compilers/openapi" - "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/load" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/operation" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/compilers/openapi/internal/schema" @@ -25,16 +23,13 @@ import ( // fixtures are specs, not hand-built values, and reaching the compiler needs an // external test package. -// sourceOf wraps a spec string as a compilers.Source. -func sourceOf(src string) compilers.Source { - return compilers.Source{Path: "spec.yaml", Data: []byte(src)} -} - -// parseFull runs the whole public compiler pipeline over src. +// parseFull runs the whole public compiler pipeline over src. That reach back +// through openapi is also why openapitest cannot hold it — see that package's +// doc comment. func parseFull(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { t.Helper() - doc, diags, err := openapi.New().Compile(t.Context(), []compilers.Source{sourceOf(src)}, - compilers.Options{}) + doc, diags, err := openapi.New().Compile(t.Context(), + []compilers.Source{openapitest.SourceOf(src)}, compilers.Options{}) require.NoError(t, err) require.NotNil(t, doc) return doc, diags @@ -49,145 +44,14 @@ func lowerServiceSpec(t *testing.T, src string) (*ir.Document, ir.Service, []ir. return doc, doc.Services[0], diags } -// findOp returns the operation whose source name matches. -func findOp(t *testing.T, doc *ir.Document, source string) ir.Operation { - t.Helper() - for _, g := range doc.Services[0].Groups { - for _, op := range g.Operations { - if op.Name.Source == source { - return op - } - } - } - t.Fatalf("operation %q not found", source) - return ir.Operation{} -} - -// firstOp returns the operation at svc.Groups[0].Operations[0], requiring both -// to be non-empty first rather than letting a malformed fixture fail with a -// bare index-out-of-range panic. -func firstOp(t *testing.T, svc ir.Service) ir.Operation { - t.Helper() - require.NotEmpty(t, svc.Groups, "service has no operation groups") - require.NotEmpty(t, svc.Groups[0].Operations, "first group has no operations") - return svc.Groups[0].Operations[0] -} - -// requireNoErrorDiags fails the test if any diagnostic has error severity. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - -// pathsSpec wraps a paths block in a minimal 3.1 document with no components. -func pathsSpec(paths string) string { - return pathsSpecVer("3.1.0", paths) -} - -// pathsSpecVer wraps a paths block in a minimal document of the given OpenAPI -// version, with no components. -func pathsSpecVer(version, paths string) string { - return "openapi: " + version + "\n" + - "info: {title: T, version: \"1\"}\n" + - "paths:\n" + paths -} - -// typeByName returns the named component schema's lowered TypeDef. +// typeByName returns the named component schema's lowered TypeDef. It stays a +// per-package copy for the reason openapitest's doc comment gives: a +// spelled-out ID belongs in a test file, where internal/archtest's ID-grammar +// sweep permits it. func typeByName(doc *ir.Document, name string) ir.TypeDef { return doc.Types[ir.TypeID("t/openapi/components/schemas/"+name)] } -// strNode builds a bare string-scalar yaml.Node. -func strNode(val string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: val} -} - -// assertHasCode requires diags to carry a diagnostic with the given code at the -// given severity. -func assertHasCode(t *testing.T, diags []ir.Diagnostic, code string, sev ir.Severity) { - t.Helper() - for _, d := range diags { - if d.Code == code && d.Severity == sev { - return - } - } - t.Fatalf("expected a %v diagnostic with code %q, got %+v", sev, code, diags) -} - -// hasDiag reports whether diags contains a diagnostic with the exact code, at -// any severity. -func hasDiag(diags []ir.Diagnostic, code string) bool { - for _, d := range diags { - if d.Code == code { - return true - } - } - return false -} - -// hasDiagCodeAt reports whether diags carries code at exactly pointer. -func hasDiagCodeAt(diags []ir.Diagnostic, code, pointer string) bool { - for _, d := range diags { - if d.Code == code && d.Provenance.Pointer == pointer { - return true - } - } - return false -} - -// countDiagsAt counts the diagnostics matching code and sev exactly. code is an -// exact match with no wildcard: countDiagsAt(diags, "", ir.SeverityError) -// matches only diagnostics whose code is literally empty. -func countDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { - var n int - for _, d := range diags { - if d.Code == code && d.Severity == sev { - n++ - } - } - return n -} - -// firstDegradedWarning returns the first degraded-construct warning in diags, -// and whether one was found. -func firstDegradedWarning(diags []ir.Diagnostic) (ir.Diagnostic, bool) { - for _, d := range diags { - if d.Code == diag.DegradedConstruct && d.Severity == ir.SeverityWarning { - return d, true - } - } - return ir.Diagnostic{}, false -} - -// diagMessageAt returns the message of the single diagnostic matching code, -// severity and provenance pointer. -func diagMessageAt(t *testing.T, diags []ir.Diagnostic, code string, sev ir.Severity, pointer string) string { - t.Helper() - var found []string - for _, d := range diags { - if d.Code == code && d.Severity == sev && d.Provenance.Pointer == pointer { - found = append(found, d.Message) - } - } - require.Len(t, found, 1, "want exactly one %v %q at %q, got %+v", sev, code, pointer, diags) - return found[0] -} - -// indexBy builds a lookup keyed by key(item). -func indexBy[T any, K comparable](items []T, key func(T) K) map[K]T { - out := make(map[K]T, len(items)) - for _, item := range items { - out[key(item)] = item - } - return out -} - -// propsByWire indexes a model's properties by wire name. -func propsByWire(props []ir.Property) map[string]ir.Property { - return indexBy(props, func(p ir.Property) string { return p.WireName }) -} - // serviceWithGrouping runs the phases beneath the service walk and then the walk // itself under a chosen grouping strategy. It names the strategy directly rather // than routing one through the compiler's public options, because grouping is @@ -195,7 +59,7 @@ func propsByWire(props []ir.Property) map[string]ir.Property { // compiler's business, not theirs. func serviceWithGrouping(t *testing.T, src string, grouping lowering.GroupingStrategy) (ir.Service, []ir.Diagnostic) { t.Helper() - loadedDoc, loadDiags, err := load.Load(t.Context(), 0, sourceOf(src), load.Options{}) + loadedDoc, loadDiags, err := load.Load(t.Context(), 0, openapitest.SourceOf(src), load.Options{}) require.NoError(t, err) require.NotNil(t, loadedDoc) @@ -209,54 +73,3 @@ func serviceWithGrouping(t *testing.T, src string, grouping lowering.GroupingStr acc.AppendAll(svcDiags) return svc, append(loadDiags, acc.List()...) } - -// The inline-probe helpers below also exist in the schema package's tests and in -// the compiler's own. Each package needs them and none can see another's test -// scaffolding; each copy is held to its meaning by the tests that use it. -// The inline-probe helpers below are duplicated in the schema package's own -// tests. Both sides need them and neither package can see the other's test -// scaffolding, which is the cost of the split; each copy is held to its meaning -// by the tests that use it, so a copy that drifts fails on its own side. -// inlineProbeBody is the body every inline-position case below writes: one -// annotation of each kind attachDeclaredAnnotations reads, one validation-only -// keyword, and one value constraint — all of them position-scoped, so a -// position that lowers this to the shared string primitive loses every one. All -// three documentation keywords are here because a home that keeps only the -// description passes a probe that writes only a description. -const inlineProbeBody = `{type: string, title: SUM, description: DOC, ` + - `externalDocs: {url: 'https://e.example', description: ED}, deprecated: true, ` + - `example: abc, x-vendor: V, xml: {name: X}, not: {const: N}, maxLength: 3}` - -// assertProbeDocsKept checks all three documentation keywords inlineProbeBody -// writes reached d, wherever the position's home turned out to be. -func assertProbeDocsKept(t *testing.T, d ir.Docs) { - t.Helper() - assert.Equal(t, "SUM", d.Summary, "title") - assert.Equal(t, "DOC", d.Description, "description") - if assert.Len(t, d.ExternalDocs, 1, "externalDocs") { - assert.Equal(t, "https://e.example", d.ExternalDocs[0].URL) - assert.Equal(t, "ED", d.ExternalDocs[0].Description) - } -} - -// assertProbeExample checks the single example inlineProbeBody writes reached -// the home under test with its value intact. -func assertProbeExample(t *testing.T, examples []ir.Example) { - t.Helper() - if !assert.Len(t, examples, 1, "examples") { - return - } - require.NotNil(t, examples[0].Value) - assert.Equal(t, "abc", examples[0].Value.Str) -} - -// assertInfoDiagAt requires one info diagnostic stamped at pointer. -func assertInfoDiagAt(t *testing.T, diags []ir.Diagnostic, pointer string) { - t.Helper() - for _, d := range diags { - if d.Severity == ir.SeverityInfo && d.Provenance.Pointer == pointer { - return - } - } - assert.Fail(t, "nothing announced this", "no info diagnostic at %q; got %+v", pointer, diags) -} diff --git a/compilers/openapi/internal/operation/operations_internal_test.go b/compilers/openapi/internal/operation/operations_internal_test.go index cadd30af..a61db6ea 100644 --- a/compilers/openapi/internal/operation/operations_internal_test.go +++ b/compilers/openapi/internal/operation/operations_internal_test.go @@ -13,6 +13,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" "github.com/dexpace/morphic/compilers/openapi/internal/load" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/resolve" "github.com/dexpace/morphic/ir" ) @@ -29,7 +30,7 @@ webhooks: responses: {"200": {description: ok}} ` svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) var group ir.OperationGroup found := false for _, g := range svc.Groups { @@ -47,7 +48,7 @@ webhooks: func TestCallbacks_RegisteredAndBound(t *testing.T) { t.Parallel() - spec := pathsSpec(` /subscribe: + spec := openapitest.PathsSpec(` /subscribe: post: operationId: sub callbacks: @@ -59,11 +60,11 @@ func TestCallbacks_RegisteredAndBound(t *testing.T) { responses: {"200": {description: ok}} `) svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) require.Len(t, svc.Groups, 1) group := svc.Groups[0] require.Len(t, group.Operations, 2, "parent op and callback op both registered") - byName := indexBy(group.Operations, func(op ir.Operation) string { return op.Name.Source }) + byName := openapitest.IndexBy(group.Operations, func(op ir.Operation) string { return op.Name.Source }) sub, ok := byName["sub"] require.True(t, ok) cb, ok := byName["cbPost"] @@ -78,7 +79,7 @@ func TestCallbacks_RegisteredAndBound(t *testing.T) { func TestParameters_PathItemMergeOverride(t *testing.T) { t.Parallel() - spec := pathsSpec(` /users/{id}: + spec := openapitest.PathsSpec(` /users/{id}: parameters: - {name: id, in: path, required: true, schema: {type: string}, description: path-level} - {name: trace, in: header, schema: {type: string}} diff --git a/compilers/openapi/internal/operation/operations_test.go b/compilers/openapi/internal/operation/operations_test.go index 9662578a..f4e02945 100644 --- a/compilers/openapi/internal/operation/operations_test.go +++ b/compilers/openapi/internal/operation/operations_test.go @@ -14,6 +14,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -35,9 +36,9 @@ paths: responses: {"200": {description: ok}} ` doc, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) require.Len(t, svc.Groups, 2) - byName := indexBy(svc.Groups, func(g ir.OperationGroup) string { return g.Name.Source }) + byName := openapitest.IndexBy(svc.Groups, func(g ir.OperationGroup) string { return g.Name.Source }) users, ok := byName["users"] require.True(t, ok) assert.Equal(t, "User ops", users.Docs.Description) @@ -53,7 +54,7 @@ paths: func TestResponses_ErrorSplitAndRanges(t *testing.T) { t.Parallel() - spec := pathsSpec(` /w: + spec := openapitest.PathsSpec(` /w: get: operationId: w responses: @@ -65,8 +66,8 @@ func TestResponses_ErrorSplitAndRanges(t *testing.T) { default: {description: anything else} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Responses, 1) assert.Equal(t, []ir.StatusRange{{From: 200, To: 200}}, op.Responses[0].Conditions.StatusCodes) require.Len(t, op.Errors, 3) @@ -90,7 +91,7 @@ func TestResponses_ErrorSplitAndRanges(t *testing.T) { // carries no Naming at all, so there is no channel on it to leave empty. func TestResponses_NamedByStatusKey(t *testing.T) { t.Parallel() - spec := pathsSpec(` /w: + spec := openapitest.PathsSpec(` /w: get: operationId: w responses: @@ -101,8 +102,8 @@ func TestResponses_NamedByStatusKey(t *testing.T) { default: {description: anything else} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Responses, 3) hints := make([]string, 0, len(op.Responses)) @@ -120,7 +121,7 @@ func TestResponses_NamedByStatusKey(t *testing.T) { // arrived as the same shape. Both are declared here, and they must not. func TestResponses_InvalidStatusKeyIsReported(t *testing.T) { t.Parallel() - spec := pathsSpec(` /w: + spec := openapitest.PathsSpec(` /w: get: operationId: w responses: @@ -131,15 +132,15 @@ func TestResponses_InvalidStatusKeyIsReported(t *testing.T) { default: {description: anything else} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) - msg := diagMessageAt(t, diags, diag.InvalidStatusKey, ir.SeverityWarning, + msg := openapitest.DiagMessageAt(t, diags, diag.InvalidStatusKey, ir.SeverityWarning, "/paths/~1w/get/responses/wat") assert.Contains(t, msg, `"wat"`, "the message names the key that could not be read") require.Len(t, op.Responses, 2) - byHint := indexBy(op.Responses, func(r ir.Response) string { return r.Name.Hint }) + byHint := openapitest.IndexBy(op.Responses, func(r ir.Response) string { return r.Name.Hint }) ok200, found := byHint["200"] require.True(t, found) assert.Equal(t, []ir.StatusRange{{From: 200, To: 200}}, ok200.Conditions.StatusCodes) @@ -160,7 +161,7 @@ func TestResponses_InvalidStatusKeyIsReported(t *testing.T) { // documents that are entirely correct. func TestResponses_ValidStatusKeysAreNotReported(t *testing.T) { t.Parallel() - spec := pathsSpec(` /w: + spec := openapitest.PathsSpec(` /w: get: operationId: w responses: @@ -172,8 +173,8 @@ func TestResponses_ValidStatusKeysAreNotReported(t *testing.T) { default: {description: anything else} `) _, _, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - assert.False(t, hasDiag(diags, diag.InvalidStatusKey), + openapitest.RequireNoErrorDiags(t, diags) + assert.False(t, openapitest.HasDiag(diags, diag.InvalidStatusKey), "every key here names a status; got %+v", diags) } @@ -181,7 +182,7 @@ func TestResponses_ErrorHeadersPreserved(t *testing.T) { t.Parallel() // ErrorCase has no Headers field; a 429's Retry-After header must not be // dropped silently — it is kept verbatim under Unmodeled with a diag. - spec := pathsSpec(` /w: + spec := openapitest.PathsSpec(` /w: get: operationId: w responses: @@ -192,8 +193,8 @@ func TestResponses_ErrorHeadersPreserved(t *testing.T) { Retry-After: {schema: {type: integer}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Errors, 1) raw, ok := op.Errors[0].Unmodeled["openapi:headers"] require.True(t, ok, "error response headers kept under Unmodeled") @@ -211,7 +212,7 @@ func TestResponses_ErrorHeadersPreserved(t *testing.T) { func TestOperation_ExplicitlyPublicSecurity(t *testing.T) { t.Parallel() - spec := pathsSpec(` /open: + spec := openapitest.PathsSpec(` /open: get: operationId: open security: [] @@ -222,7 +223,7 @@ func TestOperation_ExplicitlyPublicSecurity(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) ops := map[string]ir.Operation{} for _, g := range svc.Groups { for _, op := range g.Operations { @@ -236,7 +237,7 @@ func TestOperation_ExplicitlyPublicSecurity(t *testing.T) { func TestResponses_HeadersLowered(t *testing.T) { t.Parallel() - spec := pathsSpec(` /h: + spec := openapitest.PathsSpec(` /h: get: operationId: h responses: @@ -246,8 +247,8 @@ func TestResponses_HeadersLowered(t *testing.T) { X-Rate-Limit: {required: true, schema: {type: integer}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Responses, 1) require.Len(t, op.Responses[0].Headers, 1) h := op.Responses[0].Headers[0] @@ -264,7 +265,7 @@ func TestResponses_HeadersLowered(t *testing.T) { // unrelated parameter count. func TestParameters_PathItemSharedAcrossOperationsInternsOnce(t *testing.T) { t.Parallel() - spec := pathsSpec(` /pets/{petId}: + spec := openapitest.PathsSpec(` /pets/{petId}: parameters: - name: petId in: path @@ -280,16 +281,16 @@ func TestParameters_PathItemSharedAcrossOperationsInternsOnce(t *testing.T) { responses: {"200": {description: ok}} `) doc, _, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - getPet := findOp(t, doc, "getPet") - deletePet := findOp(t, doc, "deletePet") + openapitest.RequireNoErrorDiags(t, diags) + getPet := openapitest.FindOp(t, doc, "getPet") + deletePet := openapitest.FindOp(t, doc, "deletePet") require.Len(t, getPet.Params, 1, "get inherits only the shared path-item parameter") require.Len(t, deletePet.Params, 2, "delete gets its own force plus the shared path-item parameter") wantID := ir.TypeID("t/anon/paths/~1pets~1{petId}/parameters/0/schema") assert.Equal(t, wantID, getPet.Params[0].Type.Target, "get resolves the shared path-item schema") - byName := indexBy(deletePet.Params, func(p ir.Parameter) string { return p.Name.Source }) + byName := openapitest.IndexBy(deletePet.Params, func(p ir.Parameter) string { return p.Name.Source }) assert.Equal(t, wantID, byName["petId"].Type.Target, "delete resolves the same shared schema, not a copy") typeDef, ok := doc.Types[wantID] @@ -308,7 +309,7 @@ func TestParameters_PathItemSharedAcrossOperationsInternsOnce(t *testing.T) { // path-item-level parameter's schema. func TestParameters_PathItemSchemaIDStableAcrossOperationParamChanges(t *testing.T) { t.Parallel() - before := pathsSpec(` /pets/{petId}: + before := openapitest.PathsSpec(` /pets/{petId}: parameters: - name: petId in: path @@ -318,7 +319,7 @@ func TestParameters_PathItemSchemaIDStableAcrossOperationParamChanges(t *testing operationId: getPet responses: {"200": {description: ok}} `) - after := pathsSpec(` /pets/{petId}: + after := openapitest.PathsSpec(` /pets/{petId}: parameters: - name: petId in: path @@ -331,16 +332,16 @@ func TestParameters_PathItemSchemaIDStableAcrossOperationParamChanges(t *testing responses: {"200": {description: ok}} `) _, svcBefore, diagsBefore := lowerServiceSpec(t, before) - requireNoErrorDiags(t, diagsBefore) + openapitest.RequireNoErrorDiags(t, diagsBefore) _, svcAfter, diagsAfter := lowerServiceSpec(t, after) - requireNoErrorDiags(t, diagsAfter) + openapitest.RequireNoErrorDiags(t, diagsAfter) - opBefore := firstOp(t, svcBefore) - opAfter := firstOp(t, svcAfter) + opBefore := openapitest.FirstOp(t, svcBefore) + opAfter := openapitest.FirstOp(t, svcAfter) require.Len(t, opBefore.Params, 1) require.Len(t, opAfter.Params, 2, "the unrelated verbose parameter adds a second entry") - byName := indexBy(opAfter.Params, func(p ir.Parameter) string { return p.Name.Source }) + byName := openapitest.IndexBy(opAfter.Params, func(p ir.Parameter) string { return p.Name.Source }) assert.Equal(t, opBefore.Params[0].Type.Target, byName["petId"].Type.Target, "an unrelated operation-level parameter must not shift the shared path-item schema's ID") } @@ -364,8 +365,8 @@ webhooks: responses: {"200": {description: ok}} ` doc, _, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "onPetEvent") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "onPetEvent") require.Len(t, op.Params, 1) wantID := ir.TypeID("t/anon/webhooks/petEvent/parameters/0/schema") @@ -381,7 +382,7 @@ webhooks: // callback expression. func TestParameters_CallbackPathItemParameterPointer(t *testing.T) { t.Parallel() - spec := pathsSpec(` /subscribe: + spec := openapitest.PathsSpec(` /subscribe: post: operationId: subscribe callbacks: @@ -397,8 +398,8 @@ func TestParameters_CallbackPathItemParameterPointer(t *testing.T) { responses: {"200": {description: ok}} `) doc, _, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - cbOp := findOp(t, doc, "onEvent") + openapitest.RequireNoErrorDiags(t, diags) + cbOp := openapitest.FindOp(t, doc, "onEvent") require.Len(t, cbOp.Params, 1) wantPointer := "/paths/~1subscribe/post/callbacks/onEvent/{$request.body#~1callbackUrl}/parameters/0/schema" @@ -415,7 +416,7 @@ func TestParameters_CallbackPathItemParameterPointer(t *testing.T) { // pointer — the shadowed path-item schema is never lowered at all. func TestParameters_ShadowedPathItemParamUsesOperationPointer(t *testing.T) { t.Parallel() - spec := pathsSpec(` /pets/{petId}: + spec := openapitest.PathsSpec(` /pets/{petId}: parameters: - name: petId in: path @@ -431,8 +432,8 @@ func TestParameters_ShadowedPathItemParamUsesOperationPointer(t *testing.T) { responses: {"200": {description: ok}} `) doc, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Params, 1, "the operation parameter shadows the path-item one; no duplicate") wantID := ir.TypeID("t/anon/paths/~1pets~1{petId}/get/parameters/0/schema") @@ -449,7 +450,7 @@ func TestParameters_ShadowedPathItemParamUsesOperationPointer(t *testing.T) { func TestResponses_LinksPreserved(t *testing.T) { t.Parallel() - spec := pathsSpec(` /l: + spec := openapitest.PathsSpec(` /l: get: operationId: l responses: @@ -459,8 +460,8 @@ func TestResponses_LinksPreserved(t *testing.T) { GetUserByUserId: {operationId: getUser} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Responses, 1) raw, ok := op.Responses[0].Unmodeled["openapi:links"] require.True(t, ok, "response links preserved raw for later promotion") @@ -470,14 +471,14 @@ func TestResponses_LinksPreserved(t *testing.T) { func TestGrouping_ByPathPrefixInferred(t *testing.T) { t.Parallel() - spec := pathsSpec(` /users/{id}: + spec := openapitest.PathsSpec(` /users/{id}: get: {operationId: getUser, responses: {"200": {description: ok}}} /orders: get: {operationId: listOrders, responses: {"200": {description: ok}}} `) svc, diags := serviceWithGrouping(t, spec, lowering.GroupByPathPrefix) - requireNoErrorDiags(t, diags) - byName := indexBy(svc.Groups, func(g ir.OperationGroup) string { return g.Name.Source }) + openapitest.RequireNoErrorDiags(t, diags) + byName := openapitest.IndexBy(svc.Groups, func(g ir.OperationGroup) string { return g.Name.Source }) _, hasUsers := byName["users"] _, hasOrders := byName["orders"] assert.True(t, hasUsers, "first path segment forms a group") @@ -491,13 +492,13 @@ func TestGrouping_ByPathPrefixInferred(t *testing.T) { func TestOperation_NoOperationIdHint(t *testing.T) { t.Parallel() - spec := pathsSpec(` /ping: + spec := openapitest.PathsSpec(` /ping: get: responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) assert.Empty(t, op.Name.Source, "no operationId leaves an empty source name") assert.Equal(t, "get_ping", op.Name.Hint, "the hint is canonicalized, not the raw method and template") } @@ -570,7 +571,7 @@ func TestOperations_MethodsTagsServersRefs(t *testing.T) { assert.True(t, methods[m], "method %s lowered", m) } - getA := findOp(t, doc, "getA") + getA := openapitest.FindOp(t, doc, "getA") assert.NotEmpty(t, getA.Unmodeled, "op x-* extension") assert.NotEmpty(t, getA.Docs.ExternalDocs, "op externalDocs") _, hasServers := getA.Unmodeled["openapi:servers"] @@ -592,14 +593,14 @@ func TestOperations_MethodsTagsServersRefs(t *testing.T) { assert.NotEmpty(t, doc.TagDefs[0].Docs.ExternalDocs, "declared tag externalDocs") // Callback operation registered alongside its parent. - assert.NotEmpty(t, findOp(t, doc, "cbPost").ID) + assert.NotEmpty(t, openapitest.FindOp(t, doc, "cbPost").ID) _ = diags } func TestOperations_NoResponses(t *testing.T) { t.Parallel() doc, _ := parseFull(t, opsSpec) - trace := findOp(t, doc, "traceA") + trace := openapitest.FindOp(t, doc, "traceA") assert.Empty(t, trace.Responses) assert.Empty(t, trace.Errors) } @@ -617,14 +618,14 @@ components: func TestWebhooks_PathItemRefResolved(t *testing.T) { t.Parallel() doc, _ := parseFull(t, webhookRefSpec) - op := findOp(t, doc, "onPing") + op := openapitest.FindOp(t, doc, "onPing") require.NotEmpty(t, op.Bindings.HTTP) assert.True(t, op.Bindings.HTTP[0].IsWebhook) } func TestGrouping_PathPrefixRootPath(t *testing.T) { t.Parallel() - spec := pathsSpec(` /: + spec := openapitest.PathsSpec(` /: get: {operationId: root, responses: {"200": {description: ok}}} `) svc, _ := serviceWithGrouping(t, spec, lowering.GroupByPathPrefix) @@ -635,7 +636,7 @@ func TestGrouping_PathPrefixRootPath(t *testing.T) { func TestRawChildNode(t *testing.T) { t.Parallel() assert.Nil(t, annotation.RawChildNode(nil, "x"), "nil root") - assert.Nil(t, annotation.RawChildNode(strNode("x"), "k"), "non-mapping root") + assert.Nil(t, annotation.RawChildNode(openapitest.StrNode("x"), "k"), "non-mapping root") var doc yaml.Node require.NoError(t, yaml.Unmarshal([]byte("a: 1\nb: 2"), &doc)) @@ -675,9 +676,9 @@ components: func TestResponses_ComponentRefSharedAcrossOperationsInternsOnce(t *testing.T) { t.Parallel() doc, diags := parseFull(t, componentResponseRefSpec) - requireNoErrorDiags(t, diags) - getA := findOp(t, doc, "getA") - getB := findOp(t, doc, "getB") + openapitest.RequireNoErrorDiags(t, diags) + getA := openapitest.FindOp(t, doc, "getA") + getB := openapitest.FindOp(t, doc, "getB") require.Len(t, getA.Responses, 1) require.Len(t, getB.Responses, 1) @@ -725,7 +726,7 @@ components: func TestPathItem_RefSharedAcrossMountsKeepsDistinctOpIDs(t *testing.T) { t.Parallel() doc, diags := parseFull(t, sharedPathItemSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) opA := opByPath(t, doc, "GET", "/a") opB := opByPath(t, doc, "GET", "/b") @@ -789,9 +790,9 @@ components: func TestCallbacks_RefSharedAcrossParentsKeepsDistinctOpIDs(t *testing.T) { t.Parallel() doc, diags := parseFull(t, sharedCallbackSpec) - requireNoErrorDiags(t, diags) - parentC := findOp(t, doc, "parentC") - parentD := findOp(t, doc, "parentD") + openapitest.RequireNoErrorDiags(t, diags) + parentC := openapitest.FindOp(t, doc, "parentC") + parentD := openapitest.FindOp(t, doc, "parentD") require.Len(t, parentC.Bindings.HTTP[0].Callbacks, 1) require.Len(t, parentD.Bindings.HTTP[0].Callbacks, 1) require.Len(t, parentC.Bindings.HTTP[0].Callbacks[0].Operations, 1) @@ -814,7 +815,7 @@ func TestCallbacks_RefSharedAcrossParentsKeepsDistinctOpIDs(t *testing.T) { } // opsByID collects the operations matching any of ids into a lookup. It -// exists because findOp disambiguates by Name.Source, which two mounts of one +// exists because openapitest.FindOp disambiguates by Name.Source, which two mounts of one // $ref'd path item or callback share (they are the same declared operationId // mounted twice); OpID is the only thing that still tells them apart. func opsByID(t *testing.T, doc *ir.Document, opIDs ...ir.OpID) map[ir.OpID]ir.Operation { @@ -918,7 +919,7 @@ components: func TestProvenance_EveryPointerResolvesInSource(t *testing.T) { t.Parallel() doc, diags := parseFull(t, provenanceSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) var root yaml.Node require.NoError(t, yaml.Unmarshal([]byte(provenanceSpec), &root)) @@ -1044,7 +1045,7 @@ components: func TestWebhooks_RefdPathItemSharedAcrossHooksKeepsDistinctOpIDs(t *testing.T) { t.Parallel() doc, diags := parseFull(t, refdWebhookPathItemSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) idA := ir.OpID("op/openapi/webhooks/onA/post") idB := ir.OpID("op/openapi/webhooks/onB/post") @@ -1105,9 +1106,9 @@ components: func TestCallbacks_RefdPathItemInternsAtDeclaration(t *testing.T) { t.Parallel() doc, diags := parseFull(t, refdCallbackPathItemSpec) - requireNoErrorDiags(t, diags) - parentC := findOp(t, doc, "parentC") - parentD := findOp(t, doc, "parentD") + openapitest.RequireNoErrorDiags(t, diags) + parentC := openapitest.FindOp(t, doc, "parentC") + parentD := openapitest.FindOp(t, doc, "parentD") require.Len(t, parentC.Bindings.HTTP[0].Callbacks[0].Operations, 1) require.Len(t, parentD.Bindings.HTTP[0].Callbacks[0].Operations, 1) @@ -1161,14 +1162,14 @@ components: func TestResponses_RefdErrorAndDefaultInternAtDeclaration(t *testing.T) { t.Parallel() doc, diags := parseFull(t, refdErrorResponseSpec) - requireNoErrorDiags(t, diags) - getA := findOp(t, doc, "getA") - getB := findOp(t, doc, "getB") + openapitest.RequireNoErrorDiags(t, diags) + getA := openapitest.FindOp(t, doc, "getA") + getB := openapitest.FindOp(t, doc, "getB") require.Len(t, getA.Errors, 2) require.Len(t, getB.Errors, 2) byFault := func(op ir.Operation) map[string]ir.ErrorCase { - return indexBy(op.Errors, func(ec ir.ErrorCase) string { return ec.Fault }) + return openapitest.IndexBy(op.Errors, func(ec ir.ErrorCase) string { return ec.Fault }) } aErrs, bErrs := byFault(getA), byFault(getB) @@ -1238,7 +1239,7 @@ func TestDiag_SharedDeclarationReportsEachDefectOnce(t *testing.T) { } // Every defect still surfaces — de-duplication must not silence any of them. - assert.Equal(t, 3, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.Equal(t, 3, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "the optional body, the homeless error headers and the homeless error media type "+ "are three distinct defects") } @@ -1283,7 +1284,7 @@ func TestOperations_DuplicateOperationIDReported(t *testing.T) { opB := opByPath(t, doc, "GET", "/b") assert.Equal(t, opA.Name.Source, opB.Name.Source, "the IR still records what the document said") - require.Equal(t, 1, countDiagsAt(diags, diag.DuplicateOperationID, ir.SeverityWarning), + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DuplicateOperationID, ir.SeverityWarning), "the second claim is reported once, not both claims") for _, d := range diags { if d.Code == diag.DuplicateOperationID { @@ -1297,7 +1298,7 @@ func TestOperations_DuplicateOperationIDReported(t *testing.T) { // with their own ids, and an operation with none at all, raise nothing. func TestOperations_DistinctOperationIDsClean(t *testing.T) { t.Parallel() - _, diags := parseFull(t, pathsSpec(` /a: + _, diags := parseFull(t, openapitest.PathsSpec(` /a: get: operationId: getA responses: {"200": {description: ok}} @@ -1305,7 +1306,7 @@ func TestOperations_DistinctOperationIDsClean(t *testing.T) { get: responses: {"200": {description: ok}} `)) - assert.False(t, hasDiag(diags, diag.DuplicateOperationID)) + assert.False(t, openapitest.HasDiag(diags, diag.DuplicateOperationID)) } // TestOperation_UnserializableExtensionStillWarns pins the operation's half of @@ -1318,7 +1319,7 @@ func TestOperations_DistinctOperationIDsClean(t *testing.T) { // an operation that quietly loses an extension it could not represent. func TestOperation_UnserializableExtensionStillWarns(t *testing.T) { t.Parallel() - spec := pathsSpec(` /x: + spec := openapitest.PathsSpec(` /x: get: x-bad: {1: intkey} responses: {"200": {description: ok}} @@ -1422,14 +1423,14 @@ func TestGhostRefs_AllResolversDegradeGracefully(t *testing.T) { // has nothing else to read. func TestOperation_DeprecatedIsCarried(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /a: + doc, diags := parseFull(t, openapitest.PathsSpec(` /a: get: {operationId: getA, deprecated: true, responses: {"200": {description: ok}}} post: {operationId: postA, responses: {"200": {description: ok}}} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - assert.NotNil(t, findOp(t, doc, "getA").Deprecation, "the declared flag is carried") - assert.Nil(t, findOp(t, doc, "postA").Deprecation, "and an operation that declares none has none") + assert.NotNil(t, openapitest.FindOp(t, doc, "getA").Deprecation, "the declared flag is carried") + assert.Nil(t, openapitest.FindOp(t, doc, "postA").Deprecation, "and an operation that declares none has none") } // pathItemServersSpec declares the same `servers` override on each of the three @@ -1471,7 +1472,7 @@ webhooks: func TestOperations_PathItemServersKeptOnEveryRoute(t *testing.T) { t.Parallel() doc, diags := parseFull(t, pathItemServersSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) // kept is where the preserved list is recorded (the servers keyword); // reported is where the diagnostic is stamped (the operation itself). @@ -1482,13 +1483,13 @@ func TestOperations_PathItemServersKeptOnEveryRoute(t *testing.T) { "/paths/~1p/post/callbacks/onEvent/{$request.body#~1url}/servers", "/paths/~1p/post/callbacks/onEvent/{$request.body#~1url}/post"}, } { - entry, ok := findOp(t, doc, tc.op).Unmodeled["openapi:servers"] + entry, ok := openapitest.FindOp(t, doc, tc.op).Unmodeled["openapi:servers"] require.True(t, ok, "%s keeps its path item's servers", tc.op) assert.Equal(t, ir.ReasonNoIRHome, entry.Reason) assert.JSONEq(t, `[{"url":"`+tc.url+`"}]`, string(entry.Value), "%s keeps the list its own path item declared", tc.op) assert.Equal(t, tc.kept, entry.Provenance.Pointer) - assert.True(t, hasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), "%s reports the degradation as the paths route already did", tc.op) } } @@ -1501,7 +1502,7 @@ func TestOperations_PathItemServersKeptOnEveryRoute(t *testing.T) { // its key is the same loss as several losing all but the first. func TestErrorCase_SingleMediaTypeKeepsContentMap(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /x: + doc, diags := parseFull(t, openapitest.PathsSpec(` /x: get: operationId: getX responses: @@ -1513,8 +1514,8 @@ func TestErrorCase_SingleMediaTypeKeepsContentMap(t *testing.T) { schema: {type: object} "409": {description: conflict} `)) - requireNoErrorDiags(t, diags) - errs := indexBy(findOp(t, doc, "getX").Errors, + openapitest.RequireNoErrorDiags(t, diags) + errs := openapitest.IndexBy(openapitest.FindOp(t, doc, "getX").Errors, func(ec ir.ErrorCase) int { return ec.Conditions.StatusCodes[0].From }) entry, ok := errs[404].Unmodeled["openapi:content"] @@ -1524,7 +1525,7 @@ func TestErrorCase_SingleMediaTypeKeepsContentMap(t *testing.T) { "the media type the map is keyed by is what would otherwise be lost") assert.Equal(t, "/paths/~1x/get/responses/404/content", entry.Provenance.Pointer) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/paths/~1x/get/responses/404"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/paths/~1x/get/responses/404"), "media type has no ErrorCase home", "the single-entry case names its own loss, not the multi-entry one") @@ -1590,7 +1591,7 @@ webhooks: func TestOperations_OwnServersKeptBesideThePathItems(t *testing.T) { t.Parallel() doc, diags := parseFull(t, operationServersSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) // own/inherited are the operation's own list and its path item's; reported is // where the operation's degradation is stamped (the operation itself). @@ -1608,7 +1609,7 @@ func TestOperations_OwnServersKeptBesideThePathItems(t *testing.T) { "/paths/~1both/post/callbacks/onEvent/{$request.body#~1url}/servers", "/paths/~1both/post/callbacks/onEvent/{$request.body#~1url}/post"}, } { - op := findOp(t, doc, tc.op) + op := openapitest.FindOp(t, doc, tc.op) own, ok := op.Unmodeled["openapi:operationServers"] require.True(t, ok, "%s keeps its operation's own servers", tc.op) @@ -1623,7 +1624,7 @@ func TestOperations_OwnServersKeptBesideThePathItems(t *testing.T) { assert.Equal(t, tc.inheritedAt, inherited.Provenance.Pointer, "each entry keeps the coordinate of the object that declared it") - assert.True(t, hasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), "%s reports the operation's own list as the path item's already was", tc.op) } } @@ -1634,14 +1635,14 @@ func TestOperations_OwnServersKeptBesideThePathItems(t *testing.T) { func TestOperations_ServersKeysAreIndependent(t *testing.T) { t.Parallel() doc, diags := parseFull(t, operationServersSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - opOnly := findOp(t, doc, "getOperationOnly") + opOnly := openapitest.FindOp(t, doc, "getOperationOnly") assert.Contains(t, opOnly.Unmodeled, "openapi:operationServers") assert.NotContains(t, opOnly.Unmodeled, "openapi:servers", "an operation whose path item declares none records none for it") - pathOnly := findOp(t, doc, "getPathItemOnly") + pathOnly := openapitest.FindOp(t, doc, "getPathItemOnly") assert.Contains(t, pathOnly.Unmodeled, "openapi:servers") assert.NotContains(t, pathOnly.Unmodeled, "openapi:operationServers", "and an operation declaring none of its own records none") @@ -1655,16 +1656,16 @@ func TestOperations_ServersKeysAreIndependent(t *testing.T) { // that a later edit can silently break. func TestOperations_OwnServersSurviveBesideExtensions(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /x: + doc, diags := parseFull(t, openapitest.PathsSpec(` /x: get: operationId: getX servers: [{url: 'https://operation.example'}] x-vendor: kept responses: {"200": {description: ok}} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - op := findOp(t, doc, "getX") + op := openapitest.FindOp(t, doc, "getX") servers, ok := op.Unmodeled["openapi:operationServers"] require.True(t, ok, "the servers survive the extensions assignment") assert.JSONEq(t, `[{"url":"https://operation.example"}]`, string(servers.Value)) @@ -1716,7 +1717,7 @@ webhooks: func TestPathItem_DocsKeptOnEveryRoute(t *testing.T) { t.Parallel() doc, diags := parseFull(t, pathItemDocsSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) // kept is the path item's own pointer, where the pair is declared; // reported is the operation, which is what carries the entry. @@ -1727,7 +1728,7 @@ func TestPathItem_DocsKeptOnEveryRoute(t *testing.T) { "/paths/~1p/post/callbacks/onEvent/{$request.body#~1url}", "/paths/~1p/post/callbacks/onEvent/{$request.body#~1url}/post"}, } { - op := findOp(t, doc, tc.op) + op := openapitest.FindOp(t, doc, tc.op) for _, field := range []struct{ key, keyword string }{ {"openapi:pathItemSummary", "summary"}, {"openapi:pathItemDescription", "description"}, @@ -1739,13 +1740,13 @@ func TestPathItem_DocsKeptOnEveryRoute(t *testing.T) { "%s keeps the text its own path item declared", tc.op) assert.Equal(t, tc.kept+"/"+field.keyword, entry.Provenance.Pointer) } - assert.True(t, hasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.DegradedConstruct, tc.reported), "%s reports the path item's documentation as kept rather than lowered", tc.op) } - assert.Equal(t, "operation summary", findOp(t, doc, "postP").Docs.Summary, + assert.Equal(t, "operation summary", openapitest.FindOp(t, doc, "postP").Docs.Summary, "an operation's own summary is what Docs holds; the path item's never displaces it") - assert.Empty(t, findOp(t, doc, "onHook").Docs.Summary, + assert.Empty(t, openapitest.FindOp(t, doc, "onHook").Docs.Summary, "and an operation that declares none gets none invented for it") } @@ -1754,15 +1755,15 @@ func TestPathItem_DocsKeptOnEveryRoute(t *testing.T) { // is not a per-operation constant. func TestPathItem_DocsAbsentKeepNothing(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /p: + doc, diags := parseFull(t, openapitest.PathsSpec(` /p: get: {operationId: getP, responses: {"200": {description: ok}}} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - op := findOp(t, doc, "getP") + op := openapitest.FindOp(t, doc, "getP") assert.NotContains(t, op.Unmodeled, "openapi:pathItemSummary") assert.NotContains(t, op.Unmodeled, "openapi:pathItemDescription") - assert.False(t, hasDiagCodeAt(diags, diag.DegradedConstruct, "/paths/~1p/get")) + assert.False(t, openapitest.HasDiagCodeAt(diags, diag.DegradedConstruct, "/paths/~1p/get")) } // pathItemOperationsSpec declares a 3.2 operation with no fixed field of its own @@ -1814,7 +1815,7 @@ webhooks: func TestPathItem_AdditionalOperationsLowerOnEveryRoute(t *testing.T) { t.Parallel() doc, diags := parseFull(t, pathItemOperationsSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, tc := range []struct{ op, method, id string }{ {"queryP", "QUERY", "op/openapi/paths/~1p/query"}, @@ -1824,19 +1825,19 @@ func TestPathItem_AdditionalOperationsLowerOnEveryRoute(t *testing.T) { {"purgeCallback", "PURGE", "op/openapi/paths/~1p/post/callbacks/onEvent/{$request.body#~1url}/additionalOperations/PURGE"}, } { - op := findOp(t, doc, tc.op) + op := openapitest.FindOp(t, doc, tc.op) assert.Equal(t, ir.OpID(tc.id), op.ID, "%s is identified by where it is written", tc.op) require.Len(t, op.Bindings.HTTP, 1) assert.Equal(t, tc.method, op.Bindings.HTTP[0].Method, "%s binds the method key as the source spelled it", tc.op) } - assert.True(t, findOp(t, doc, "flushHook").Bindings.HTTP[0].IsWebhook, + assert.True(t, openapitest.FindOp(t, doc, "flushHook").Bindings.HTTP[0].IsWebhook, "a webhook mount marks the binding whichever field declared the operation") // Nothing reachable only through a dropped operation reached the registry // either: the request body's schema was not interned at all. - purge := findOp(t, doc, "purgeP") + purge := openapitest.FindOp(t, doc, "purgeP") require.NotNil(t, purge.Request) require.Len(t, purge.Request.Contents, 1) assert.NotNil(t, doc.Types[purge.Request.Contents[0].Type.Target], @@ -1850,12 +1851,12 @@ func TestPathItem_AdditionalOperationsLowerOnEveryRoute(t *testing.T) { func TestPathItem_AdditionalOperationsBindCallbacks(t *testing.T) { t.Parallel() doc, diags := parseFull(t, pathItemOperationsSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - parent := findOp(t, doc, "postP") + parent := openapitest.FindOp(t, doc, "postP") require.Len(t, parent.Bindings.HTTP, 1) require.Len(t, parent.Bindings.HTTP[0].Callbacks, 1) - assert.Equal(t, []ir.OpID{findOp(t, doc, "purgeCallback").ID}, + assert.Equal(t, []ir.OpID{openapitest.FindOp(t, doc, "purgeCallback").ID}, parent.Bindings.HTTP[0].Callbacks[0].Operations) } @@ -1878,15 +1879,15 @@ paths: operationId: nameless responses: {"204": {description: done}} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - op := findOp(t, doc, "nameless") + op := openapitest.FindOp(t, doc, "nameless") require.Len(t, op.Bindings.HTTP, 1) assert.Empty(t, op.Bindings.HTTP[0].Method, "the key binds as written, empty or not") assert.Equal(t, ir.OpID("op/openapi/paths/~1p/additionalOperations/"), op.ID, "and the operation is still mounted where the source writes it") - assert.True(t, hasDiagCodeAt(diags, diag.InvalidMethodKey, "/paths/~1p/additionalOperations/"), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.InvalidMethodKey, "/paths/~1p/additionalOperations/"), "the unusable method is reported at the entry that declares it") for _, d := range diags { if d.Code == diag.InvalidMethodKey { diff --git a/compilers/openapi/internal/operation/params_internal_test.go b/compilers/openapi/internal/operation/params_internal_test.go index fb6d4cf6..e288f1d5 100644 --- a/compilers/openapi/internal/operation/params_internal_test.go +++ b/compilers/openapi/internal/operation/params_internal_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/annotation" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -16,7 +17,7 @@ func TestFillParamSchema_EmptyEitherNoOp(t *testing.T) { t.Parallel() l := newRawLowerer(&soa.OpenAPI{}) param := &ir.Parameter{} - diags := fillParamSchema(l.ctx, l.types, param, emptyEitherSchema(), "/p") + diags := fillParamSchema(l.ctx, l.types, param, openapitest.EmptyEitherSchema(), "/p") assert.Nil(t, param.Constraints) assert.Nil(t, param.Default) assert.Empty(t, diags) @@ -28,7 +29,7 @@ func TestFillParamSchema_EmptyEitherNoOp(t *testing.T) { // record nothing rather than announce a preservation it did not make. func TestPreserveParamXML_ModelSetWithoutRawSourceRecordsNothing(t *testing.T) { t.Parallel() - l, _ := loweredFor(t, componentSpec(" A: {type: string}\n")) + l, _ := loweredFor(t, openapitest.ComponentSpec(" A: {type: string}\n")) s := &oas3.Schema{XML: &oas3.XML{}} require.NotNil(t, s.GetXML(), "the model reports the hint as set") require.Nil(t, annotation.RawPropertyNode(s, "xml"), "and no raw node backs it") diff --git a/compilers/openapi/internal/operation/params_test.go b/compilers/openapi/internal/operation/params_test.go index ab7feed2..623a6a6c 100644 --- a/compilers/openapi/internal/operation/params_test.go +++ b/compilers/openapi/internal/operation/params_test.go @@ -7,12 +7,13 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) func TestParams_LocationsAndSerializationDefaults(t *testing.T) { t.Parallel() - spec := pathsSpec(` /items/{id}: + spec := openapitest.PathsSpec(` /items/{id}: get: operationId: getItem parameters: @@ -24,11 +25,11 @@ func TestParams_LocationsAndSerializationDefaults(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Params, 5) require.Len(t, op.Bindings.HTTP, 1) - bindings := indexBy(op.Bindings.HTTP[0].ParamBindings, func(b ir.HTTPParamBinding) string { return b.Param }) + bindings := openapitest.IndexBy(op.Bindings.HTTP[0].ParamBindings, func(b ir.HTTPParamBinding) string { return b.Param }) require.Len(t, bindings, 5, "every logical param bound exactly once") id := bindings["id"] @@ -47,7 +48,7 @@ func TestParams_LocationsAndSerializationDefaults(t *testing.T) { assert.Equal(t, ir.HTTPLocationHeader, bindings["X-Trace"].Location) assert.Equal(t, ir.HTTPLocationCookie, bindings["session"].Location) - params := indexBy(op.Params, func(p ir.Parameter) string { return p.Name.Source }) + params := openapitest.IndexBy(op.Params, func(p ir.Parameter) string { return p.Name.Source }) assert.True(t, params["id"].Required, "path params are always required") require.NotNil(t, params["limit"].Default) assert.Equal(t, ir.BigVal("20"), params["limit"].Default.Num) @@ -55,7 +56,7 @@ func TestParams_LocationsAndSerializationDefaults(t *testing.T) { func TestParams_ContentStyleParameter(t *testing.T) { t.Parallel() - spec := pathsSpec(` /search: + spec := openapitest.PathsSpec(` /search: get: operationId: search parameters: @@ -67,8 +68,8 @@ func TestParams_ContentStyleParameter(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Bindings.HTTP, 1) require.Len(t, op.Bindings.HTTP[0].ParamBindings, 1) binding := op.Bindings.HTTP[0].ParamBindings[0] @@ -81,7 +82,7 @@ func TestParams_ContentStyleParameter(t *testing.T) { func TestParams_UnconvertibleExampleDiagnosed(t *testing.T) { t.Parallel() - spec := pathsSpec(` /items: + spec := openapitest.PathsSpec(` /items: get: operationId: getItem parameters: @@ -92,11 +93,11 @@ func TestParams_UnconvertibleExampleDiagnosed(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - op := firstOp(t, svc) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Params, 1) assert.Empty(t, op.Params[0].Examples, "the unconvertible example is skipped, not appended") - require.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) - d, ok := firstDegradedWarning(diags) + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok) assert.Equal(t, "/paths/~1items/get/parameters/0/example", d.Provenance.Pointer) assert.Contains(t, d.Message, "example:") @@ -104,7 +105,7 @@ func TestParams_UnconvertibleExampleDiagnosed(t *testing.T) { func TestParams_SchemaConstraints(t *testing.T) { t.Parallel() - spec := pathsSpec(` /people: + spec := openapitest.PathsSpec(` /people: get: operationId: listPeople parameters: @@ -112,8 +113,8 @@ func TestParams_SchemaConstraints(t *testing.T) { responses: {"200": {description: ok}} `) _, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) - op := firstOp(t, svc) + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FirstOp(t, svc) require.Len(t, op.Params, 1) c := op.Params[0].Constraints require.NotNil(t, c, "param scalar constraints land via annotation.Constraints") @@ -160,8 +161,8 @@ paths: func TestParams_AllLocationsAndStyles(t *testing.T) { t.Parallel() doc, diags := parseFull(t, paramSpec) - op := findOp(t, doc, "search") - byName := indexBy(op.Bindings.HTTP[0].ParamBindings, func(b ir.HTTPParamBinding) string { return b.Param }) + op := openapitest.FindOp(t, doc, "search") + byName := openapitest.IndexBy(op.Bindings.HTTP[0].ParamBindings, func(b ir.HTTPParamBinding) string { return b.Param }) assert.Equal(t, ir.HTTPLocationPath, byName["id"].Location) assert.Equal(t, ir.HTTPLocationQuery, byName["q"].Location) assert.Equal(t, ir.HTTPLocationHeader, byName["X-Tok"].Location) @@ -169,20 +170,20 @@ func TestParams_AllLocationsAndStyles(t *testing.T) { assert.Equal(t, "deepObject", byName["filter"].Style) assert.Equal(t, "application/json", byName["complex"].ContentType) - logical := indexBy(op.Params, func(p ir.Parameter) string { return p.Name.Source }) + logical := openapitest.IndexBy(op.Params, func(p ir.Parameter) string { return p.Name.Source }) assert.True(t, logical["id"].Required, "path param always required") require.NotNil(t, logical["q"].Deprecation) assert.NotEmpty(t, logical["q"].Examples) assert.NotEmpty(t, logical["filter"].Unmodeled) require.NotNil(t, logical["q"].Constraints) - assert.True(t, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, "malformed param default warns") - assert.True(t, hasDiag(diags, diag.NumericPrecision), "malformed param constraint warns") + assert.True(t, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, "malformed param default warns") + assert.True(t, openapitest.HasDiag(diags, diag.NumericPrecision), "malformed param constraint warns") } func TestParams_QueryStringLocation(t *testing.T) { t.Parallel() - spec := pathsSpecVer("3.2.0", ` /q: + spec := openapitest.PathsSpecVer("3.2.0", ` /q: get: operationId: q parameters: @@ -191,7 +192,7 @@ func TestParams_QueryStringLocation(t *testing.T) { "200": {description: ok} `) doc, _ := parseFull(t, spec) - op := findOp(t, doc, "q") + op := openapitest.FindOp(t, doc, "q") assert.Equal(t, ir.HTTPLocationQuerystring, op.Bindings.HTTP[0].ParamBindings[0].Location) } @@ -223,9 +224,9 @@ components: func TestParams_ComponentRefSharedAcrossOperationsInternsOnce(t *testing.T) { t.Parallel() doc, diags := parseFull(t, componentParamRefSpec) - requireNoErrorDiags(t, diags) - getA := findOp(t, doc, "getA") - getB := findOp(t, doc, "getB") + openapitest.RequireNoErrorDiags(t, diags) + getA := openapitest.FindOp(t, doc, "getA") + getB := openapitest.FindOp(t, doc, "getB") require.Len(t, getA.Params, 1) require.Len(t, getB.Params, 1) @@ -273,9 +274,9 @@ components: func TestParams_ContentStyleComponentRefInternsOnce(t *testing.T) { t.Parallel() doc, diags := parseFull(t, componentContentParamRefSpec) - requireNoErrorDiags(t, diags) - getA := findOp(t, doc, "getA") - getB := findOp(t, doc, "getB") + openapitest.RequireNoErrorDiags(t, diags) + getA := openapitest.FindOp(t, doc, "getA") + getB := openapitest.FindOp(t, doc, "getB") require.Len(t, getA.Params, 1) require.Len(t, getB.Params, 1) @@ -293,16 +294,16 @@ func TestParams_ContentStyleComponentRefInternsOnce(t *testing.T) { // though Parameter has Docs, Examples and Unmodeled (GitHub #116). func TestParams_SchemaAnnotationsReachTheParameter(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n parameters:\n"+ - " - name: q\n in: query\n schema: "+inlineProbeBody+"\n"+ + " - name: q\n in: query\n schema: "+openapitest.InlineProbeBody+"\n"+ " responses: {\"204\": {description: ok}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - p := firstOp(t, svc).Params[0] - assertProbeDocsKept(t, p.Docs) + p := openapitest.FirstOp(t, svc).Params[0] + openapitest.AssertProbeDocsKept(t, p.Docs) assert.NotNil(t, p.Deprecation) - assertProbeExample(t, p.Examples) + openapitest.AssertProbeExample(t, p.Examples) assert.Contains(t, p.Unmodeled, "openapi:x-vendor") assert.Contains(t, p.Unmodeled, "openapi:not") require.NotNil(t, p.Constraints) @@ -321,23 +322,23 @@ func TestParams_SchemaAnnotationsSurviveARefNamingTheSchema(t *testing.T) { doc, svc, diags := lowerServiceSpec(t, "openapi: 3.1.0\ninfo: {title: T, version: \"1\"}\npaths:\n"+ " /x:\n get:\n operationId: g\n parameters:\n"+ - " - name: q\n in: query\n schema: "+inlineProbeBody+"\n"+ + " - name: q\n in: query\n schema: "+openapitest.InlineProbeBody+"\n"+ " responses: {\"204\": {description: ok}}\n"+ "components:\n schemas:\n"+ " Outsider: {$ref: '#/paths/~1x/get/parameters/0/schema'}\n") - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - p := firstOp(t, svc).Params[0] + p := openapitest.FirstOp(t, svc).Params[0] assert.Equal(t, ir.TypeID("t/prim/string"), p.Type.Target, "the parameter's own type is unchanged by the outside reference") - assertProbeDocsKept(t, p.Docs) + openapitest.AssertProbeDocsKept(t, p.Docs) assert.NotNil(t, p.Deprecation) assert.Contains(t, p.Unmodeled, "openapi:x-vendor") assert.Contains(t, p.Unmodeled, "openapi:not") sc, ok := doc.Types["t/anon/paths/~1x/get/parameters/0/schema"].(*ir.Scalar) require.True(t, ok, "and the referenced pointer still names the schema written there") - assertProbeDocsKept(t, sc.Docs) + openapitest.AssertProbeDocsKept(t, sc.Docs) } // paramRefInheritSpec gives every parameter the same referent and varies only @@ -369,7 +370,7 @@ components: // paramsOf indexes an operation's parameters by source name. func paramsOf(t *testing.T, svc ir.Service) map[string]ir.Parameter { t.Helper() - return indexBy(firstOp(t, svc).Params, func(p ir.Parameter) string { return p.Name.Source }) + return openapitest.IndexBy(openapitest.FirstOp(t, svc).Params, func(p ir.Parameter) string { return p.Name.Source }) } // TestParams_RefSchemaInheritsFromItsReferent covers the referent-fallback half @@ -379,7 +380,7 @@ func paramsOf(t *testing.T, svc ir.Service) map[string]ir.Parameter { func TestParams_RefSchemaInheritsFromItsReferent(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramRefInheritSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := paramsOf(t, svc)["bare"] assert.Equal(t, "REFERENT", p.Docs.Description, "the referent's description reaches the parameter") @@ -396,7 +397,7 @@ func TestParams_RefSchemaInheritsFromItsReferent(t *testing.T) { func TestParams_RefSchemaUseSiteWinsOverItsReferent(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramRefInheritSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := paramsOf(t, svc)["override"] assert.Equal(t, "USE_SITE", p.Docs.Description, "the use-site description wins") @@ -412,7 +413,7 @@ func TestParams_RefSchemaUseSiteWinsOverItsReferent(t *testing.T) { func TestParams_RefSchemaInheritsThroughARefChain(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramRefInheritSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := paramsOf(t, svc)["chained"] assert.Equal(t, "REFERENT", p.Docs.Description, "one hop reaches Hop, which declares nothing") @@ -445,7 +446,7 @@ paths: func TestParams_SchemaXMLHintsKeptUnderUnmodeled(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramXMLSpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) params := paramsOf(t, svc) cases := []struct { @@ -464,7 +465,7 @@ func TestParams_SchemaXMLHintsKeptUnderUnmodeled(t *testing.T) { assert.Equal(t, tc.schemaPtr+"/xml", entry.Provenance.Pointer, "located at the xml keyword itself") assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, tc.schemaPtr+"/xml"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, tc.schemaPtr+"/xml"), "xml hints", "and announced once, at the same keyword the entry locates") }) } @@ -476,11 +477,11 @@ func TestParams_SchemaXMLHintsKeptUnderUnmodeled(t *testing.T) { // one declaration two homes that can drift. func TestParams_SchemaXMLHintsStayOnAnOwnedNode(t *testing.T) { t.Parallel() - doc, svc, diags := lowerServiceSpec(t, pathsSpec( + doc, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n parameters:\n"+ " - {name: obj, in: query, schema: {type: object, xml: {name: XOBJ}}}\n"+ " responses: {\"204\": {description: ok}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := paramsOf(t, svc)["obj"] node, ok := doc.Types[p.Type.Target] @@ -488,7 +489,7 @@ func TestParams_SchemaXMLHintsStayOnAnOwnedNode(t *testing.T) { require.NotNil(t, node.Common().XML) assert.Equal(t, "XOBJ", node.Common().XML.Name) assert.NotContains(t, p.Unmodeled, "openapi:xml", "so the parameter keeps no second copy") - assert.Equal(t, 0, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.Equal(t, 0, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "and nothing is announced as homeless") } @@ -517,7 +518,7 @@ paths: func TestParams_SchemaVisibilityKeptUnderUnmodeled(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramVisibilitySpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) params := paramsOf(t, svc) cases := []struct{ param, keyword, schemaPtr string }{ @@ -535,7 +536,7 @@ func TestParams_SchemaVisibilityKeptUnderUnmodeled(t *testing.T) { assert.JSONEq(t, `true`, string(entry.Value), "kept verbatim") assert.Equal(t, at, entry.Provenance.Pointer, "located at the keyword itself") assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, at), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, at), tc.keyword+" has no ir.Parameter home", "and announced once") }) } @@ -547,7 +548,7 @@ func TestParams_SchemaVisibilityKeptUnderUnmodeled(t *testing.T) { func TestParams_SchemaDefaultIsNotAlsoKeptVerbatim(t *testing.T) { t.Parallel() _, svc, diags := lowerServiceSpec(t, paramVisibilitySpec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := paramsOf(t, svc)["ro"] require.NotNil(t, p.Default, "the default lands in its own field") @@ -564,14 +565,14 @@ func TestParams_SchemaDefaultIsNotAlsoKeptVerbatim(t *testing.T) { // describes the type, and the more specific of the two wins. func TestParams_OwnAnnotationsWinOverTheSchema(t *testing.T) { t.Parallel() - _, svc, diags := lowerServiceSpec(t, pathsSpec( + _, svc, diags := lowerServiceSpec(t, openapitest.PathsSpec( " /x:\n get:\n operationId: g\n parameters:\n"+ " - name: q\n in: query\n description: PARAM\n"+ " x-scope: param\n schema: {type: string, description: SCHEMA, x-scope: schema}\n"+ " responses: {\"204\": {description: ok}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) - p := firstOp(t, svc).Params[0] + p := openapitest.FirstOp(t, svc).Params[0] assert.Equal(t, "PARAM", p.Docs.Description, "the parameter's own description wins") raw, ok := p.Unmodeled["openapi:x-scope"] require.True(t, ok) @@ -587,7 +588,7 @@ func TestParams_OwnAnnotationsWinOverTheSchema(t *testing.T) { // node. func TestParams_SchemaVisibilityKeptWhenTheSchemaOwnsANode(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /x: + doc, diags := parseFull(t, openapitest.PathsSpec(` /x: get: parameters: - {name: obj, in: query, schema: {type: object, properties: {a: {type: string}}, readOnly: true}} @@ -595,7 +596,7 @@ func TestParams_SchemaVisibilityKeptWhenTheSchemaOwnsANode(t *testing.T) { - {name: enm, in: query, schema: {type: string, enum: [a, b], readOnly: true}} responses: {"200": {description: ok}} `)) - op := findOp(t, doc, "") + op := openapitest.FindOp(t, doc, "") require.Len(t, op.Params, 3) for i, want := range []string{"openapi:readOnly", "openapi:writeOnly", "openapi:readOnly"} { @@ -603,7 +604,7 @@ func TestParams_SchemaVisibilityKeptWhenTheSchemaOwnsANode(t *testing.T) { entry, ok := param.Unmodeled[want] require.True(t, ok, "%s: %s kept on the carrier that has no field for it", param.Name.Source, want) assert.Equal(t, ir.ReasonNoIRHome, entry.Reason) - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) } } @@ -617,7 +618,7 @@ func TestParams_SchemaVisibilityKeptWhenTheSchemaOwnsANode(t *testing.T) { // which declarations count. func TestParams_AllowEmptyValueKept(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /x: + doc, diags := parseFull(t, openapitest.PathsSpec(` /x: get: operationId: getX parameters: @@ -626,8 +627,8 @@ func TestParams_AllowEmptyValueKept(t *testing.T) { - {name: silent, in: query, schema: {type: string}} responses: {"200": {description: ok}} `)) - requireNoErrorDiags(t, diags) - params := indexBy(findOp(t, doc, "getX").Params, func(p ir.Parameter) string { return p.Name.Source }) + openapitest.RequireNoErrorDiags(t, diags) + params := openapitest.IndexBy(openapitest.FindOp(t, doc, "getX").Params, func(p ir.Parameter) string { return p.Name.Source }) for _, tc := range []struct{ name, want, index string }{ {"on", `true`, "0"}, @@ -639,7 +640,7 @@ func TestParams_AllowEmptyValueKept(t *testing.T) { assert.JSONEq(t, tc.want, string(entry.Value)) assert.Equal(t, "/paths/~1x/get/parameters/"+tc.index+"/allowEmptyValue", entry.Provenance.Pointer, "kept at the keyword's own coordinate") - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) } assert.NotContains(t, params["silent"].Unmodeled, "openapi:allowEmptyValue", @@ -670,23 +671,23 @@ func TestParams_ReservedHeaderNamesAreReported(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pathsSpec(` /x: + doc, diags := parseFull(t, openapitest.PathsSpec(` /x: get: operationId: getX parameters: - {name: `+tc.param+`, in: `+tc.in+`, schema: {type: string}} responses: {"200": {description: ok}} `)) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "getX") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "getX") require.Len(t, op.Params, 1, "the parameter lowers either way; nothing is dropped") assert.Equal(t, tc.param, op.Params[0].Name.Source) assert.Equal(t, tc.reported, - hasDiagCodeAt(diags, diag.ReservedHeaderName, "/paths/~1x/get/parameters/0"), + openapitest.HasDiagCodeAt(diags, diag.ReservedHeaderName, "/paths/~1x/get/parameters/0"), "reported at the parameter's own pointer") if tc.reported { - assertHasCode(t, diags, diag.ReservedHeaderName, ir.SeverityWarning) + openapitest.AssertHasCode(t, diags, diag.ReservedHeaderName, ir.SeverityWarning) } }) } diff --git a/compilers/openapi/internal/resolve/entry_test.go b/compilers/openapi/internal/resolve/entry_test.go index f2788384..eb0aac48 100644 --- a/compilers/openapi/internal/resolve/entry_test.go +++ b/compilers/openapi/internal/resolve/entry_test.go @@ -1,7 +1,6 @@ package resolve_test import ( - "context" "os" "strconv" "strings" @@ -15,6 +14,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/resolve" "github.com/dexpace/morphic/ir" ) @@ -26,37 +26,18 @@ import ( // what makes the fixture real. It costs an import of the package that imports // this one, which only an external test package may have. -// parseFull runs the whole public compiler pipeline over src. +// parseFull runs the whole public compiler pipeline over src. That reach back +// through openapi is also why openapitest cannot hold it — see that package's +// doc comment. func parseFull(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { t.Helper() - doc, diags, err := openapi.New().Compile(context.Background(), - []compilers.Source{{Path: "spec.yaml", Data: []byte(src)}}, compilers.Options{}) + doc, diags, err := openapi.New().Compile(t.Context(), + []compilers.Source{openapitest.SourceOf(src)}, compilers.Options{}) require.NoError(t, err) require.NotNil(t, doc) return doc, diags } -// requireNoErrorDiags fails the test if any diagnostic has error severity. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - -// findOp returns the operation whose source name matches. -func findOp(t *testing.T, doc *ir.Document, source string) ir.Operation { - t.Helper() - for _, g := range doc.Services[0].Groups { - for _, op := range g.Operations { - if op.Name.Source == source { - return op - } - } - } - t.Fatalf("operation %q not found", source) - return ir.Operation{} -} - // TestObject_NilEntryIsNil pins the guard every caller relies on: a component // slot the document leaves empty arrives as a typed nil pointer, and reading an // object out of it must answer nil rather than dereference it. Each aliased @@ -121,9 +102,9 @@ func TestObjectAt_AliasChainLeavingDocumentKeepsUseSitePointer(t *testing.T) { func TestObjectAt_AliasedComponentChainInternsAtFinalDeclaration(t *testing.T) { t.Parallel() doc, diags := parseFull(t, aliasedComponentChainSpec) - requireNoErrorDiags(t, diags) - getA := findOp(t, doc, "getA") - getB := findOp(t, doc, "getB") + openapitest.RequireNoErrorDiags(t, diags) + getA := openapitest.FindOp(t, doc, "getA") + getB := openapitest.FindOp(t, doc, "getB") require.Len(t, getA.Params, 1) require.Len(t, getB.Params, 1) @@ -152,8 +133,8 @@ func TestObjectAt_AliasChainWithinBoundReachesDeclaration(t *testing.T) { t.Parallel() const hops = 8 doc, diags := parseFull(t, chainedAliasSpec(hops)) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "getA") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "getA") require.Len(t, op.Params, 1) assert.Equal(t, ir.TypeID("t/anon/components/parameters/P8/schema"), op.Params[0].Type.Target, "the walk follows every hop to the final declaration") @@ -168,8 +149,8 @@ func TestObjectAt_AliasChainWithinBoundReachesDeclaration(t *testing.T) { func TestObjectAt_AliasChainBeyondBoundFallsBackToUseSite(t *testing.T) { t.Parallel() doc, diags := parseFull(t, chainedAliasSpec(resolve.MaxRefChain+4)) - requireNoErrorDiags(t, diags) - op := findOp(t, doc, "getA") + openapitest.RequireNoErrorDiags(t, diags) + op := openapitest.FindOp(t, doc, "getA") require.Len(t, op.Params, 1) assert.Equal(t, ir.TypeID("t/anon/paths/~1a/get/parameters/0/schema"), op.Params[0].Type.Target, "an over-long chain keeps the one pointer that is certainly addressable") @@ -249,7 +230,7 @@ func compileFixture(t *testing.T, path string) *ir.Document { compilers.Options{FormatOptions: openapi.Options{AllowExternalRefs: true}}) require.NoError(t, err) require.NotNil(t, doc) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) return doc } diff --git a/compilers/openapi/internal/schema/compose_internal_test.go b/compilers/openapi/internal/schema/compose_internal_test.go index 75046e87..b9dbf4f2 100644 --- a/compilers/openapi/internal/schema/compose_internal_test.go +++ b/compilers/openapi/internal/schema/compose_internal_test.go @@ -13,6 +13,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/ir" ) @@ -36,7 +37,7 @@ func TestRefLastSegment(t *testing.T) { func TestMappingTargetID(t *testing.T) { t.Parallel() l := &lowerer{ - ctx: lowering.New(0, docDeclaring("Cat", "Dog", "A/B"), ir.SourceInfo{}, "", overlay.Origin{}), + ctx: lowering.New(0, openapitest.DocDeclaring("Cat", "Dog", "A/B"), ir.SourceInfo{}, "", overlay.Origin{}), out: &ir.Document{Types: ir.TypeRegistry{}}, } // A $ref to a declared component. @@ -63,7 +64,7 @@ func TestMappingTargetID(t *testing.T) { // (issue #14, f31). It gets a context of its own rather than being added to the // one above: the declared set is derived from the document now, so saying "and // also this one" means saying it to a document. - empty := lowering.New(0, docDeclaring(""), ir.SourceInfo{}, "", overlay.Origin{}) + empty := lowering.New(0, openapitest.DocDeclaring(""), ir.SourceInfo{}, "", overlay.Origin{}) id, ok = mappingTargetID(empty, l.types, "") require.True(t, ok) assert.Equal(t, ids.AnonType(ids.Ptr("components", "schemas", "")), id) @@ -72,7 +73,7 @@ func TestMappingTargetID(t *testing.T) { func TestDiscriminatorDefault_ResolvesDeclaredComponent(t *testing.T) { t.Parallel() - l := newRawLowerer(docDeclaring("Cat")) + l := newRawLowerer(openapitest.DocDeclaring("Cat")) d := &oas3.Discriminator{PropertyName: "kind", DefaultMapping: new("Cat")} id, diags := discriminatorDefault(l.ctx, l.types, d, "/components/schemas/Pet") @@ -121,9 +122,9 @@ func TestRawMappingKeys_OnlyEnumeratesAMapping(t *testing.T) { want []string }{ {"a nil node has no keys", nil, nil}, - {"a sequence is not a mapping", yamlNode(t, "- a\n- b\n"), nil}, - {"a bare scalar is not a mapping", yamlNode(t, "plain"), nil}, - {"a mapping yields its keys in source order", yamlNode(t, "b: 1\na: 2\n"), []string{"b", "a"}}, + {"a sequence is not a mapping", openapitest.YAMLNode(t, "- a\n- b\n"), nil}, + {"a bare scalar is not a mapping", openapitest.YAMLNode(t, "plain"), nil}, + {"a mapping yields its keys in source order", openapitest.YAMLNode(t, "b: 1\na: 2\n"), []string{"b", "a"}}, {"a document unwraps to the mapping inside it", &doc, []string{"a", "b"}}, {"an alias yields the anchored mapping's keys", useValue(t, "anchor: &a {b: 1, c: 2}\nuse: *a\n"), []string{"b", "c"}}, @@ -258,13 +259,13 @@ func TestMappingTargetID_FallsBackToAnInternedPointer(t *testing.T) { t.Parallel() const sub = "#/components/schemas/Pet/properties/kind" - empty := newRawLowerer(docDeclaring("Pet")) + empty := newRawLowerer(openapitest.DocDeclaring("Pet")) _, ok := mappingTargetID(empty.ctx, empty.types, sub) assert.False(t, ok, "nothing is interned at that pointer, so the target does not resolve") // A nested object owns a node at its own pointer, where a scalar property // would reduce to the shared primitive and leave the pointer backing nothing. - l, _ := loweredFor(t, componentSpec(" Pet:\n type: object\n"+ + l, _ := loweredFor(t, openapitest.ComponentSpec(" Pet:\n type: object\n"+ " properties: {kind: {type: object, properties: {a: {type: string}}}}\n")) l.diags.AppendAll(LowerComponentSchemas(l.ctx, l.types, &l.anchors)) diff --git a/compilers/openapi/internal/schema/compose_test.go b/compilers/openapi/internal/schema/compose_test.go index a44e5d0f..447db217 100644 --- a/compilers/openapi/internal/schema/compose_test.go +++ b/compilers/openapi/internal/schema/compose_test.go @@ -9,12 +9,13 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) func TestAllOf_SoleRefBecomesBase(t *testing.T) { t.Parallel() - spec := componentSpec(` Animal: + spec := openapitest.ComponentSpec(` Animal: type: object properties: name: {type: string} @@ -26,7 +27,7 @@ func TestAllOf_SoleRefBecomesBase(t *testing.T) { bark: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) dog, ok := doc.Types[componentID("Dog")].(*ir.Model) require.True(t, ok, "Dog should be a model") require.NotNil(t, dog.Base, "sole $ref becomes Base") @@ -44,7 +45,7 @@ func TestAllOf_OverlappingInlineBranchesReconcile(t *testing.T) { // webhook `forkee` uses (a documented object plus a doc-stripped duplicate // that marks some fields required). allOf is an intersection, so each wire // name must reconcile to a single property, not append a duplicate. - spec := componentSpec(` Forkish: + spec := openapitest.ComponentSpec(` Forkish: allOf: - type: object properties: @@ -57,7 +58,7 @@ func TestAllOf_OverlappingInlineBranchesReconcile(t *testing.T) { url: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Forkish")].(*ir.Model) require.True(t, ok, "Forkish should be a model") @@ -86,7 +87,7 @@ func TestAllOf_ReconcileAccumulatesRicherDetailWhateverTheOrder(t *testing.T) { // The bare declaration comes first and the richer one second: reconciliation // must still surface every optional detail, so branch order never loses // information (the reverse of the forkee documented-first shape). - spec := componentSpec(` Tokenish: + spec := openapitest.ComponentSpec(` Tokenish: allOf: - type: object properties: @@ -105,7 +106,7 @@ func TestAllOf_ReconcileAccumulatesRicherDetailWhateverTheOrder(t *testing.T) { xml: {name: tok} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Tokenish")].(*ir.Model) require.True(t, ok, "Tokenish should be a model") require.Len(t, m.Properties, 1, "token reconciles to a single property") @@ -127,7 +128,7 @@ func TestAllOf_ConflictingRedeclaredDescriptionDiagnosed(t *testing.T) { // Two branches describe the same field differently. The first declaration in // source order wins the shape, but the dropped description is surfaced as an // info diagnostic rather than vanishing silently. - spec := componentSpec(` Clashish: + spec := openapitest.ComponentSpec(` Clashish: allOf: - type: object properties: @@ -137,13 +138,13 @@ func TestAllOf_ConflictingRedeclaredDescriptionDiagnosed(t *testing.T) { id: {type: integer, description: a different meaning} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) // a description clash is info-level, never an error + openapitest.RequireNoErrorDiags(t, diags) // a description clash is info-level, never an error m, ok := doc.Types[componentID("Clashish")].(*ir.Model) require.True(t, ok, "Clashish should be a model") require.Len(t, m.Properties, 1, "id still reconciles to one property") assert.Equal(t, "the first meaning", m.Properties[0].Docs.Description, "the first declaration in source order wins the description") - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "a differing redeclared description is surfaced, not dropped silently") } @@ -154,7 +155,7 @@ func TestAllOf_ConflictingRedeclaredTypeDiagnosed(t *testing.T) { // first declaration's shape (as before) but must no longer swallow the // conflict — it names the field and both branch sites so the author can find // and fix them. - spec := componentSpec(` Conflictish: + spec := openapitest.ComponentSpec(` Conflictish: allOf: - type: object properties: @@ -164,7 +165,7 @@ func TestAllOf_ConflictingRedeclaredTypeDiagnosed(t *testing.T) { id: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) // a redeclaration conflict is a warning, not a refusal + openapitest.RequireNoErrorDiags(t, diags) // a redeclaration conflict is a warning, not a refusal m, ok := doc.Types[componentID("Conflictish")].(*ir.Model) require.True(t, ok, "Conflictish should be a model") require.Len(t, m.Properties, 1, "id still reconciles to one property") @@ -185,7 +186,7 @@ func TestAllOf_ConflictingRedeclaredConstraintDiagnosed(t *testing.T) { // Same target type, but the two branches pin the same keyword to different // values (maxLength 10 vs 20). The chosen winner is arbitrary source order, so // the dropped bound is surfaced rather than silently discarded. - spec := componentSpec(` Boundish: + spec := openapitest.ComponentSpec(` Boundish: allOf: - type: object properties: @@ -195,7 +196,7 @@ func TestAllOf_ConflictingRedeclaredConstraintDiagnosed(t *testing.T) { code: {type: string, maxLength: 20} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Boundish")].(*ir.Model) require.True(t, ok, "Boundish should be a model") require.Len(t, m.Properties, 1, "code reconciles to one property") @@ -218,7 +219,7 @@ func TestAllOf_CompatibleRedeclarationStaysSilent(t *testing.T) { // The reconcilable case: identical target type, the second branch only adds // `required`. This must stay silent — a redeclaration is not by itself a // conflict, only an incompatible one is. - spec := componentSpec(` Compatish: + spec := openapitest.ComponentSpec(` Compatish: allOf: - type: object properties: @@ -229,7 +230,7 @@ func TestAllOf_CompatibleRedeclarationStaysSilent(t *testing.T) { id: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Compatish")].(*ir.Model) require.True(t, ok, "Compatish should be a model") require.Len(t, m.Properties, 1, "id reconciles to one property") @@ -244,7 +245,7 @@ func TestAllOf_PropertyAlongsideAllOfReconciles(t *testing.T) { // A property declared directly on the allOf schema redeclares a field an inline // branch already contributed; the sibling and the branch declaration reconcile // into one property instead of colliding on the wire. - spec := componentSpec(` Mixish: + spec := openapitest.ComponentSpec(` Mixish: required: [id] properties: id: {type: integer} @@ -255,7 +256,7 @@ func TestAllOf_PropertyAlongsideAllOfReconciles(t *testing.T) { name: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Mixish")].(*ir.Model) require.True(t, ok, "Mixish should be a model") require.Len(t, m.Properties, 2, "id (branch + sibling) reconciles to one; name stays its own") @@ -287,7 +288,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { }{ { name: "required-only branch after the branch declaring the property (issue #29 repro)", - spec: componentSpec(` Thing: + spec: openapitest.ComponentSpec(` Thing: allOf: - type: object properties: @@ -299,7 +300,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { }, { name: "required-only branch before the branch declaring the property", - spec: componentSpec(` ThingB: + spec: openapitest.ComponentSpec(` ThingB: allOf: - required: [id] - type: object @@ -311,7 +312,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { }, { name: "required-only branch, property declared alongside allOf (mirrors allof_required.yaml's Bar)", - spec: componentSpec(` Foo: + spec: openapitest.ComponentSpec(` Foo: type: object properties: a: {type: string} @@ -329,7 +330,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { }, { name: "branch declares properties: {} (empty, non-nil) plus required", - spec: componentSpec(` ThingF: + spec: openapitest.ComponentSpec(` ThingF: allOf: - type: object properties: @@ -343,7 +344,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { }, { name: "the schema's own required (sibling of allOf, no sibling properties) names a property declared inside a branch", - spec: componentSpec(` ThingG: + spec: openapitest.ComponentSpec(` ThingG: required: [id] allOf: - type: object @@ -358,13 +359,13 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, tc.spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID(tc.model)].(*ir.Model) require.True(t, ok, "%s should be a model", tc.model) - p, ok := propsByWire(m.Properties)[tc.wire] + p, ok := openapitest.PropsByWire(m.Properties)[tc.wire] require.True(t, ok, "%s should have a %q property", tc.model, tc.wire) assert.True(t, p.Required, "%s.%s should be required via composition-scope required", tc.model, tc.wire) - assert.False(t, hasDiag(diags, diag.UnattachableRequired), + assert.False(t, openapitest.HasDiag(diags, diag.UnattachableRequired), "every required name matches an own property; no unattachable-required diagnostic expected") }) } @@ -379,7 +380,7 @@ func TestAllOf_CompositionRequiredAttaches(t *testing.T) { // to an unrelated property. func TestAllOf_RequiredOnlyBranchNamingBaseOwnedPropertyDiagnosed(t *testing.T) { t.Parallel() - spec := componentSpec(` Human: + spec := openapitest.ComponentSpec(` Human: type: object properties: name: {type: string} @@ -397,9 +398,9 @@ func TestAllOf_RequiredOnlyBranchNamingBaseOwnedPropertyDiagnosed(t *testing.T) require.NotNil(t, m.Base, "the sole $ref still becomes Base") assert.Equal(t, componentID("Human"), m.Base.Target) - _, hasLevel := propsByWire(m.Properties)["level"] + _, hasLevel := openapitest.PropsByWire(m.Properties)["level"] assert.False(t, hasLevel, "level belongs to the base, not to SuperBaby's own properties") - _, hasGender := propsByWire(m.Properties)["gender"] + _, hasGender := openapitest.PropsByWire(m.Properties)["gender"] assert.True(t, hasGender, "gender is SuperBaby's own property") var unattachable []ir.Diagnostic @@ -425,7 +426,7 @@ func TestAllOf_RequiredOnlyBranchNamingBaseOwnedPropertyDiagnosed(t *testing.T) // the diagnostic is info, not warning. func TestAllOf_RequiredOnlyBranchNoBaseOrMixinDiagnosedInfo(t *testing.T) { t.Parallel() - spec := componentSpec(` Thing: + spec := openapitest.ComponentSpec(` Thing: allOf: - type: object properties: @@ -433,19 +434,19 @@ func TestAllOf_RequiredOnlyBranchNoBaseOrMixinDiagnosedInfo(t *testing.T) { - required: [ghost] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Thing")].(*ir.Model) require.True(t, ok, "Thing should be a model") assert.Nil(t, m.Base, "no $ref branch at all: no Base") assert.Empty(t, m.Mixins, "no $ref branch at all: no Mixins") - id, hasID := propsByWire(m.Properties)["id"] + id, hasID := openapitest.PropsByWire(m.Properties)["id"] require.True(t, hasID, "the branch's own id property still lowers") assert.False(t, id.Required, "ghost's requiredness never misattaches to id") - _, hasGhost := propsByWire(m.Properties)["ghost"] + _, hasGhost := openapitest.PropsByWire(m.Properties)["ghost"] assert.False(t, hasGhost, "ghost is never invented as a property") - require.Equal(t, 1, countDiagsAt(diags, diag.UnattachableRequired, ir.SeverityInfo), + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.UnattachableRequired, ir.SeverityInfo), "exactly one info-severity unattachable-required diagnostic") var unattachable ir.Diagnostic for _, d := range diags { @@ -467,7 +468,7 @@ func TestAllOf_RequiredOnlyBranchNoBaseOrMixinDiagnosedInfo(t *testing.T) { // node actually exposes for a $ref-with-siblings branch in this library. func TestAllOf_RefBranchWithSiblingRequired31(t *testing.T) { t.Parallel() - spec := componentSpec(` Human: + spec := openapitest.ComponentSpec(` Human: type: object properties: name: {type: string} @@ -480,12 +481,12 @@ func TestAllOf_RefBranchWithSiblingRequired31(t *testing.T) { level: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("SuperBoyLike")].(*ir.Model) require.True(t, ok, "SuperBoyLike should be a model") require.NotNil(t, m.Base, "the sole $ref still becomes Base despite the sibling required") - level, ok := propsByWire(m.Properties)["level"] + level, ok := openapitest.PropsByWire(m.Properties)["level"] require.True(t, ok, "level is declared by the inline branch") assert.True(t, level.Required, "a required sibling on a $ref branch is read off the branch's own local schema and attaches to level") @@ -497,7 +498,7 @@ func TestAllOf_RefBranchWithSiblingRequired31(t *testing.T) { // diagnostic. The branch now survives verbatim beside the composed model. func TestAllOf_InlineBranchResidueKeptVerbatim(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: allOf: - type: object properties: {a: {type: string}} @@ -509,11 +510,11 @@ func TestAllOf_InlineBranchResidueKeptVerbatim(t *testing.T) { x-vendor: keepme `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "S").(*ir.Model) require.True(t, ok, "S should be a model") - a, ok := propsByWire(m.Properties)["a"] + a, ok := openapitest.PropsByWire(m.Properties)["a"] require.True(t, ok, "the merge still contributes the branch's properties") assert.True(t, a.Required, "and still ORs the branch's required list onto them") @@ -529,7 +530,7 @@ func TestAllOf_InlineBranchResidueKeptVerbatim(t *testing.T) { assert.Contains(t, string(entry.Value), kept, "the whole branch is preserved") } assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S/allOf/0"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S/allOf/0"), "additionalProperties, not, minProperties, description, x-vendor", "the diagnostic names every keyword the merge left behind, in source order") } @@ -541,12 +542,12 @@ func TestAllOf_InlineBranchResidueKeptVerbatim(t *testing.T) { // something wrong rather than merely incomplete. func TestAllOf_InlineBranchNonObjectTypeWarns(t *testing.T) { t.Parallel() - spec := componentSpec(` T: + spec := openapitest.ComponentSpec(` T: allOf: - {type: string, maxLength: 3} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "T").(*ir.Model) require.True(t, ok, "the composed node is still a model") assert.Empty(t, m.Properties, "a scalar branch declares no properties to merge") @@ -556,9 +557,9 @@ func TestAllOf_InlineBranchNonObjectTypeWarns(t *testing.T) { assert.Equal(t, ir.ReasonDegradedLowering, entry.Reason) assert.JSONEq(t, `{"type":"string","maxLength":3}`, string(entry.Value)) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityWarning, "/components/schemas/T/allOf/0"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityWarning, "/components/schemas/T/allOf/0"), "declares a type that is not an object") - assert.False(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.False(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "a contradicted model is a warning, not the info a merely narrowed one gets") } @@ -571,13 +572,13 @@ func TestAllOf_InlineBranchNonObjectTypeWarns(t *testing.T) { // or a residue named `<<`, which names nothing a reader can act on. func TestAllOf_BranchWrittenByReferenceStillDerivesResidue(t *testing.T) { t.Parallel() - spec := componentSpec(` Shared: &br {type: string, maxLength: 3, description: Aliased} + spec := openapitest.ComponentSpec(` Shared: &br {type: string, maxLength: 3, description: Aliased} Aliased: {allOf: [*br]} Merged: {allOf: [{<<: *br}]} Overridden: {allOf: [{<<: *br, type: object, properties: {a: {type: string}}}]} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) cases := []struct { name, wantKeys string @@ -599,7 +600,7 @@ func TestAllOf_BranchWrittenByReferenceStillDerivesResidue(t *testing.T) { assert.Contains(t, string(entry.Value), `"maxLength":3`, "the payload resolves the reference too, so the branch is recoverable") assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, tc.sev, + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, tc.sev, "/components/schemas/"+tc.name+"/allOf/0"), "the branch ("+tc.wantKeys+")", "the keywords the branch effectively declares are named, not `<<`") @@ -617,7 +618,7 @@ func TestAllOf_BranchWrittenByReferenceStillDerivesResidue(t *testing.T) { // from the corpus, which writes no such branch. func TestAllOf_MergedBranchKeywordsAreNotResidue(t *testing.T) { t.Parallel() - spec := componentSpec(` Base: {type: object, properties: {id: {type: string}}} + spec := openapitest.ComponentSpec(` Base: {type: object, properties: {id: {type: string}}} TypedBranch: allOf: - {$ref: '#/components/schemas/Base'} @@ -635,14 +636,14 @@ func TestAllOf_MergedBranchKeywordsAreNotResidue(t *testing.T) { - required: [name] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, name := range []string{"TypedBranch", "UntypedBranch", "RequiredOnlyBranch"} { m, ok := typeByName(doc, name).(*ir.Model) require.True(t, ok, "%s should be a model", name) assert.Empty(t, m.Unmodeled, "%s: properties, required and a bare `type: object` are merged, not residue", name) } - assert.Zero(t, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.Zero(t, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "a branch the merge fully consumes announces nothing; got %+v", diags) } @@ -654,7 +655,7 @@ func TestAllOf_MergedBranchKeywordsAreNotResidue(t *testing.T) { // exactly `object`, so the null a set also declares stays recoverable. func TestAllOf_BranchResidueDerivedFromDeclaredKeys(t *testing.T) { t.Parallel() - spec := componentSpec(` Unknown: + spec := openapitest.ComponentSpec(` Unknown: allOf: - type: object properties: {a: {type: string}} @@ -665,7 +666,7 @@ func TestAllOf_BranchResidueDerivedFromDeclaredKeys(t *testing.T) { properties: {a: {type: string}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for name, want := range map[string]string{ "Unknown": `"futureKeyword":{"some":"thing"}`, @@ -678,7 +679,7 @@ func TestAllOf_BranchResidueDerivedFromDeclaredKeys(t *testing.T) { require.True(t, ok, "%s keeps the branch verbatim; got %+v", name, m.Unmodeled) assert.Contains(t, string(entry.Value), want, "%s", name) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/"+name+"/allOf/0"), "kept verbatim under Unmodeled", name) } @@ -688,7 +689,7 @@ func TestAllOf_BranchResidueDerivedFromDeclaredKeys(t *testing.T) { // must not overwrite each other's entry, and each is reported at its own branch. func TestAllOf_EachInlineBranchKeyedSeparately(t *testing.T) { t.Parallel() - spec := componentSpec(` Multi: + spec := openapitest.ComponentSpec(` Multi: allOf: - type: object properties: {a: {type: string}} @@ -698,7 +699,7 @@ func TestAllOf_EachInlineBranchKeyedSeparately(t *testing.T) { minProperties: 1 `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "Multi").(*ir.Model) require.True(t, ok, "Multi should be a model") require.Len(t, m.Properties, 2, "both branches still merge their properties") @@ -708,7 +709,7 @@ func TestAllOf_EachInlineBranchKeyedSeparately(t *testing.T) { for i, want := range []string{"description", "minProperties"} { at := fmt.Sprintf("/components/schemas/Multi/allOf/%d", i) - assert.Contains(t, diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, at), want, + assert.Contains(t, openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, at), want, "branch %d is reported at its own pointer, naming its own residue", i) } } @@ -720,7 +721,7 @@ func TestAllOf_EachInlineBranchKeyedSeparately(t *testing.T) { // however many variants carry it. func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) { t.Parallel() - spec := componentSpec(` Base: {type: object, properties: {id: {type: string}}} + spec := openapitest.ComponentSpec(` Base: {type: object, properties: {id: {type: string}}} A: {type: object, properties: {a: {type: string}}} B: {type: object, properties: {b: {type: string}}} Distributed: @@ -734,7 +735,7 @@ func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) { - {$ref: '#/components/schemas/B'} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Distributed").(*ir.Union) require.True(t, ok, "Distributed distributes into a union") require.Len(t, u.Variants, 2) @@ -746,7 +747,7 @@ func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) { require.True(t, ok, "variant %d carries the branch residue; got %+v", i, variant.Unmodeled) assert.Contains(t, string(entry.Value), `"description":"BranchDoc"`, "variant %d", i) } - assert.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "one branch, one diagnostic, however many variants carry it; got %+v", diags) } @@ -757,24 +758,24 @@ func TestAllOf_BranchResidueRidesEveryDistributedVariant(t *testing.T) { // exactly as before — this path never reaches applyCompositionRequired. func TestModel_PlainRequiredUndeclaredPropertyUnaffected(t *testing.T) { t.Parallel() - spec := componentSpec(` Plain: + spec := openapitest.ComponentSpec(` Plain: type: object required: [ghost] properties: id: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Plain")].(*ir.Model) require.True(t, ok, "Plain should be a model") require.Len(t, m.Properties, 1) - assert.False(t, hasDiag(diags, diag.UnattachableRequired), + assert.False(t, openapitest.HasDiag(diags, diag.UnattachableRequired), "the plain (non-allOf) path is untouched by this fix; no new diagnostic") } func TestAllOf_ExtraRefsBecomeMixins(t *testing.T) { t.Parallel() - spec := componentSpec(` A: + spec := openapitest.ComponentSpec(` A: type: object properties: a: {type: string} @@ -791,7 +792,7 @@ func TestAllOf_ExtraRefsBecomeMixins(t *testing.T) { c: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) c, ok := doc.Types[componentID("C")].(*ir.Model) require.True(t, ok, "C should be a model") assert.Nil(t, c.Base, "two non-hierarchy refs, neither sole: no Base") @@ -804,7 +805,7 @@ func TestAllOf_ExtraRefsBecomeMixins(t *testing.T) { func TestAllOf_DiscriminatorSubtypeValue(t *testing.T) { t.Parallel() - spec := componentSpec(` Pet: + spec := openapitest.ComponentSpec(` Pet: type: object discriminator: propertyName: petType @@ -826,7 +827,7 @@ func TestAllOf_DiscriminatorSubtypeValue(t *testing.T) { bark: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) pet, ok := doc.Types[componentID("Pet")].(*ir.Model) require.True(t, ok, "Pet should be a model") @@ -848,7 +849,7 @@ func TestAllOf_DiscriminatorSubtypeValue(t *testing.T) { func TestOneOf_WithDiscriminator(t *testing.T) { t.Parallel() - spec := componentSpec(` Cat: + spec := openapitest.ComponentSpec(` Cat: type: object properties: petType: {type: string} @@ -866,7 +867,7 @@ func TestOneOf_WithDiscriminator(t *testing.T) { cat: "#/components/schemas/Cat" `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) pet, ok := doc.Types[componentID("Pet")].(*ir.Union) require.True(t, ok, "Pet should be a union") assert.True(t, pet.Exclusive, "oneOf is exclusive") @@ -880,13 +881,13 @@ func TestOneOf_WithDiscriminator(t *testing.T) { func TestAnyOf_IsNonExclusiveUnion(t *testing.T) { t.Parallel() - spec := componentSpec(` U: + spec := openapitest.ComponentSpec(` U: anyOf: - {type: string} - {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := doc.Types[componentID("U")].(*ir.Union) require.True(t, ok, "U should be a union") assert.False(t, u.Exclusive, "anyOf is non-exclusive") @@ -895,7 +896,7 @@ func TestAnyOf_IsNonExclusiveUnion(t *testing.T) { func TestOneOf_NullVariantCollapses(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: p: @@ -904,7 +905,7 @@ func TestOneOf_NullVariantCollapses(t *testing.T) { - {type: "null"} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) s, ok := doc.Types[componentID("S")].(*ir.Model) require.True(t, ok) require.Len(t, s.Properties, 1) @@ -920,7 +921,7 @@ func TestOneOf_ThreeVariantsWithNullStripsNullLiftsNullable(t *testing.T) { // A oneOf with two non-null branches plus a null branch stays a Union of the // two non-null variants (the null branch is NOT emitted as an `any` variant), // and the enclosing ref becomes Nullable. - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: p: @@ -930,7 +931,7 @@ func TestOneOf_ThreeVariantsWithNullStripsNullLiftsNullable(t *testing.T) { - {type: "null"} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) s := doc.Types[componentID("S")].(*ir.Model) require.Len(t, s.Properties, 1) ref := s.Properties[0].Type @@ -947,12 +948,12 @@ func TestOneOf_ThreeVariantsWithNullStripsNullLiftsNullable(t *testing.T) { func TestEnum_StringClosed(t *testing.T) { t.Parallel() - spec := componentSpec(` E: + spec := openapitest.ComponentSpec(` E: type: string enum: [a, b] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) e, ok := doc.Types[componentID("E")].(*ir.Enum) require.True(t, ok, "E should be an enum") assert.True(t, e.Closed, "JSON Schema enum is closed") @@ -966,7 +967,7 @@ func TestEnum_StringClosed(t *testing.T) { // enumPropertySpec puts a schema at one property of a model S, so a test can // read both the node the schema lowers to and the reference the position holds. func enumPropertySpec(version, schema string) string { - return componentSpecVer(version, ` S: + return openapitest.ComponentSpecVer(version, ` S: type: object properties: p: `+schema+"\n") @@ -1035,8 +1036,8 @@ func TestEnum_NullMemberNormalizesToNullable(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, enumPropertySpec(tc.version, tc.schema)) - requireNoErrorDiags(t, diags) - assert.False(t, hasDiag(diags, diag.DegradedConstruct), + openapitest.RequireNoErrorDiags(t, diags) + assert.False(t, openapitest.HasDiag(diags, diag.DegradedConstruct), "a normalized nullable enum is not a degradation; got %+v", diags) m, ok := doc.Types[componentID("S")].(*ir.Model) @@ -1121,8 +1122,8 @@ func TestEnum_NullMemberKeepsUnionFallback(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, enumPropertySpec(tc.version, tc.schema)) - requireNoErrorDiags(t, diags) - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + openapitest.RequireNoErrorDiags(t, diags) + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "the degraded-enum diagnostic still fires; got %+v", diags) u, ok := doc.Types[ir.TypeID("t/anon/components/schemas/S/properties/p")].(*ir.Union) @@ -1144,11 +1145,11 @@ func TestEnum_NullMemberKeepsUnionFallback(t *testing.T) { func TestConst_BecomesLiteral(t *testing.T) { t.Parallel() - spec := componentSpec(` K: + spec := openapitest.ComponentSpec(` K: const: "fixed" `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) k, ok := doc.Types[componentID("K")].(*ir.Literal) require.True(t, ok, "K should be a literal") assert.Equal(t, ir.Value{Kind: ir.ValueString, Str: "fixed"}, k.Value) @@ -1159,7 +1160,7 @@ func TestHoistLiteral_UnconvertibleConstBecomesAny(t *testing.T) { // A custom tag is structurally unconvertible (no scalarValue case resolves // it), forcing hoistLiteral's fallback. Before the fix this silently // produced a Literal asserting the value is null, which the spec never said. - spec := componentSpec(" K:\n const: !foo bar\n") + spec := openapitest.ComponentSpec(" K:\n const: !foo bar\n") doc, diags := lowerSpec(t, spec) k, ok := doc.Types[componentID("K")].(*ir.Any) require.True(t, ok, "an unconvertible const hoists the schemaless top type at its own pointer") @@ -1168,9 +1169,9 @@ func TestHoistLiteral_UnconvertibleConstBecomesAny(t *testing.T) { _, isLiteral := td.(*ir.Literal) assert.False(t, isLiteral, "no Literal is produced anywhere; nothing lies about the value being null") } - require.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "exactly one warning fires for the unconvertible value") - d, ok := firstDegradedWarning(diags) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok) assert.Equal(t, "/components/schemas/K", d.Provenance.Pointer) } @@ -1179,7 +1180,7 @@ func TestEnumAsUnion_UnconvertibleMemberBecomesAny(t *testing.T) { t.Parallel() // The convertible member ("ok") must still hoist a real Literal; only the // genuinely unconvertible member ("!foo bar") falls back to the top type. - spec := componentSpec(" M:\n enum: [ok, !foo bar]\n") + spec := openapitest.ComponentSpec(" M:\n enum: [ok, !foo bar]\n") doc, diags := lowerSpec(t, spec) u, ok := doc.Types[componentID("M")].(*ir.Union) require.True(t, ok, "heterogeneous enum still lowers to a union of literals") @@ -1193,9 +1194,9 @@ func TestEnumAsUnion_UnconvertibleMemberBecomesAny(t *testing.T) { require.True(t, ok, "the unconvertible member hoists the schemaless top type, not a lying null Literal") assert.Equal(t, ir.KindAny, member1.Kind()) - require.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "exactly one warning for the unconvertible member, distinct from the heterogeneous-enum info diagnostic") - d, ok := firstDegradedWarning(diags) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok) assert.Equal(t, "/components/schemas/M/enum/1", d.Provenance.Pointer) } @@ -1210,7 +1211,7 @@ func TestEnum_UnquotedDatesStayClosedEnum(t *testing.T) { // as residue (it binds a use of the type, so the type node has no field for // it), which is the one diagnostic here; see // TestProperty_UnquotedDateDefaultPreserved for the default path. - spec := componentSpec(` D: + spec := openapitest.ComponentSpec(` D: type: string format: date default: 2021-01-01 @@ -1233,7 +1234,7 @@ func TestProperty_UnquotedDateDefaultPreserved(t *testing.T) { // The repro's default sits at the component level, which nothing lowers by // itself; this covers the path that actually surfaces the bug in practice — // a date default declared on an object property. - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: d: @@ -1252,7 +1253,7 @@ func TestProperty_UnquotedDateDefaultPreserved(t *testing.T) { func TestAllOf_DiscriminatorHierarchy(t *testing.T) { t.Parallel() - spec := componentSpecVer("3.2.0", ` Pet: + spec := openapitest.ComponentSpecVer("3.2.0", ` Pet: type: object discriminator: propertyName: petType @@ -1293,7 +1294,7 @@ func TestAllOf_DiscriminatorHierarchy(t *testing.T) { func TestModelDiscriminator_UndeclaredPropertyAndBadMapping(t *testing.T) { t.Parallel() - spec := componentSpec(` Vehicle: + spec := openapitest.ComponentSpec(` Vehicle: type: object discriminator: propertyName: kind @@ -1306,12 +1307,12 @@ func TestModelDiscriminator_UndeclaredPropertyAndBadMapping(t *testing.T) { require.NotNil(t, v.Discriminator) assert.Empty(t, v.Discriminator.Property, "undeclared property") assert.Equal(t, "kind", v.Discriminator.PropertyName) - assert.True(t, hasDiag(diags, diag.UnresolvedRef), "bad mapping target diagnostic") + assert.True(t, openapitest.HasDiag(diags, diag.UnresolvedRef), "bad mapping target diagnostic") } func TestOneOf_DiscriminatorWithDefault(t *testing.T) { t.Parallel() - spec := componentSpecVer("3.2.0", ` Shape: + spec := openapitest.ComponentSpecVer("3.2.0", ` Shape: oneOf: - {$ref: '#/components/schemas/Circle'} - {$ref: '#/components/schemas/Square'} @@ -1331,14 +1332,14 @@ func TestOneOf_DiscriminatorWithDefault(t *testing.T) { func TestAnyOf_ThreeVariantsWithNull(t *testing.T) { t.Parallel() - spec := componentSpec(` N: + spec := openapitest.ComponentSpec(` N: anyOf: - {type: string} - {type: integer} - {type: 'null'} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u := typeByName(doc, "N").(*ir.Union) assert.Len(t, u.Variants, 2, "null branch stripped from variants") assert.False(t, u.Exclusive, "anyOf is not exclusive") @@ -1346,14 +1347,14 @@ func TestAnyOf_ThreeVariantsWithNull(t *testing.T) { func TestUnion_VariantHints(t *testing.T) { t.Parallel() - spec := componentSpec(` U: + spec := openapitest.ComponentSpec(` U: oneOf: - {$ref: '#/components/schemas/Named', description: sibling} - {type: string} Named: {type: object, properties: {a: {type: string}}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u := typeByName(doc, "U").(*ir.Union) require.Len(t, u.Variants, 2) hints := []string{u.Variants[0].Name.Hint, u.Variants[1].Name.Hint} @@ -1363,19 +1364,19 @@ func TestUnion_VariantHints(t *testing.T) { func TestAllOf_UnresolvedRefBranch(t *testing.T) { t.Parallel() - spec := componentSpec(` Bad: + spec := openapitest.ComponentSpec(` Bad: allOf: - {$ref: '#'} - {type: object, properties: {a: {type: string}}} `) doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.True(t, hasDiag(diags, diag.UnresolvedRef)) + assert.True(t, openapitest.HasDiag(diags, diag.UnresolvedRef)) } func TestAllOf_MultiRefWithUnresolvedDoesNotAnchor(t *testing.T) { t.Parallel() - spec := componentSpec(` Base: + spec := openapitest.ComponentSpec(` Base: type: object discriminator: {propertyName: t} properties: {t: {type: string}} @@ -1395,7 +1396,7 @@ func TestAllOf_MultiRefWithUnresolvedDoesNotAnchor(t *testing.T) { func TestEnum_ValueTypeVariants(t *testing.T) { t.Parallel() - spec := componentSpec(` EInt: {type: integer, enum: [1, 2]} + spec := openapitest.ComponentSpec(` EInt: {type: integer, enum: [1, 2]} ENum: {type: number, enum: [1.5, 2.5]} EBool: {type: boolean, enum: [true, false]} ENoTypeBool: {enum: [true, false]} @@ -1404,7 +1405,7 @@ func TestEnum_ValueTypeVariants(t *testing.T) { EBytes: {enum: [!!binary aGk=, !!binary Ynll]} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) want := map[string]ir.PrimKind{ "EInt": ir.PrimInteger, "ENum": ir.PrimNumber, "EBool": ir.PrimBool, "ENoTypeBool": ir.PrimBool, "ENoTypeNum": ir.PrimNumber, @@ -1424,9 +1425,9 @@ func TestEnum_ValueTypeVariants(t *testing.T) { // name, no diagnostic. func TestEnum_ByteMembersAreNamedAndTyped(t *testing.T) { t.Parallel() - spec := componentSpec(" EBytes: {enum: [!!binary aGk=, !!binary Ynll]}\n") + spec := openapitest.ComponentSpec(" EBytes: {enum: [!!binary aGk=, !!binary Ynll]}\n") doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) e, ok := typeByName(doc, "EBytes").(*ir.Enum) require.True(t, ok, "a homogeneous byte enum stays an enum") @@ -1441,20 +1442,20 @@ func TestEnum_ByteMembersAreNamedAndTyped(t *testing.T) { func TestEnum_HeterogeneousBecomesUnionWithBadValue(t *testing.T) { t.Parallel() - spec := componentSpec(" Mixed:\n enum: [active, .inf]\n") + spec := openapitest.ComponentSpec(" Mixed:\n enum: [active, .inf]\n") doc, diags := lowerSpec(t, spec) u, ok := typeByName(doc, "Mixed").(*ir.Union) require.True(t, ok, "heterogeneous enum lowers to a union of literals") assert.Len(t, u.Variants, 2) - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "heterogeneous-enum info diagnostic") - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityWarning), + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "unconvertible literal value warning") } func TestAllOf_ModelWithOwnDiscriminator(t *testing.T) { t.Parallel() - spec := componentSpec(` Base: + spec := openapitest.ComponentSpec(` Base: allOf: - {$ref: '#/components/schemas/Common'} discriminator: {propertyName: kind} @@ -1462,7 +1463,7 @@ func TestAllOf_ModelWithOwnDiscriminator(t *testing.T) { Common: {type: object, properties: {id: {type: string}}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) base := typeByName(doc, "Base").(*ir.Model) require.NotNil(t, base.Discriminator, "allOf model may declare its own discriminator") assert.NotEmpty(t, base.Discriminator.Property) @@ -1470,7 +1471,7 @@ func TestAllOf_ModelWithOwnDiscriminator(t *testing.T) { func TestAllOf_BoolRefBranchHasNoDiscriminator(t *testing.T) { t.Parallel() - spec := componentSpec(` BoolComp: false + spec := openapitest.ComponentSpec(` BoolComp: false Sub: allOf: - {$ref: '#/components/schemas/BoolComp'} @@ -1494,7 +1495,7 @@ func TestAllOf_BoolRefBranchHasNoDiscriminator(t *testing.T) { // own property and its own required both still lower normally. func TestAllOf_BoolBranchSkippedInCompositionRequired(t *testing.T) { t.Parallel() - spec := componentSpec(` Thing: + spec := openapitest.ComponentSpec(` Thing: allOf: - type: object required: [id] @@ -1503,22 +1504,22 @@ func TestAllOf_BoolBranchSkippedInCompositionRequired(t *testing.T) { - true `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Thing")].(*ir.Model) require.True(t, ok, "Thing should be a model") assert.Nil(t, m.Base, "a bare boolean branch is never a $ref, so no Base") assert.Empty(t, m.Mixins) - id, hasID := propsByWire(m.Properties)["id"] + id, hasID := openapitest.PropsByWire(m.Properties)["id"] require.True(t, hasID, "the object branch's own property still lowers despite the sibling bool branch") assert.True(t, id.Required, "the object branch's own required still attaches to its own property") - assert.False(t, hasDiag(diags, diag.UnattachableRequired), + assert.False(t, openapitest.HasDiag(diags, diag.UnattachableRequired), "the boolean branch contributes no required names, so nothing goes unattached") } func TestEnum_NonScalarAndMidListMismatch(t *testing.T) { t.Parallel() - spec := componentSpec(` ObjEnum: + spec := openapitest.ComponentSpec(` ObjEnum: enum: - {a: 1} - {b: 2} @@ -1541,7 +1542,7 @@ func TestEnum_NonScalarAndMidListMismatch(t *testing.T) { // — `Base ∧ (A | B)` written as `(Base ∧ A) | (Base ∧ B)`. func TestOneOf_CoDeclaredCompositionDistributes(t *testing.T) { t.Parallel() - spec := componentSpec(` Base: {type: object, properties: {id: {type: string}}} + spec := openapitest.ComponentSpec(` Base: {type: object, properties: {id: {type: string}}} A: {type: object, properties: {a: {type: string}}} B: {type: object, properties: {b: {type: string}}} Combo: @@ -1551,7 +1552,7 @@ func TestOneOf_CoDeclaredCompositionDistributes(t *testing.T) { - {$ref: '#/components/schemas/B'} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Combo").(*ir.Union) require.True(t, ok, "the union is the schema's value, not a preserved sibling") @@ -1567,7 +1568,7 @@ func TestOneOf_CoDeclaredCompositionDistributes(t *testing.T) { assert.Equal(t, componentID(branch), v.Mixins[0].Target) assert.Equal(t, branch, u.Variants[i].Name.Hint) } - assert.Equal(t, 1, countDiagsAt(diags, diag.CompositionLowering, ir.SeverityInfo), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.CompositionLowering, ir.SeverityInfo), "the reshaping is reported once; got %+v", diags) } @@ -1576,7 +1577,7 @@ func TestOneOf_CoDeclaredCompositionDistributes(t *testing.T) { // anyOf stays non-exclusive through distribution. func TestAnyOf_CoDeclaredPropertiesDistributeAsBase(t *testing.T) { t.Parallel() - spec := componentSpec(` A: {type: object, properties: {a: {type: string}}} + spec := openapitest.ComponentSpec(` A: {type: object, properties: {a: {type: string}}} B: {type: object, properties: {b: {type: string}}} Combo: type: object @@ -1588,7 +1589,7 @@ func TestAnyOf_CoDeclaredPropertiesDistributeAsBase(t *testing.T) { - {$ref: '#/components/schemas/B'} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Combo").(*ir.Union) require.True(t, ok) @@ -1620,9 +1621,9 @@ func comboRefStealSpec(branch int, outsiderFirst bool) string { oneOf: [{$ref: '#/components/schemas/A'}, {$ref: '#/components/schemas/B'}] ` if outsiderFirst { - return componentSpec(outsider + combo) + return openapitest.ComponentSpec(outsider + combo) } - return componentSpec(combo + outsider) + return openapitest.ComponentSpec(combo + outsider) } // TestOneOf_CoDeclaredVariantNotStolenByRefToBranch is the regression for the @@ -1645,7 +1646,7 @@ func TestOneOf_CoDeclaredVariantNotStolenByRefToBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, comboRefStealSpec(tc.branch, tc.outsiderFirst)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Combo").(*ir.Union) require.True(t, ok) @@ -1691,9 +1692,9 @@ func comboDiscriminatedSpec(baseFirst bool) string { oneOf: [{$ref: '#/components/schemas/A'}, {$ref: '#/components/schemas/B'}] ` if baseFirst { - return componentSpec(base + rest) + return openapitest.ComponentSpec(base + rest) } - return componentSpec(rest + base) + return openapitest.ComponentSpec(rest + base) } // TestOneOf_CoDeclaredDistributionIsOrderIndependent states the property the @@ -1724,9 +1725,9 @@ func TestOneOf_CoDeclaredDistributionIsOrderIndependent(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() first, diags := parseFull(t, tc.spec(true)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) last, diags := parseFull(t, tc.spec(false)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, cmp.Diff(first, last, orderInvariantIR()...), "declaring the permuted component before or after the union must not change the IR") @@ -1749,9 +1750,9 @@ func branchAliasSpec(kind string, hostFirst bool) string { rest := " Base: {type: object, properties: {a: {type: string}}}\n" + " Outside: {$ref: '#/components/schemas/Host/" + kind + "/0'}\n" if hostFirst { - return componentSpec(host + rest) + return openapitest.ComponentSpec(host + rest) } - return componentSpec(rest + host) + return openapitest.ComponentSpec(rest + host) } // TestComposition_BranchAliasIsOrderIndependent pins the hint on the node a @@ -1768,9 +1769,9 @@ func TestComposition_BranchAliasIsOrderIndependent(t *testing.T) { t.Run(kind, func(t *testing.T) { t.Parallel() first, diags := parseFull(t, branchAliasSpec(kind, true)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) last, diags := parseFull(t, branchAliasSpec(kind, false)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) branch := ir.TypeID("t/anon/components/schemas/Host/" + kind + "/0") require.Contains(t, first.Types, branch, "the branch position owns a node") @@ -1788,7 +1789,7 @@ func TestComposition_BranchAliasIsOrderIndependent(t *testing.T) { func TestOneOf_CoDeclaredVariantCarriesDiscriminatorValue(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, comboDiscriminatedSpec(true)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Combo").(*ir.Union) require.True(t, ok) @@ -1807,7 +1808,7 @@ func TestOneOf_CoDeclaredVariantCarriesDiscriminatorValue(t *testing.T) { // must not stamp its own branch name on the shared node. func TestOneOf_CoDeclaredAdditionalPropsHintNamesTheBody(t *testing.T) { t.Parallel() - spec := componentSpec(` A: {type: object, properties: {a: {type: string}}} + spec := openapitest.ComponentSpec(` A: {type: object, properties: {a: {type: string}}} B: {type: object, properties: {b: {type: string}}} Combo: type: object @@ -1816,7 +1817,7 @@ func TestOneOf_CoDeclaredAdditionalPropsHintNamesTheBody(t *testing.T) { oneOf: [{$ref: '#/components/schemas/A'}, {$ref: '#/components/schemas/B'}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u := typeByName(doc, "Combo").(*ir.Union) v, ok := doc.Types[u.Variants[0].Type.Target].(*ir.Model) @@ -1832,7 +1833,7 @@ func TestOneOf_CoDeclaredAdditionalPropsHintNamesTheBody(t *testing.T) { // source declared. fillAllOf classifies an allOf entry the same way. func TestOneOf_CoDeclaredNonModelBranchIsCarriedAsWritten(t *testing.T) { t.Parallel() - spec := componentSpec(` Scalar: {type: string, minLength: 2} + spec := openapitest.ComponentSpec(` Scalar: {type: string, minLength: 2} Choice: {enum: [a, b]} Combo: type: object @@ -1840,7 +1841,7 @@ func TestOneOf_CoDeclaredNonModelBranchIsCarriedAsWritten(t *testing.T) { oneOf: [{$ref: '#/components/schemas/Scalar'}, {$ref: '#/components/schemas/Choice'}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "Combo").(*ir.Union) require.True(t, ok) @@ -1862,7 +1863,7 @@ func TestOneOf_CoDeclaredNonModelBranchIsCarriedAsWritten(t *testing.T) { // nothing, and each still reports the broken reference. func TestOneOf_CoDeclaredUnresolvableBranchIsNotDistributed(t *testing.T) { t.Parallel() - spec := componentSpec(` A: {type: object, properties: {a: {type: string}}} + spec := openapitest.ComponentSpec(` A: {type: object, properties: {a: {type: string}}} Undeclared: type: object properties: {kind: {type: string}} @@ -1889,11 +1890,11 @@ func TestOneOf_CoDeclaredUnresolvableBranchIsNotDistributed(t *testing.T) { require.True(t, ok, "%s keeps every branch verbatim", name) assert.Equal(t, ir.ReasonDegradedLowering, entry.Reason, "%s", name) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/"+name), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/"+name), "names no referent this compilation resolves", name) // One error per offending branch, at the branch itself. assert.Contains(t, - diagMessageAt(t, diags, diag.UnresolvedRef, ir.SeverityError, "/components/schemas/"+name+"/oneOf/0"), + openapitest.DiagMessageAt(t, diags, diag.UnresolvedRef, ir.SeverityError, "/components/schemas/"+name+"/oneOf/0"), "resolves to nothing this document declares", name) } for _, d := range diags { @@ -1910,7 +1911,7 @@ func TestOneOf_CoDeclaredUnresolvableBranchIsNotDistributed(t *testing.T) { // telling four otherwise identical diagnostics apart. func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) { t.Parallel() - spec := componentSpec(` A: {type: object, properties: {a: {type: string}}} + spec := openapitest.ComponentSpec(` A: {type: object, properties: {a: {type: string}}} NotAModel: const: fixed oneOf: [{$ref: '#/components/schemas/A'}] @@ -1933,7 +1934,7 @@ func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) { oneOf: [{$ref: '#/components/schemas/A'}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for name, reason := range map[string]string{ "NotAModel": "the body is not a model, so it carries no composition to distribute into", @@ -1946,7 +1947,7 @@ func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) { require.True(t, ok, "%s keeps its union verbatim", name) assert.Equal(t, ir.ReasonDegradedLowering, entry.Reason, "%s", name) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/"+name), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/"+name), reason, name) } both := typeByName(doc, "BothCombinators").(*ir.Model) @@ -1955,7 +1956,7 @@ func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) { nullBranch := typeByName(doc, "NullBranch").(*ir.Model) assert.Contains(t, string(nullBranch.Unmodeled["openapi:oneOf"].Value), `"null"`, "a null branch is written inline, so it blocks distribution rather than lifting to Nullable") - assert.Equal(t, 5, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.Equal(t, 5, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "each declined shape is reported once; got %+v", diags) } @@ -1967,11 +1968,11 @@ func TestOneOf_CoDeclaredNotDistributedReasons(t *testing.T) { // union and the other is kept beside it. func TestUnionCombinators_PassedOverBranchSetIsKept(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` S: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` S: oneOf: [{type: string}, {type: integer}] anyOf: [{type: number}, {type: boolean}] `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "S").(*ir.Union) require.True(t, ok, "the elected branch set still becomes the union") @@ -1981,7 +1982,7 @@ func TestUnionCombinators_PassedOverBranchSetIsKept(t *testing.T) { assert.Equal(t, ir.ReasonDegradedLowering, entry.Reason) assert.JSONEq(t, `[{"type":"number"},{"type":"boolean"}]`, string(entry.Value)) assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), "lowered as its oneOf, with anyOf kept verbatim under Unmodeled") } @@ -1993,11 +1994,11 @@ func TestUnionCombinators_PassedOverBranchSetIsKept(t *testing.T) { // the first place, so it stays a Union and keeps the loser on it. func TestUnionCombinators_NullBranchDoesNotCollapsePastTheAnyOf(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` S: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` S: oneOf: [{type: string}, {type: "null"}] anyOf: [{type: number}, {type: boolean}] `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "S").(*ir.Union) require.True(t, ok, "the collapse is declined; got %v", typeByName(doc, "S")) @@ -2013,7 +2014,7 @@ func TestUnionCombinators_NullBranchDoesNotCollapsePastTheAnyOf(t *testing.T) { // at the keyword, and nothing claims the branch set survived. func TestUnionCombinators_UnpreservableIsNotAnnounced(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` S: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` S: oneOf: [{type: string}, {type: integer}] anyOf: [{type: number, x-t: `+unpreservableValue+`}] `)) @@ -2021,7 +2022,7 @@ func TestUnionCombinators_UnpreservableIsNotAnnounced(t *testing.T) { u, ok := typeByName(doc, "S").(*ir.Union) require.True(t, ok) assert.NotContains(t, u.Unmodeled, "openapi:anyOf", "the conversion failed, so nothing was kept") - assert.True(t, hasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") + assert.True(t, openapitest.HasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") assert.Empty(t, preservationClaims(diags), "nothing was written under Unmodeled, so nothing may announce that it was") } @@ -2048,10 +2049,10 @@ func TestUnionCombinators_KeepingIsOrderIndependent(t *testing.T) { anyOf: [{type: number}, {type: boolean}] ` outsider := " Outsider: {$ref: '#/components/schemas/S/oneOf/0'}\n" - first, diags := parseFull(t, componentSpec(outsider+host)) - requireNoErrorDiags(t, diags) - last, diags := parseFull(t, componentSpec(host+outsider)) - requireNoErrorDiags(t, diags) + first, diags := parseFull(t, openapitest.ComponentSpec(outsider+host)) + openapitest.RequireNoErrorDiags(t, diags) + last, diags := parseFull(t, openapitest.ComponentSpec(host+outsider)) + openapitest.RequireNoErrorDiags(t, diags) for _, doc := range []*ir.Document{first, last} { u, ok := typeByName(doc, "S").(*ir.Union) diff --git a/compilers/openapi/internal/schema/helpers_internal_test.go b/compilers/openapi/internal/schema/helpers_internal_test.go index 8cbc6c17..52d614b6 100644 --- a/compilers/openapi/internal/schema/helpers_internal_test.go +++ b/compilers/openapi/internal/schema/helpers_internal_test.go @@ -3,18 +3,16 @@ package schema import ( "testing" - oas3 "github.com/speakeasy-api/openapi/jsonschema/oas3" soa "github.com/speakeasy-api/openapi/openapi" - "github.com/speakeasy-api/openapi/sequencedmap" "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" - "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/compile" "github.com/dexpace/morphic/compilers/openapi/internal/annotation" "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/load" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/ir" ) @@ -28,23 +26,6 @@ import ( // exercises the interning-lookup paths rather than the named-component path. const deepPointer = "/components/schemas/Obj/properties/inner" -// sourceOf wraps a spec string as a compilers.Source. -func sourceOf(src string) compilers.Source { - return compilers.Source{Path: "spec.yaml", Data: []byte(src)} -} - -// docDeclaring builds a document declaring the named component schemas, with no -// parser and no fixture — the shape a test wants when what it needs from the -// document is only which components it declares. -func docDeclaring(names ...string) *soa.OpenAPI { - elems := make([]*sequencedmap.Element[string, *oas3.JSONSchema[oas3.Referenceable]], 0, len(names)) - for _, n := range names { - elems = append(elems, sequencedmap.NewElem(n, - oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{}))) - } - return &soa.OpenAPI{Components: &soa.Components{Schemas: sequencedmap.New(elems...)}} -} - // nestedAnchor builds a mapping chain of the given depth with a $dynamicAnchor // at the bottom. It is built rather than parsed because nesting this deep in // source text is indentation arithmetic, and duplicate keys at one level would @@ -52,10 +33,10 @@ func docDeclaring(names ...string) *soa.OpenAPI { func nestedAnchor(depth int) *yaml.Node { n := &yaml.Node{ Kind: yaml.MappingNode, - Content: []*yaml.Node{strNode("$dynamicAnchor"), strNode("toodeep")}, + Content: []*yaml.Node{openapitest.StrNode("$dynamicAnchor"), openapitest.StrNode("toodeep")}, } for range depth + 1 { - n = &yaml.Node{Kind: yaml.MappingNode, Content: []*yaml.Node{strNode("k"), n}} + n = &yaml.Node{Kind: yaml.MappingNode, Content: []*yaml.Node{openapitest.StrNode("k"), n}} } return n } @@ -66,7 +47,7 @@ func nestedAnchor(depth int) *yaml.Node { // mapping it stands for. func useValue(t *testing.T, src string) *yaml.Node { t.Helper() - node := annotation.RawChildNode(yamlNode(t, src), "use") + node := annotation.RawChildNode(openapitest.YAMLNode(t, src), "use") require.NotNil(t, node, `src writes no "use" key`) return node } @@ -82,19 +63,27 @@ type lowerer struct { anchors AnchorIndex } +// lowererOver is the only place the fixture's fields are initialised. Both +// entry points below build on it, so a field added to lowerer cannot reach one +// of them and miss the other. +func lowererOver(ctx lowering.Ctx) *lowerer { + types := compile.NewTypes(0) + return &lowerer{ + ctx: ctx, + out: &ir.Document{Types: types.Registry()}, + types: types, + } +} + // loweredFor loads src and returns the fixture over it with nothing lowered yet, // plus the load diagnostics, so a test can drive one entry point at a time. func loweredFor(t *testing.T, src string) (*lowerer, []ir.Diagnostic) { t.Helper() - loadedDoc, diags, err := load.Load(t.Context(), 0, sourceOf(src), load.Options{}) + loadedDoc, diags, err := load.Load(t.Context(), 0, openapitest.SourceOf(src), load.Options{}) require.NoError(t, err) require.NotNil(t, loadedDoc, "load returned no document: %+v", diags) - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, loadedDoc.Doc, loadedDoc.Source, lowering.GroupByTags, overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - }, diags + return lowererOver(lowering.New(0, loadedDoc.Doc, loadedDoc.Source, + lowering.GroupByTags, overlay.Origin{})), diags } // lowerSpec loads src and lowers its component schemas, returning the document @@ -109,55 +98,7 @@ func lowerSpec(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { // newRawLowerer builds a fixture over a hand-constructed document, bypassing the // parser so nil slice/map entries (which the parser panics on) can be exercised. func newRawLowerer(doc *soa.OpenAPI) *lowerer { - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - } -} - -// requireNoErrorDiags fails the test if any diagnostic has error severity, -// reporting the first offending diagnostic. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - -// componentSpec wraps a components/schemas block in a minimal 3.1 document. -// -// The version is inlined where package schema_test's copy takes it as a -// parameter, because nothing on this side varies it. The two produce the same -// document for 3.1.0, which is what any fixture compared across the two halves -// depends on. -func componentSpec(schemas string) string { - return "openapi: 3.1.0\n" + - "info: {title: T, version: \"1\"}\n" + - "paths: {}\n" + - "components:\n schemas:\n" + schemas -} - -// emptyEitherSchema is a JSONSchema whose either-value has neither a Left schema -// nor a Right bool set: IsSchema() is true (IsLeft defaults true) yet GetSchema() -// is nil. The parser never produces this, so it drives the nil-schema guards. -func emptyEitherSchema() *oas3.JSONSchema[oas3.Referenceable] { - return oas3.NewJSONSchemaFromSchema[oas3.Referenceable](nil) -} - -// yamlNode parses a YAML snippet and returns its root value node (the -// document node's single content child), matching what schema fields expose. -func yamlNode(t *testing.T, src string) *yaml.Node { - t.Helper() - var doc yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) - require.Len(t, doc.Content, 1, "expected a single document node") - return doc.Content[0] -} - -// strNode builds a bare string-scalar yaml.Node. -func strNode(val string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: val} + return lowererOver(lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{})) } // assertInternalInvariant requires diags to report a broken internal invariant. diff --git a/compilers/openapi/internal/schema/helpers_test.go b/compilers/openapi/internal/schema/helpers_test.go index 970699ed..7737b372 100644 --- a/compilers/openapi/internal/schema/helpers_test.go +++ b/compilers/openapi/internal/schema/helpers_test.go @@ -1,13 +1,10 @@ package schema_test import ( - "context" "testing" - oas3 "github.com/speakeasy-api/openapi/jsonschema/oas3" soa "github.com/speakeasy-api/openapi/openapi" "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v3" "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/compile" @@ -15,6 +12,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/load" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/compilers/openapi/internal/schema" "github.com/dexpace/morphic/ir" @@ -28,19 +26,15 @@ import ( // package either way: the package under test is the instrumented one, whichever // side of the boundary the caller sits on. -// sourceOf wraps a spec string as a compilers.Source. -func sourceOf(src string) compilers.Source { - return compilers.Source{Path: "spec.yaml", Data: []byte(src)} -} - // parseFull runs the whole public compiler pipeline over src. It reaches back // through openapi deliberately: a schema position under a parameter or a request // body is only hoisted once the operation walk has resolved its way down to it, -// and no amount of driving this package alone produces that. +// and no amount of driving this package alone produces that. That reach is also +// why openapitest cannot hold it — see that package's doc comment. func parseFull(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { t.Helper() - doc, diags, err := openapi.New().Compile(context.Background(), []compilers.Source{sourceOf(src)}, - compilers.Options{}) + doc, diags, err := openapi.New().Compile(t.Context(), + []compilers.Source{openapitest.SourceOf(src)}, compilers.Options{}) require.NoError(t, err) require.NotNil(t, doc) return doc, diags @@ -58,19 +52,27 @@ type lowerer struct { anchors schema.AnchorIndex } +// lowererOver is the only place the fixture's fields are initialised. Both +// entry points below build on it, so a field added to lowerer cannot reach one +// of them and miss the other. +func lowererOver(ctx lowering.Ctx) *lowerer { + types := compile.NewTypes(0) + return &lowerer{ + ctx: ctx, + out: &ir.Document{Types: types.Registry()}, + types: types, + } +} + // loweredFor loads src and returns the fixture over it with nothing lowered yet, // plus the load diagnostics, so a test can drive one entry point at a time. func loweredFor(t *testing.T, src string) (*lowerer, []ir.Diagnostic) { t.Helper() - loadedDoc, diags, err := load.Load(t.Context(), 0, sourceOf(src), load.Options{}) + loadedDoc, diags, err := load.Load(t.Context(), 0, openapitest.SourceOf(src), load.Options{}) require.NoError(t, err) require.NotNil(t, loadedDoc, "load returned no document: %+v", diags) - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, loadedDoc.Doc, loadedDoc.Source, lowering.GroupByTags, overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - }, diags + return lowererOver(lowering.New(0, loadedDoc.Doc, loadedDoc.Source, + lowering.GroupByTags, overlay.Origin{})), diags } // lowerSpec loads src and lowers its component schemas, returning the document @@ -85,45 +87,13 @@ func lowerSpec(t *testing.T, src string) (*ir.Document, []ir.Diagnostic) { // newRawLowerer builds a fixture over a hand-constructed document, bypassing the // parser so nil slice/map entries (which the parser panics on) can be exercised. func newRawLowerer(doc *soa.OpenAPI) *lowerer { - types := compile.NewTypes(0) - return &lowerer{ - ctx: lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{}), - out: &ir.Document{Types: types.Registry()}, - types: types, - } -} - -// requireNoErrorDiags fails the test if any diagnostic has error severity, -// reporting the first offending diagnostic. -func requireNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { - t.Helper() - d, ok := ir.FirstError(diags) - require.False(t, ok, "unexpected error diagnostic: %+v", d) -} - -// componentSpec wraps a components/schemas block in a minimal 3.1 document. -func componentSpec(schemas string) string { - return componentSpecVer("3.1.0", schemas) -} - -// componentSpecVer wraps a components/schemas block in a minimal document of the -// given OpenAPI version. -func componentSpecVer(version, schemas string) string { - return "openapi: " + version + "\n" + - "info: {title: T, version: \"1\"}\n" + - "paths: {}\n" + - "components:\n schemas:\n" + schemas -} - -// pathsSpec wraps a paths block in a minimal 3.1 document with no components. -func pathsSpec(paths string) string { - return "openapi: 3.1.0\n" + - "info: {title: T, version: \"1\"}\n" + - "paths:\n" + paths + return lowererOver(lowering.New(0, doc, ir.SourceInfo{}, "", overlay.Origin{})) } // componentID is the stable TypeID of a components-named schema, or of a -// sub-schema beneath one ("Holder/properties/inner"). +// sub-schema beneath one ("Holder/properties/inner"). It stays a per-package +// copy for the reason openapitest's doc comment gives: a spelled-out ID belongs +// in a test file, where internal/archtest's ID-grammar sweep permits it. func componentID(name string) ir.TypeID { return ir.TypeID("t/openapi/components/schemas/" + name) } @@ -143,100 +113,3 @@ func conflictDiags(diags []ir.Diagnostic) []ir.Diagnostic { } return out } - -// emptyEitherSchema is a JSONSchema whose either-value has neither a Left schema -// nor a Right bool set: IsSchema() is true (IsLeft defaults true) yet GetSchema() -// is nil. The parser never produces this, so it drives the nil-schema guards. -func emptyEitherSchema() *oas3.JSONSchema[oas3.Referenceable] { - return oas3.NewJSONSchemaFromSchema[oas3.Referenceable](nil) -} - -// yamlNode parses a YAML snippet and returns its root value node (the -// document node's single content child), matching what schema fields expose. -func yamlNode(t *testing.T, src string) *yaml.Node { - t.Helper() - var doc yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) - require.Len(t, doc.Content, 1, "expected a single document node") - return doc.Content[0] -} - -// strNode builds a bare string-scalar yaml.Node. -func strNode(val string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: val} -} - -// hasDiag reports whether diags contains a diagnostic with the exact code, at -// any severity. It is the existential half of the vocabulary: use it where a -// test only needs to know a diagnostic fired, not how many or at what severity. -func hasDiag(diags []ir.Diagnostic, code string) bool { - for _, d := range diags { - if d.Code == code { - return true - } - } - return false -} - -// hasDiagAt reports whether diags contains a diagnostic with the exact code at -// the exact severity. -func hasDiagAt(diags []ir.Diagnostic, code string, sev ir.Severity) bool { - return countDiagsAt(diags, code, sev) > 0 -} - -// firstDegradedWarning returns the first diag.DegradedConstruct warning in -// diags, and whether one was found — the pointer/message inspection counterpart -// to hasDiagAt/countDiagsAt. -func firstDegradedWarning(diags []ir.Diagnostic) (ir.Diagnostic, bool) { - for _, d := range diags { - if d.Code == diag.DegradedConstruct && d.Severity == ir.SeverityWarning { - return d, true - } - } - return ir.Diagnostic{}, false -} - -// countDiagsAt counts the diagnostics in diags matching code and sev exactly. -// code is an exact match with no wildcard: countDiagsAt(diags, "", -// ir.SeverityError) matches only diagnostics whose code is literally empty — it -// is not a way to spell "every error," and reads dangerously like one, so -// callers who want that must filter on severity alone instead. -func countDiagsAt(diags []ir.Diagnostic, code string, sev ir.Severity) int { - var n int - for _, d := range diags { - if d.Code == code && d.Severity == sev { - n++ - } - } - return n -} - -// diagMessageAt returns the message of the single diagnostic matching code, -// severity and provenance pointer. Tests that only compare a diagnostic's code -// cannot tell two lowerings apart when both report the same code with different -// reasons, so the reason itself needs an assertable handle. -func diagMessageAt(t *testing.T, diags []ir.Diagnostic, code string, sev ir.Severity, pointer string) string { - t.Helper() - var found []string - for _, d := range diags { - if d.Code == code && d.Severity == sev && d.Provenance.Pointer == pointer { - found = append(found, d.Message) - } - } - require.Len(t, found, 1, "want exactly one %v %q at %q, got %+v", sev, code, pointer, diags) - return found[0] -} - -// indexBy builds a lookup keyed by key(item). -func indexBy[T any, K comparable](items []T, key func(T) K) map[K]T { - out := make(map[K]T, len(items)) - for _, item := range items { - out[key(item)] = item - } - return out -} - -// propsByWire indexes a model's properties by wire name. -func propsByWire(props []ir.Property) map[string]ir.Property { - return indexBy(props, func(p ir.Property) string { return p.WireName }) -} diff --git a/compilers/openapi/internal/schema/schema_internal_test.go b/compilers/openapi/internal/schema/schema_internal_test.go index e1b560a6..1d1dbb98 100644 --- a/compilers/openapi/internal/schema/schema_internal_test.go +++ b/compilers/openapi/internal/schema/schema_internal_test.go @@ -17,6 +17,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -31,7 +32,7 @@ func TestLower_DepthCapExceeded(t *testing.T) { indent += " " } b.WriteString(indent + "type: string\n") - doc, diags := lowerSpec(t, componentSpec(b.String())) + doc, diags := lowerSpec(t, openapitest.ComponentSpec(b.String())) require.NotNil(t, doc) var sawCap bool for _, d := range diags { @@ -44,7 +45,7 @@ func TestLower_DepthCapExceeded(t *testing.T) { func TestIsNullSchema_EmptyEitherFalse(t *testing.T) { t.Parallel() - assert.False(t, isNullSchema(emptyEitherSchema()), "empty either is not a null schema") + assert.False(t, isNullSchema(openapitest.EmptyEitherSchema()), "empty either is not a null schema") } func TestPreserveUnionSiblings_MissingNode(t *testing.T) { @@ -117,7 +118,7 @@ func TestResolveSchemaRef_ReusesInternedSubSchema(t *testing.T) { l := newRawLowerer(&soa.OpenAPI{}) l.types.Intern(deepPointer, "t/anon/prev", func() ir.TypeDef { return &ir.Any{} }) - id, ok, diags := resolveSchemaRef(l.ctx, l.types, &l.anchors, TopLevelDepth, emptyEitherSchema(), "#"+deepPointer) + id, ok, diags := resolveSchemaRef(l.ctx, l.types, &l.anchors, TopLevelDepth, openapitest.EmptyEitherSchema(), "#"+deepPointer) require.True(t, ok, "a $ref to an already-hoisted sub-schema reuses its ID") assert.Equal(t, ir.TypeID("t/anon/prev"), id) assert.Empty(t, diags, "reusing an interned node reports nothing") @@ -228,32 +229,32 @@ func TestDynamicAnchors_WalksEveryNodeShape(t *testing.T) { {"a nil node yields nothing", nil, map[string][]string{}}, { "a bare scalar declares no anchor", - yamlNode(t, `just-a-string`), + openapitest.YAMLNode(t, `just-a-string`), map[string][]string{}, }, { "a sequence indexes its elements by ordinal", - yamlNode(t, "- {$dynamicAnchor: first}\n- {other: 1}\n- {$dynamicAnchor: third}\n"), + openapitest.YAMLNode(t, "- {$dynamicAnchor: first}\n- {other: 1}\n- {$dynamicAnchor: third}\n"), map[string][]string{"first": {"/0"}, "third": {"/2"}}, }, { "a sequence element standing in for a mapping is followed", - yamlNode(t, "- &a {$dynamicAnchor: first}\n- *a\n"), + openapitest.YAMLNode(t, "- &a {$dynamicAnchor: first}\n- *a\n"), map[string][]string{"first": {"/0", "/1"}}, }, { "a non-string key cannot name a keyword and is skipped", - yamlNode(t, "? [a, b]\n: {$dynamicAnchor: buried}\n$dynamicAnchor: reached\n"), + openapitest.YAMLNode(t, "? [a, b]\n: {$dynamicAnchor: buried}\n$dynamicAnchor: reached\n"), map[string][]string{"reached": {""}}, }, { "an empty anchor name is not indexed", - yamlNode(t, `{$dynamicAnchor: ""}`), + openapitest.YAMLNode(t, `{$dynamicAnchor: ""}`), map[string][]string{}, }, { "a non-scalar anchor value is not indexed", - yamlNode(t, `{$dynamicAnchor: [a]}`), + openapitest.YAMLNode(t, `{$dynamicAnchor: [a]}`), map[string][]string{}, }, } @@ -298,7 +299,7 @@ func TestDynamicAnchors_CountsWhatAnAliasBringsIn(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, complete := dynamicAnchors(yamlNode(t, tc.source)) + got, complete := dynamicAnchors(openapitest.YAMLNode(t, tc.source)) assert.True(t, complete) assert.Equal(t, tc.want, got["tail"]) }) @@ -343,7 +344,7 @@ func TestDynamicAnchors_StopsAtTheDepthCap(t *testing.T) { // many more paths than the tree has nodes; the budget is what caps the total. func TestAnchorWalk_StopsAtTheNodeBudget(t *testing.T) { t.Parallel() - source := yamlNode(t, "a: {$dynamicAnchor: first}\nb: {$dynamicAnchor: second}\n") + source := openapitest.YAMLNode(t, "a: {$dynamicAnchor: first}\nb: {$dynamicAnchor: second}\n") w := newAnchorWalk(2) // the root mapping and its first value, and no more w.walk(source, "", 0) @@ -361,8 +362,8 @@ func TestDynamicAnchorIndex_ReportsATruncatedWalk(t *testing.T) { // The walk descends into every key, so an extension at the document root // nests the tree past the cap without the schema lowering ever seeing it. deep := strings.Repeat("{a: ", maxDynamicAnchorDepth) + "1" + strings.Repeat("}", maxDynamicAnchorDepth) - l, diags := loweredFor(t, componentSpec(" A: {type: string}\n")+"x-deep: "+deep+"\n") - requireNoErrorDiags(t, diags) + l, diags := loweredFor(t, openapitest.ComponentSpec(" A: {type: string}\n")+"x-deep: "+deep+"\n") + openapitest.RequireNoErrorDiags(t, diags) _, got := l.anchors.sites(l.ctx, "absent") require.NotNil(t, l.anchors.byName, "the index is built even when partial") @@ -459,11 +460,11 @@ func TestRefNullable_AnUnresolvedRefIsNotNullable(t *testing.T) { // the wrong answer visible: it is a name like any other here. func TestComponentSchemaAt_OnlyATopLevelComponentPointerHasABody(t *testing.T) { t.Parallel() - l, diags := loweredFor(t, componentSpec( + l, diags := loweredFor(t, openapitest.ComponentSpec( " Outer:\n type: object\n title: outer\n"+ " properties: {inner: {type: string, title: inner}}\n"+ " \"\": {type: string, title: empty}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) tests := []struct { name, pointer, wantTitle string @@ -542,8 +543,8 @@ func TestDynamicHop_HopsOnlyWhenExactlyOneAnchorSiteIsNamed(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - l, diags := loweredFor(t, componentSpec(tc.schemas)) - requireNoErrorDiags(t, diags) + l, diags := loweredFor(t, openapitest.ComponentSpec(tc.schemas)) + openapitest.RequireNoErrorDiags(t, diags) if tc.anchor != "" { sites, siteDiags := l.anchors.sites(l.ctx, tc.anchor) require.Len(t, sites, tc.wantSites, "the fixture must set up the case it is named for") diff --git a/compilers/openapi/internal/schema/schema_test.go b/compilers/openapi/internal/schema/schema_test.go index 4a9be7c1..bc15552f 100644 --- a/compilers/openapi/internal/schema/schema_test.go +++ b/compilers/openapi/internal/schema/schema_test.go @@ -16,6 +16,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/annotation" "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/overlay" "github.com/dexpace/morphic/compilers/openapi/internal/schema" "github.com/dexpace/morphic/ir" @@ -46,7 +47,7 @@ func TestSchemaRef_NullableNormalization(t *testing.T) { " properties:\n" + " p: " + tc.schema + "\n" doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) model, ok := doc.Types[componentID("S")].(*ir.Model) require.True(t, ok) require.Len(t, model.Properties, 1) @@ -60,14 +61,14 @@ func TestLower_NamedScalarComponentResolves(t *testing.T) { t.Parallel() // A named component whose body is a plain scalar must register a resolvable // node at its own component pointer, so a $ref to it never dangles. - spec := componentSpec(` MyId: {type: string, format: uuid} + spec := openapitest.ComponentSpec(` MyId: {type: string, format: uuid} Holder: type: object properties: id: {$ref: "#/components/schemas/MyId"} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) scalar, ok := doc.Types[componentID("MyId")].(*ir.Scalar) require.True(t, ok, "named scalar component registers a Scalar at its own ID") @@ -91,7 +92,7 @@ func TestLower_ConstraintOnlyUnionIsValidationOnly(t *testing.T) { // logic, not shape — dependentRequired's sibling — so the structural body // survives and the union is preserved under ReasonValidationOnly, the reason // a validation emitter selects on (ir-design §4.7). - spec := componentSpec(` Thing: + spec := openapitest.ComponentSpec(` Thing: type: object additionalProperties: false required: [common] @@ -102,7 +103,7 @@ func TestLower_ConstraintOnlyUnionIsValidationOnly(t *testing.T) { - {required: [b]} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Thing")].(*ir.Model) require.True(t, ok, "structural body lowers to a Model, not a bare Union") @@ -116,7 +117,7 @@ func TestLower_ConstraintOnlyUnionIsValidationOnly(t *testing.T) { assert.Equal(t, ir.ReasonValidationOnly, raw.Reason, "constraint-only branches narrow the body without reshaping it (ir-design §4.7)") assert.Equal(t, "/components/schemas/Thing/oneOf", raw.Provenance.Pointer) - assert.Equal(t, 1, countDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo), "the union is reported with §4.7's keyword family; got %+v", diags) } @@ -128,7 +129,7 @@ func TestLower_BooleanUnionBranchDeclaresNoShape(t *testing.T) { // as a constraint-only branch does, and takes the same validation-only // lowering. The branch carries no schema object at all, which is what // separates it from a branch that declares nothing structural. - spec := componentSpec(` Flag: + spec := openapitest.ComponentSpec(` Flag: type: object required: [common] properties: @@ -138,7 +139,7 @@ func TestLower_BooleanUnionBranchDeclaresNoShape(t *testing.T) { - false `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Flag")].(*ir.Model) require.True(t, ok, "the structural body survives as a Model") @@ -155,7 +156,7 @@ func TestLower_BooleanUnionBranchDeclaresNoShape(t *testing.T) { func TestLower_AllOfWithOneOfKeepsBoth(t *testing.T) { t.Parallel() // allOf co-declared with oneOf must not drop the allOf composition. - spec := componentSpec(` Base: + spec := openapitest.ComponentSpec(` Base: type: object properties: id: {type: string} @@ -167,7 +168,7 @@ func TestLower_AllOfWithOneOfKeepsBoth(t *testing.T) { - {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("Combo")].(*ir.Model) require.True(t, ok, "allOf composition survives (Model), oneOf preserved raw") require.NotNil(t, m.Base, "the allOf $ref becomes Base") @@ -178,13 +179,13 @@ func TestLower_AllOfWithOneOfKeepsBoth(t *testing.T) { func TestLower_RecursiveSchemaTerminates(t *testing.T) { t.Parallel() - spec := componentSpec(` Node: + spec := openapitest.ComponentSpec(` Node: type: object properties: next: {$ref: "#/components/schemas/Node"} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) node, ok := doc.Types[componentID("Node")].(*ir.Model) require.True(t, ok) require.Equal(t, ir.TypeRef{Target: "t/openapi/components/schemas/Node"}, node.Properties[0].Type) @@ -192,7 +193,7 @@ func TestLower_RecursiveSchemaTerminates(t *testing.T) { func TestLower_InlineSchemaHoistedOnce(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: tags: @@ -203,7 +204,7 @@ func TestLower_InlineSchemaHoistedOnce(t *testing.T) { name: {type: string} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) itemsID := ir.TypeID("t/anon/components/schemas/S/properties/tags/items") item, ok := doc.Types[itemsID].(*ir.Model) require.True(t, ok, "items object should be hoisted as a model") @@ -214,7 +215,7 @@ func TestLower_InlineSchemaHoistedOnce(t *testing.T) { func TestSchemaRef_BooleanAndUntypedShapes(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: anything: true @@ -223,9 +224,9 @@ func TestSchemaRef_BooleanAndUntypedShapes(t *testing.T) { withprops: {properties: {x: {type: string}}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "S").(*ir.Model) - byWire := propsByWire(m.Properties) + byWire := openapitest.PropsByWire(m.Properties) assert.Equal(t, ir.TypeID("t/prim/any"), byWire["anything"].Type.Target) // `false` schema lowered to a closed empty model. nothing := doc.Types[byWire["nothing"].Type.Target] @@ -235,18 +236,18 @@ func TestSchemaRef_BooleanAndUntypedShapes(t *testing.T) { assert.Equal(t, ir.TypeID("t/prim/any"), byWire["untyped"].Type.Target) assert.Equal(t, ir.KindModel, doc.Types[byWire["withprops"].Type.Target].Kind()) - assert.True(t, hasDiagAt(diags, diag.FalseSchema, ir.SeverityInfo), "false schema info diagnostic") + assert.True(t, openapitest.HasDiagAt(diags, diag.FalseSchema, ir.SeverityInfo), "false schema info diagnostic") } func TestLower_MultiTypeUnion(t *testing.T) { t.Parallel() - spec := componentSpec(` MT: + spec := openapitest.ComponentSpec(` MT: type: [object, array, string] properties: {x: {type: string}} items: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "MT").(*ir.Union) require.True(t, ok) require.Len(t, u.Variants, 3) @@ -255,7 +256,7 @@ func TestLower_MultiTypeUnion(t *testing.T) { func TestScalar_UnknownFormatPerBaseType(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: i: {type: integer, format: weird} @@ -264,7 +265,7 @@ func TestScalar_UnknownFormatPerBaseType(t *testing.T) { s: {type: string, format: weird} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "S").(*ir.Model) bases := map[string]ir.PrimKind{} for _, p := range m.Properties { @@ -282,13 +283,13 @@ func TestScalar_UnknownFormatPerBaseType(t *testing.T) { func TestLower_TupleWithTrailingItems(t *testing.T) { t.Parallel() - spec := componentSpec(` Tup: + spec := openapitest.ComponentSpec(` Tup: type: array prefixItems: [{type: string}, {type: integer}] items: {type: boolean} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) tup, ok := typeByName(doc, "Tup").(*ir.Tuple) require.True(t, ok) require.Len(t, tup.Elems, 2) @@ -315,7 +316,7 @@ func hasDegradedDiag(diags []ir.Diagnostic, want string) bool { func TestLower_ListConstraints(t *testing.T) { t.Parallel() - spec := componentSpec(` L: + spec := openapitest.ComponentSpec(` L: type: array items: {type: string} minItems: 1 @@ -323,7 +324,7 @@ func TestLower_ListConstraints(t *testing.T) { uniqueItems: true `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) l, ok := typeByName(doc, "L").(*ir.List) require.True(t, ok) require.NotNil(t, l.Constraints) @@ -335,8 +336,8 @@ func TestLower_ListConstraints(t *testing.T) { func TestLower_ListWithoutItems(t *testing.T) { t.Parallel() // No `items` → schema.Ref(nil) → element is `any`. - doc, diags := lowerSpec(t, componentSpec(" L: {type: array}\n")) - requireNoErrorDiags(t, diags) + doc, diags := lowerSpec(t, openapitest.ComponentSpec(" L: {type: array}\n")) + openapitest.RequireNoErrorDiags(t, diags) l, ok := typeByName(doc, "L").(*ir.List) require.True(t, ok) assert.Equal(t, ir.TypeID("t/prim/any"), l.Elem.Target) @@ -344,7 +345,7 @@ func TestLower_ListWithoutItems(t *testing.T) { func TestLower_ValidationOnlyKeywords(t *testing.T) { t.Parallel() - spec := componentSpec(` V: + spec := openapitest.ComponentSpec(` V: type: object properties: {a: {type: string}} if: {required: [a]} @@ -358,7 +359,7 @@ func TestLower_ValidationOnlyKeywords(t *testing.T) { unevaluatedItems: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "V").(*ir.Model) // An entry the source writes as one keyword is located at that keyword; the // three that synthesize several into one object fall back to the schema, @@ -375,7 +376,7 @@ func TestLower_ValidationOnlyKeywords(t *testing.T) { require.True(t, ok, "keyword %s preserved", key) assert.Equal(t, want, entry.Provenance.Pointer, "entry provenance for %s", key) } - assert.GreaterOrEqual(t, countDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo), 5) + assert.GreaterOrEqual(t, openapitest.CountDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo), 5) } // TestLower_PropertyNamesPreserved pins the whole §4.7 contract for @@ -383,13 +384,13 @@ func TestLower_ValidationOnlyKeywords(t *testing.T) { // emitter selects on, and the one info diagnostic naming the keyword (#117). func TestLower_PropertyNamesPreserved(t *testing.T) { t.Parallel() - spec := componentSpec(` Codes: + spec := openapitest.ComponentSpec(` Codes: type: object propertyNames: {type: string, pattern: "^[a-z]+$"} additionalProperties: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "Codes").(*ir.Model) require.True(t, ok) @@ -397,7 +398,7 @@ func TestLower_PropertyNamesPreserved(t *testing.T) { require.True(t, ok, "propertyNames kept verbatim; got %v", m.Unmodeled) assert.JSONEq(t, `{"type":"string","pattern":"^[a-z]+$"}`, string(entry.Value)) assert.Equal(t, ir.ReasonValidationOnly, entry.Reason) - assert.Equal(t, 1, countDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo)) + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.ValidationOnlyKeyword, ir.SeverityInfo)) // The keyword constrains keys only: the map's value lowering is untouched. require.NotNil(t, m.AdditionalProps) @@ -407,7 +408,7 @@ func TestLower_PropertyNamesPreserved(t *testing.T) { func TestLower_PropertyDetailRichSchema(t *testing.T) { t.Parallel() - spec := componentSpec(` D: + spec := openapitest.ComponentSpec(` D: type: object externalDocs: {url: 'https://x', description: more} properties: @@ -421,7 +422,7 @@ func TestLower_PropertyDetailRichSchema(t *testing.T) { doc, diags := lowerSpec(t, spec) m := typeByName(doc, "D").(*ir.Model) assert.NotEmpty(t, m.Docs.ExternalDocs) - byWire := propsByWire(m.Properties) + byWire := openapitest.PropsByWire(m.Properties) require.NotNil(t, byWire["withXml"].XML) assert.Equal(t, "attribute", byWire["withXml"].XML.NodeType) assert.Equal(t, "urn:x", byWire["withXml"].XML.Namespace) @@ -439,7 +440,7 @@ func TestLower_PropertyDetailRichSchema(t *testing.T) { func TestLower_RefTargetDescriptionFallback(t *testing.T) { t.Parallel() - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: ref: {$ref: '#/components/schemas/Target'} @@ -448,26 +449,26 @@ func TestLower_RefTargetDescriptionFallback(t *testing.T) { description: target-desc `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "Owner").(*ir.Model) assert.Equal(t, "target-desc", m.Properties[0].Docs.Description) } func TestLower_UnresolvedRefDiagnostics(t *testing.T) { t.Parallel() - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: ghost: {$ref: '#/components/schemas/Ghost'} `) doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.True(t, hasDiag(diags, diag.UnresolvedRef), "unresolved ref diagnostic emitted") + assert.True(t, openapitest.HasDiag(diags, diag.UnresolvedRef), "unresolved ref diagnostic emitted") } func TestLower_UnionWithStructuralSiblingVariants(t *testing.T) { t.Parallel() - spec := componentSpec(` A: + spec := openapitest.ComponentSpec(` A: type: object properties: {x: {type: string}} required: [x] @@ -484,7 +485,7 @@ func TestLower_UnionWithStructuralSiblingVariants(t *testing.T) { oneOf: [{$ref: '#/components/schemas/A'}, {type: integer}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, name := range []string{"A", "B", "C", "D"} { td := typeByName(doc, name) require.NotNil(t, td, "type %s present", name) @@ -497,7 +498,7 @@ func TestLower_UnionWithStructuralSiblingVariants(t *testing.T) { func TestModel_FourOptionalityStates(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object required: [reqPlain, reqNull] properties: @@ -507,11 +508,11 @@ func TestModel_FourOptionalityStates(t *testing.T) { optNull: {type: [string, "null"]} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("S")].(*ir.Model) require.True(t, ok) require.Len(t, m.Properties, 4) - byName := propsByWire(m.Properties) + byName := openapitest.PropsByWire(m.Properties) assert.True(t, byName["reqPlain"].Required) assert.False(t, byName["reqPlain"].Type.Nullable) assert.True(t, byName["reqNull"].Required) @@ -524,7 +525,7 @@ func TestModel_FourOptionalityStates(t *testing.T) { func TestModel_ValidationOnlyKeywordPreserved(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: {a: {type: string}} not: {required: [b]} @@ -551,13 +552,13 @@ func TestModel_ValidationOnlyKeywordPreserved(t *testing.T) { func TestModel_DefaultBigLiteral(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: n: {type: integer, default: 9007199254740993} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) require.NotNil(t, m.Properties[0].Default) assert.Equal(t, ir.ValueNumber, m.Properties[0].Default.Kind) @@ -569,13 +570,13 @@ func TestFillPropertyDetail_UnconvertibleExampleDiagnosed(t *testing.T) { // A custom tag is structurally unconvertible; the example must be skipped // (an example is an annotation, not a structural hole) but never silently — // the conversion error was previously discarded on the floor. - spec := componentSpec(" S:\n type: object\n properties:\n n:\n type: string\n example: !foo bar\n") + spec := openapitest.ComponentSpec(" S:\n type: object\n properties:\n n:\n type: string\n example: !foo bar\n") doc, diags := lowerSpec(t, spec) m, ok := doc.Types[componentID("S")].(*ir.Model) require.True(t, ok) assert.Empty(t, m.Properties[0].Examples, "the unconvertible example is skipped, not appended") - require.Equal(t, 1, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) - d, ok := firstDegradedWarning(diags) + require.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning)) + d, ok := openapitest.FirstDegradedWarning(diags) require.True(t, ok) assert.Equal(t, "/components/schemas/S/properties/n/example", d.Provenance.Pointer) assert.Contains(t, d.Message, "example:") @@ -583,16 +584,16 @@ func TestFillPropertyDetail_UnconvertibleExampleDiagnosed(t *testing.T) { func TestModel_ReadOnlyWriteOnlyVisibility(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: r: {type: string, readOnly: true} w: {type: string, writeOnly: true} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) - byName := propsByWire(m.Properties) + byName := openapitest.PropsByWire(m.Properties) assert.Equal(t, ir.Visibility{Only: []ir.Lifecycle{ir.LifecycleRead, ir.LifecycleDelete, ir.LifecycleQuery}}, byName["r"].Visibility) assert.Equal(t, ir.Visibility{Only: []ir.Lifecycle{ir.LifecycleCreate, ir.LifecycleUpdate}}, byName["w"].Visibility) } @@ -604,61 +605,61 @@ func TestModel_ReadOnlyWriteOnlyVisibility(t *testing.T) { // two allOf branches already intersected to None and warned (GitHub #276). func TestModel_ReadOnlyAndWriteOnlyTogetherAreVisibleNowhere(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: both: {type: string, readOnly: true, writeOnly: true} r: {type: string, readOnly: true} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) - byName := propsByWire(m.Properties) + byName := openapitest.PropsByWire(m.Properties) assert.Equal(t, ir.Visibility{None: true}, byName["both"].Visibility) assert.NotEqual(t, byName["r"].Visibility, byName["both"].Visibility, "declaring both flags must not lower exactly as declaring readOnly alone does") - msg := diagMessageAt(t, diags, diag.DisjointVisibility, ir.SeverityWarning, + msg := openapitest.DiagMessageAt(t, diags, diag.DisjointVisibility, ir.SeverityWarning, "/components/schemas/S/properties/both") assert.Contains(t, msg, "writeOnly", "the report names the keyword that was being dropped") - assert.Equal(t, 1, countDiagsAt(diags, diag.DisjointVisibility, ir.SeverityWarning), + assert.Equal(t, 1, openapitest.CountDiagsAt(diags, diag.DisjointVisibility, ir.SeverityWarning), "the property declaring one flag is not reported") } func TestModel_PasswordFormatSecret(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: pw: {type: string, format: password} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.True(t, m.Properties[0].Secret) } func TestModel_AdditionalPropertiesFalseClosed(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: {a: {type: string}} additionalProperties: false `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.Equal(t, ir.AdditionalClosed, m.Additional) } func TestModel_AdditionalPropertiesSchema(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object additionalProperties: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) require.NotNil(t, m.AdditionalProps) assert.Equal(t, ir.TypeID("t/prim/integer"), m.AdditionalProps.Value.Target) @@ -666,14 +667,14 @@ func TestModel_AdditionalPropertiesSchema(t *testing.T) { func TestModel_PatternPropertiesOrder(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object patternProperties: "^x-": {type: string} "^y-": {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) require.NotNil(t, m.AdditionalProps) require.Len(t, m.AdditionalProps.Patterns, 2) @@ -683,26 +684,26 @@ func TestModel_PatternPropertiesOrder(t *testing.T) { func TestModel_UnevaluatedPropertiesClosedAfterComposition(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: {a: {type: string}} unevaluatedProperties: false `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.Equal(t, ir.AdditionalClosedAfterComposition, m.Additional) } func TestModel_SchemaExtensionPreserved(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object x-rate-limit: 100 properties: {a: {type: string}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) raw, ok := m.Unmodeled["openapi:x-rate-limit"] require.True(t, ok) @@ -714,14 +715,14 @@ func TestModel_SchemaExtensionPreserved(t *testing.T) { func TestModel_TitleDescriptionDocs(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object title: "My Title" description: "My Desc" properties: {a: {type: string}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.Equal(t, "My Title", m.Docs.Summary) assert.Equal(t, "My Desc", m.Docs.Description) @@ -729,26 +730,26 @@ func TestModel_TitleDescriptionDocs(t *testing.T) { func TestModel_PropertyDeprecation(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: old: {type: string, deprecated: true} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.NotNil(t, m.Properties[0].Deprecation) } func TestModel_PropertyXML(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: p: {type: string, xml: {name: n, attribute: true}} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) require.NotNil(t, m.Properties[0].XML) assert.Equal(t, "n", m.Properties[0].XML.Name) @@ -757,7 +758,7 @@ func TestModel_PropertyXML(t *testing.T) { func TestModel_RefSiblingDescriptionWins(t *testing.T) { t.Parallel() - spec := componentSpec(` Target: {type: string, description: "target desc"} + spec := openapitest.ComponentSpec(` Target: {type: string, description: "target desc"} S: type: object properties: @@ -766,7 +767,7 @@ func TestModel_RefSiblingDescriptionWins(t *testing.T) { description: "sibling desc" `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := doc.Types[componentID("S")].(*ir.Model) assert.Equal(t, "sibling desc", m.Properties[0].Docs.Description) } @@ -774,7 +775,7 @@ func TestModel_RefSiblingDescriptionWins(t *testing.T) { func TestSchemaRef_EmptyEitherIsAny(t *testing.T) { t.Parallel() l := newRawLowerer(&soa.OpenAPI{}) - ref, diags := schema.Ref(l.ctx, l.types, &l.anchors, schema.TopLevelDepth, emptyEitherSchema(), "/p", "h") + ref, diags := schema.Ref(l.ctx, l.types, &l.anchors, schema.TopLevelDepth, openapitest.EmptyEitherSchema(), "/p", "h") assert.Equal(t, ir.TypeID("t/prim/any"), ref.Target) assert.Empty(t, diags, "an empty either lowers to any without complaint") } @@ -803,7 +804,7 @@ components: in: query schema: {type: string, example: sub-example} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) td, ok := doc.Types["t/anon/components/parameters/P/schema"] require.True(t, ok, "the referenced sub-schema is hoisted at its own pointer") require.Len(t, td.Common().Examples, 1) @@ -832,7 +833,7 @@ components: $ref: '#/components/schemas/Base' example: alias-level `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) alias, ok := doc.Types["t/openapi/components/schemas/Alias"] require.True(t, ok) require.Len(t, alias.Common().Examples, 1) @@ -855,7 +856,7 @@ func TestSiteSchema_BodylessPositions(t *testing.T) { func TestSchema_Ref30NullableSiblings(t *testing.T) { t.Parallel() - spec := componentSpecVer("3.0.3", ` Owner: + spec := openapitest.ComponentSpecVer("3.0.3", ` Owner: type: object properties: p: {$ref: '#/components/schemas/Target', nullable: true} @@ -1162,9 +1163,9 @@ func TestSchema_RefNullableAcrossSpellings(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - spec := componentSpecVer(tc.version, tc.schemas) + spec := openapitest.ComponentSpecVer(tc.version, tc.schemas) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "Owner").(*ir.Model) require.Len(t, m.Properties, 1) assert.Equal(t, tc.wantNullable, m.Properties[0].Type.Nullable, tc.msg) @@ -1189,7 +1190,7 @@ oneOf: [{type: string}, {type: "null"}]` pad := strings.Repeat(" ", n) return pad + strings.ReplaceAll(body, "\n", "\n"+pad) + "\n" } - spec := componentSpec(" Target:\n" + indent(6) + + spec := openapitest.ComponentSpec(" Target:\n" + indent(6) + ` Owner: type: object properties: @@ -1197,9 +1198,9 @@ oneOf: [{type: string}, {type: "null"}]` inline: ` + indent(10)) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "Owner").(*ir.Model) - props := propsByWire(m.Properties) + props := openapitest.PropsByWire(m.Properties) require.Len(t, props, 2) assert.Equal(t, props["inline"].Type.Nullable, props["viaRef"].Type.Nullable, @@ -1212,7 +1213,7 @@ oneOf: [{type: string}, {type: "null"}]` // $ref used as a list element, not just a model property. func TestSchema_RefNullableAtNonPropertyPosition(t *testing.T) { t.Parallel() - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: p: @@ -1222,7 +1223,7 @@ func TestSchema_RefNullableAtNonPropertyPosition(t *testing.T) { oneOf: [{type: string}, {type: integer}, {type: "null"}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) list, ok := doc.Types["t/anon/components/schemas/Owner/properties/p"].(*ir.List) require.True(t, ok) assert.True(t, list.Elem.Nullable, @@ -1232,7 +1233,7 @@ func TestSchema_RefNullableAtNonPropertyPosition(t *testing.T) { func TestSchema_UnionSiblingsAdditionalAndRequired(t *testing.T) { t.Parallel() - spec := componentSpec(` A: + spec := openapitest.ComponentSpec(` A: additionalProperties: {type: string} oneOf: [{type: string}, {type: integer}] B: @@ -1240,7 +1241,7 @@ func TestSchema_UnionSiblingsAdditionalAndRequired(t *testing.T) { oneOf: [{type: string}, {type: integer}] `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, name := range []string{"A", "B"} { _, ok := typeByName(doc, name).Common().Unmodeled["openapi:oneOf"] assert.True(t, ok, "%s preserves its union", name) @@ -1249,28 +1250,28 @@ func TestSchema_UnionSiblingsAdditionalAndRequired(t *testing.T) { func TestSchema_RefTargetReadOnlyVisibility(t *testing.T) { t.Parallel() - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: p: {$ref: '#/components/schemas/RO'} RO: {type: string, readOnly: true} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m := typeByName(doc, "Owner").(*ir.Model) assert.NotEmpty(t, m.Properties[0].Visibility.Only, "readOnly from the ref target applies") } func TestSchema_UnserializableExtension(t *testing.T) { t.Parallel() - spec := componentSpec(` S: + spec := openapitest.ComponentSpec(` S: type: object properties: {a: {type: string}} x-bad: {1: intkey} `) doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "unserializable extension warns") + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityWarning), "unserializable extension warns") m := typeByName(doc, "S").(*ir.Model) _, hasBad := m.Unmodeled["openapi:x-bad"] assert.False(t, hasBad, "unserializable extension is dropped, not stored") @@ -1278,14 +1279,14 @@ func TestSchema_UnserializableExtension(t *testing.T) { func TestSchema_EmptyFragmentRef(t *testing.T) { t.Parallel() - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: p: {$ref: '#'} `) doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.True(t, hasDiag(diags, diag.UnresolvedRef), "the '#' ref form is unresolved") + assert.True(t, openapitest.HasDiag(diags, diag.UnresolvedRef), "the '#' ref form is unresolved") } func TestSchema_EmptyStringRefMirrorBranches(t *testing.T) { @@ -1293,7 +1294,7 @@ func TestSchema_EmptyStringRefMirrorBranches(t *testing.T) { // An empty-string $ref has IsReference()==false (the ref value is "") yet a // non-nil oas3 Ref pointer, exercising that mirror path in schema.Ref and the // branchHint fallback. - spec := componentSpec(` Owner: + spec := openapitest.ComponentSpec(` Owner: type: object properties: p: {$ref: ''} @@ -1304,7 +1305,7 @@ func TestSchema_EmptyStringRefMirrorBranches(t *testing.T) { `) doc, diags := lowerSpec(t, spec) require.NotNil(t, doc) - assert.GreaterOrEqual(t, countDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError), 2, "both empty refs are unresolved") + assert.GreaterOrEqual(t, openapitest.CountDiagsAt(diags, diag.UnresolvedRef, ir.SeverityError), 2, "both empty refs are unresolved") u, ok := typeByName(doc, "U").(*ir.Union) require.True(t, ok) assert.Contains(t, []string{u.Variants[0].Name.Hint, u.Variants[1].Name.Hint}, "variant_0") @@ -1315,7 +1316,7 @@ func TestAllOf_UntypedRedeclarationDoesNotConflict(t *testing.T) { // One branch leaves the field schemaless (the top type), the other types it. // `any` intersects with everything under allOf, so this is a narrowing, not a // contradiction — it must not be reported. - spec := componentSpec(` Anyish: + spec := openapitest.ComponentSpec(` Anyish: allOf: - type: object properties: @@ -1325,7 +1326,7 @@ func TestAllOf_UntypedRedeclarationDoesNotConflict(t *testing.T) { id: {type: integer} `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "Anyish").(*ir.Model) require.True(t, ok, "Anyish should be a model") require.Len(t, m.Properties, 1, "id reconciles to one property") @@ -1337,7 +1338,7 @@ func TestAllOf_EquivalentNumericBoundsDoNotConflict(t *testing.T) { t.Parallel() // The same bound spelled two ways (10 and 10.0) denotes one value, so it must // compare equal by magnitude and stay silent. - spec := componentSpec(` Boundish: + spec := openapitest.ComponentSpec(` Boundish: allOf: - type: object properties: @@ -1347,7 +1348,7 @@ func TestAllOf_EquivalentNumericBoundsDoNotConflict(t *testing.T) { n: {type: number, minimum: 10.0} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "10 and 10.0 are the same numeric bound, not a conflict") } @@ -1356,7 +1357,7 @@ func TestAllOf_DifferingNumericBoundsConflict(t *testing.T) { t.Parallel() // Two branches pin the same lower bound to different magnitudes; the kept // winner is arbitrary source order, so the dropped bound is surfaced. - spec := componentSpec(` Boundish: + spec := openapitest.ComponentSpec(` Boundish: allOf: - type: object properties: @@ -1366,7 +1367,7 @@ func TestAllOf_DifferingNumericBoundsConflict(t *testing.T) { n: {type: integer, minimum: 10} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1, "differing numeric bounds are diagnosed once") assert.Contains(t, conflicts[0].Message, `"n"`) @@ -1375,7 +1376,7 @@ func TestAllOf_DifferingNumericBoundsConflict(t *testing.T) { func TestAllOf_ScalarVersusObjectRedeclarationConflicts(t *testing.T) { t.Parallel() // A scalar in one branch and a structural type in the other cannot both hold. - spec := componentSpec(` Mixed: + spec := openapitest.ComponentSpec(` Mixed: allOf: - type: object properties: @@ -1388,7 +1389,7 @@ func TestAllOf_ScalarVersusObjectRedeclarationConflicts(t *testing.T) { x: {type: string} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1, "a scalar against an object is diagnosed once") assert.Contains(t, conflicts[0].Message, `"f"`) @@ -1400,7 +1401,7 @@ func TestAllOf_DistinctInlineObjectsDoNotConflict(t *testing.T) { // own model at its own pointer, so the targets differ — but two objects of the // same kind are not provably contradictory, and conflict detection never // guesses. - spec := componentSpec(` Objish: + spec := openapitest.ComponentSpec(` Objish: allOf: - type: object properties: @@ -1416,7 +1417,7 @@ func TestAllOf_DistinctInlineObjectsDoNotConflict(t *testing.T) { x: {type: string} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "two distinct inline objects for one field are not a provable conflict") } @@ -1425,7 +1426,7 @@ func TestAllOf_DistinctInlineObjectsDoNotConflict(t *testing.T) { // declare field v with the given flow-style schemas, for exercising per-keyword // redeclaration conflict detection. func allOfConflictSpec(schemaA, schemaB string) string { - return componentSpec( + return openapitest.ComponentSpec( " T:\n" + " allOf:\n" + " - type: object\n" + @@ -1495,7 +1496,7 @@ func TestAllOf_ConstraintAndFormatConflictsDiagnosed(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, diags := lowerSpec(t, allOfConflictSpec(tc.a, tc.b)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1, "%s is diagnosed exactly once", tc.name) assert.Contains(t, conflicts[0].Message, `"v"`, "the diagnostic names the field") @@ -1581,7 +1582,7 @@ func TestAllOf_CompatibleConstraintRedeclarationsStaySilent(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := lowerSpec(t, allOfConflictSpec(tc.a, tc.b)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "%s must not be reported as a conflict", tc.name) if tc.assertMerged == nil { return @@ -1599,7 +1600,7 @@ func TestAllOf_OpaqueScalarVsPrimitiveNoConflict(t *testing.T) { // An opaque scalar (format without a base type) is unknown, not structural, // so it's not provably incompatible with a primitive. The "never guess" // principle means we don't flag this as a conflict. - spec := componentSpec(` OpaqueTest: + spec := openapitest.ComponentSpec(` OpaqueTest: allOf: - type: object properties: @@ -1609,7 +1610,7 @@ func TestAllOf_OpaqueScalarVsPrimitiveNoConflict(t *testing.T) { id: {format: custom} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "opaque scalar vs primitive is not flagged as a conflict") } @@ -1619,7 +1620,7 @@ func TestAllOf_ThreeWayRedeclarationProducesTwoDiagnostics(t *testing.T) { // When three allOf branches declare the same field with different types, // reconciliation runs twice: branch[1] vs branch[0], then branch[2] vs branch[0]. // Each incompatible pair produces one diagnostic, so we expect two total. - spec := componentSpec(` ThreeWay: + spec := openapitest.ComponentSpec(` ThreeWay: allOf: - type: object properties: @@ -1632,7 +1633,7 @@ func TestAllOf_ThreeWayRedeclarationProducesTwoDiagnostics(t *testing.T) { id: {type: boolean} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 2, "three-way redeclaration produces two diagnostics") } @@ -1641,7 +1642,7 @@ func TestAllOf_ThreeWayCompatibleRedeclarationStaysSilent(t *testing.T) { t.Parallel() // When three allOf branches declare the same field with compatible types // (all the same), no conflict is reported. - spec := componentSpec(` ThreeWayCompat: + spec := openapitest.ComponentSpec(` ThreeWayCompat: allOf: - type: object properties: @@ -1654,7 +1655,7 @@ func TestAllOf_ThreeWayCompatibleRedeclarationStaysSilent(t *testing.T) { id: {type: string} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "three-way compatible redeclaration stays silent") } @@ -1678,7 +1679,7 @@ func TestAllOf_SatisfiableNarrowingsStaySilent(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, diags := lowerSpec(t, allOfConflictSpec(tc.a, tc.b)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Empty(t, conflictDiags(diags), "%s must not be reported as a conflict", tc.name) }) } @@ -1693,7 +1694,7 @@ func TestAllOf_EnumVersusIncompatibleTypeStillConflicts(t *testing.T) { // goes through the same aok&&bok path as two plain scalars. _, diags := lowerSpec(t, allOfConflictSpec( "{type: string, enum: [active, inactive]}", "{type: integer}")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1, "enum of strings vs integer is still a provable conflict") assert.Contains(t, conflicts[0].Message, `"v"`, "the diagnostic names the conflicting field") @@ -1705,7 +1706,7 @@ func TestAllOf_TypeConflictMessageNamesBothTypes(t *testing.T) { // that something did: the two conflicting type identities, so the author // can see at a glance what disagreed without cross-referencing the spec. _, diags := lowerSpec(t, allOfConflictSpec("{type: string}", "{type: integer}")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1) assert.Contains(t, conflicts[0].Message, "t/prim/string", "names the first branch's type") @@ -1720,7 +1721,7 @@ func TestAllOf_PropertyAlongsideAllOfConflictMessageIsAccurate(t *testing.T) { // so the message must not claim "allOf branches redeclare" — it must read // correctly for a co-declared sibling property too (mergeProperty folds // both cases the same way; see its doc comment). - spec := componentSpec(` Along: + spec := openapitest.ComponentSpec(` Along: type: object properties: id: {type: string} @@ -1730,7 +1731,7 @@ func TestAllOf_PropertyAlongsideAllOfConflictMessageIsAccurate(t *testing.T) { id: {type: integer} `) _, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) conflicts := conflictDiags(diags) require.Len(t, conflicts, 1, "the co-declared property conflict is diagnosed once") d := conflicts[0] @@ -1755,10 +1756,10 @@ func TestRawFromNode_SeparatesAbsentFromUnconvertible(t *testing.T) { }{ {name: "absent node is neither raw nor error", node: nil}, {name: "undecodable node errors", node: &yaml.Node{Kind: yaml.Kind(99)}, wantErr: true}, - {name: "non-string key does not decode", node: yamlNode(t, "? [a, b]\n: v"), wantErr: true}, - {name: "int key does not marshal", node: yamlNode(t, "1: a\n2: b"), wantErr: true}, - {name: "nan decodes but does not marshal", node: yamlNode(t, "{a: .nan}"), wantErr: true}, - {name: "convertible node yields json", node: yamlNode(t, "{a: 1}"), want: `{"a":1}`}, + {name: "non-string key does not decode", node: openapitest.YAMLNode(t, "? [a, b]\n: v"), wantErr: true}, + {name: "int key does not marshal", node: openapitest.YAMLNode(t, "1: a\n2: b"), wantErr: true}, + {name: "nan decodes but does not marshal", node: openapitest.YAMLNode(t, "{a: .nan}"), wantErr: true}, + {name: "convertible node yields json", node: openapitest.YAMLNode(t, "{a: 1}"), want: `{"a":1}`}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -1786,13 +1787,13 @@ func TestRawPropertyNode_NilSchema(t *testing.T) { func TestSchema_OneOfWithBoolBranch(t *testing.T) { t.Parallel() - spec := componentSpec(` U: + spec := openapitest.ComponentSpec(` U: anyOf: - {type: string} - true `) doc, diags := lowerSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) u, ok := typeByName(doc, "U").(*ir.Union) require.True(t, ok) assert.Len(t, u.Variants, 2, "the boolean branch is a variant, not a null strip") @@ -1816,7 +1817,7 @@ components: $ref: '#/components/schemas/Base' minimum: 5 `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) bounded, ok := doc.Types["t/openapi/components/schemas/Bounded"].(*ir.Scalar) require.True(t, ok, "a component aliasing another schema interns as a Scalar") require.NotNil(t, bounded.Constraints) @@ -1860,7 +1861,7 @@ components: minimum: 7 example: 9 `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) inner, ok := doc.Types["t/anon/components/schemas/Holder/properties/inner"].(*ir.Scalar) require.True(t, ok, "the referenced sub-schema hoists an alias at its own pointer") @@ -1902,7 +1903,7 @@ components: properties: inner: {$ref: '#/components/schemas/Base'} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) inner, ok := doc.Types["t/anon/components/schemas/Holder/properties/inner"].(*ir.Scalar) require.True(t, ok, "a bare $ref sub-schema aliases rather than copying its target") require.NotNil(t, inner.Base) @@ -1912,16 +1913,6 @@ components: assert.True(t, isModel, "the structure lives at the component it was declared at") } -// inlineProbeBody is the body every inline-position case below writes: one -// annotation of each kind attachDeclaredAnnotations reads, one validation-only -// keyword, and one value constraint — all of them position-scoped, so a -// position that lowers this to the shared string primitive loses every one. All -// three documentation keywords are here because a home that keeps only the -// description passes a probe that writes only a description. -const inlineProbeBody = `{type: string, title: SUM, description: DOC, ` + - `externalDocs: {url: 'https://e.example', description: ED}, deprecated: true, ` + - `example: abc, x-vendor: V, xml: {name: X}, not: {const: N}, maxLength: 3}` - // inlinePosition is one schema position with no ir.Property or ir.Parameter to // carry a declaration's annotations, so the declaration must own a node. type inlinePosition struct { @@ -1937,33 +1928,33 @@ type inlinePosition struct { func inlinePositions() []inlinePosition { const prim = ir.TypeID("t/prim/string") return []inlinePosition{ - {"items", func(b string) string { return componentSpec(" A: {type: array, items: " + b + "}\n") }, + {"items", func(b string) string { return openapitest.ComponentSpec(" A: {type: array, items: " + b + "}\n") }, "t/anon/components/schemas/A/items", prim}, {"nested-items", func(b string) string { - return componentSpec(" A: {type: array, items: {type: array, items: " + b + "}}\n") + return openapitest.ComponentSpec(" A: {type: array, items: {type: array, items: " + b + "}}\n") }, "t/anon/components/schemas/A/items/items", prim}, {"additionalProperties", func(b string) string { - return componentSpec(" A: {type: object, additionalProperties: " + b + "}\n") + return openapitest.ComponentSpec(" A: {type: object, additionalProperties: " + b + "}\n") }, "t/anon/components/schemas/A/additionalProperties", prim}, {"prefixItems", func(b string) string { - return componentSpec(" A: {type: array, prefixItems: [" + b + "]}\n") + return openapitest.ComponentSpec(" A: {type: array, prefixItems: [" + b + "]}\n") }, "t/anon/components/schemas/A/prefixItems/0", prim}, {"patternProperties", func(b string) string { - return componentSpec(" A: {type: object, patternProperties: {\"^x\": " + b + "}}\n") + return openapitest.ComponentSpec(" A: {type: object, patternProperties: {\"^x\": " + b + "}}\n") }, "t/anon/components/schemas/A/patternProperties/^x", prim}, {"oneOf-branch", func(b string) string { - return componentSpec(" A: {oneOf: [" + b + ", {type: integer}]}\n") + return openapitest.ComponentSpec(" A: {oneOf: [" + b + ", {type: integer}]}\n") }, "t/anon/components/schemas/A/oneOf/0", prim}, {"anyOf-branch", func(b string) string { - return componentSpec(" A: {anyOf: [" + b + ", {type: integer}]}\n") + return openapitest.ComponentSpec(" A: {anyOf: [" + b + ", {type: integer}]}\n") }, "t/anon/components/schemas/A/anyOf/0", prim}, {"request-media-type", func(b string) string { - return pathsSpec(" /x:\n post:\n operationId: p\n requestBody:\n" + + return openapitest.PathsSpec(" /x:\n post:\n operationId: p\n requestBody:\n" + " content: {application/json: {schema: " + b + "}}\n" + " responses: {\"204\": {description: ok}}\n") }, "t/anon/paths/~1x/post/requestBody/content/application~1json/schema", prim}, {"response-media-type", func(b string) string { - return pathsSpec(" /x:\n get:\n operationId: g\n responses:\n" + + return openapitest.PathsSpec(" /x:\n get:\n operationId: g\n responses:\n" + " \"200\":\n description: ok\n" + " content: {application/json: {schema: " + b + "}}\n") }, "t/anon/paths/~1x/get/responses/200/content/application~1json/schema", prim}, @@ -1980,8 +1971,8 @@ func TestInlinePosition_DeclarationOwnsANode(t *testing.T) { for _, pos := range inlinePositions() { t.Run(pos.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, pos.spec(inlineProbeBody)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, pos.spec(openapitest.InlineProbeBody)) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[pos.id].(*ir.Scalar) require.True(t, ok, "the declaration owns a Scalar at its own pointer; got %v", doc.Types[pos.id]) @@ -1992,13 +1983,13 @@ func TestInlinePosition_DeclarationOwnsANode(t *testing.T) { } } -// assertProbeAnnotationsKept checks every keyword inlineProbeBody writes +// assertProbeAnnotationsKept checks every keyword openapitest.InlineProbeBody writes // survived onto the node the declaration owns. func assertProbeAnnotationsKept(t *testing.T, sc *ir.Scalar) { t.Helper() - assertProbeDocsKept(t, sc.Docs) + openapitest.AssertProbeDocsKept(t, sc.Docs) assert.NotNil(t, sc.Deprecation, "deprecation") - assertProbeExample(t, sc.Examples) + openapitest.AssertProbeExample(t, sc.Examples) if assert.NotNil(t, sc.XML, "xml hints") { assert.Equal(t, "X", sc.XML.Name) } @@ -2017,29 +2008,6 @@ func assertProbeAnnotationsKept(t *testing.T, sc *ir.Scalar) { } } -// assertProbeDocsKept checks all three documentation keywords inlineProbeBody -// writes reached d, wherever the position's home turned out to be. -func assertProbeDocsKept(t *testing.T, d ir.Docs) { - t.Helper() - assert.Equal(t, "SUM", d.Summary, "title") - assert.Equal(t, "DOC", d.Description, "description") - if assert.Len(t, d.ExternalDocs, 1, "externalDocs") { - assert.Equal(t, "https://e.example", d.ExternalDocs[0].URL) - assert.Equal(t, "ED", d.ExternalDocs[0].Description) - } -} - -// assertProbeExample checks the single example inlineProbeBody writes reached -// the home under test with its value intact. -func assertProbeExample(t *testing.T, examples []ir.Example) { - t.Helper() - if !assert.Len(t, examples, 1, "examples") { - return - } - require.NotNil(t, examples[0].Value) - assert.Equal(t, "abc", examples[0].Value.Str) -} - // TestInlinePosition_BareScalarStaysShared is the control that bounds the fix: // a position declaring nothing of its own gains nothing by owning a node, so it // must keep resolving straight to the shared primitive rather than growing an @@ -2050,7 +2018,7 @@ func TestInlinePosition_BareScalarStaysShared(t *testing.T) { t.Run(pos.name, func(t *testing.T) { t.Parallel() doc, diags := parseFull(t, pos.spec("{type: string}")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.NotContains(t, doc.Types, pos.id, "a bare declaration must not hoist a node") assert.Contains(t, doc.Types, pos.target, "it resolves to the shared primitive instead") }) @@ -2063,9 +2031,9 @@ func TestInlinePosition_BareScalarStaysShared(t *testing.T) { // shared node a bare string does. Nullability still lifts onto the refs. func TestInlinePosition_NullStrippedScalarKeepsAnnotations(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {type: array, items: {type: [string, \"null\"], description: DOC}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types["t/anon/components/schemas/A/items"].(*ir.Scalar) require.True(t, ok, "a null-stripped scalar declaration owns a node like any other") @@ -2084,10 +2052,10 @@ func TestInlinePosition_NullStrippedScalarKeepsAnnotations(t *testing.T) { // them itself. func TestInlinePosition_RefSiblingsKeepTheirPosition(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " S: {type: string}\n"+ " A: {type: array, items: {$ref: '#/components/schemas/S', maxLength: 25, description: D}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types["t/anon/components/schemas/A/items"].(*ir.Scalar) require.True(t, ok, "siblings beside a $ref give the position a node of its own") @@ -2108,10 +2076,10 @@ func TestInlinePosition_RefSiblingsKeepTheirPosition(t *testing.T) { // $ref with nothing beside it still points straight at its target. func TestInlinePosition_BareRefStaysDirect(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " S: {type: string}\n"+ " A: {type: array, items: {$ref: '#/components/schemas/S'}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) list, ok := doc.Types[componentID("A")].(*ir.List) require.True(t, ok) @@ -2135,22 +2103,22 @@ type stolenPosition struct { func (p stolenPosition) spec(refFirst bool) string { outsider := " Outsider: {$ref: '#" + strings.TrimPrefix(string(p.id), "t/anon") + "'}\n" if refFirst { - return componentSpec(outsider + p.owner) + return openapitest.ComponentSpec(outsider + p.owner) } - return componentSpec(p.owner + outsider) + return openapitest.ComponentSpec(p.owner + outsider) } func stolenPositions() []stolenPosition { return []stolenPosition{ - {"items", " A: {type: array, items: " + inlineProbeBody + "}\n", + {"items", " A: {type: array, items: " + openapitest.InlineProbeBody + "}\n", "t/anon/components/schemas/A/items"}, - {"additionalProperties", " A: {type: object, additionalProperties: " + inlineProbeBody + "}\n", + {"additionalProperties", " A: {type: object, additionalProperties: " + openapitest.InlineProbeBody + "}\n", "t/anon/components/schemas/A/additionalProperties"}, - {"prefixItems", " A: {type: array, prefixItems: [" + inlineProbeBody + "]}\n", + {"prefixItems", " A: {type: array, prefixItems: [" + openapitest.InlineProbeBody + "]}\n", "t/anon/components/schemas/A/prefixItems/0"}, - {"patternProperties", " A: {type: object, patternProperties: {\"^x\": " + inlineProbeBody + "}}\n", + {"patternProperties", " A: {type: object, patternProperties: {\"^x\": " + openapitest.InlineProbeBody + "}}\n", "t/anon/components/schemas/A/patternProperties/^x"}, - {"oneOf-branch", " A: {oneOf: [" + inlineProbeBody + ", {type: integer}]}\n", + {"oneOf-branch", " A: {oneOf: [" + openapitest.InlineProbeBody + ", {type: integer}]}\n", "t/anon/components/schemas/A/oneOf/0"}, } } @@ -2171,12 +2139,12 @@ func TestInlinePosition_OutsideRefDoesNotMoveTheHome(t *testing.T) { for _, pos := range stolenPositions() { t.Run(pos.name, func(t *testing.T) { t.Parallel() - alone, diags := parseFull(t, componentSpec(pos.owner)) - requireNoErrorDiags(t, diags) + alone, diags := parseFull(t, openapitest.ComponentSpec(pos.owner)) + openapitest.RequireNoErrorDiags(t, diags) refFirst, diags := parseFull(t, pos.spec(true)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) refLast, diags := parseFull(t, pos.spec(false)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, with := range []*ir.Document{refFirst, refLast} { assert.Empty(t, cmp.Diff(alone.Types[componentID("A")], with.Types[componentID("A")]), @@ -2218,24 +2186,24 @@ func orderInvariantIR() []cmp.Option { // second. func TestPropertyAnnotations_KeptWhenAnOutsideRefNamesTheProperty(t *testing.T) { t.Parallel() - owner := " A: {type: object, properties: {p: " + inlineProbeBody + "}}\n" + owner := " A: {type: object, properties: {p: " + openapitest.InlineProbeBody + "}}\n" outsider := " Outsider: {$ref: '#/components/schemas/A/properties/p'}\n" for _, tc := range []struct{ name, spec string }{ - {"reference declared first", componentSpec(outsider + owner)}, - {"reference declared last", componentSpec(owner + outsider)}, + {"reference declared first", openapitest.ComponentSpec(outsider + owner)}, + {"reference declared last", openapitest.ComponentSpec(owner + outsider)}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() doc, diags := parseFull(t, tc.spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := doc.Types[componentID("A")].(*ir.Model) require.True(t, ok) - p, ok := propsByWire(m.Properties)["p"] + p, ok := openapitest.PropsByWire(m.Properties)["p"] require.True(t, ok) assert.Equal(t, ir.TypeID("t/prim/string"), p.Type.Target, "the property's own type is unchanged by the outside reference") - assertProbeDocsKept(t, p.Docs) + openapitest.AssertProbeDocsKept(t, p.Docs) assert.Contains(t, p.Unmodeled, "openapi:x-vendor") assert.Contains(t, p.Unmodeled, "openapi:not") @@ -2252,14 +2220,14 @@ func TestPropertyAnnotations_KeptWhenAnOutsideRefNamesTheProperty(t *testing.T) // copies can never drift apart. func TestPropertyAnnotations_OneHomeWhenSchemaOwnsANode(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A:\n type: object\n properties:\n"+ " p: {type: object, description: DOC, deprecated: true, xml: {name: X}, x-vendor: V}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) owner, ok := doc.Types[componentID("A")].(*ir.Model) require.True(t, ok) - p, ok := propsByWire(owner.Properties)["p"] + p, ok := openapitest.PropsByWire(owner.Properties)["p"] require.True(t, ok) assert.Empty(t, p.Docs.Description, "the node the schema owns is the one home") assert.Nil(t, p.Deprecation) @@ -2281,19 +2249,19 @@ func TestPropertyAnnotations_OneHomeWhenSchemaOwnsANode(t *testing.T) { // property's declaration has ir.Property to land on already. func TestPropertyAnnotations_CarriedWhenSchemaOwnsNoNode(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( - " A:\n type: object\n properties:\n p: "+inlineProbeBody+"\n")) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec( + " A:\n type: object\n properties:\n p: "+openapitest.InlineProbeBody+"\n")) + openapitest.RequireNoErrorDiags(t, diags) owner, ok := doc.Types[componentID("A")].(*ir.Model) require.True(t, ok) - p, ok := propsByWire(owner.Properties)["p"] + p, ok := openapitest.PropsByWire(owner.Properties)["p"] require.True(t, ok) assert.Equal(t, ir.TypeID("t/prim/string"), p.Type.Target, "a property keeps resolving to the shared primitive") assert.NotContains(t, doc.Types, ir.TypeID("t/anon/components/schemas/A/properties/p")) - assertProbeDocsKept(t, p.Docs) + openapitest.AssertProbeDocsKept(t, p.Docs) assert.NotNil(t, p.Deprecation) - assertProbeExample(t, p.Examples) + openapitest.AssertProbeExample(t, p.Examples) require.NotNil(t, p.XML) assert.Equal(t, "X", p.XML.Name) assert.Contains(t, p.Unmodeled, "openapi:x-vendor") @@ -2348,7 +2316,7 @@ func propertyOf(t *testing.T, doc *ir.Document, model, wire string) ir.Property t.Helper() m, ok := doc.Types[componentID(model)].(*ir.Model) require.True(t, ok, "%s lowers to a model", model) - p, ok := propsByWire(m.Properties)[wire] + p, ok := openapitest.PropsByWire(m.Properties)[wire] require.True(t, ok, "%s declares a property %q", model, wire) return p } @@ -2364,9 +2332,9 @@ func TestPropertyDocs_RefTargetReachesTheCarrier(t *testing.T) { for _, kw := range carrierDocKeywords() { t.Run(kw.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(docTarget()+ + doc, diags := parseFull(t, openapitest.ComponentSpec(docTarget()+ " Owner: {type: object, properties: {p: {$ref: '#/components/schemas/Target'}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := propertyOf(t, doc, "Owner", "p") assert.Equal(t, componentID("Target"), p.Type.Target, "a bare $ref needs no alias") @@ -2386,10 +2354,10 @@ func TestPropertyDocs_UseSiteWinsKeywordByKeyword(t *testing.T) { for _, written := range carrierDocKeywords() { t.Run(written.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(docTarget()+ + doc, diags := parseFull(t, openapitest.ComponentSpec(docTarget()+ " Owner: {type: object, properties: {p: {$ref: '#/components/schemas/Target', "+ written.write("SITE")+"}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := propertyOf(t, doc, "Owner", "p") assert.Equal(t, componentID("Target"), p.Type.Target, "a carrier hoists no alias for its siblings") @@ -2456,9 +2424,9 @@ func TestInlinePosition_HoistGateFollowsWhatIsKept(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {type: array, items: {type: string, "+tc.keyword+"}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Contains(t, doc.Types, ir.TypeID("t/anon/components/schemas/A/items"), "%s binds the position it is written at, so the position must own a node", tc.keyword) }) @@ -2478,9 +2446,9 @@ func TestInlinePosition_NothingToHoldHoistsNoNode(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {type: array, items: {"+tc.keyword+"}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.NotContains(t, doc.Types, ir.TypeID("t/anon/components/schemas/A/items")) list, ok := doc.Types[componentID("A")].(*ir.List) require.True(t, ok) @@ -2515,8 +2483,8 @@ func TestInlinePosition_ResidueIsKeptAtEveryHomeOwnNodePosition(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(strings.ReplaceAll(tc.body, "RESIDUE", residue))) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(strings.ReplaceAll(tc.body, "RESIDUE", residue))) + openapitest.RequireNoErrorDiags(t, diags) td, ok := doc.Types[ir.TypeID(tc.at)] require.True(t, ok, "the position owns a node to hold what it wrote") @@ -2552,10 +2520,10 @@ func assertResidueKeptAndAnnounced(t *testing.T, p ir.Unmodeled, diags []ir.Diag // residue would restate what the carrier already holds. func TestPropertyPosition_ResidueStaysOutOfTheTypeNode(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {type: object, properties: {p: {type: array, items: {type: string}, "+ "default: [], readOnly: true}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := propertyOf(t, doc, "A", "p") require.NotNil(t, p.Default, "default lands in the property's own field") @@ -2829,8 +2797,8 @@ func TestVocabulary2020_12_EveryKeywordIsLoweredOrKept(t *testing.T) { // case "differ" for free. func compileVocabIR(t *testing.T, schemas string) string { t.Helper() - doc, diags := parseFull(t, componentSpec(schemas)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(schemas)) + openapitest.RequireNoErrorDiags(t, diags) doc.Diagnostics = nil doc.Sources = nil out, err := json.Marshal(doc) @@ -2883,8 +2851,8 @@ func TestContentVocabulary_LowersToEncoding(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(tc.schemas)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(tc.schemas)) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[tc.at].(*ir.Scalar) require.True(t, ok, "the content vocabulary needs a Scalar of its own at %s", tc.at) @@ -2925,8 +2893,8 @@ func TestContentVocabulary_KeepsTheBoundsWrittenBesideIt(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(tc.schemas)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(tc.schemas)) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[tc.at].(*ir.Scalar) require.True(t, ok, "the content vocabulary hoists a Scalar at %s", tc.at) @@ -2965,8 +2933,8 @@ func TestContentVocabulary_KeptWhereNoEncodingHolds(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(tc.schemas)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(tc.schemas)) + openapitest.RequireNoErrorDiags(t, diags) td, ok := doc.Types[tc.at] require.True(t, ok) @@ -2974,7 +2942,7 @@ func TestContentVocabulary_KeptWhereNoEncodingHolds(t *testing.T) { require.True(t, ok, "%s must be kept verbatim under Unmodeled", tc.key) assert.JSONEq(t, tc.wantJSON, string(entry.Value)) assert.Equal(t, ir.ReasonNoIRHome, entry.Reason) - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) }) } } @@ -2984,16 +2952,16 @@ func TestContentVocabulary_KeptWhereNoEncodingHolds(t *testing.T) { // primitive, where the ir.Property is the only home the keyword has. func TestContentVocabulary_KeptOnACarrierWithNoNode(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {type: object, properties: {p: {contentMediaType: application/json}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := propertyOf(t, doc, "A", "p") assert.Equal(t, ir.TypeID("t/prim/any"), p.Type.Target, "an untyped schema stays schemaless") entry, ok := p.Unmodeled["openapi:contentMediaType"] require.True(t, ok, "the carrier is the only home when the schema hoisted no node") assert.Equal(t, ir.ReasonNoIRHome, entry.Reason) - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) } // TestDynamicRef_ExpandsAgainstTheOneMatchingAnchor pins the resolvable half of @@ -3001,11 +2969,11 @@ func TestContentVocabulary_KeptOnACarrierWithNoNode(t *testing.T) { // the keyword worth having. func TestDynamicRef_ExpandsAgainstTheOneMatchingAnchor(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " Meta: {$dynamicAnchor: meta, type: object, properties: {n: {type: string}}}\n"+ " Uses: {type: object, properties: {m: {$dynamicRef: '#meta'}}}\n"+ " Tree: {$dynamicAnchor: node, type: object, properties: {kids: {type: array, items: {$dynamicRef: '#node'}}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) assert.Equal(t, componentID("Meta"), propertyOf(t, doc, "Uses", "m").Type.Target, "the reference resolves to the anchor's own component, not to the top type") @@ -3025,7 +2993,7 @@ func TestDynamicRef_ExpandsAgainstTheOneMatchingAnchor(t *testing.T) { // Expansion collapses an indirection the source left to evaluation, so it is // announced under its own code rather than sharing the composition one. - assert.Equal(t, 2, countDiagsAt(diags, diag.DynamicRefExpanded, ir.SeverityInfo), + assert.Equal(t, 2, openapitest.CountDiagsAt(diags, diag.DynamicRefExpanded, ir.SeverityInfo), "each expanded reference is announced once") } @@ -3045,8 +3013,8 @@ func TestDynamicRef_FragmentIsPercentDecoded(t *testing.T) { for name, ref := range cases { t.Run(name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(anchor+" A: {$dynamicRef: '"+ref+"'}\n")) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(anchor+" A: {$dynamicRef: '"+ref+"'}\n")) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[componentID("A")].(*ir.Scalar) require.True(t, ok, "the reference position owns a node") @@ -3070,7 +3038,7 @@ func TestDynamicRef_FragmentIsPercentDecoded(t *testing.T) { // The lowering still has to agree with itself about what each side spells. func TestDynamicRef_ReachesAnAnchorSpelledWithAPercent(t *testing.T) { t.Parallel() - doc, _ := parseFull(t, componentSpec( + doc, _ := parseFull(t, openapitest.ComponentSpec( " B: {$dynamicAnchor: 'pct%name', type: string}\n"+ " A: {$dynamicRef: '#pct%25name'}\n")) @@ -3165,8 +3133,8 @@ func TestDynamicRef_IrreducibleIsKeptAndSaysWhy(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(tc.schemas)) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(tc.schemas)) + openapitest.RequireNoErrorDiags(t, diags) at := tc.at if at == "" { @@ -3190,11 +3158,11 @@ func TestDynamicRef_IrreducibleIsKeptAndSaysWhy(t *testing.T) { // irverify checks for aliases. func TestDynamicRef_CycleIsRefusedAtEveryEdge(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {$dynamicAnchor: a, $dynamicRef: '#b'}\n"+ " B: {$dynamicAnchor: b, $dynamicRef: '#a'}\n"+ " Self: {$dynamicAnchor: s, $dynamicRef: '#s'}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) for _, name := range []string{"A", "B", "Self"} { sc, ok := doc.Types[componentID(name)].(*ir.Scalar) @@ -3205,7 +3173,7 @@ func TestDynamicRef_CycleIsRefusedAtEveryEdge(t *testing.T) { assert.Contains(t, sc.Unmodeled, "openapi:$dynamicRef", "%s keeps the reference it could not take", name) } - assert.Equal(t, 0, countDiagsAt(diags, diag.DynamicRefExpanded, ir.SeverityInfo), + assert.Equal(t, 0, openapitest.CountDiagsAt(diags, diag.DynamicRefExpanded, ir.SeverityInfo), "no edge of the cycle is reported as expanded") } @@ -3215,11 +3183,11 @@ func TestDynamicRef_CycleIsRefusedAtEveryEdge(t *testing.T) { // top type rather than a link that loops. func TestDynamicRef_ExpandsIntoACycleItIsNotPartOf(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A: {$dynamicAnchor: a, $dynamicRef: '#b'}\n"+ " B: {$dynamicAnchor: b, $dynamicRef: '#a'}\n"+ " Outside: {$dynamicRef: '#a'}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[componentID("Outside")].(*ir.Scalar) require.True(t, ok) @@ -3244,8 +3212,8 @@ func TestDynamicRef_ChainEndsAtAnAnchorItCannotFollow(t *testing.T) { for name, schemas := range cases { t.Run(name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec(schemas+" A: {$dynamicRef: '#mid'}\n")) - requireNoErrorDiags(t, diags) + doc, diags := parseFull(t, openapitest.ComponentSpec(schemas+" A: {$dynamicRef: '#mid'}\n")) + openapitest.RequireNoErrorDiags(t, diags) sc, ok := doc.Types[componentID("A")].(*ir.Scalar) require.True(t, ok) @@ -3270,14 +3238,14 @@ func TestDialectKeywords_KeptOutOfScope(t *testing.T) { require.ElementsMatch(t, want, annotation.DialectKeywords, "a keyword joining or leaving the exclusion must be decided here too") - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " A:\n"+ " $id: 'urn:example:a'\n"+ " $schema: 'https://json-schema.org/draft/2020-12/schema'\n"+ " $vocabulary: {'https://json-schema.org/draft/2020-12/vocab/core': true}\n"+ " $comment: not for end users\n"+ " type: string\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) td, ok := doc.Types[componentID("A")] require.True(t, ok) @@ -3285,23 +3253,12 @@ func TestDialectKeywords_KeptOutOfScope(t *testing.T) { entry, ok := td.Common().Unmodeled["openapi:"+keyword] require.True(t, ok, "%s must be kept verbatim", keyword) assert.Equal(t, ir.ReasonOutOfScope, entry.Reason) - assertInfoDiagAt(t, diags, entry.Provenance.Pointer) + openapitest.AssertInfoDiagAt(t, diags, entry.Provenance.Pointer) } assert.NotContains(t, td.Common().Unmodeled, "openapi:$comment", "2020-12 §8.3 forbids presenting $comment, so it is dropped rather than kept") } -// assertInfoDiagAt requires one info diagnostic stamped at pointer. -func assertInfoDiagAt(t *testing.T, diags []ir.Diagnostic, pointer string) { - t.Helper() - for _, d := range diags { - if d.Severity == ir.SeverityInfo && d.Provenance.Pointer == pointer { - return - } - } - assert.Fail(t, "nothing announced this", "no info diagnostic at %q; got %+v", pointer, diags) -} - // assertDiagContains requires one diagnostic at pointer whose message carries // substr, so a case asserts the reason it was given and not merely that it was // reported. @@ -3329,7 +3286,7 @@ func TestDynamicRef_NonScalarValueIsKeptNotExpanded(t *testing.T) { for name, schemas := range cases { t.Run(name, func(t *testing.T) { t.Parallel() - doc, diags := parseFull(t, componentSpec( + doc, diags := parseFull(t, openapitest.ComponentSpec( " M1: {$dynamicAnchor: ok, type: string}\n"+schemas)) td, ok := doc.Types[componentID("A")] @@ -3350,7 +3307,7 @@ func TestAppendExample_ConvertsAndAppends(t *testing.T) { c := lowering.New(0, &soa.OpenAPI{}, ir.SourceInfo{}, "", overlay.Origin{}) proto := ir.Example{Name: "n", Summary: "s", Description: "d"} - out, diags := schema.AppendExample(c, nil, proto, strNode("hello"), "/p", "examples", "n") + out, diags := schema.AppendExample(c, nil, proto, openapitest.StrNode("hello"), "/p", "examples", "n") assert.Empty(t, diags, "a convertible node is announced by nothing") require.Len(t, out, 1) @@ -3404,13 +3361,13 @@ func TestStampConstraintDiags_RelocatesEveryDiagnosticToTheReadingPointer(t *tes // the same answer a bare `false` schema gets in its own right. func TestAllOf_FalseBranchClosesTheComposition(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` Never: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` Never: allOf: - false - type: object properties: {id: {type: string}} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "Never").(*ir.Model) require.True(t, ok, "the composition still lowers to a model") @@ -3418,7 +3375,7 @@ func TestAllOf_FalseBranchClosesTheComposition(t *testing.T) { require.Len(t, m.Unmodeled, 1, "the branch is kept verbatim") assert.Equal(t, ir.RawValue("false"), m.Unmodeled["openapi:allOf/0"].Value, "keyed by the branch index, so sibling branches cannot overwrite one another") - assert.True(t, hasDiagAt(diags, diag.FalseSchema, ir.SeverityInfo), "and it is announced") + assert.True(t, openapitest.HasDiagAt(diags, diag.FalseSchema, ir.SeverityInfo), "and it is announced") } // TestUnhomedApplicator_KeptOnAListAndAnnounced pins the arm of the home @@ -3427,18 +3384,18 @@ func TestAllOf_FalseBranchClosesTheComposition(t *testing.T) { // kept verbatim and reported rather than silently dropped. func TestUnhomedApplicator_KeptOnAListAndAnnounced(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` Odd: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` Odd: type: array items: {type: string} properties: {p: {type: string}} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) td := typeByName(doc, "Odd") require.NotNil(t, td, "the array still lowers") assert.Contains(t, td.Common().Unmodeled, "openapi:properties", "a list has no home for properties, so it is kept") - assert.True(t, hasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), + assert.True(t, openapitest.HasDiagAt(diags, diag.DegradedConstruct, ir.SeverityInfo), "and the position says which keywords it could not carry") } @@ -3454,12 +3411,12 @@ const unpreservableValue = ".nan" // read as though the object were an array. func TestUnhomedApplicator_ObjectCarriesNoItemKeyword(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` Odd: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` Odd: type: object properties: {p: {type: string}} items: {type: string} `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) m, ok := typeByName(doc, "Odd").(*ir.Model) require.True(t, ok, "it is still a model") @@ -3474,7 +3431,7 @@ func TestUnhomedApplicator_ObjectCarriesNoItemKeyword(t *testing.T) { // looking in Unmodeled for something that is not there. func TestUnhomedApplicator_UnpreservableKeywordIsNotAnnounced(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` Odd: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` Odd: type: object properties: {p: {type: string}} items: {type: string, x-t: `+unpreservableValue+`} @@ -3483,7 +3440,7 @@ func TestUnhomedApplicator_UnpreservableKeywordIsNotAnnounced(t *testing.T) { m, ok := typeByName(doc, "Odd").(*ir.Model) require.True(t, ok) assert.NotContains(t, m.Unmodeled, "openapi:items", "the conversion failed, so nothing was kept") - assert.True(t, hasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") + assert.True(t, openapitest.HasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") assert.Empty(t, preservationClaims(diags), "nothing was written under Unmodeled, so nothing may announce that it was") } @@ -3494,7 +3451,7 @@ func TestUnhomedApplicator_UnpreservableKeywordIsNotAnnounced(t *testing.T) { // happen either. func TestAllOf_UnpreservableBranchIsNotAnnounced(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` M: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` M: allOf: - {type: string, maxLength: 3, x-t: `+unpreservableValue+`} `)) @@ -3502,7 +3459,7 @@ func TestAllOf_UnpreservableBranchIsNotAnnounced(t *testing.T) { td := typeByName(doc, "M") require.NotNil(t, td) assert.NotContains(t, td.Common().Unmodeled, "openapi:allOf/0", "the branch did not convert") - assert.True(t, hasDiag(diags, diag.UnpreservableConstruct)) + assert.True(t, openapitest.HasDiag(diags, diag.UnpreservableConstruct)) assert.Empty(t, preservationClaims(diags), "no degradation is announced for a branch that was not kept") } @@ -3515,7 +3472,7 @@ func TestAllOf_UnpreservableBranchIsNotAnnounced(t *testing.T) { // preserved construct that a claim could be about. func TestUnionSiblings_UnpreservableIsReportedNotClaimed(t *testing.T) { t.Parallel() - _, diags := lowerSpec(t, componentSpec(` S: + _, diags := lowerSpec(t, openapitest.ComponentSpec(` S: items: {type: string, x-t: `+unpreservableValue+`} oneOf: [{type: string, x-t: `+unpreservableValue+`}, {type: integer}] `)) @@ -3574,14 +3531,14 @@ func TestResidueKeywords_HandsBackACopy(t *testing.T) { // rather than dropped, which is the whole of §4.8 for this keyword. func TestUnhomedApplicator_FormatWithNoTypeIsKept(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(" Odd: {format: date-time}\n")) - requireNoErrorDiags(t, diags) + doc, diags := lowerSpec(t, openapitest.ComponentSpec(" Odd: {format: date-time}\n")) + openapitest.RequireNoErrorDiags(t, diags) td := typeByName(doc, "Odd") require.NotNil(t, td, "the position still lowers to something") assert.Contains(t, td.Common().Unmodeled, "openapi:format", "a format with no type to pair with reaches no field, so it is kept") - assert.True(t, hasDiag(diags, diag.DegradedConstruct), "and the position says so") + assert.True(t, openapitest.HasDiag(diags, diag.DegradedConstruct), "and the position says so") } // TestCoDeclaredFamily_PassedOverKeywordIsKept covers the families lower()'s @@ -3625,9 +3582,9 @@ func TestCoDeclaredFamily_PassedOverKeywordIsKept(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec( + doc, diags := lowerSpec(t, openapitest.ComponentSpec( " Base: {type: object, properties: {id: {type: string}}}\n S:\n"+tc.body)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) td := typeByName(doc, "S") require.NotNil(t, td) @@ -3639,7 +3596,7 @@ func TestCoDeclaredFamily_PassedOverKeywordIsKept(t *testing.T) { assert.Equal(t, "/components/schemas/S/"+tc.skipped, entry.Provenance.Pointer, "routable to where it was written") assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), tc.skipped+" kept verbatim under Unmodeled") }) } @@ -3650,19 +3607,19 @@ func TestCoDeclaredFamily_PassedOverKeywordIsKept(t *testing.T) { // together in one report rather than one of them standing in for the rest. func TestCoDeclaredFamily_EveryPassedOverKeywordIsKept(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` Base: {type: object, properties: {id: {type: string}}} + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` Base: {type: object, properties: {id: {type: string}}} S: const: a enum: [a, b] allOf: [{$ref: '#/components/schemas/Base'}] `)) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) p := typeByName(doc, "S").Common().Unmodeled assert.Contains(t, p, "openapi:enum") assert.Contains(t, p, "openapi:allOf") assert.Contains(t, - diagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), + openapitest.DiagMessageAt(t, diags, diag.DegradedConstruct, ir.SeverityInfo, "/components/schemas/S"), "declares enum and allOf beside its const", "both are named in the one report") } @@ -3673,7 +3630,7 @@ func TestCoDeclaredFamily_EveryPassedOverKeywordIsKept(t *testing.T) { // survived. func TestCoDeclaredFamily_UnpreservableIsNotAnnounced(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec(` S: + doc, diags := lowerSpec(t, openapitest.ComponentSpec(` S: allOf: [{type: object, x-t: `+unpreservableValue+`}] enum: [a, b] `)) @@ -3681,7 +3638,7 @@ func TestCoDeclaredFamily_UnpreservableIsNotAnnounced(t *testing.T) { td := typeByName(doc, "S") require.NotNil(t, td) assert.NotContains(t, td.Common().Unmodeled, "openapi:allOf", "the conversion failed") - assert.True(t, hasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") + assert.True(t, openapitest.HasDiag(diags, diag.UnpreservableConstruct), "the failure itself is reported") assert.Empty(t, preservationClaims(diags), "nothing was written under Unmodeled, so nothing may announce that it was") } diff --git a/compilers/openapi/internal/value/value_internal_test.go b/compilers/openapi/internal/value/value_internal_test.go index a554aea6..401a3f4f 100644 --- a/compilers/openapi/internal/value/value_internal_test.go +++ b/compilers/openapi/internal/value/value_internal_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -26,7 +27,7 @@ func TestValueFromNode_Scalars(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := FromNode(yamlNode(t, tc.src)) + got, err := FromNode(openapitest.YAMLNode(t, tc.src)) require.NoError(t, err) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) @@ -64,7 +65,7 @@ func TestNumericLiteral_YAMLBases(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := NumericLiteral(yamlNode(t, tc.src)) + got, err := NumericLiteral(openapitest.YAMLNode(t, tc.src)) require.NoError(t, err) assert.Equal(t, tc.want, got) }) @@ -89,7 +90,7 @@ func TestNumericLiteral_ExplicitIntBeyondUint64(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := NumericLiteral(yamlNode(t, tc.src)) + got, err := NumericLiteral(openapitest.YAMLNode(t, tc.src)) require.NoError(t, err) assert.Equal(t, tc.want, got) }) @@ -111,7 +112,7 @@ func TestNumericLiteral_UndecodableInt(t *testing.T) { for _, src := range []string{"12abc", "077777777777777777777777", "0x1FFFFFFFFFFFFFFFFFFFF", ""} { t.Run(src, func(t *testing.T) { t.Parallel() - _, err := NumericLiteral(scalarNode("!!int", src)) + _, err := NumericLiteral(openapitest.ScalarNode("!!int", src)) require.Error(t, err) }) } @@ -119,7 +120,7 @@ func TestNumericLiteral_UndecodableInt(t *testing.T) { func TestValueFromNode_ObjectPreservesOrder(t *testing.T) { t.Parallel() - got, err := FromNode(yamlNode(t, "b: 1\na: 2\n")) + got, err := FromNode(openapitest.YAMLNode(t, "b: 1\na: 2\n")) require.NoError(t, err) require.Equal(t, ir.ValueObject, got.Kind) require.Len(t, got.Object, 2) @@ -136,7 +137,7 @@ func TestValueFromNode_NilYieldsNull(t *testing.T) { func TestValueFromNode_AliasFollowed(t *testing.T) { t.Parallel() - target := scalarNode("!!str", "hi") + target := openapitest.ScalarNode("!!str", "hi") alias := &yaml.Node{Kind: yaml.AliasNode, Alias: target} got, err := FromNode(alias) require.NoError(t, err) @@ -147,7 +148,7 @@ func TestValueFromNode_AliasFollowed(t *testing.T) { func TestValueFromNode_Sequence(t *testing.T) { t.Parallel() seq := &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{ - scalarNode("!!int", "1"), scalarNode("!!str", "x"), + openapitest.ScalarNode("!!int", "1"), openapitest.ScalarNode("!!str", "x"), }} got, err := FromNode(seq) require.NoError(t, err) @@ -159,7 +160,7 @@ func TestValueFromNode_Sequence(t *testing.T) { func TestValueFromNode_Binary(t *testing.T) { t.Parallel() - got, err := FromNode(yamlNode(t, "!!binary aGVsbG8=")) + got, err := FromNode(openapitest.YAMLNode(t, "!!binary aGVsbG8=")) require.NoError(t, err) require.Equal(t, ir.ValueBytes, got.Kind) assert.Equal(t, []byte("hello"), got.Bytes) @@ -174,7 +175,7 @@ func TestValueFromNode_Timestamp(t *testing.T) { for _, src := range cases { t.Run(src, func(t *testing.T) { t.Parallel() - node := yamlNode(t, src) + node := openapitest.YAMLNode(t, src) require.Equal(t, "!!timestamp", node.Tag, "precondition: this spelling must resolve to !!timestamp") got, err := FromNode(node) require.NoError(t, err) @@ -190,11 +191,11 @@ func TestValueFromNode_ScalarErrors(t *testing.T) { name string node *yaml.Node }{ - {"bad bool", scalarNode("!!bool", "notabool")}, - {"bad int", scalarNode("!!int", "12abc")}, - {"bad float", scalarNode("!!float", "1.2.3")}, - {"bad binary", scalarNode("!!binary", "@@@not-base64")}, - {"unsupported tag", scalarNode("!custom", "2020-01-01")}, + {"bad bool", openapitest.ScalarNode("!!bool", "notabool")}, + {"bad int", openapitest.ScalarNode("!!int", "12abc")}, + {"bad float", openapitest.ScalarNode("!!float", "1.2.3")}, + {"bad binary", openapitest.ScalarNode("!!binary", "@@@not-base64")}, + {"unsupported tag", openapitest.ScalarNode("!custom", "2020-01-01")}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -209,7 +210,7 @@ func TestValueFromNode_OverflowNumberIsNumber(t *testing.T) { t.Parallel() // A float64-overflow literal resolves to a plain !!str node; it must be // captured as the number it is, canonicalized, not as a string. - got, err := FromNode(scalarNode("!!str", "1.8e308")) + got, err := FromNode(openapitest.ScalarNode("!!str", "1.8e308")) require.NoError(t, err) assert.Equal(t, ir.ValueNumber, got.Kind) assert.Equal(t, ir.BigVal("1.8e308"), got.Num) @@ -218,7 +219,7 @@ func TestValueFromNode_OverflowNumberIsNumber(t *testing.T) { func TestValueFromNode_QuotedNumericStaysString(t *testing.T) { t.Parallel() // A quoted numeric string is not plain, so it stays a string. - node := scalarNode("!!str", "123") + node := openapitest.ScalarNode("!!str", "123") node.Style = yaml.DoubleQuotedStyle got, err := FromNode(node) require.NoError(t, err) @@ -235,7 +236,7 @@ func TestValueFromNode_UnsupportedNodeKind(t *testing.T) { func TestValueFromNode_SequenceChildError(t *testing.T) { t.Parallel() seq := &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{ - scalarNode("!custom", "x"), + openapitest.ScalarNode("!custom", "x"), }} _, err := FromNode(seq) require.Error(t, err) @@ -244,7 +245,7 @@ func TestValueFromNode_SequenceChildError(t *testing.T) { func TestValueFromNode_MappingValueError(t *testing.T) { t.Parallel() m := &yaml.Node{Kind: yaml.MappingNode, Content: []*yaml.Node{ - scalarNode("!!str", "k"), scalarNode("!custom", "x"), + openapitest.ScalarNode("!!str", "k"), openapitest.ScalarNode("!custom", "x"), }} _, err := FromNode(m) require.Error(t, err) @@ -252,7 +253,7 @@ func TestValueFromNode_MappingValueError(t *testing.T) { func TestValueFromNode_DepthCapExceeded(t *testing.T) { t.Parallel() - n := scalarNode("!!int", "1") + n := openapitest.ScalarNode("!!int", "1") for range maxValueDepth + 2 { n = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{n}} } @@ -260,23 +261,6 @@ func TestValueFromNode_DepthCapExceeded(t *testing.T) { require.Error(t, err) } -// yamlNode parses src and returns its single document's root node. It is a copy -// of the compiler package's helper rather than a shared one: a test helper that -// crossed a package boundary would be the first thing to make this package -// depend on its parent, which is the direction the extraction exists to remove. -func yamlNode(t *testing.T, src string) *yaml.Node { - t.Helper() - var doc yaml.Node - require.NoError(t, yaml.Unmarshal([]byte(src), &doc)) - require.Len(t, doc.Content, 1, "expected a single document node") - return doc.Content[0] -} - -// scalarNode builds a bare scalar yaml.Node with the given tag and value. -func scalarNode(tag, val string) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: val} -} - // TestNumericLiteral_NonNumericTagReadsAsDecimal covers the tag this package // does not recognise as numeric. A quoted bound reaches it — YAML resolves // `minimum: "10"` to !!str — and carries no YAML-assigned base, so its text is @@ -301,7 +285,7 @@ func TestNumericLiteral_NonNumericTagReadsAsDecimal(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := NumericLiteral(scalarNode(tc.tag, tc.val)) + got, err := NumericLiteral(openapitest.ScalarNode(tc.tag, tc.val)) require.NoError(t, err) assert.Equal(t, tc.want, got) }) diff --git a/compilers/openapi/load_test.go b/compilers/openapi/load_test.go index 3491330b..cfc08acb 100644 --- a/compilers/openapi/load_test.go +++ b/compilers/openapi/load_test.go @@ -9,6 +9,7 @@ import ( "github.com/dexpace/morphic/compilers" "github.com/dexpace/morphic/compilers/openapi/internal/diag" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" ) // resolverPanicSpec is a document the parser accepts and the resolver faults on: @@ -37,7 +38,7 @@ func TestCompile_ResolverPanicIsADiagnostic(t *testing.T) { // fine on its own, so the finding is still an artifact and must not surface. func TestLoad_RecoverableLiteralSuppressesFindingAmongOtherScalars(t *testing.T) { t.Parallel() - _, diags := parseFull(t, componentSpec(` S: {type: string, default: !custom foo, example: .5}`)) - assert.False(t, hasDiag(diags, diag.Validation+"/"+string(validation.RuleValidationInvalidSyntax)), + _, diags := parseFull(t, openapitest.ComponentSpec(` S: {type: string, default: !custom foo, example: .5}`)) + assert.False(t, openapitest.HasDiag(diags, diag.Validation+"/"+string(validation.RuleValidationInvalidSyntax)), "a finding a recoverable literal explains stays suppressed: %+v", diags) } diff --git a/compilers/openapi/meta_test.go b/compilers/openapi/meta_test.go index 528f6243..e732ec27 100644 --- a/compilers/openapi/meta_test.go +++ b/compilers/openapi/meta_test.go @@ -10,6 +10,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -47,7 +48,7 @@ paths: {} x-bad: {1: intkey} ` doc, diags := parseFull(t, spec) - assert.True(t, countDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, + assert.True(t, openapitest.CountDiagsAt(diags, diag.DegradedConstruct, ir.SeverityWarning) > 0, "an entirely unserializable top-level extension still warns even though Unmodeled ends up empty") assert.Empty(t, doc.Unmodeled, "the unserializable extension is dropped, not stored") } diff --git a/compilers/openapi/openapi_internal_test.go b/compilers/openapi/openapi_internal_test.go index cbaec428..e8e64332 100644 --- a/compilers/openapi/openapi_internal_test.go +++ b/compilers/openapi/openapi_internal_test.go @@ -12,21 +12,22 @@ import ( "github.com/dexpace/morphic/compilers/compile" "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/lowering" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" ) func TestParse_UnsupportedVersion(t *testing.T) { t.Parallel() spec := "openapi: 2.0.0\ninfo: {title: T, version: \"1\"}\npaths: {}\n" - doc, diags, err := New().Compile(context.Background(), []compilers.Source{sourceOf(spec)}, compilers.Options{}) + doc, diags, err := New().Compile(context.Background(), []compilers.Source{openapitest.SourceOf(spec)}, compilers.Options{}) require.NoError(t, err) assert.Nil(t, doc, "unsupported version refuses to lower") - assert.True(t, hasDiag(diags, diag.UnsupportedVersion)) + assert.True(t, openapitest.HasDiag(diags, diag.UnsupportedVersion)) } func TestParse_UnmarshalError(t *testing.T) { t.Parallel() _, _, err := New().Compile(context.Background(), - []compilers.Source{sourceOf("\t\t: : : not valid : yaml\n\x00")}, compilers.Options{}) + []compilers.Source{openapitest.SourceOf("\t\t: : : not valid : yaml\n\x00")}, compilers.Options{}) require.Error(t, err) } diff --git a/compilers/openapi/options_test.go b/compilers/openapi/options_test.go index 0895b078..8ef1222e 100644 --- a/compilers/openapi/options_test.go +++ b/compilers/openapi/options_test.go @@ -8,12 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" ) func TestParse_WrongFormatOptions(t *testing.T) { t.Parallel() _, _, err := New().Compile(context.Background(), - []compilers.Source{sourceOf("openapi: 3.1.0\ninfo: {title: T, version: \"1\"}\npaths: {}\n")}, + []compilers.Source{openapitest.SourceOf("openapi: 3.1.0\ninfo: {title: T, version: \"1\"}\npaths: {}\n")}, compilers.Options{FormatOptions: "not-openapi-options"}) require.Error(t, err, "wrong FormatOptions type is a programmer error") } @@ -21,7 +22,7 @@ func TestParse_WrongFormatOptions(t *testing.T) { func TestParse_ExplicitOptions(t *testing.T) { t.Parallel() spec := "openapi: 3.1.0\ninfo: {title: T, version: \"1\"}\npaths:\n /a/b:\n get: {operationId: ab, responses: {\"200\": {description: ok}}}\n" - doc, _, err := New().Compile(context.Background(), []compilers.Source{sourceOf(spec)}, + doc, _, err := New().Compile(context.Background(), []compilers.Source{openapitest.SourceOf(spec)}, compilers.Options{FormatOptions: Options{Grouping: GroupByPathPrefix}}) require.NoError(t, err) require.NotNil(t, doc) diff --git a/compilers/openapi/resolve_internal_test.go b/compilers/openapi/resolve_internal_test.go index 95478a75..6e57f151 100644 --- a/compilers/openapi/resolve_internal_test.go +++ b/compilers/openapi/resolve_internal_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/compilers/openapi/internal/annotation" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/compilers/openapi/internal/schema" "github.com/dexpace/morphic/ir" ) @@ -132,7 +133,7 @@ components: holder, ok := typeByName(l.out, "Holder").(*ir.Model) require.True(t, ok, "Holder must own a Model node") - f, ok := propsByWire(holder.Properties)["f"] + f, ok := openapitest.PropsByWire(holder.Properties)["f"] require.True(t, ok, "property f must be present") require.Len(t, f.Examples, 1, "the example beside the $ref must bind the property") require.NotNil(t, f.Examples[0].Value) @@ -157,10 +158,10 @@ func TestLowerComponentSchemas_PercentEncodedRefResolves(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec( + doc, diags := lowerSpec(t, openapitest.ComponentSpec( " "+tc.decl+": {type: string}\n"+ " User: {type: object, properties: {x: {$ref: '#/components/schemas/"+tc.encoded+"'}}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) user, ok := typeByName(doc, "User").(*ir.Model) require.True(t, ok, "User must own a Model node") @@ -186,16 +187,16 @@ func TestLowerComponentSchemas_PercentEncodedRefResolves(t *testing.T) { // is wrong. func TestLowerComponentSchemas_PercentEncodedRefHoistsAtTheDeclaredCoordinate(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec( + doc, diags := lowerSpec(t, openapitest.ComponentSpec( " Foo-Bar: {type: object, properties: {inner: {type: object, properties: {n: {type: integer}}}}}\n"+ " User:\n type: object\n properties:\n"+ " a: {$ref: '#/components/schemas/Foo-Bar/properties/inner'}\n"+ " b: {$ref: '#/components/schemas/Foo%2DBar/properties/inner'}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) user, ok := typeByName(doc, "User").(*ir.Model) require.True(t, ok, "User must own a Model node") - props := propsByWire(user.Properties) + props := openapitest.PropsByWire(user.Properties) require.Len(t, props, 2) const want = ir.TypeID("t/anon/components/schemas/Foo-Bar/properties/inner") @@ -232,7 +233,7 @@ components: application/json: schema: {type: object, properties: {q: {type: string}}} `) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) const want = ir.TypeID("t/anon/components/responses/My-Resp/content/application~1json/schema") assert.Contains(t, doc.Types, want, @@ -251,11 +252,11 @@ components: // mappings alongside $ref; nothing exercised that half. func TestLowerComponentSchemas_PercentEncodedDiscriminatorMapping(t *testing.T) { t.Parallel() - doc, diags := lowerSpec(t, componentSpec( + doc, diags := lowerSpec(t, openapitest.ComponentSpec( " Cat-A: {type: object, properties: {kind: {type: string}}}\n"+ " Pet:\n oneOf: [{$ref: '#/components/schemas/Cat-A'}]\n"+ " discriminator: {propertyName: kind, mapping: {cat: '#/components/schemas/Cat%2DA'}}\n")) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) pet, ok := typeByName(doc, "Pet").(*ir.Union) require.True(t, ok, "Pet must own a Union node") diff --git a/compilers/openapi/unpreservable_test.go b/compilers/openapi/unpreservable_test.go index 62b285eb..8bd341d7 100644 --- a/compilers/openapi/unpreservable_test.go +++ b/compilers/openapi/unpreservable_test.go @@ -9,6 +9,7 @@ import ( "github.com/dexpace/morphic/compilers/openapi/internal/diag" "github.com/dexpace/morphic/compilers/openapi/internal/ids" + "github.com/dexpace/morphic/compilers/openapi/internal/openapitest" "github.com/dexpace/morphic/ir" ) @@ -48,20 +49,20 @@ func TestUnpreservable_AnnouncementNeverOutrunsTheEntry(t *testing.T) { }{ { name: "items after prefixItems", - spec: componentSpec(" T:\n type: array\n" + + spec: openapitest.ComponentSpec(" T:\n type: array\n" + " prefixItems: [{type: string}]\n" + " items: {type: string, x-t: " + unpreservableValue + "}\n"), at: "/components/schemas/T/items", }, { name: "unmerged allOf branch", - spec: componentSpec(" M:\n allOf:\n" + + spec: openapitest.ComponentSpec(" M:\n allOf:\n" + " - {type: string, maxLength: 3, x-t: " + unpreservableValue + "}\n"), at: "/components/schemas/M/allOf/0", }, { name: "error response with multiple media types", - spec: pathsSpec(" /x:\n get:\n responses:\n" + + spec: openapitest.PathsSpec(" /x:\n get:\n responses:\n" + " \"500\":\n description: bad\n content:\n" + " application/json: {schema: {type: string}, example: " + unpreservableValue + "}\n" + " application/xml: {schema: {type: string}}\n"), @@ -69,13 +70,13 @@ func TestUnpreservable_AnnouncementNeverOutrunsTheEntry(t *testing.T) { }, { name: "path-item servers", - spec: pathsSpec(" /x:\n servers: [{url: 'https://a', x-t: " + unpreservableValue + "}]\n" + + spec: openapitest.PathsSpec(" /x:\n servers: [{url: 'https://a', x-t: " + unpreservableValue + "}]\n" + " get:\n responses: {\"200\": {description: ok}}\n"), at: "/paths/~1x/servers", }, { name: "residue keyword on a declaration", - spec: componentSpec(" R:\n type: object\n" + + spec: openapitest.ComponentSpec(" R:\n type: object\n" + " default: " + unpreservableValue + "\n" + " properties: {a: {type: string}}\n"), at: "/components/schemas/R/default", @@ -86,7 +87,7 @@ func TestUnpreservable_AnnouncementNeverOutrunsTheEntry(t *testing.T) { t.Parallel() _, diags := parseFull(t, tc.spec) - assert.True(t, hasDiagCodeAt(diags, diag.UnpreservableConstruct, tc.at), + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.UnpreservableConstruct, tc.at), "the case must reach the site it is named for: %+v", diags) assert.Empty(t, preservationClaims(diags), "nothing was written under Unmodeled, so nothing may announce that it was") @@ -137,8 +138,8 @@ func TestUnpreservable_SchemaKeywordSites(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - _, diags := parseFull(t, componentSpec(" S:\n"+tc.body)) - assert.True(t, hasDiagCodeAt(diags, diag.UnpreservableConstruct, tc.at), + _, diags := parseFull(t, openapitest.ComponentSpec(" S:\n"+tc.body)) + assert.True(t, openapitest.HasDiagCodeAt(diags, diag.UnpreservableConstruct, tc.at), "the case must reach the site it is named for: %+v", diags) assert.Empty(t, preservationClaims(diags), "nothing was written under Unmodeled, so nothing may announce that it was") @@ -152,7 +153,7 @@ func TestUnpreservable_SchemaKeywordSites(t *testing.T) { // announcing and also stopped reporting would satisfy that one. func TestUnpreservable_ReportsTheFailureItself(t *testing.T) { t.Parallel() - spec := componentSpec(" T:\n type: array\n" + + spec := openapitest.ComponentSpec(" T:\n type: array\n" + " prefixItems: [{type: string}]\n" + " items: {type: string, x-t: " + unpreservableValue + "}\n") _, diags := parseFull(t, spec) @@ -171,7 +172,7 @@ func TestUnpreservable_ReportsTheFailureItself(t *testing.T) { // preserve, or the new error would fire on every well-formed document. func TestUnpreservable_AbsentConstructIsSilent(t *testing.T) { t.Parallel() - _, diags := parseFull(t, componentSpec(" T:\n type: array\n items: {type: string}\n")) + _, diags := parseFull(t, openapitest.ComponentSpec(" T:\n type: array\n items: {type: string}\n")) _, ok := firstDiagWithCode(diags, diag.UnpreservableConstruct) assert.False(t, ok, "an absent construct is not an unpreservable one: %+v", diags) } @@ -182,7 +183,7 @@ func TestUnpreservable_AbsentConstructIsSilent(t *testing.T) { // silently omitting itself from an object still labelled verbatim. func TestUnpreservable_CompositeFailsWhole(t *testing.T) { t.Parallel() - spec := componentSpec(" C:\n type: object\n" + + spec := openapitest.ComponentSpec(" C:\n type: object\n" + " if: {required: [a]}\n" + " then: {x-t: " + unpreservableValue + "}\n" + " else: {required: [b]}\n") @@ -211,16 +212,6 @@ func preservationClaims(diags []ir.Diagnostic) []string { return out } -// hasDiagCodeAt reports whether diags carries code at exactly pointer. -func hasDiagCodeAt(diags []ir.Diagnostic, code, pointer string) bool { - for _, d := range diags { - if d.Code == code && d.Provenance.Pointer == pointer { - return true - } - } - return false -} - // typeUnmodeled returns the Unmodeled map of one interned type. func typeUnmodeled(t *testing.T, doc *ir.Document, id ir.TypeID) ir.Unmodeled { t.Helper() @@ -277,7 +268,7 @@ paths: responses: {"204": {description: ok}} ` doc, svc, diags := lowerServiceSpec(t, spec) - requireNoErrorDiags(t, diags) + openapitest.RequireNoErrorDiags(t, diags) // S witnesses vendor_extension, validation_only (its constraint-only union // joins `not` there) and out_of_scope (its $schema); T's open tuple is the @@ -291,7 +282,7 @@ paths: } // The one no_ir_home site reachable from a minimal document: a requestBody // that omits `required`, which the IR has no field for (§14). - body := firstOp(t, svc).Request + body := openapitest.FirstOp(t, svc).Request require.NotNil(t, body, "the operation must own a request payload") for _, entry := range body.Unmodeled { seen[entry.Reason] = true diff --git a/internal/archtest/arch_test.go b/internal/archtest/arch_test.go index bec94f78..20f359a9 100644 --- a/internal/archtest/arch_test.go +++ b/internal/archtest/arch_test.go @@ -175,6 +175,20 @@ var rules = map[string][]string{ module + "/compilers/openapi/internal/schema", module + "/compilers/openapi/internal/value", "github.com/speakeasy-api/openapi" + subtreeSuffix, "gopkg.in/yaml.v3"}, + // The scaffolding the compiler's test packages share. It is a production + // package only in the sense that it holds non-test files; what governs it is + // that every test package under compilers/openapi must be able to import it, + // internal ones included. That is why its allowlist stops at ir, the contract + // package and diag: an internal test file may not import a package that + // imports its own, so anything further would shut out the tests of whatever + // it reached. Widening this entry is how that becomes true silently. + "compilers/openapi/internal/openapitest": {module + "/ir", module + "/compilers", + module + "/compilers/openapi/internal/diag", + "github.com/speakeasy-api/openapi/jsonschema/oas3", + "github.com/speakeasy-api/openapi/openapi", + "github.com/speakeasy-api/openapi/sequencedmap", + "github.com/stretchr/testify/assert", + "github.com/stretchr/testify/require", "gopkg.in/yaml.v3"}, "pass": {module + "/ir"}, "engine": {module + "/ir", module + "/compilers", module + "/compilers/openapi", module + "/pass", "gopkg.in/yaml.v3"},