Files
narratio/internal/config/party_test.go

288 lines
9.2 KiB
Go
Raw Permalink 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 !bytes.Equal(document.Raw, data) {
t.Fatal("canonical raw bytes were not preserved")
}
characters := document.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: "unsupported integer version", yaml: strings.Replace(valid, PartySchemaVersion, "1", 1), want: "unsupported"},
{name: "boolean version", yaml: strings.Replace(valid, PartySchemaVersion, "true", 1), want: "version.type"},
{name: "unknown top field", yaml: valid + "unknown: true\n", want: "decode.unknown_field"},
{name: "unknown nested field", yaml: strings.Replace(valid, " name: Eric\n", " name: Eric\n role: GM\n", 1), want: "decode.unknown_field"},
{name: "trailing document", yaml: valid + "---\ncharacters: {}\n", want: "yaml.multiple_documents"},
{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: "/characters/arannis/player/name"},
{name: "missing character name", yaml: strings.Replace(valid, " name: Arannis\n", "", 1), want: "/characters/arannis/character/name"},
{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: "decode.type"},
{name: "missing classes", yaml: strings.Replace(valid, " classes:\n - name: wizard\n level: 8\n", "", 1), want: "classes must be a non-empty list"},
{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: "decode.type"},
{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: "yaml.duplicate_key"},
{name: "yaml alias", yaml: strings.Replace(strings.Replace(valid, " name: Eric", " name: &player Eric", 1), " name: Arannis", " name: *player", 1), want: "yaml.anchor"},
}
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: [Knight]
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.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 TestParsePartyRejectsMissingOrUnknownSchemaVersion(t *testing.T) {
for _, test := range []struct {
name string
yaml string
want string
}{
{name: "missing", yaml: "characters: {}\n", want: "schema_version is required"},
{name: "unknown", yaml: "schema_version: narratio.party.v2\ncharacters: {}\n", want: "supported schema_version is \"narratio.party.v1\""},
} {
t.Run(test.name, func(t *testing.T) {
if _, err := ParseParty([]byte(test.yaml)); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("ParseParty() error = %v, want %q", err, test.want)
}
})
}
}
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)
}
if len(document.Characters) != 2 || document.Characters[0].ID != "zeta" || document.Characters[1].ID != "alpha" {
t.Fatalf("authored character order = %#v, want zeta then alpha", document.Characters)
}
got, err := document.PlayersYAML()
if err != nil {
t.Fatalf("PlayersYAML() error = %v", err)
}
again, err := document.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")
}
}