initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled

This commit is contained in:
Daniel Noulet 2026-10-07 15:11:35 +02:00
commit 951dd8d26b
175 changed files with 50041 additions and 0 deletions

View file

@ -0,0 +1,262 @@
package store
import (
"context"
"errors"
"testing"
"github.com/urpi/go-bananas/internal/domain"
)
func TestPreferencesDefaultToNoChoice(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
// A subject nobody has saved anything for reads as empty rather than failing,
// because empty is the documented "no choice" state.
prefs, err := s.Preferences(ctx, "nobody")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if prefs.SelectedTheme != "" || prefs.SelectedAppearance != "" {
t.Fatalf("preferences = %+v, want the zero value", prefs)
}
}
func TestPreferencesRoundTrip(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
published := publishTheme(t, s, "midnight")
stored, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: published.Slug,
SelectedAppearance: domain.AppearanceDark,
})
if err != nil {
t.Fatalf("set preferences: %v", err)
}
if stored.SelectedTheme != "midnight" || stored.SelectedAppearance != domain.AppearanceDark {
t.Fatalf("stored = %+v", stored)
}
read, err := s.Preferences(ctx, "user-1")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if read != stored {
t.Fatalf("read = %+v, want %+v", read, stored)
}
}
func TestPreferencesCanBeClearedAndOverwritten(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
publishTheme(t, s, "midnight")
if _, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: "midnight",
SelectedAppearance: domain.AppearanceLight,
}); err != nil {
t.Fatalf("set preferences: %v", err)
}
// Writing again replaces rather than accumulating.
if _, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: "midnight",
SelectedAppearance: domain.AppearanceDark,
}); err != nil {
t.Fatalf("overwrite preferences: %v", err)
}
read, err := s.Preferences(ctx, "user-1")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if read.SelectedAppearance != domain.AppearanceDark {
t.Fatalf("appearance = %q, want dark", read.SelectedAppearance)
}
// An empty theme is the absence of a choice and is storable.
if _, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{}); err != nil {
t.Fatalf("clear by writing empty: %v", err)
}
read, err = s.Preferences(ctx, "user-1")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if read.SelectedTheme != "" {
t.Fatalf("theme = %q, want empty", read.SelectedTheme)
}
removed, err := s.ClearPreferences(ctx, "user-1")
if err != nil {
t.Fatalf("clear preferences: %v", err)
}
if !removed {
t.Error("clear reported no row removed")
}
}
// TestPreferencesRejectUnknownAndDraftThemes covers the two ways a name can fail:
// it does not exist, or it exists but runtime consumers cannot load it.
func TestPreferencesRejectUnknownAndDraftThemes(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if err := s.Create(ctx, sampleTheme("draft-one"), ""); err != nil {
t.Fatalf("create theme: %v", err)
}
for _, slug := range []string{"no-such-theme", "draft-one"} {
_, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: slug,
SelectedAppearance: domain.AppearanceLight,
})
if !errors.Is(err, domain.ErrInvalid) {
t.Errorf("SetPreferences(%q) = %v, want ErrInvalid", slug, err)
}
}
}
// TestPreferencesFallBackWhenTheThemeDisappears is the reason Preferences resolves
// the name on read: a theme that is later deleted must not leave somebody pinned to
// a selection the UI cannot load.
func TestPreferencesFallBackWhenTheThemeDisappears(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
theme := publishTheme(t, s, "midnight")
if _, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: theme.Slug,
SelectedAppearance: domain.AppearanceDark,
}); err != nil {
t.Fatalf("set preferences: %v", err)
}
if err := s.Delete(ctx, theme.Slug); err != nil {
t.Fatalf("delete theme: %v", err)
}
read, err := s.Preferences(ctx, "user-1")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if read.SelectedTheme != "" || read.SelectedAppearance != "" {
t.Fatalf("preferences = %+v, want the zero value once the theme is gone", read)
}
}
func TestPreferencesRejectMalformedInput(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
publishTheme(t, s, "midnight")
rejected := map[string]domain.UserPreferences{
"uppercase slug": {SelectedTheme: "Midnight"},
"slug with a space": {SelectedTheme: "mid night"},
"leading dash": {SelectedTheme: "-midnight"},
"unknown appearance": {
SelectedTheme: "midnight",
SelectedAppearance: "sepia",
},
}
for name, prefs := range rejected {
t.Run(name, func(t *testing.T) {
if _, err := s.SetPreferences(ctx, "user-1", prefs); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("SetPreferences(%+v) = %v, want ErrInvalid", prefs, err)
}
})
}
// Clearing with an empty theme is valid, and an appearance sent alongside it
// is dropped rather than stored as a scheme with nothing to apply it to.
stored, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: "",
SelectedAppearance: domain.AppearanceDark,
})
if err != nil {
t.Fatalf("clearing with a stray appearance: %v", err)
}
if stored.SelectedAppearance != "" {
t.Fatalf("stored = %+v, want the appearance dropped", stored)
}
if _, err := s.Preferences(ctx, ""); !errors.Is(err, domain.ErrInvalid) {
t.Errorf("Preferences(\"\") = %v, want ErrInvalid", err)
}
if _, err := s.SetPreferences(ctx, "", domain.UserPreferences{}); !errors.Is(err, domain.ErrInvalid) {
t.Errorf("SetPreferences with empty subject = %v, want ErrInvalid", err)
}
}
// TestPreferencesSurviveLogin matters because UpsertUser rewrites the users row on
// every login, which is exactly why preferences are not columns on it.
func TestPreferencesSurviveLogin(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
publishTheme(t, s, "midnight")
if _, err := s.SetPreferences(ctx, "user-1", domain.UserPreferences{
SelectedTheme: "midnight",
SelectedAppearance: domain.AppearanceDark,
}); err != nil {
t.Fatalf("set preferences: %v", err)
}
for range 2 {
if err := s.UpsertUser(ctx, &domain.Identity{
Subject: "user-1", Email: "u@example.test", Roles: []domain.Role{domain.RoleViewer},
}); err != nil {
t.Fatalf("upsert user: %v", err)
}
}
read, err := s.Preferences(ctx, "user-1")
if err != nil {
t.Fatalf("read preferences: %v", err)
}
if read.SelectedTheme != "midnight" || read.SelectedAppearance != domain.AppearanceDark {
t.Fatalf("preferences = %+v, want them preserved across logins", read)
}
}
func TestThemePreferenceCount(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
publishTheme(t, s, "midnight")
for _, subject := range []string{"a", "b"} {
if _, err := s.SetPreferences(ctx, subject, domain.UserPreferences{
SelectedTheme: "midnight",
SelectedAppearance: domain.AppearanceLight,
}); err != nil {
t.Fatalf("set preferences for %s: %v", subject, err)
}
}
count, err := s.ThemePreferenceCount(ctx, "midnight")
if err != nil {
t.Fatalf("count: %v", err)
}
if count != 2 {
t.Fatalf("count = %d, want 2", count)
}
if _, err := s.ThemePreferenceCount(ctx, "Not A Slug"); !errors.Is(err, domain.ErrInvalid) {
t.Errorf("ThemePreferenceCount with a bad slug = %v, want ErrInvalid", err)
}
}
// publishTheme creates a theme and publishes it, returning the stored value.
//
// StatusPublished is a constant rather than a variable, so the pointer
// UpdateFields wants has to be taken from a local.
func publishTheme(t *testing.T, s *Store, slug string) *domain.Theme {
t.Helper()
ctx := context.Background()
if err := s.Create(ctx, sampleTheme(slug), ""); err != nil {
t.Fatalf("create theme %s: %v", slug, err)
}
published := domain.StatusPublished
theme, err := s.Update(ctx, slug, UpdateFields{Status: &published})
if err != nil {
t.Fatalf("publish theme %s: %v", slug, err)
}
return theme
}