package enemyevents import ( "encoding/json" "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" ) func validList() dnd.EnemyEventList { return dnd.EnemyEventList{Events: []dnd.EnemyEvent{ {Name: "Ashfang", Kind: dnd.EnemyEventKindEngaged, SourceRefs: refs(1, 2)}, {Name: "Ashfang", Kind: dnd.EnemyEventKindFled, SourceRefs: refs(5, 6)}, }} } func refs(start, end int) []source.SourceRef { return []source.SourceRef{{SourceID: "session", StartUnitID: start, EndUnitID: end}} } func TestCodecRoundTripAndIdentity(t *testing.T) { codec := New() value := validList() content, err := codec.Encode(value) if err != nil { t.Fatalf("Encode() error = %v", err) } decoded, err := codec.Decode(content) if err != nil || !reflect.DeepEqual(decoded, value) { t.Fatalf("Decode() = %#v, %v; want %#v", decoded, err, value) } schema := codec.Schema() if codec.Kind() != dnd.EnemyEventListKind || codec.MediaType() != MediaType || schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) { t.Fatalf("codec identity/schema = %q/%q %#v", codec.Kind(), codec.MediaType(), schema) } registry := pipeline.NewArtifactCodecRegistry() if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil { t.Fatal(err) } spec, ok := registry.Spec(dnd.EnemyEventListKind) if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) { t.Fatalf("registered spec = %#v, %t", spec, ok) } } func TestCodecRejectsStrictJSONAndMissingRequiredFields(t *testing.T) { validJSON := `{"events":[{"name":"Ashfang","kind":"engaged","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}` tests := []struct { name, raw, want string }{ {"invalid JSON", `{`, "decode dnd enemy event list"}, {"unknown root field", `{"events":[],"unexpected":true}`, "unknown field"}, {"unknown event field", strings.Replace(validJSON, `"kind":"engaged"`, `"kind":"engaged","unexpected":true`, 1), "unknown field"}, {"missing events", `{}`, "events must be present"}, {"missing name", strings.Replace(validJSON, `"name":"Ashfang",`, "", 1), "events[0].name must be present"}, {"missing kind", strings.Replace(validJSON, `"kind":"engaged",`, "", 1), "events[0].kind must be present"}, {"missing refs", strings.Replace(validJSON, `,"source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]`, "", 1), "events[0].source_refs must be present"}, {"missing source ID", strings.Replace(validJSON, `"source_id":"session",`, "", 1), "source_refs[0].source_id must be present"}, {"trailing JSON", `{"events":[]} {}`, "multiple JSON values"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if _, err := New().Decode([]byte(test.raw)); err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("Decode() error = %v, want %q", err, test.want) } }) } } func TestCodecDefensivelyOwnsValuesAndDefersSemanticValidation(t *testing.T) { codec := New() candidate := dnd.EnemyEventList{Events: []dnd.EnemyEvent{{ Name: " ", Kind: "unsupported", SourceRefs: []source.SourceRef{{SourceID: "", StartUnitID: 0, EndUnitID: -1}}, }}} content, err := codec.EncodeCandidate(candidate) if err != nil { t.Fatal(err) } decoded, err := codec.Decode(content) if err != nil || !reflect.DeepEqual(decoded, candidate) { t.Fatalf("Decode() = %#v, %v; want semantic candidate preservation", decoded, err) } decoded.Events[0].SourceRefs[0].SourceID = "changed" if candidate.Events[0].SourceRefs[0].SourceID != "" { t.Fatal("Decode() retained caller-owned source references") } first := codec.Schema() first.JSONSchema[0] = '[' if second := codec.Schema(); !json.Valid(second.JSONSchema) || second.JSONSchema[0] == '[' { t.Fatal("Schema() returned shared bytes") } }