Add canonical party domain and players projection
This commit is contained in:
286
internal/config/party_test.go
Normal file
286
internal/config/party_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
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: [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.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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user