package test import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/site" "RenewCMS/internal/infrastructure/useCases" "errors" "strings" "testing" ) func TestSiteDomainRules(t *testing.T) { t.Run("NormalizeBaseURL accepts what the rule allows and strips the trailing slash", func(t *testing.T) { accepted := map[string]string{ "https://shop.example.com": "https://shop.example.com", "https://shop.example.com/": "https://shop.example.com", "https://SHOP.example.com:8443": "https://shop.example.com:8443", "http://localhost": "http://localhost", "http://[::1]:8080/": "http://[::1]:8080", "HTTPS://shop.example.com": "https://shop.example.com", } for sent, stored := range accepted { if got, err := site.NormalizeBaseURL(sent); err != nil || got != stored { t.Errorf("%q gave %q, %v; expected %q", sent, got, err, stored) } } }) t.Run("NormalizeBaseURL refuses everything else", func(t *testing.T) { for _, sent := range []string{"", "http://shop.example.com", "https://", "https://shop.example.com/x", "https://shop.example.com?x", "https://shop.example.com/#top", "https://u:p@shop.example.com", "ftp://shop.example.com", "shop.example.com", "localhost:8080", "https://shop example.com"} { if got, err := site.NormalizeBaseURL(sent); !errors.Is(err, site.ErrInvalidSiteURL) { t.Errorf("%q gave %q, %v; expected ErrInvalidSiteURL", sent, got, err) } } }) t.Run("SeedBaseURL drops the path and upgrades non-loopback http", func(t *testing.T) { accepted := map[string]string{ "https://www.example.com/blog": "https://www.example.com", "http://www.example.com": "https://www.example.com", "http://localhost:5173": "http://localhost:5173", "http://[::1]:8080/x": "http://[::1]:8080", "https://user@www.example.com": "https://www.example.com", } for sent, stored := range accepted { if got, err := site.SeedBaseURL(sent); err != nil || got != stored { t.Errorf("%q gave %q, %v; expected %q", sent, got, err, stored) } } for _, sent := range []string{"", "://not a url", "ftp://example.com", "example.com"} { if _, err := site.SeedBaseURL(sent); err == nil { t.Errorf("%q must be refused", sent) } } }) t.Run("NormalizeLocale keeps valid tags and turns empty into none", func(t *testing.T) { for _, locale := range []string{"en", "fr-CA", "zh-Hant-TW", "yue"} { if got, err := site.NormalizeLocale(&locale); err != nil || got == nil || *got != locale { t.Errorf("%q gave %v, %v", locale, got, err) } } empty := "" if got, err := site.NormalizeLocale(&empty); err != nil || got != nil { t.Errorf("empty gave %v, %v", got, err) } if got, err := site.NormalizeLocale(nil); err != nil || got != nil { t.Errorf("nil gave %v, %v", got, err) } for _, locale := range []string{"english", "fr_FR", "f", "FR", "fr-"} { if _, err := site.NormalizeLocale(&locale); !errors.Is(err, site.ErrInvalidLocale) { t.Errorf("%q must be refused", locale) } } }) t.Run("IsValidKey enforces the pattern and the length", func(t *testing.T) { valid := []string{"ab", "shop", "my-shop-2", "a1", strings.Repeat("a", 32)} invalid := []string{"", "a", "Shop", "-shop", "shop-", "my--shop", "my_shop", strings.Repeat("a", 33)} for _, key := range valid { if !site.IsValidKey(key) { t.Errorf("%q must be valid", key) } } for _, key := range invalid { if site.IsValidKey(key) { t.Errorf("%q must be invalid", key) } } }) } func TestSiteUseCaseRules(t *testing.T) { t.Run("Creating an article assigns it to the default site", func(t *testing.T) { repo := newMockArticleRepo() created, err := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()).CreateArticle(testCtx, useCases.CreateArticleCommand{Title: "Valid Title"}) if err != nil || created.SiteID != 1 { t.Errorf("created %+v, %v", created, err) } }) t.Run("Creating an article fails when no default site can be read", func(t *testing.T) { repo := newMockArticleRepo() sites := newMockSiteRepo(repo) uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, sites, plainAuthors()) _, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: "Valid Title"}) if !errors.Is(err, site.ErrSiteNotFound) || repo.createCalled { t.Errorf("err = %v, create called = %v", err, repo.createCalled) } }) t.Run("A destination site that does not exist leaves the article untouched", func(t *testing.T) { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1, Title: "Valid Title", SiteID: 1} sites := newMockSiteRepo(repo) sites.seed(1, "Main", "main", true) uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, sites, plainAuthors()) missing := uint32(9) _, err := uc.Update(testCtx, 1, useCases.UpdateArticleCommand{SiteID: &missing}) if !errors.Is(err, site.ErrSiteNotFound) || repo.articles[1].SiteID != 1 { t.Errorf("err = %v, stored %+v", err, repo.articles[1]) } }) t.Run("Deleting a site validates the destination before touching anything", func(t *testing.T) { repo := newMockArticleRepo() sites := newMockSiteRepo(repo) sites.seed(1, "Main", "main", true) sites.seed(2, "Shop", "shop", false) repo.articles[1] = article.Article{ID: 1, Title: "Valid Title", SiteID: 2} uc := useCases.NewDeleteSiteUseCase(sites, discardRecorder) self := uint32(2) for name, command := range map[string]useCases.DeleteSiteCommand{ "itself": {ID: 2, ReassignTo: &self}, "invalid": {ID: 2, ReassignInvalid: true}, "zero": {ID: 2, ReassignTo: idPointer(0)}, } { if err := uc.Delete(testCtx, command); !errors.Is(err, site.ErrInvalidReassign) { t.Errorf("%s: err = %v", name, err) } } if sites.deleteCalls != 0 { t.Errorf("the repository was called %d times", sites.deleteCalls) } }) t.Run("Editing a site with is_default is refused", func(t *testing.T) { repo := newMockArticleRepo() sites := newMockSiteRepo(repo) sites.seed(2, "Shop", "shop", false) uc := useCases.NewUpdateSiteUseCase(sites, newMockImageRepo(), discardRecorder) flag := true _, err := uc.Update(testCtx, 2, site.Patch{IsDefault: &flag}) if !errors.Is(err, site.ErrUseSetDefault) { t.Errorf("err = %v", err) } }) }