Skip to content

Test/fuzz normalizer - #299

Merged
fredbi merged 7 commits into
go-openapi:masterfrom
fredbi:test/fuzz-normalizer
Aug 16, 2026
Merged

Test/fuzz normalizer#299
fredbi merged 7 commits into
go-openapi:masterfrom
fredbi:test/fuzz-normalizer

Conversation

@fredbi

@fredbi fredbi commented Aug 16, 2026

Copy link
Copy Markdown
Member

Change type

Please select: 🆕 New feature or enhancement|🔧 Bug fix'|📃 Documentation update

Short description

Fixes

Full description

Checklist

  • I have signed all my commits with my name and email (see DCO. This does not require a PGP-signed commit
  • I have rebased and squashed my work, so only one commit remains
  • I have added tests to cover my changes.
  • I have properly enriched go doc comments in code.
  • I have properly documented any breaking change.

fredbi and others added 7 commits August 16, 2026 22:45
A base that carries a scheme but no path of its own ("a:", "smb://host")
leaves the joined path relative. Rendering that gives "scheme://path", which
promotes the path to an authority component: "a://some file.json" no longer
parses, and normalizing such a $ref panics instead of resolving it.

Anchor the joined path so that the rendered URI stays canonical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
denormalizeRef rewrites a $ref when expansion stops on a circular reference.
Four ways it could point the rewritten $ref at another document:

- the base was cut off a $ref as a plain string prefix, so a sibling named
  spec.json.orig ended up rebased as ".orig" against the wrong folder;
- a $ref to the folder holding the base collapsed to an empty $ref, which
  denotes the base document itself;
- the query component was dropped, losing "?raw=true" and the like;
- the userinfo component was dropped, losing credentials.

rebase now cuts on path boundaries, and keeps a $ref absolute whenever a
relative one could not carry the query or the credentials it needs.

Its boolean is renamed isID, because the two callers mean different things by
the URI they hand it: an "id" anchors a namespace, so a $ref below it rebases
on the id itself, whereas a base is a document, so a $ref rebases on the folder
holding it and only a $ref to that very document collapses to an empty $ref.
Conflating the two is what produced the first two defects above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
FuzzNormalizer walks arbitrary ($ref, base) pairs through normalizeBase,
normalizeURI and denormalizeRef. It asserts that normalizing yields a parseable
URI carrying a scheme, that normalizing twice changes nothing, and that
denormalizing a canonical $ref yields a shorthand that normalizes back to it.

That last property is skipped for URIs which jsonreference respells: turning a
URI into a Ref lower-cases the host, drops a default port and re-derives the
escapes, where the normalizer keeps the spelling it was handed.

The corpus checked in under testdata holds the minimized inputs of the defects
this target found, so they run as ordinary test cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
SchemaOrArray fell back on the single-schema form whenever the array was empty,
so a schema carrying "items": [] was written back as "items": null - which is
not a schema, and which reads back as no items at all.

Marshal the array form whenever the array is there, empty or not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
FuzzSwaggerRoundTrip unmarshals arbitrary JSON into Swagger, writes it back and
reads it again, requiring the two renderings to agree. That covers the two dozen
hand-written UnmarshalJSON methods a document lands on, several of which are
union types switching on the shape of a value rather than on a discriminator:
writing back is where one that guessed wrong shows up.

The seeds aim at those union members rather than at coverage of the
specification. The corpus checked in under testdata holds the minimized input
that caught the empty "items" array.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
FuzzExpandSpec expands arbitrary documents, requiring that expansion terminates
and that what it returns is still a document: an expansion that succeeds but
leaves behind something we can no longer read corrupts the caller's spec
silently.

The document loader is stubbed, so the target reads no file and opens no socket.
The stub answers every path with the same document, and that document holds a
$ref back out to another one, so a remote $ref chains forever unless cycle
detection and the expansion budget stop it. Seeds cover $refs in each position
that accepts one, and cycles - direct, mutual, and through a remote document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
A default port and an upper-case host are equivalent spellings of one authority.
jsonreference normalizes both away when a URI becomes a Ref, whereas the
normalizer keeps the spelling it was handed, and denormalizeRef compared the two
against each other: a base of "https://example.com:443/spec.json" made every
$ref look like it belonged to another host, so rebasing declined and left it
absolute. Expanding an ordinary document, whose own $refs are all local, then
yielded remote absolute $refs - the AbsoluteCircularRef behaviour, given to a
caller who did not ask for it, on account of how the base was spelled.

Canonicalize the base, and the schema id, before rebasing compares them.

The new fixture holds two cycles sharing a node, which is what forces expansion
to cut one of them with a $ref. Which node receives it depends on the order of
the walk and is deliberately not asserted; that no remaining $ref leaves the
document is.

This addresses the authority. Path and fragment escaping are respelled by
jsonreference too, and still differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.77%. Comparing base (e592343) to head (72866dd).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
normalizer.go 90.24% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #299      +/-   ##
==========================================
+ Coverage   68.43%   68.77%   +0.34%     
==========================================
  Files          30       30              
  Lines        2433     2463      +30     
==========================================
+ Hits         1665     1694      +29     
- Misses        599      602       +3     
+ Partials      169      167       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@fredbi
fredbi merged commit 3838171 into go-openapi:master Aug 16, 2026
23 checks passed
@fredbi
fredbi deleted the test/fuzz-normalizer branch August 16, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant