Files
guestbook/reactions_test.go
T
Akili 3a68f64eb6 feat: add emoji reactions, light/dark themes and rollout observability
Add a set of guest-facing and operator-facing features that make the
deployment-strategy demos (recreate, rolling, canary) more visual:

- Emoji reactions (heart/tada/thumbsup/smile): visitors react without an
  account, a first-party cookie keeps counts honest, the tally streams
  live to every tab over SSE. Backed by a new reactions table with an
  idempotent upsert; whitelisted emoji so the endpoint can't be abused.
- Light & dark themes: the wall follows the OS preference, visitors can
  toggle it (persisted locally), and a moderator can force a theme for
  everyone from the admin console, pushed live over SSE.
- Per-replica request counter: a middleware tallies requests answered by
  each replica; surfaced in /healthz, /api/info and the admin replica
  table. Under load-balancing the counts diverge, and a rolling update
  resets them one container at a time — the rollout made visible.
- Rolling-update wave in the Chaos lab: bounces replicas one at a time
  (via the crash endpoint) while polling the replica table, so a training
  session can watch each replica reset and rejoin — what a rolling update
  looks like from the inside.
- Admin console: reactions tile, per-replica Requests column, reaction
  tallies in moderation, and a wall-theme setting.
- Favicon + /favicon.ico redirect.

Tests cover reactions (idempotency, tallies, whitelist) and the theme
setting round-trip.

Akili-Task: tsk_xgg6sfuzkzymyc6gstr4
Akili-Agent: goldfish
2026-10-05 21:36:23 +02:00

100 lines
2.5 KiB
Go

package main
import (
"context"
"testing"
)
func TestReactions(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
e, err := s.Create(ctx, "ada", "hello miabi", "r1", "")
if err != nil {
t.Fatal(err)
}
// Two visitors react, one of them twice (idempotent).
for _, tc := range []struct {
who, emoji string
want int
}{
{"alice", "heart", 1},
{"bob", "heart", 2},
{"bob", "heart", 2},
{"alice", "tada", 1},
} {
got, err := s.React(ctx, uint64(e.ID), tc.emoji, tc.who)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Fatalf("React(%s, %s) = %d, want %d", tc.who, tc.emoji, got, tc.want)
}
}
counts, err := s.ReactionCounts(ctx, uint64(e.ID))
if err != nil {
t.Fatal(err)
}
if counts["heart"] != 2 || counts["tada"] != 1 {
t.Fatalf("counts = %+v, want heart=2 tada=1", counts)
}
// Unreacting twice is a no-op after the first removal.
if got, _ := s.Unreact(ctx, uint64(e.ID), "heart", "bob"); got != 1 {
t.Fatalf("unreact = %d, want 1", got)
}
if got, _ := s.Unreact(ctx, uint64(e.ID), "heart", "bob"); got != 1 {
t.Fatalf("second unreact = %d, want 1", got)
}
// List and Get attach the tally; totals count every reaction.
wall, err := s.List(ctx, 10, 0)
if err != nil || len(wall) != 1 {
t.Fatalf("list: %v (%d entries)", err, len(wall))
}
if wall[0].Reactions["heart"] != 1 || wall[0].Reactions["tada"] != 1 {
t.Fatalf("list reactions = %+v", wall[0].Reactions)
}
one, err := s.Get(ctx, uint64(e.ID))
if err != nil || one.Reactions["heart"] != 1 {
t.Fatalf("get reactions = %+v, err=%v", one.Reactions, err)
}
if n, _ := s.ReactionTotals(ctx); n != 2 {
t.Fatalf("totals = %d, want 2", n)
}
// Deleting a reaction must not touch the entry itself.
if n, _ := s.Count(ctx); n != 1 {
t.Fatalf("entries = %d, want 1", n)
}
}
func TestReactionWhitelist(t *testing.T) {
for _, emoji := range ReactionEmojis() {
if !validReaction(emoji) {
t.Fatalf("whitelist rejects its own emoji %q", emoji)
}
}
if validReaction("poop") || validReaction("") {
t.Fatal("whitelist accepted a non-whitelisted emoji")
}
}
func TestSettingsTheme(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
if st, _ := s.Settings(ctx); st.Theme != "" {
t.Fatalf("default theme = %q, want empty (system)", st.Theme)
}
want := Settings{SigningPaused: false, Banner: "hi", Theme: "light"}
if err := s.SaveSettings(ctx, want); err != nil {
t.Fatal(err)
}
if got, _ := s.Settings(ctx); got != want {
t.Fatalf("got %+v, want %+v", got, want)
}
}