package site_test import ( "context" "testing" "github.com/google/uuid" "backend/internal/modules/site" ) // fakeRepository is an in-memory site.Repository backing the single // site_settings row, used to unit-test site.Service without a real database. type fakeRepository struct { settings site.Settings } func newFakeRepository() *fakeRepository { return &fakeRepository{settings: site.Settings{ID: 1}} } func strPtr(s string) *string { return &s } func boolPtr(b bool) *bool { return &b } func (r *fakeRepository) Get(_ context.Context) (*site.Settings, error) { cp := r.settings return &cp, nil } func (r *fakeRepository) Update(_ context.Context, s *site.Settings) error { r.settings.Name = s.Name r.settings.Description = s.Description r.settings.OrdersEnabled = s.OrdersEnabled return nil } func (r *fakeRepository) UpdateAppearance(_ context.Context, s *site.Settings) error { r.settings.HeaderBgColor = s.HeaderBgColor r.settings.HeaderTextColor = s.HeaderTextColor r.settings.BodyBgColor = s.BodyBgColor r.settings.BodyTextColor = s.BodyTextColor r.settings.FooterBgColor = s.FooterBgColor r.settings.FooterTextColor = s.FooterTextColor r.settings.AccentColor = s.AccentColor r.settings.ProductLayout = s.ProductLayout r.settings.ProductColumns = s.ProductColumns r.settings.ProductScroll = s.ProductScroll r.settings.ContactCardTransparent = s.ContactCardTransparent r.settings.ContactCardBgColor = s.ContactCardBgColor r.settings.LogoMediaID = s.LogoMediaID r.settings.HeroMediaID = s.HeroMediaID r.settings.HeroPages = s.HeroPages return nil } func (r *fakeRepository) UpdateCustomerAuth(_ context.Context, s *site.Settings) error { r.settings.CustomerLoginEnabled = s.CustomerLoginEnabled r.settings.CustomerRegistrationEnabled = s.CustomerRegistrationEnabled r.settings.CustomerVerificationRequired = s.CustomerVerificationRequired r.settings.VerificationContactLinkID = s.VerificationContactLinkID return nil } func TestService_Update_PersistsFields(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() updated, err := svc.Update(ctx, "My Shop", "A small shop", true) if err != nil { t.Fatalf("Update() error = %v", err) } if updated.Name != "My Shop" || updated.Description != "A small shop" || !updated.OrdersEnabled { t.Fatalf("Update() = %+v, fields not persisted as expected", updated) } fetched, err := svc.Get(ctx) if err != nil { t.Fatalf("Get() error = %v", err) } if fetched.Name != "My Shop" { t.Fatalf("Get() after Update() = %+v, want name %q", fetched, "My Shop") } } func TestService_UpdateAppearance_DoesNotRequireIdentityFields(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() // The Appearance admin page never touches name/description, so it must // be able to save even when the site identity was never configured // (name still empty) -- this used to fail because both concerns shared // one PUT that validated identity fields on every save. updated, err := svc.UpdateAppearance(ctx, site.AppearanceInput{ HeaderBgColor: strPtr("#111111"), HeaderTextColor: strPtr("#ffffff"), BodyBgColor: strPtr("#222222"), BodyTextColor: strPtr("#eeeeee"), FooterBgColor: strPtr("#333333"), FooterTextColor: strPtr("#dddddd"), AccentColor: strPtr("#ff00ff"), ProductLayout: strPtr("list"), }) if err != nil { t.Fatalf("UpdateAppearance() error = %v", err) } if updated.AccentColor != "#ff00ff" || updated.ProductLayout != "list" { t.Fatalf("UpdateAppearance() = %+v, fields not persisted as expected", updated) } if updated.Name != "" { t.Fatalf("UpdateAppearance() = %+v, should not touch identity fields", updated) } } func TestService_UpdateAppearance_PartialUpdateKeepsOtherFields(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() _, err := svc.UpdateAppearance(ctx, site.AppearanceInput{ HeaderBgColor: strPtr("#111111"), HeaderTextColor: strPtr("#ffffff"), BodyBgColor: strPtr("#222222"), BodyTextColor: strPtr("#eeeeee"), FooterBgColor: strPtr("#333333"), FooterTextColor: strPtr("#dddddd"), AccentColor: strPtr("#ff00ff"), ProductLayout: strPtr("grid"), }) if err != nil { t.Fatalf("UpdateAppearance() error = %v", err) } // The admin only changes the product layout; every color must keep its // previously saved value, not be wiped out. updated, err := svc.UpdateAppearance(ctx, site.AppearanceInput{ProductLayout: strPtr("list")}) if err != nil { t.Fatalf("UpdateAppearance() partial error = %v", err) } if updated.ProductLayout != "list" { t.Fatalf("UpdateAppearance() partial = %+v, want product_layout list", updated) } if updated.AccentColor != "#ff00ff" || updated.HeaderBgColor != "#111111" { t.Fatalf("UpdateAppearance() partial = %+v, unrelated fields should be unchanged", updated) } } func TestService_UpdateAppearance_LogoAndHeroClearedOnlyWhenExplicit(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() logoID := uuid.New() updated, err := svc.UpdateAppearance(ctx, site.AppearanceInput{ LogoMediaIDSet: true, LogoMediaID: &logoID, HeroPages: strPtr("home,contact"), }) if err != nil { t.Fatalf("UpdateAppearance() error = %v", err) } if updated.LogoMediaID == nil || *updated.LogoMediaID != logoID { t.Fatalf("UpdateAppearance() = %+v, want logo set", updated) } if updated.HeroPages != "home,contact" { t.Fatalf("UpdateAppearance() = %+v, want hero_pages persisted", updated) } // Saving an unrelated field (no LogoMediaIDSet) must not wipe the logo. updated, err = svc.UpdateAppearance(ctx, site.AppearanceInput{ProductLayout: strPtr("list")}) if err != nil { t.Fatalf("UpdateAppearance() second call error = %v", err) } if updated.LogoMediaID == nil || *updated.LogoMediaID != logoID { t.Fatalf("UpdateAppearance() = %+v, logo should survive an unrelated save", updated) } // Explicitly clearing the logo (Set=true, ID=nil) must remove it. updated, err = svc.UpdateAppearance(ctx, site.AppearanceInput{LogoMediaIDSet: true, LogoMediaID: nil}) if err != nil { t.Fatalf("UpdateAppearance() clear error = %v", err) } if updated.LogoMediaID != nil { t.Fatalf("UpdateAppearance() = %+v, want logo cleared", updated) } } func TestService_UpdateCustomerAuth_PartialUpdateKeepsOtherFields(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() linkID := uuid.New() _, err := svc.UpdateCustomerAuth(ctx, site.CustomerAuthInput{ LoginEnabled: boolPtr(true), VerificationRequired: boolPtr(true), VerificationContactLinkSet: true, VerificationContactLinkID: &linkID, }) if err != nil { t.Fatalf("UpdateCustomerAuth() error = %v", err) } // Flipping just login_enabled must not disturb the verification // requirement or the chosen contact link. updated, err := svc.UpdateCustomerAuth(ctx, site.CustomerAuthInput{LoginEnabled: boolPtr(false)}) if err != nil { t.Fatalf("UpdateCustomerAuth() partial error = %v", err) } if updated.CustomerLoginEnabled { t.Fatalf("UpdateCustomerAuth() partial = %+v, want login disabled", updated) } if !updated.CustomerVerificationRequired { t.Fatalf("UpdateCustomerAuth() partial = %+v, verification_required should be unchanged", updated) } if updated.VerificationContactLinkID == nil || *updated.VerificationContactLinkID != linkID { t.Fatalf("UpdateCustomerAuth() partial = %+v, contact link should be unchanged", updated) } } func TestService_UpdateCustomerAuth_CanClearContactLink(t *testing.T) { svc := site.NewService(newFakeRepository()) ctx := context.Background() linkID := uuid.New() _, err := svc.UpdateCustomerAuth(ctx, site.CustomerAuthInput{VerificationContactLinkSet: true, VerificationContactLinkID: &linkID}) if err != nil { t.Fatalf("UpdateCustomerAuth() error = %v", err) } updated, err := svc.UpdateCustomerAuth(ctx, site.CustomerAuthInput{VerificationContactLinkSet: true, VerificationContactLinkID: nil}) if err != nil { t.Fatalf("UpdateCustomerAuth() clear error = %v", err) } if updated.VerificationContactLinkID != nil { t.Fatalf("UpdateCustomerAuth() clear = %+v, want nil contact link", updated) } } func TestService_Get_DefaultsBeforeAnyUpdate(t *testing.T) { svc := site.NewService(newFakeRepository()) settings, err := svc.Get(context.Background()) if err != nil { t.Fatalf("Get() error = %v", err) } if settings.Name != "" { t.Fatalf("Get() before any update = %+v, want empty defaults", settings) } }