Files
narratio/internal/config/party_test.go

287 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"bytes"
"strings"
"testing"
)
func TestParsePartyCanonicalContract(t *testing.T) {
data := []byte(`schema_version: narratio.party.v1
characters:
arannis:
player:
name: Eric
character:
name: Arannis
alias:
- Ari
- The Grey Owl
classes:
- name: wizard
level: 8
- name: fighter
`)
document, err := ParseParty(data)
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if !document.IsCanonical() || document.Mode != PartyModeCanonical {
t.Fatalf("document mode = %#v, want canonical", document)
}
if !bytes.Equal(document.Canonical.Raw, data) {
t.Fatal("canonical raw bytes were not preserved")
}
characters := document.Canonical.Characters
if len(characters) != 1 || characters[0].ID != "arannis" {
t.Fatalf("characters = %#v, want ordered arannis character", characters)
}
if got, want := characters[0].ClassSummary(), "wizard 8 / fighter"; got != want {
t.Fatalf("ClassSummary() = %q, want %q", got, want)
}
if got, want := characters[0].AliasSummary(), "Ari, The Grey Owl"; got != want {
t.Fatalf("AliasSummary() = %q, want %q", got, want)
}
}
func TestParsePartyCanonicalRejectsInvalidDocuments(t *testing.T) {
valid := `schema_version: narratio.party.v1
characters:
arannis:
player:
name: Eric
character:
name: Arannis
classes:
- name: wizard
level: 8
`
tests := []struct {
name string
yaml string
want string
}{
{name: "unsupported version", yaml: strings.Replace(valid, PartySchemaVersion, "narratio.party.v2", 1), want: "unsupported"},
{name: "non-string version", yaml: strings.Replace(valid, PartySchemaVersion, "1", 1), want: "must be a non-empty string"},
{name: "unknown top field", yaml: valid + "unknown: true\n", want: "unknown field"},
{name: "unknown nested field", yaml: strings.Replace(valid, " name: Eric\n", " name: Eric\n role: GM\n", 1), want: "unknown field"},
{name: "trailing document", yaml: valid + "---\ncharacters: {}\n", want: "exactly one YAML document"},
{name: "empty characters", yaml: "schema_version: narratio.party.v1\ncharacters: {}\n", want: "must be non-empty"},
{name: "invalid id", yaml: strings.Replace(valid, " arannis:", " bad-id:", 1), want: "invalid character id"},
{name: "whitespace id", yaml: strings.Replace(valid, " arannis:", " ' arannis':", 1), want: "invalid character id"},
{name: "missing player name", yaml: strings.Replace(valid, "player:\n name: Eric", "player: {}", 1), want: "player.name is required"},
{name: "missing character name", yaml: strings.Replace(valid, " name: Arannis\n", "", 1), want: "character.name is required"},
{name: "blank player name", yaml: strings.Replace(valid, "name: Eric", "name: ' Eric'", 1), want: "non-empty and trimmed"},
{name: "control character", yaml: strings.Replace(valid, "name: Arannis", "name: \"Ara\\tnnis\"", 1), want: "control characters"},
{name: "non-string display", yaml: strings.Replace(valid, "name: Eric", "name: 42", 1), want: "display string"},
{name: "missing classes", yaml: strings.Replace(valid, " classes:\n - name: wizard\n level: 8\n", "", 1), want: "classes is required"},
{name: "empty classes", yaml: strings.Replace(valid, " classes:\n - name: wizard\n level: 8\n", " classes: []\n", 1), want: "non-empty list"},
{name: "zero level", yaml: strings.Replace(valid, "level: 8", "level: 0", 1), want: "positive integer"},
{name: "negative level", yaml: strings.Replace(valid, "level: 8", "level: -2", 1), want: "positive integer"},
{name: "non-integer level", yaml: strings.Replace(valid, "level: 8", "level: eight", 1), want: "positive integer"},
{name: "duplicate class", yaml: strings.Replace(valid, " level: 8\n", " level: 8\n - name: WIZARD\n", 1), want: "duplicate class"},
{name: "duplicate field", yaml: strings.Replace(valid, " name: Eric\n", " name: Eric\n name: Erin\n", 1), want: "duplicate field"},
{name: "yaml alias", yaml: strings.Replace(strings.Replace(valid, " name: Eric", " name: &player Eric", 1), " name: Arannis", " name: *player", 1), want: "must not use YAML aliases"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ParseParty([]byte(test.yaml))
if err == nil {
t.Fatal("ParseParty() error = nil, want rejection")
}
if !strings.Contains(err.Error(), test.want) {
t.Fatalf("ParseParty() error = %q, want %q", err, test.want)
}
})
}
}
func TestParsePartyRejectsGlobalAliasAmbiguityWithUnicodeCaseFolding(t *testing.T) {
tests := []struct {
name string
yaml string
}{
{
name: "alias equals primary on same character",
yaml: `schema_version: narratio.party.v1
characters:
arannis:
player: {name: Eric}
character:
name: Arannis
alias: [arannis]
classes: [{name: wizard}]
`,
},
{
name: "primary collides with prior alias",
yaml: `schema_version: narratio.party.v1
characters:
arannis:
player: {name: Eric}
character:
name: Arannis
alias: [Ari]
classes: [{name: wizard}]
brenna:
player: {name: Jane}
character:
name: ari
classes: [{name: paladin}]
`,
},
{
name: "aliases collide on one character",
yaml: `schema_version: narratio.party.v1
characters:
arannis:
player: {name: Eric}
character:
name: Arannis
alias: [Ari, ari]
classes: [{name: wizard}]
`,
},
{
name: "unicode simple fold collision",
yaml: `schema_version: narratio.party.v1
characters:
kelvin:
player: {name: Eric}
character:
name: Kelvin
alias: [night]
classes: [{name: wizard}]
knight:
player: {name: Jane}
character:
name: knight
classes: [{name: paladin}]
`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ParseParty([]byte(test.yaml))
if err == nil || !strings.Contains(err.Error(), "ambiguous") {
t.Fatalf("ParseParty() error = %v, want ambiguity rejection", err)
}
})
}
}
func TestParsePartyAllowsRepeatedPlayersAndEmptyAliasList(t *testing.T) {
document, err := ParseParty([]byte(`schema_version: narratio.party.v1
characters:
arannis:
player: {name: Eric}
character:
name: Arannis
alias: []
classes: [{name: wizard}]
brenna:
player: {name: Eric}
character:
name: Brenna
classes: [{name: paladin}]
`))
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if got := len(document.Canonical.Characters); got != 2 {
t.Fatalf("character count = %d, want 2", got)
}
}
func TestParsePartyRejectsEmptyAlias(t *testing.T) {
_, err := ParseParty([]byte(`schema_version: narratio.party.v1
characters:
arannis:
player: {name: Eric}
character:
name: Arannis
alias: [""]
classes: [{name: wizard}]
`))
if err == nil || !strings.Contains(err.Error(), "non-empty") {
t.Fatalf("ParseParty() error = %v, want empty alias rejection", err)
}
}
func TestParsePartyClassifiesUnversionedDocumentAsLegacy(t *testing.T) {
document, err := ParseParty([]byte("players:\n - not canonical\n---\nlegacy: remains opaque\n"))
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
if document.Mode != PartyModeLegacy || document.Canonical != nil || document.IsCanonical() {
t.Fatalf("document = %#v, want opaque legacy classification", document)
}
}
func TestCanonicalPartyPlayersYAML(t *testing.T) {
document, err := ParseParty([]byte(`schema_version: narratio.party.v1
characters:
zeta:
player: {name: Shared Player}
character:
name: Zeta
alias: [Z, The Last]
classes: [{name: wizard, level: 8}]
alpha:
player: {name: Shared Player}
character:
name: Alpha
classes: [{name: ranger}]
`))
if err != nil {
t.Fatalf("ParseParty() error = %v", err)
}
got, err := document.PlayersYAML()
if err != nil {
t.Fatalf("PlayersYAML() error = %v", err)
}
again, err := document.Canonical.PlayersYAML()
if err != nil {
t.Fatalf("PlayersYAML() repeat error = %v", err)
}
if !bytes.Equal(got, again) {
t.Fatalf("PlayersYAML() was not deterministic:\nfirst:\n%s\nsecond:\n%s", got, again)
}
want := `schema_version: narratio.players.v1
players:
- name: Shared Player
character:
id: alpha
name: Alpha
- name: Shared Player
character:
id: zeta
name: Zeta
alias:
- Z
- The Last
`
if string(got) != want {
t.Fatalf("PlayersYAML() =\n%s\nwant:\n%s", got, want)
}
if !strings.HasSuffix(string(got), "\n") || strings.Contains(string(got), "classes") {
t.Fatalf("players projection leaked unsupported data: %q", got)
}
}
func TestCanonicalPartyPlayersYAMLRequiresCanonicalParty(t *testing.T) {
var party *CanonicalParty
if _, err := party.PlayersYAML(); err == nil {
t.Fatal("PlayersYAML() error = nil, want canonical party requirement")
}
legacy, err := ParseParty([]byte("legacy: party\n"))
if err != nil {
t.Fatalf("ParseParty() legacy error = %v", err)
}
if _, err := legacy.PlayersYAML(); err == nil {
t.Fatal("legacy PlayersYAML() error = nil, want canonical party requirement")
}
}