package rtpconn import ( "regexp" "strings" "testing" "git.stormux.org/storm/skald/hall" ) func TestParseDiceExpression(t *testing.T) { tests := []struct { expression string count int64 sides int64 modifier int64 }{ {"2d4", 2, 4, 0}, {"10d8 + 5", 10, 8, 5}, {"3 d 2+5", 3, 2, 5}, {"2 D 20 - 1", 2, 20, -1}, {" 1d6 ", 1, 6, 0}, } for _, test := range tests { t.Run(test.expression, func(t *testing.T) { got, err := parseDiceExpression(test.expression) if err != nil { t.Fatalf("parseDiceExpression(%q): %v", test.expression, err) } if got.Count != test.count || got.Sides != test.sides || got.Modifier != test.modifier { t.Fatalf("parseDiceExpression(%q) = %#v", test.expression, got) } }) } } func TestParseDiceExpressionRejectsInvalidAndOversizedRolls(t *testing.T) { tests := []string{ "", "d20", "2d", "2d1", "0d6", "101d6", "2d1000001", "2d6 + 1000001", "2 0d6", "2d6 trailing", } for _, expression := range tests { t.Run(expression, func(t *testing.T) { if _, err := parseDiceExpression(expression); err == nil { t.Fatalf("parseDiceExpression(%q) unexpectedly succeeded", expression) } }) } } func TestFormatDiceAnnouncement(t *testing.T) { tests := []struct { name string roll diceRoll expected string }{ { "two dice", diceRoll{Count: 2, Sides: 4, Results: []int64{1, 2}, Total: 3}, "Storm rolls 2 4-sided dice: 1 and 2, for a total of 3.", }, { "many dice with modifier", diceRoll{Count: 10, Sides: 10, Modifier: 5, Results: []int64{2, 3, 4, 3, 4, 1, 2, 1, 2, 3}, Total: 30}, "Storm rolls 10 10-sided dice: 2, 3, 4, 3, 4, and 5 more, plus 5, for a total of 30.", }, { "one die with negative modifier", diceRoll{Count: 1, Sides: 20, Modifier: -1, Results: []int64{17}, Total: 16}, "Storm rolls 1 20-sided die: 17, minus 1, for a total of 16.", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := formatDiceAnnouncement("Storm", test.roll); got != test.expected { t.Fatalf("formatDiceAnnouncement() = %q, want %q", got, test.expected) } }) } } func TestGenerateDiceRollStaysWithinBounds(t *testing.T) { roll, err := generateDiceRoll(diceRoll{Count: 100, Sides: 6, Modifier: -50}) if err != nil { t.Fatalf("generateDiceRoll: %v", err) } if len(roll.Results) != 100 { t.Fatalf("generated %d results, want 100", len(roll.Results)) } wantTotal := int64(-50) for _, result := range roll.Results { if result < 1 || result > 6 { t.Fatalf("generated result %d outside 1..6", result) } wantTotal += result } if roll.Total != wantTotal { t.Fatalf("total = %d, want %d", roll.Total, wantTotal) } } func TestDiceRollIsServerGeneratedAndStoredInHistory(t *testing.T) { hallName := newChalkboardTestHall(t) roller := addTestWebClient(t, hallName, "Participant", "message") observer := addTestWebClient(t, hallName, "Operator", "op") if err := handleClientMessage(roller, clientMessage{ Type: "hallaction", Kind: "roll", Value: "2d4 + 5", }); err != nil { t.Fatalf("handleClientMessage(roll): %v", err) } for name, messages := range map[string][]clientMessage{ "roller": drainMessages(roller), "observer": drainMessages(observer), } { if len(messages) != 1 { t.Fatalf("%s received %d messages, want 1: %#v", name, len(messages), messages) } message := messages[0] if message.Type != "chat" || message.Kind != "dice" || !message.Privileged { t.Fatalf("%s received non-authoritative dice message: %#v", name, message) } if message.Source != "" || message.Username == nil || *message.Username != "Dice" { t.Fatalf("%s received dice message with unexpected identity: %#v", name, message) } text, ok := message.Value.(string) if !ok || !regexp.MustCompile(`^Participant rolls 2 4-sided dice: [1-4] and [1-4], plus 5, for a total of (7|8|9|10|11|12|13)\.$`).MatchString(text) { t.Fatalf("%s received malformed dice announcement %q", name, text) } } history := roller.hall.GetChatHistory() if len(history) != 1 || history[0].Kind != "dice" { t.Fatalf("dice history = %#v", history) } newcomer := testWebClient("admin-id") adminName := "Admin" if _, err := hall.AddClient(hallName, newcomer, hall.ClientCredentials{ Username: &adminName, Password: "pw", }); err != nil { t.Fatalf("AddClient(Admin): %v", err) } newcomer.hall = hall.Get(hallName) drainActions(t, newcomer) var historicalDice *clientMessage for _, message := range drainMessages(newcomer) { if message.Type == "chathistory" && message.Kind == "dice" { message := message historicalDice = &message break } } if historicalDice == nil || !historicalDice.Privileged { t.Fatalf("new participant did not receive authoritative dice history: %#v", historicalDice) } } func TestDiceRollRequiresMessagePermission(t *testing.T) { hallName := newChalkboardTestHall(t) roller := addTestWebClient(t, hallName, "Participant", "message") roller.permissions = remove("message", roller.permissions) if err := handleClientMessage(roller, clientMessage{ Type: "hallaction", Kind: "roll", Value: "1d6", }); err != nil { t.Fatalf("handleClientMessage(roll): %v", err) } if got := roller.hall.GetChatHistory(); len(got) != 0 { t.Fatalf("unauthorised roll entered history: %#v", got) } messages := drainMessages(roller) if len(messages) != 1 || messages[0].Kind != "error" || !strings.Contains(messages[0].Value.(string), "not authorised") { t.Fatalf("unauthorised roll response = %#v", messages) } } func TestClientsCannotSubmitServerGeneratedMessages(t *testing.T) { hallName := newChalkboardTestHall(t) roller := addTestWebClient(t, hallName, "Participant", "message") for _, messageKind := range []string{"dice", "eightball", "coin"} { for _, messageType := range []string{"chat", "usermessage"} { drainMessages(roller) if err := handleClientMessage(roller, clientMessage{ Type: messageType, Kind: messageKind, Value: "whatever result I wanted", }); err != nil { t.Fatalf("handleClientMessage(%s, %s): %v", messageType, messageKind, err) } messages := drainMessages(roller) if len(messages) != 1 || messages[0].Kind != "error" { t.Fatalf("reserved %s/%s response = %#v", messageType, messageKind, messages) } } } if got := roller.hall.GetChatHistory(); len(got) != 0 { t.Fatalf("forged dice message entered history: %#v", got) } } func TestChatIdentityComesFromConnection(t *testing.T) { hallName := newChalkboardTestHall(t) sender := addTestWebClient(t, hallName, "Participant", "message") observer := addTestWebClient(t, hallName, "Operator", "op") if err := handleClientMessage(sender, clientMessage{ Type: "chat", Value: "hello", }); err != nil { t.Fatalf("handleClientMessage(chat): %v", err) } messages := drainMessages(observer) if len(messages) != 1 || messages[0].Source != sender.id || messages[0].Username == nil || *messages[0].Username != "Participant" { t.Fatalf("chat did not use connection identity: %#v", messages) } }