2026-08-03 23:40:35 +00:00
package locationoccurrences
import (
"bytes"
"encoding/json"
"os"
"reflect"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
)
func validList ( ) dnd . LocationOccurrenceList {
refs := [ ] source . SourceRef { { SourceID : "session-alpha" , StartUnitID : 1 , EndUnitID : 2 } }
return dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { {
LocationID : identity . DeriveID ( "The Old Tavern" , refs ) ,
Name : "The Old Tavern" ,
Kind : dnd . LocationOccurrenceKindVisited ,
SourceRefs : refs ,
} } }
}
func TestCodecMatchesMaintainedDurableFixture ( t * testing . T ) {
raw , err := os . ReadFile ( "testdata/dnd_location_occurrences.v1.json" )
if err != nil {
t . Fatalf ( "read durable fixture: %v" , err )
}
codec := New ( )
value , err := codec . Decode ( raw )
if err != nil {
t . Fatalf ( "Decode() error = %v, want nil" , err )
}
if want := validList ( ) ; ! reflect . DeepEqual ( value , want ) {
t . Fatalf ( "Decode() = %#v, want %#v" , value , want )
}
encoded , err := codec . Encode ( value )
if err != nil {
t . Fatalf ( "Encode() error = %v, want nil" , err )
}
var compact bytes . Buffer
if err := json . Compact ( & compact , raw ) ; err != nil {
t . Fatalf ( "compact durable fixture: %v" , err )
}
if ! bytes . Equal ( encoded , compact . Bytes ( ) ) {
t . Fatalf ( "Encode() = %s, want %s" , encoded , compact . Bytes ( ) )
}
}
func TestCodecOwnsDurableSchemaAndMetadata ( t * testing . T ) {
codec := New ( )
schema := codec . Schema ( )
if codec . Kind ( ) != dnd . LocationOccurrenceListKind || codec . MediaType ( ) != MediaType {
t . Fatalf ( "codec identity = %q/%q" , codec . Kind ( ) , codec . MediaType ( ) )
}
if schema . ID != SchemaID || schema . Name != SchemaName || schema . Version != SchemaVersion || ! json . Valid ( schema . JSONSchema ) {
t . Fatalf ( "schema = %#v, want durable location occurrence schema" , schema )
}
var document map [ string ] any
if err := json . Unmarshal ( schema . JSONSchema , & document ) ; err != nil || document [ "$id" ] != SchemaID {
t . Fatalf ( "durable schema document = %#v, %v" , document , err )
}
registry := pipeline . NewArtifactCodecRegistry ( )
if err := pipeline . RegisterArtifactCodec ( registry , codec ) ; err != nil {
t . Fatalf ( "RegisterArtifactCodec() error = %v" , err )
}
if spec , ok := registry . Spec ( dnd . LocationOccurrenceListKind ) ; ! ok || spec . SchemaDigest != contracts . DigestArtifactSchema ( schema ) {
t . Fatalf ( "registered spec = %#v, %t" , spec , ok )
}
first := schema . JSONSchema
first [ 0 ] = '['
if next := codec . Schema ( ) . JSONSchema ; ! json . Valid ( next ) || next [ 0 ] == '[' {
t . Fatal ( "Schema() returned shared bytes" )
}
metadata := codec . Metadata ( validList ( ) )
metadata [ "other" ] = true
if next := codec . Metadata ( validList ( ) ) ; len ( next ) != 1 || next [ "occurrence_count" ] != 1 {
t . Fatalf ( "Metadata() = %#v" , next )
}
}
func TestCodecSupportsEmptyListsAndCandidateSemanticFailures ( t * testing . T ) {
codec := New ( )
empty := dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { } }
if content , err := codec . Encode ( empty ) ; err != nil || string ( content ) != ` { "occurrences":[]} ` {
t . Fatalf ( "Encode() = %s, %v" , content , err )
}
for _ , candidate := range [ ] dnd . LocationOccurrenceList {
{ } ,
empty ,
{ Occurrences : [ ] dnd . LocationOccurrence { { LocationID : "bad" , Name : " " , Kind : "unsupported" , SourceRefs : nil } } } ,
{ Occurrences : [ ] dnd . LocationOccurrence { { LocationID : "bad" , Name : " " , Kind : "unsupported" , SourceRefs : [ ] source . SourceRef { } } } } ,
{ Occurrences : [ ] dnd . LocationOccurrence { { LocationID : "bad" , Name : " " , Kind : "unsupported" , SourceRefs : [ ] source . SourceRef { { StartUnitID : 0 , EndUnitID : - 1 } } } } } ,
} {
content , err := codec . EncodeCandidate ( candidate )
if err != nil || ! json . Valid ( content ) {
t . Fatalf ( "EncodeCandidate() = %s, %v" , content , err )
}
decoded , err := codec . DecodeCandidate ( content )
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "DecodeCandidate() = %#v, %v, want %#v" , decoded , err , candidate )
}
}
}
2026-08-09 00:57:03 +00:00
func TestCodecCandidateDecodeOwnsValues ( t * testing . T ) {
codec := New ( )
input := validList ( )
content , err := codec . EncodeCandidate ( input )
if err != nil {
t . Fatal ( err )
}
first , err := codec . DecodeCandidate ( content )
if err != nil {
t . Fatal ( err )
}
second , err := codec . DecodeCandidate ( content )
if err != nil {
t . Fatal ( err )
}
nameOffset := bytes . Index ( content , [ ] byte ( "The Old Tavern" ) )
if nameOffset < 0 {
t . Fatal ( "candidate JSON does not contain location name" )
}
content [ nameOffset ] = 'X'
if first . Occurrences [ 0 ] . Name != "The Old Tavern" {
t . Fatal ( "DecodeCandidate() retained input bytes" )
}
first . Occurrences [ 0 ] . Name = "changed"
first . Occurrences [ 0 ] . SourceRefs [ 0 ] . SourceID = "changed"
if input . Occurrences [ 0 ] . Name != "The Old Tavern" || input . Occurrences [ 0 ] . SourceRefs [ 0 ] . SourceID != "session-alpha" {
t . Fatal ( "DecodeCandidate() retained input values" )
}
if second . Occurrences [ 0 ] . Name != "The Old Tavern" || second . Occurrences [ 0 ] . SourceRefs [ 0 ] . SourceID != "session-alpha" {
t . Fatal ( "DecodeCandidate() returned aliased values" )
}
}
2026-08-09 01:54:43 +00:00
func TestCodecRejectsInvalidDurableRangesWhileCandidatesPreserveThem ( t * testing . T ) {
codec := New ( )
for _ , test := range [ ] struct {
name string
mutate func ( * dnd . LocationOccurrenceList )
want string
} {
{ name : "nonpositive start" , mutate : func ( value * dnd . LocationOccurrenceList ) { value . Occurrences [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 0 } , want : "start_unit_id" } ,
{ name : "nonpositive end" , mutate : func ( value * dnd . LocationOccurrenceList ) { value . Occurrences [ 0 ] . SourceRefs [ 0 ] . EndUnitID = 0 } , want : "end_unit_id" } ,
{ name : "reversed" , mutate : func ( value * dnd . LocationOccurrenceList ) {
value . Occurrences [ 0 ] . SourceRefs [ 0 ] . StartUnitID = 2
value . Occurrences [ 0 ] . SourceRefs [ 0 ] . EndUnitID = 1
} , want : "must not exceed" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
candidate := validList ( )
test . mutate ( & candidate )
content , err := codec . EncodeCandidate ( candidate )
if err != nil {
t . Fatalf ( "EncodeCandidate() error = %v" , err )
}
decoded , err := codec . DecodeCandidate ( content )
if err != nil || ! reflect . DeepEqual ( decoded , candidate ) {
t . Fatalf ( "DecodeCandidate() = %#v, %v; want %#v" , decoded , err , candidate )
}
if _ , err := codec . Encode ( candidate ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Encode() error = %v, want %q" , err , test . want )
}
if _ , err := codec . Decode ( content ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Decode() error = %v, want %q" , err , test . want )
}
} )
}
}
2026-08-03 23:40:35 +00:00
func TestCodecRejectsStructuralJSONBeforeSemanticApproval ( t * testing . T ) {
valid := ` { "occurrences":[ { "location_id":"location:sha256:0000000000000000000000000000000000000000000000000000000000000000","name":"The Tavern","kind":"visited","source_refs":[ { "source_id":"session","start_unit_id":1,"end_unit_id":1}]}]} `
for _ , test := range [ ] struct { name , raw , want string } {
{ "malformed" , ` { ` , "decode dnd location occurrence list" } ,
{ "unknown top-level" , ` { "occurrences":[],"unexpected":true} ` , "unknown field" } ,
{ "unknown occurrence field" , strings . Replace ( valid , ` "kind":"visited" ` , ` "kind":"visited","unexpected":true ` , 1 ) , "unknown field" } ,
{ "unknown source reference field" , strings . Replace ( valid , ` "end_unit_id":1 ` , ` "end_unit_id":1,"unexpected":true ` , 1 ) , "unknown field" } ,
{ "invalid type" , ` { "occurrences":"not-an-array"} ` , "cannot unmarshal string" } ,
{ "trailing" , ` { "occurrences":[]} { } ` , "multiple JSON values" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( ) . DecodeCandidate ( [ ] byte ( test . raw ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "DecodeCandidate() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecRejectsApprovedShapeEnumAndReferenceBoundaries ( t * testing . T ) {
base := validList ( ) . Occurrences [ 0 ]
for _ , test := range [ ] struct {
name string
value dnd . LocationOccurrenceList
want string
} {
{ "nil occurrences" , dnd . LocationOccurrenceList { } , "occurrences must be present" } ,
{ "invalid ID" , dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { { LocationID : "bad" , Name : base . Name , Kind : base . Kind , SourceRefs : base . SourceRefs } } } , "location_id must match location ID pattern" } ,
{ "blank name" , dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { { LocationID : base . LocationID , Name : " " , Kind : base . Kind , SourceRefs : base . SourceRefs } } } , "name must not be empty" } ,
{ "invalid kind" , dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { { LocationID : base . LocationID , Name : base . Name , Kind : "unsupported" , SourceRefs : base . SourceRefs } } } , "kind must be supported" } ,
{ "empty source refs" , dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { { LocationID : base . LocationID , Name : base . Name , Kind : base . Kind } } } , "source_refs must contain" } ,
{ "malformed source reference" , dnd . LocationOccurrenceList { Occurrences : [ ] dnd . LocationOccurrence { { LocationID : base . LocationID , Name : base . Name , Kind : base . Kind , SourceRefs : [ ] source . SourceRef { { SourceID : "session" , StartUnitID : 1 , EndUnitID : 0 } } } } } , "end_unit_id must be positive" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
if _ , err := New ( ) . Encode ( test . value ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "Encode() error = %v, want %q" , err , test . want )
}
} )
}
}
func TestCodecAcceptsEveryOccurrenceKind ( t * testing . T ) {
for _ , kind := range [ ] dnd . LocationOccurrenceKind {
dnd . LocationOccurrenceKindVisited ,
dnd . LocationOccurrenceKindPlanned ,
dnd . LocationOccurrenceKindRecalled ,
dnd . LocationOccurrenceKindMentioned ,
} {
value := validList ( )
value . Occurrences [ 0 ] . Kind = kind
if _ , err := New ( ) . Encode ( value ) ; err != nil {
t . Fatalf ( "Encode(%q) error = %v" , kind , err )
}
}
}