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
262 lines
8 KiB
Go
262 lines
8 KiB
Go
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
|
|
}
|