Repository: ogen-go/ogen
Pull request #509 (2022-07-28 03:56:02)

test: move parser negative tests to `parser` package

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
> Merging [#509](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (b436733) into [main](https://codecov.io/gh/ogen-go/ogen/commit/4c2cbd6360e3496bea622da66e364c799540c2dc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (4c2cbd6) will **increase** coverage by `0.75%`.
> The diff coverage is `97.40%`.

```diff
@@            Coverage Diff             @@
##             main     #509      +/-   ##
==========================================
+ Coverage   71.19%   71.94%   +0.75%     
==========================================
  Files         152      152              
  Lines       10989    10998       +9     
==========================================
+ Hits         7824     7913      +89     
+ Misses       2723     2658      -65     
+ Partials      442      427      -15     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [openapi/parser/resolve.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcmVzb2x2ZS5nbw==) | `84.61% <66.66%> (+17.70%)` | :arrow_up: |
| [openapi/parser/parse\_header.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfaGVhZGVyLmdv) | `100.00% <100.00%> (+5.55%)` | :arrow_up: |
| [openapi/parser/parse\_mediatype.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfbWVkaWF0eXBlLmdv) | `99.15% <100.00%> (+8.08%)` | :arrow_up: |
| [openapi/parser/parse\_parameter.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfcGFyYW1ldGVyLmdv) | `94.66% <100.00%> (+1.71%)` | :arrow_up: |
| [openapi/parser/settings.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvc2V0dGluZ3MuZ28=) | `100.00% <100.00%> (ø)` | |
| [gen/generator.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbmVyYXRvci5nbw==) | `66.66% <0.00%> (-3.58%)` | :arrow_down: |
| [gen/errors.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2Vycm9ycy5nbw==) | `43.85% <0.00%> (-3.51%)` | :arrow_down: |
| [jsonschema/parser.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9wYXJzZXIuZ28=) | `78.91% <0.00%> (+0.60%)` | :arrow_up: |
| [openapi/parser/path\_parser.go](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGF0aF9wYXJzZXIuZ28=) | `96.51% <0.00%> (+3.48%)` | :arrow_up: |
| ... and [6 more](https://codecov.io/gh/ogen-go/ogen/pull/509/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Last update [4c2cbd6...b436733](https://codecov.io/gh/ogen-go/ogen/pull/509?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/_testdata/negative/securityScheme/unknown_type.json b/openapi/parser/_testdata/negative/securityScheme/unknown_type.json
similarity index 100%
rename from _testdata/negative/securityScheme/unknown_type.json
rename to openapi/parser/_testdata/negative/securityScheme/unknown_type.json
diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go
index 6e15653d..41ce545b 100644
--- a/openapi/parser/errors.go
+++ b/openapi/parser/errors.go
@@ -9,6 +9,20 @@ import (
 // LocationError is a wrapper for an error that has a location.
 type LocationError = location.Error
 
+func (p *parser) wrapRef(filename string, l location.Locator, err error) error {
+	if err == nil || p == nil {
+		return err
+	}
+	return p.wrapLocation(filename, l.Field("$ref"), err)
+}
+
+func (p *parser) wrapField(field, filename string, l location.Locator, err error) error {
+	if err == nil || p == nil {
+		return err
+	}
+	return p.wrapLocation(filename, l.Field(field), err)
+}
+
 func (p *parser) wrapLocation(filename string, l location.Locator, err error) error {
 	var locErr *LocationError
 	// Do not wrap error if it is nil or is already a LocationError.
@@ -29,10 +43,3 @@ func (p *parser) wrapLocation(filename string, l location.Locator, err error) er
 		Err:  err,
 	}
 }
-
-func (p *parser) wrapField(field, filename string, l location.Locator, err error) error {
-	if err == nil || p == nil {
-		return err
-	}
-	return p.wrapLocation(filename, l.Field(field), err)
-}
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index eab714c2..5b96bc98 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -1,8 +1,6 @@
 package parser
 
 import (
-	"github.com/go-faster/errors"
-
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -11,17 +9,16 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Exam
 	if e == nil {
 		return nil, nil
 	}
-
+	defer func() {
+		rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
+	}()
 	if ref := e.Ref; ref != "" {
-		ex, err := p.resolveExample(ref, ctx)
+		resolved, err := p.resolveExample(ref, ctx)
 		if err != nil {
-			return nil, errors.Wrapf(err, "resolve %q", ref)
+			return nil, p.wrapRef(ctx.lastLoc(), e.Locator, err)
 		}
-		return ex, nil
+		return resolved, nil
 	}
-	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
-	}()
 
 	return &openapi.Example{
 		Summary:       e.Summary,
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index 6e8d2945..f883b244 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -31,11 +31,11 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 		rerr = p.wrapLocation(ctx.lastLoc(), header.Locator, rerr)
 	}()
 	if ref := header.Ref; ref != "" {
-		parsed, err := p.resolveHeader(name, ref, ctx)
+		resolved, err := p.resolveHeader(name, ref, ctx)
 		if err != nil {
-			return nil, errors.Wrapf(err, "resolve %q reference", ref)
+			return nil, p.wrapRef(ctx.lastLoc(), header.Locator, err)
 		}
-		return parsed, nil
+		return resolved, nil
 	}
 
 	mustNotSpecified := func(name, got string) error {
@@ -82,13 +82,11 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 		Locator:     header.Locator,
 	}
 
-	if header.Content != nil {
-		// TODO: Validate content?
-		return op, nil
-	}
-
-	if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
-		return nil, err
+	// TODO: Validate content?
+	if header.Content == nil {
+		if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
+			return nil, err
+		}
 	}
 
 	return op, nil
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 24e68513..2bee6441 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -23,12 +23,13 @@ func (p *parser) parseContent(
 
 	result := make(map[string]*openapi.MediaType, len(content))
 	for name, m := range content {
-		if _, _, err := mime.ParseMediaType(name); err != nil {
+		ct, _, err := mime.ParseMediaType(name)
+		if err != nil {
 			err := errors.Wrapf(err, "content type %q", name)
 			return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(name), err)
 		}
 
-		result[name], err = p.parseMediaType(m, ctx)
+		result[name], err = p.parseMediaType(ct, m, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, name)
 		}
@@ -47,7 +48,7 @@ func (p *parser) parseParameterContent(
 	}
 	if len(content) != 1 {
 		for key := range content {
-			locator = locator.Field(key)
+			locator = locator.Key(key)
 			break
 		}
 
@@ -56,12 +57,13 @@ func (p *parser) parseParameterContent(
 	}
 
 	for name, m := range content {
-		if _, _, err := mime.ParseMediaType(name); err != nil {
+		ct, _, err := mime.ParseMediaType(name)
+		if err != nil {
 			err := errors.Wrapf(err, "content type %q", name)
 			return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(name), err)
 		}
 
-		media, err := p.parseMediaType(m, ctx)
+		media, err := p.parseMediaType(ct, m, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, name)
 		}
@@ -74,7 +76,7 @@ func (p *parser) parseParameterContent(
 	panic("unreachable")
 }
 
-func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.MediaType, rerr error) {
+func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *openapi.MediaType, rerr error) {
 	defer func() {
 		rerr = p.wrapLocation(ctx.lastLoc(), m.Locator, rerr)
 	}()
@@ -85,58 +87,64 @@ func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.Media
 	}
 
 	encodings := make(map[string]*openapi.Encoding, len(m.Encoding))
-	if s != nil && len(m.Encoding) > 0 {
-		names := make(map[string]jsonschema.Property, len(s.Properties))
-		for _, prop := range s.Properties {
-			names[prop.Name] = prop
-		}
-
-		parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) {
-			defer func() {
-				rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
-			}()
-
-			encoding := &openapi.Encoding{
-				ContentType:   e.ContentType,
-				Headers:       map[string]*openapi.Header{},
-				Style:         inferParamStyle(openapi.LocationQuery, e.Style),
-				Explode:       inferParamExplode(openapi.LocationQuery, e.Explode),
-				AllowReserved: e.AllowReserved,
-				Locator:       e.Locator,
+	if len(m.Encoding) > 0 {
+		switch ct {
+		case "application/x-www-form-urlencoded", "multipart/form-data":
+			var names map[string]jsonschema.Property
+			if s != nil {
+				names = make(map[string]jsonschema.Property, len(s.Properties))
+				for _, prop := range s.Properties {
+					names[prop.Name] = prop
+				}
 			}
-			encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
-			if err != nil {
-				return p.wrapField("headers", ctx.lastLoc(), e.Locator, err)
-			}
-			encodings[name] = encoding
-
-			if err := p.validateParamStyle(&openapi.Parameter{
-				Name:          name,
-				Schema:        prop.Schema,
-				In:            openapi.LocationQuery,
-				Style:         encoding.Style,
-				Explode:       encoding.Explode,
-				Required:      prop.Required,
-				AllowReserved: encoding.AllowReserved,
-				Locator:       encoding.Locator,
-			}, ctx.lastLoc()); err != nil {
-				return errors.Wrap(err, "param style")
-			}
-
-			return nil
-		}
 
-		encodingLoc := m.Locator.Field("encoding")
-		for name, e := range m.Encoding {
-			prop, ok := names[name]
-			if !ok {
-				loc := encodingLoc.Key(name)
-				err := errors.Errorf("unknown property %q", name)
-				return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+			parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) {
+				defer func() {
+					rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
+				}()
+
+				encoding := &openapi.Encoding{
+					ContentType:   e.ContentType,
+					Headers:       map[string]*openapi.Header{},
+					Style:         inferParamStyle(openapi.LocationQuery, e.Style),
+					Explode:       inferParamExplode(openapi.LocationQuery, e.Explode),
+					AllowReserved: e.AllowReserved,
+					Locator:       e.Locator,
+				}
+				encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
+				if err != nil {
+					return p.wrapField("headers", ctx.lastLoc(), e.Locator, err)
+				}
+				encodings[name] = encoding
+
+				if err := p.validateParamStyle(&openapi.Parameter{
+					Name:          name,
+					Schema:        prop.Schema,
+					In:            openapi.LocationQuery,
+					Style:         encoding.Style,
+					Explode:       encoding.Explode,
+					Required:      prop.Required,
+					AllowReserved: encoding.AllowReserved,
+					Locator:       encoding.Locator,
+				}, ctx.lastLoc()); err != nil {
+					return errors.Wrap(err, "param style")
+				}
+
+				return nil
 			}
 
-			if err := parseEncoding(name, prop, e); err != nil {
-				return nil, errors.Wrapf(err, "encoding property %q", name)
+			encodingLoc := m.Locator.Field("encoding")
+			for name, e := range m.Encoding {
+				prop, ok := names[name]
+				if !ok {
+					loc := encodingLoc.Key(name)
+					err := errors.Errorf("unknown property %q", name)
+					return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+				}
+
+				if err := parseEncoding(name, prop, e); err != nil {
+					return nil, errors.Wrapf(err, "encoding property %q", name)
+				}
 			}
 		}
 	}
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index c0a25739..b8d830ec 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -122,7 +122,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 	if ref := param.Ref; ref != "" {
 		parsed, err := p.resolveParameter(ref, ctx)
 		if err != nil {
-			return nil, errors.Wrapf(err, "resolve %q reference", ref)
+			return nil, p.wrapRef(ctx.lastLoc(), param.Locator, err)
 		}
 		return parsed, nil
 	}
@@ -170,13 +170,11 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 		Locator:       param.Locator,
 	}
 
-	if param.Content != nil {
-		// TODO: Validate content?
-		return op, nil
-	}
-
-	if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
-		return nil, err
+	// TODO: Validate content?
+	if param.Content == nil {
+		if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
+			return nil, err
+		}
 	}
 
 	return op, nil
diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go
index 893f8d22..f1b916ab 100644
--- a/openapi/parser/parse_path_item.go
+++ b/openapi/parser/parse_path_item.go
@@ -26,7 +26,7 @@ func (p *parser) parsePathItem(
 	if ref := item.Ref; ref != "" {
 		ops, err := p.resolvePathItem(path, ref, operationIDs, ctx)
 		if err != nil {
-			return nil, errors.Wrap(err, "resolve pathItem")
+			return nil, p.wrapRef(ctx.lastLoc(), item.Locator, err)
 		}
 		return ops, nil
 	}
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index 19be0762..5e973271 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -15,12 +15,11 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *o
 		rerr = p.wrapLocation(ctx.lastLoc(), body.Locator, rerr)
 	}()
 	if ref := body.Ref; ref != "" {
-		reqBody, err := p.resolveRequestBody(ref, ctx)
+		resolved, err := p.resolveRequestBody(ref, ctx)
 		if err != nil {
-			return nil, errors.Wrapf(err, "resolve %q reference", ref)
+			return nil, p.wrapRef(ctx.lastLoc(), body.Locator, err)
 		}
-
-		return reqBody, nil
+		return resolved, nil
 	}
 
 	content, err := func() (map[string]*openapi.MediaType, error) {
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 30150d29..06aa26d0 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -45,12 +45,11 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi
 		rerr = p.wrapLocation(ctx.lastLoc(), resp.Locator, rerr)
 	}()
 	if ref := resp.Ref; ref != "" {
-		resp, err := p.resolveResponse(ref, ctx)
+		resolved, err := p.resolveResponse(ref, ctx)
 		if err != nil {
-			return nil, errors.Wrapf(err, "resolve %q reference", ref)
+			return nil, p.wrapRef(ctx.lastLoc(), resp.Locator, err)
 		}
-
-		return resp, nil
+		return resolved, nil
 	}
 
 	content, err := p.parseContent(resp.Content, resp.Locator.Field("content"), ctx)
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index 4c9f01f7..bc664d32 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -23,11 +23,11 @@ func (p *parser) parseSecurityScheme(
 	}()
 
 	if ref := scheme.Ref; ref != "" {
-		sch, err := p.resolveSecurityScheme(ref, ctx)
+		resolved, err := p.resolveSecurityScheme(ref, ctx)
 		if err != nil {
-			return nil, errors.Wrap(err, "resolve security schema")
+			return nil, p.wrapRef(ctx.lastLoc(), scheme.Locator, err)
 		}
-		return sch, nil
+		return resolved, nil
 	}
 
 	if err := func() error {
diff --git a/openapi/parser/resolve.go b/openapi/parser/resolve.go
index bf78cf06..92f29406 100644
--- a/openapi/parser/resolve.go
+++ b/openapi/parser/resolve.go
@@ -300,8 +300,13 @@ func (p *parser) resolveSecurityScheme(ref string, ctx *resolveCtx) (*ogen.Secur
 		}
 	}
 
-	p.refs.securitySchemes[ref] = component
-	return component, nil
+	ss, err := p.parseSecurityScheme(component, ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	p.refs.securitySchemes[ref] = ss
+	return ss, nil
 }
 
 func (p *parser) resolvePathItem(
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 1bb6c528..4f82502b 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -30,6 +30,9 @@ type Settings struct {
 }
 
 func (s *Settings) setDefaults() {
+	if s.External == nil {
+		s.External = jsonschema.NoExternal{}
+	}
 	if s.DepthLimit == 0 {
 		s.DepthLimit = 1000
 	}


Test patch:
diff --git a/gen_test.go b/gen_test.go
index 761c5432..b23d0512 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/ogen-go/ogen/gen/genfs"
 	"github.com/ogen-go/ogen/gen/ir"
 	"github.com/ogen-go/ogen/internal/location"
+	"github.com/ogen-go/ogen/openapi/parser"
 )
 
 func testGenerate(t *testing.T, filename string, data []byte, aliases ctAliases, ignore ...string) {
@@ -156,6 +157,11 @@ func TestNegative(t *testing.T) {
 		spec, err := ogen.Parse(data)
 		a.NoError(err)
 
+		_, err = parser.Parse(spec, parser.Settings{
+			Filename: name,
+		})
+		a.NoError(err, "If the error is related to parser, move this test to parser package testdata")
+
 		_, err = gen.NewGenerator(spec, gen.Options{
 			Filename: name,
 		})
diff --git a/_testdata/negative/cyclic/example.json b/openapi/parser/_testdata/negative/cyclic/example.json
similarity index 100%
rename from _testdata/negative/cyclic/example.json
rename to openapi/parser/_testdata/negative/cyclic/example.json
diff --git a/openapi/parser/_testdata/negative/cyclic/header.json b/openapi/parser/_testdata/negative/cyclic/header.json
new file mode 100644
index 00000000..4eaee4ec
--- /dev/null
+++ b/openapi/parser/_testdata/negative/cyclic/header.json
@@ -0,0 +1,30 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "headers": {
+              "X-Foo": {
+                "$ref": "#/components/headers/Header"
+              }
+            },
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "headers": {
+      "Header": {
+        "$ref": "#/components/headers/Header"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/cyclic/parameter.json b/openapi/parser/_testdata/negative/cyclic/parameter.json
similarity index 100%
rename from _testdata/negative/cyclic/parameter.json
rename to openapi/parser/_testdata/negative/cyclic/parameter.json
diff --git a/_testdata/negative/cyclic/pathItem.json b/openapi/parser/_testdata/negative/cyclic/pathItem.json
similarity index 100%
rename from _testdata/negative/cyclic/pathItem.json
rename to openapi/parser/_testdata/negative/cyclic/pathItem.json
diff --git a/_testdata/negative/cyclic/request.json b/openapi/parser/_testdata/negative/cyclic/requestBody.json
similarity index 100%
rename from _testdata/negative/cyclic/request.json
rename to openapi/parser/_testdata/negative/cyclic/requestBody.json
diff --git a/_testdata/negative/cyclic/response.json b/openapi/parser/_testdata/negative/cyclic/response.json
similarity index 100%
rename from _testdata/negative/cyclic/response.json
rename to openapi/parser/_testdata/negative/cyclic/response.json
diff --git a/openapi/parser/_testdata/negative/cyclic/securityScheme.json b/openapi/parser/_testdata/negative/cyclic/securityScheme.json
new file mode 100644
index 00000000..0ebd6975
--- /dev/null
+++ b/openapi/parser/_testdata/negative/cyclic/securityScheme.json
@@ -0,0 +1,30 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": []
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "$ref": "#/components/securitySchemes/api_key"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/headers/header_empty_content.json b/openapi/parser/_testdata/negative/headers/header_empty_content.json
similarity index 100%
rename from _testdata/negative/headers/header_empty_content.json
rename to openapi/parser/_testdata/negative/headers/header_empty_content.json
diff --git a/_testdata/negative/headers/header_multiple_content_entries.json b/openapi/parser/_testdata/negative/headers/header_multiple_content_entries.json
similarity index 100%
rename from _testdata/negative/headers/header_multiple_content_entries.json
rename to openapi/parser/_testdata/negative/headers/header_multiple_content_entries.json
diff --git a/_testdata/negative/headers/header_with_content_and_schema.json b/openapi/parser/_testdata/negative/headers/header_with_content_and_schema.json
similarity index 100%
rename from _testdata/negative/headers/header_with_content_and_schema.json
rename to openapi/parser/_testdata/negative/headers/header_with_content_and_schema.json
diff --git a/_testdata/negative/headers/header_with_in.json b/openapi/parser/_testdata/negative/headers/header_with_in.json
similarity index 100%
rename from _testdata/negative/headers/header_with_in.json
rename to openapi/parser/_testdata/negative/headers/header_with_in.json
diff --git a/_testdata/negative/headers/header_with_name.json b/openapi/parser/_testdata/negative/headers/header_with_name.json
similarity index 100%
rename from _testdata/negative/headers/header_with_name.json
rename to openapi/parser/_testdata/negative/headers/header_with_name.json
diff --git a/_testdata/negative/headers/invalid_content_type.json b/openapi/parser/_testdata/negative/headers/invalid_content_type.json
similarity index 100%
rename from _testdata/negative/headers/invalid_content_type.json
rename to openapi/parser/_testdata/negative/headers/invalid_content_type.json
diff --git a/_testdata/negative/headers/invalid_header_name.json b/openapi/parser/_testdata/negative/headers/invalid_header_name.json
similarity index 100%
rename from _testdata/negative/headers/invalid_header_name.json
rename to openapi/parser/_testdata/negative/headers/invalid_header_name.json
diff --git a/openapi/parser/_testdata/negative/headers/invalid_header_schema.json b/openapi/parser/_testdata/negative/headers/invalid_header_schema.json
new file mode 100644
index 00000000..e7f153d0
--- /dev/null
+++ b/openapi/parser/_testdata/negative/headers/invalid_header_schema.json
@@ -0,0 +1,25 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "headers": {
+              "X-FooBar": {
+                "schema": {
+                  "type": "foobar"
+                }
+              }
+            },
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/headers/unknown_header_style.json b/openapi/parser/_testdata/negative/headers/unknown_header_style.json
similarity index 100%
rename from _testdata/negative/headers/unknown_header_style.json
rename to openapi/parser/_testdata/negative/headers/unknown_header_style.json
diff --git a/_testdata/negative/null/components_header.json b/openapi/parser/_testdata/negative/null/components_header.json
similarity index 100%
rename from _testdata/negative/null/components_header.json
rename to openapi/parser/_testdata/negative/null/components_header.json
diff --git a/_testdata/negative/null/components_parameter.json b/openapi/parser/_testdata/negative/null/components_parameter.json
similarity index 100%
rename from _testdata/negative/null/components_parameter.json
rename to openapi/parser/_testdata/negative/null/components_parameter.json
diff --git a/_testdata/negative/null/components_pathItem.json b/openapi/parser/_testdata/negative/null/components_pathItem.json
similarity index 100%
rename from _testdata/negative/null/components_pathItem.json
rename to openapi/parser/_testdata/negative/null/components_pathItem.json
diff --git a/_testdata/negative/null/components_requestBody.json b/openapi/parser/_testdata/negative/null/components_requestBody.json
similarity index 100%
rename from _testdata/negative/null/components_requestBody.json
rename to openapi/parser/_testdata/negative/null/components_requestBody.json
diff --git a/_testdata/negative/null/components_response.json b/openapi/parser/_testdata/negative/null/components_response.json
similarity index 100%
rename from _testdata/negative/null/components_response.json
rename to openapi/parser/_testdata/negative/null/components_response.json
diff --git a/_testdata/negative/null/components_securityScheme.json b/openapi/parser/_testdata/negative/null/components_securityScheme.json
similarity index 100%
rename from _testdata/negative/null/components_securityScheme.json
rename to openapi/parser/_testdata/negative/null/components_securityScheme.json
diff --git a/_testdata/negative/null/default_value.json b/openapi/parser/_testdata/negative/null/default_value.json
similarity index 100%
rename from _testdata/negative/null/default_value.json
rename to openapi/parser/_testdata/negative/null/default_value.json
diff --git a/_testdata/negative/null/operation_parameter.json b/openapi/parser/_testdata/negative/null/operation_parameter.json
similarity index 100%
rename from _testdata/negative/null/operation_parameter.json
rename to openapi/parser/_testdata/negative/null/operation_parameter.json
diff --git a/_testdata/negative/null/parameter_schema_content.json b/openapi/parser/_testdata/negative/null/parameter_schema_content.json
similarity index 100%
rename from _testdata/negative/null/parameter_schema_content.json
rename to openapi/parser/_testdata/negative/null/parameter_schema_content.json
diff --git a/_testdata/negative/null/pathItem.json b/openapi/parser/_testdata/negative/null/pathItem.json
similarity index 100%
rename from _testdata/negative/null/pathItem.json
rename to openapi/parser/_testdata/negative/null/pathItem.json
diff --git a/_testdata/negative/null/pathItem_parameter.json b/openapi/parser/_testdata/negative/null/pathItem_parameter.json
similarity index 100%
rename from _testdata/negative/null/pathItem_parameter.json
rename to openapi/parser/_testdata/negative/null/pathItem_parameter.json
diff --git a/_testdata/negative/null/requestBody.json b/openapi/parser/_testdata/negative/null/requestBody.json
similarity index 100%
rename from _testdata/negative/null/requestBody.json
rename to openapi/parser/_testdata/negative/null/requestBody.json
diff --git a/_testdata/negative/null/requestBody_content.json b/openapi/parser/_testdata/negative/null/requestBody_content.json
similarity index 100%
rename from _testdata/negative/null/requestBody_content.json
rename to openapi/parser/_testdata/negative/null/requestBody_content.json
diff --git a/_testdata/negative/null/response.json b/openapi/parser/_testdata/negative/null/response.json
similarity index 100%
rename from _testdata/negative/null/response.json
rename to openapi/parser/_testdata/negative/null/response.json
diff --git a/_testdata/negative/null/securityScheme_flows.json b/openapi/parser/_testdata/negative/null/securityScheme_flows.json
similarity index 100%
rename from _testdata/negative/null/securityScheme_flows.json
rename to openapi/parser/_testdata/negative/null/securityScheme_flows.json
diff --git a/_testdata/negative/parameters/duplicate_parameter.json b/openapi/parser/_testdata/negative/parameters/duplicate_parameter.json
similarity index 100%
rename from _testdata/negative/parameters/duplicate_parameter.json
rename to openapi/parser/_testdata/negative/parameters/duplicate_parameter.json
diff --git a/_testdata/negative/parameters/invalid_content_type.json b/openapi/parser/_testdata/negative/parameters/invalid_content_type.json
similarity index 100%
rename from _testdata/negative/parameters/invalid_content_type.json
rename to openapi/parser/_testdata/negative/parameters/invalid_content_type.json
diff --git a/openapi/parser/_testdata/negative/parameters/invalid_parameter_content_schema.json b/openapi/parser/_testdata/negative/parameters/invalid_parameter_content_schema.json
new file mode 100644
index 00000000..c979fdb8
--- /dev/null
+++ b/openapi/parser/_testdata/negative/parameters/invalid_parameter_content_schema.json
@@ -0,0 +1,31 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "parameters": [
+          {
+            "name": "foo",
+            "in": "query",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "foobar"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json b/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json
new file mode 100644
index 00000000..ff3a2173
--- /dev/null
+++ b/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "parameters": [
+          {
+            "name": "foo",
+            "in": "query",
+            "schema": {
+                "type": "foobar"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/parameters/parameter_empty_content.json b/openapi/parser/_testdata/negative/parameters/parameter_empty_content.json
similarity index 100%
rename from _testdata/negative/parameters/parameter_empty_content.json
rename to openapi/parser/_testdata/negative/parameters/parameter_empty_content.json
diff --git a/_testdata/negative/parameters/parameter_multiple_content_entries.json b/openapi/parser/_testdata/negative/parameters/parameter_multiple_content_entries.json
similarity index 100%
rename from _testdata/negative/parameters/parameter_multiple_content_entries.json
rename to openapi/parser/_testdata/negative/parameters/parameter_multiple_content_entries.json
diff --git a/_testdata/negative/parameters/parameter_with_content_and_schema.json b/openapi/parser/_testdata/negative/parameters/parameter_with_content_and_schema.json
similarity index 100%
rename from _testdata/negative/parameters/parameter_with_content_and_schema.json
rename to openapi/parser/_testdata/negative/parameters/parameter_with_content_and_schema.json
diff --git a/_testdata/negative/parameters/unknown_parameter_in.json b/openapi/parser/_testdata/negative/parameters/unknown_parameter_in.json
similarity index 100%
rename from _testdata/negative/parameters/unknown_parameter_in.json
rename to openapi/parser/_testdata/negative/parameters/unknown_parameter_in.json
diff --git a/_testdata/negative/parameters/unknown_parameter_style.json b/openapi/parser/_testdata/negative/parameters/unknown_parameter_style.json
similarity index 100%
rename from _testdata/negative/parameters/unknown_parameter_style.json
rename to openapi/parser/_testdata/negative/parameters/unknown_parameter_style.json
diff --git a/_testdata/negative/paths/duplicate_path_item.json b/openapi/parser/_testdata/negative/paths/duplicate_path_item.json
similarity index 100%
rename from _testdata/negative/paths/duplicate_path_item.json
rename to openapi/parser/_testdata/negative/paths/duplicate_path_item.json
diff --git a/_testdata/negative/paths/invalid_path.json b/openapi/parser/_testdata/negative/paths/invalid_path.json
similarity index 100%
rename from _testdata/negative/paths/invalid_path.json
rename to openapi/parser/_testdata/negative/paths/invalid_path.json
diff --git a/_testdata/negative/paths/optional_path_parameter.json b/openapi/parser/_testdata/negative/paths/optional_path_parameter.json
similarity index 100%
rename from _testdata/negative/paths/optional_path_parameter.json
rename to openapi/parser/_testdata/negative/paths/optional_path_parameter.json
diff --git a/openapi/parser/_testdata/negative/ref/invalid_example_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_example_ref_url.json
new file mode 100644
index 00000000..f870d729
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_example_ref_url.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info",
+            "content": {
+              "application/json": {
+                "examples": {
+                  "ParameterExample": {
+                    "$ref": "\u0000-invalid-url"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_header_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_header_ref_url.json
new file mode 100644
index 00000000..12a06b66
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_header_ref_url.json
@@ -0,0 +1,23 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "headers": {
+              "X-Foo": {
+                "$ref": "\u0000-invalid-url"
+              }
+            },
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_parameter_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_parameter_ref_url.json
new file mode 100644
index 00000000..3f2ac924
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_parameter_ref_url.json
@@ -0,0 +1,23 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "parameters": [
+          {
+            "$ref": "\u0000-invalid-url"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_pathItem_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_pathItem_ref_url.json
new file mode 100644
index 00000000..442ebf22
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_pathItem_ref_url.json
@@ -0,0 +1,12 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "$ref": "\u0000-invalid-url"
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_requestBody_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_requestBody_ref_url.json
new file mode 100644
index 00000000..2e3ac63a
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_requestBody_ref_url.json
@@ -0,0 +1,21 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "requestBody": {
+          "$ref": "\u0000-invalid-url"
+        },
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_response_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_response_ref_url.json
new file mode 100644
index 00000000..3359b41b
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_response_ref_url.json
@@ -0,0 +1,18 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "$ref": "\u0000-invalid-url"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/invalid_securityScheme_ref_url.json b/openapi/parser/_testdata/negative/ref/invalid_securityScheme_ref_url.json
new file mode 100644
index 00000000..3dbf7b87
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/invalid_securityScheme_ref_url.json
@@ -0,0 +1,30 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": []
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "$ref": "\u0000-invalid-url"
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_example_ref.json b/openapi/parser/_testdata/negative/ref/unknown_example_ref.json
new file mode 100644
index 00000000..f73e1648
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_example_ref.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info",
+            "content": {
+              "application/json": {
+                "examples": {
+                  "ParameterExample": {
+                    "$ref": "#/uknown-ref"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_header_ref.json b/openapi/parser/_testdata/negative/ref/unknown_header_ref.json
new file mode 100644
index 00000000..f340e7e9
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_header_ref.json
@@ -0,0 +1,23 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "headers": {
+              "X-Foo": {
+                "$ref": "#/uknown-ref"
+              }
+            },
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_parameter_ref.json b/openapi/parser/_testdata/negative/ref/unknown_parameter_ref.json
new file mode 100644
index 00000000..df22b049
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_parameter_ref.json
@@ -0,0 +1,23 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "parameters": [
+          {
+            "$ref": "#/uknown-ref"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_pathItem_ref.json b/openapi/parser/_testdata/negative/ref/unknown_pathItem_ref.json
new file mode 100644
index 00000000..ff171fe9
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_pathItem_ref.json
@@ -0,0 +1,12 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "$ref": "#/uknown-ref"
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_requestBody_ref.json b/openapi/parser/_testdata/negative/ref/unknown_requestBody_ref.json
new file mode 100644
index 00000000..a8618cec
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_requestBody_ref.json
@@ -0,0 +1,21 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "requestBody": {
+          "$ref": "#/uknown-ref"
+        },
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_response_ref.json b/openapi/parser/_testdata/negative/ref/unknown_response_ref.json
new file mode 100644
index 00000000..16d0e49e
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_response_ref.json
@@ -0,0 +1,18 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "$ref": "#/uknown-ref"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/ref/unknown_securityScheme_ref.json b/openapi/parser/_testdata/negative/ref/unknown_securityScheme_ref.json
new file mode 100644
index 00000000..a993ffdc
--- /dev/null
+++ b/openapi/parser/_testdata/negative/ref/unknown_securityScheme_ref.json
@@ -0,0 +1,30 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": []
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "$ref": "#/uknown-ref"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/requestBody/invalid_content_type.json b/openapi/parser/_testdata/negative/requestBody/invalid_content_type.json
similarity index 100%
rename from _testdata/negative/requestBody/invalid_content_type.json
rename to openapi/parser/_testdata/negative/requestBody/invalid_content_type.json
diff --git a/openapi/parser/_testdata/negative/requestBody/invalid_encoding_header.json b/openapi/parser/_testdata/negative/requestBody/invalid_encoding_header.json
new file mode 100644
index 00000000..97dc5829
--- /dev/null
+++ b/openapi/parser/_testdata/negative/requestBody/invalid_encoding_header.json
@@ -0,0 +1,41 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "post": {
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "encoding": {
+                "foo": {
+                  "headers": {
+                    "X-Foo": {
+                      "style": "amongus"
+                    }
+                  }
+                }
+              },
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "foo": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/requestBody/unknown_encoding_property.json b/openapi/parser/_testdata/negative/requestBody/unknown_encoding_property.json
similarity index 100%
rename from _testdata/negative/requestBody/unknown_encoding_property.json
rename to openapi/parser/_testdata/negative/requestBody/unknown_encoding_property.json
diff --git a/_testdata/negative/requestBody/unknown_encoding_style.json b/openapi/parser/_testdata/negative/requestBody/unknown_encoding_style.json
similarity index 100%
rename from _testdata/negative/requestBody/unknown_encoding_style.json
rename to openapi/parser/_testdata/negative/requestBody/unknown_encoding_style.json
diff --git a/_testdata/negative/responses/invalid_content_type.json b/openapi/parser/_testdata/negative/responses/invalid_content_type.json
similarity index 100%
rename from _testdata/negative/responses/invalid_content_type.json
rename to openapi/parser/_testdata/negative/responses/invalid_content_type.json
diff --git a/_testdata/negative/responses/invalid_status_code.json b/openapi/parser/_testdata/negative/responses/invalid_status_code.json
similarity index 100%
rename from _testdata/negative/responses/invalid_status_code.json
rename to openapi/parser/_testdata/negative/responses/invalid_status_code.json
diff --git a/openapi/parser/_testdata/negative/responses/no_responses.json b/openapi/parser/_testdata/negative/responses/no_responses.json
new file mode 100644
index 00000000..1b35c66b
--- /dev/null
+++ b/openapi/parser/_testdata/negative/responses/no_responses.json
@@ -0,0 +1,14 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {}
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/responses/unknown_status_code.json b/openapi/parser/_testdata/negative/responses/unknown_status_code.json
similarity index 100%
rename from _testdata/negative/responses/unknown_status_code.json
rename to openapi/parser/_testdata/negative/responses/unknown_status_code.json
diff --git a/_testdata/negative/securityScheme/invalid_apiKey_name.json b/openapi/parser/_testdata/negative/securityScheme/invalid_apiKey_name.json
similarity index 100%
rename from _testdata/negative/securityScheme/invalid_apiKey_name.json
rename to openapi/parser/_testdata/negative/securityScheme/invalid_apiKey_name.json
diff --git a/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json b/openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json
similarity index 100%
rename from _testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json
rename to openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json
diff --git a/_testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json b/openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
similarity index 100%
rename from _testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
rename to openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
diff --git a/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json b/openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json
similarity index 100%
rename from _testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json
rename to openapi/parser/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json
diff --git a/openapi/parser/_testdata/negative/securityScheme/invalid_openIdConnect_url.json b/openapi/parser/_testdata/negative/securityScheme/invalid_openIdConnect_url.json
new file mode 100644
index 00000000..d4222f99
--- /dev/null
+++ b/openapi/parser/_testdata/negative/securityScheme/invalid_openIdConnect_url.json
@@ -0,0 +1,33 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": [
+              "read"
+            ]
+          }
+        ],
+        "responses": {
+          "default": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "type": "openIdConnect",
+        "openIdConnectUrl": "-"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/securityScheme/unexpected_scopes.json b/openapi/parser/_testdata/negative/securityScheme/unexpected_apiKey_scopes.json
similarity index 100%
rename from _testdata/negative/securityScheme/unexpected_scopes.json
rename to openapi/parser/_testdata/negative/securityScheme/unexpected_apiKey_scopes.json
diff --git a/openapi/parser/_testdata/negative/securityScheme/unexpected_http_scopes.json b/openapi/parser/_testdata/negative/securityScheme/unexpected_http_scopes.json
new file mode 100644
index 00000000..7d066c13
--- /dev/null
+++ b/openapi/parser/_testdata/negative/securityScheme/unexpected_http_scopes.json
@@ -0,0 +1,34 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": [
+              "read"
+            ]
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "type": "http",
+        "name": "api_key",
+        "scheme": "basic"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/securityScheme/unknown_apiKey_in.json b/openapi/parser/_testdata/negative/securityScheme/unknown_apiKey_in.json
similarity index 100%
rename from _testdata/negative/securityScheme/unknown_apiKey_in.json
rename to openapi/parser/_testdata/negative/securityScheme/unknown_apiKey_in.json
diff --git a/_testdata/negative/securityScheme/unknown_http_scheme.json b/openapi/parser/_testdata/negative/securityScheme/unknown_http_scheme.json
similarity index 100%
rename from _testdata/negative/securityScheme/unknown_http_scheme.json
rename to openapi/parser/_testdata/negative/securityScheme/unknown_http_scheme.json
diff --git a/openapi/parser/_testdata/negative/securityScheme/unknown_ref_apiKey_in.json b/openapi/parser/_testdata/negative/securityScheme/unknown_ref_apiKey_in.json
new file mode 100644
index 00000000..7e2c0d2c
--- /dev/null
+++ b/openapi/parser/_testdata/negative/securityScheme/unknown_ref_apiKey_in.json
@@ -0,0 +1,34 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": []
+          }
+        ],
+        "responses": {
+          "default": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "$ref": "#/components/securitySchemes/real_api_key"
+      },
+      "real_api_key": {
+        "type": "apiKey",
+        "in": "amongus"
+      }
+    }
+  }
+}
diff --git a/openapi/parser/parser_test.go b/openapi/parser/parser_test.go
new file mode 100644
index 00000000..3df209bc
--- /dev/null
+++ b/openapi/parser/parser_test.go
@@ -0,0 +1,68 @@
+package parser_test
+
+import (
+	"embed"
+	"path"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/location"
+	"github.com/ogen-go/ogen/openapi/parser"
+)
+
+//go:embed _testdata
+var testdata embed.FS
+
+func walkTestdata(t *testing.T, root string, cb func(t *testing.T, file string, data []byte)) {
+	t.Helper()
+
+	dir, err := testdata.ReadDir(root)
+	require.NoError(t, err)
+
+	for _, e := range dir {
+		entryName := e.Name()
+		filePath := path.Join(root, entryName)
+		if e.IsDir() {
+			t.Run(entryName, func(t *testing.T) {
+				walkTestdata(t, filePath, cb)
+			})
+			continue
+		}
+
+		testName := strings.TrimSuffix(entryName, ".json")
+		testName = strings.TrimSuffix(testName, ".yml")
+		testName = strings.TrimSuffix(testName, ".yaml")
+
+		t.Run(testName, func(t *testing.T) {
+			data, err := testdata.ReadFile(filePath)
+			require.NoError(t, err)
+			cb(t, filePath, data)
+		})
+	}
+}
+
+func TestNegative(t *testing.T) {
+	walkTestdata(t, "_testdata/negative", func(t *testing.T, file string, data []byte) {
+		a := require.New(t)
+		_, name := path.Split(file)
+
+		spec, err := ogen.Parse(data)
+		a.NoError(err)
+
+		_, err = parser.Parse(spec, parser.Settings{
+			Filename: name,
+		})
+		a.Error(err)
+
+		var buf strings.Builder
+		ok := location.PrintPrettyError(&buf, name, data, err)
+		// Ensure that the error message is pretty printed.
+		//
+		// There should be a good reason to remove this line.
+		a.True(ok)
+		t.Log(buf.String())
+	})
+}
diff --git a/openapi/parser/path_parser_test.go b/openapi/parser/path_parser_test.go
index c02b30f6..71ba14ad 100644
--- a/openapi/parser/path_parser_test.go
+++ b/openapi/parser/path_parser_test.go
@@ -63,6 +63,10 @@ func TestPathParser(t *testing.T) {
 				{Raw: "/foo bar"},
 			},
 		},
+		{
+			Path:      "\x00",
+			ExpectErr: `parse "\x00": net/url: invalid control character in URL`,
+		},
 		{
 			Path:      "/foo/{bar}/{baz}",
 			Params:    []*openapi.Parameter{bar},