package test import ( "RenewCMS/internal/domain/article" imageDomain "RenewCMS/internal/domain/image" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "bytes" "errors" "fmt" "reflect" "strings" "testing" "time" ) func TestCreateArticleTitleLengthLimit(t *testing.T) { cases := []struct { length int expectValid bool }{ {0, false}, {2, false}, {3, true}, {200, true}, {201, false}, } for _, tc := range cases { t.Run(fmt.Sprintf("length_%d", tc.length), func(t *testing.T) { repo := newMockArticleRepo() uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) title := strings.Repeat("a", tc.length) result, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: title, Body: "Some body text"}) if tc.expectValid { if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID == 0 { t.Errorf("expected repository-assigned id, got 0") } if result.IsOnline { t.Errorf("expected is_online false, got true") } if len(repo.articles) != 1 { t.Errorf("expected 1 article stored, got %d", len(repo.articles)) } } else { if !errors.Is(err, article.ErrInvalidTitle) { t.Errorf("expected article.ErrInvalidTitle, got %v", err) } if len(repo.articles) != 0 { t.Errorf("expected no article stored, got %d", len(repo.articles)) } } }) } } func TestCreateArticlePropagatesRepositoryFailure(t *testing.T) { repo := newMockArticleRepo() repo.fail.failNext("Create") uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) _, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: "Backyard Renovation Update", Body: "Some body text"}) if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected repository failure, got %v", err) } if len(repo.articles) != 0 { t.Errorf("expected no article stored, got %d", len(repo.articles)) } } func TestGetArticleAndGetOnlineArticle(t *testing.T) { newRepo := func() *mockArticleRepo { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1, Title: "Online Article", IsOnline: true} repo.articles[2] = article.Article{ID: 2, Title: "Offline Article", IsOnline: false} return repo } t.Run("GetArticle_1", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) result, err := uc.GetArticle(1, article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected article id 1, got %d", result.ID) } }) t.Run("GetArticle_2", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) result, err := uc.GetArticle(2, article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 2 { t.Errorf("expected article id 2, got %d", result.ID) } }) t.Run("GetArticle_9", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) _, err := uc.GetArticle(9, article.BodyFormatHTML) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) t.Run("GetOnlineArticle_1", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) result, err := uc.GetOnlineArticle(1, article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected article id 1, got %d", result.ID) } }) t.Run("GetOnlineArticle_2", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) _, err := uc.GetOnlineArticle(2, article.BodyFormatHTML) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) t.Run("GetOnlineArticle_9", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) _, err := uc.GetOnlineArticle(9, article.BodyFormatHTML) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) } func TestGetArticleByNameDelegatesVerbatim(t *testing.T) { newRepo := func() *mockArticleRepo { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1, Title: "Backyard Renovation Update"} return repo } t.Run("Backyard_Renovation_Update", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) result, err := uc.GetArticleByName("Backyard Renovation Update") if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected article id 1, got %d", result.ID) } }) t.Run("Unknown_Title", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}) _, err := uc.GetArticleByName("Unknown Title") if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) } func assertArticleIds(t *testing.T, articles []article.Article, expected []uint32) { t.Helper() if len(articles) != len(expected) { t.Fatalf("expected %d articles, got %d", len(expected), len(articles)) } for i, id := range expected { if articles[i].ID != id { t.Errorf("expected article id %d at index %d, got %d", id, i, articles[i].ID) } } } func TestListArticlesFiltersByIsOnline(t *testing.T) { newRepo := func() *mockArticleRepo { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1, Title: "A", IsOnline: true} repo.articles[2] = article.Article{ID: 2, Title: "B", IsOnline: false} repo.articles[3] = article.Article{ID: 3, Title: "C", IsOnline: true} return repo } t.Run("ListOnlineArticles", func(t *testing.T) { repo := newRepo() uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}) result, err := uc.ListOnlineArticles(article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } assertArticleIds(t, result, []uint32{1, 3}) }) t.Run("ListAllArticles", func(t *testing.T) { repo := newRepo() uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}) result, err := uc.ListAllArticles(article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } assertArticleIds(t, result, []uint32{1, 2, 3}) }) } func TestListOnlineArticlesEmptyRepositoryReturnsEmptySlice(t *testing.T) { repo := newMockArticleRepo() uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}) result, err := uc.ListOnlineArticles(article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result == nil { t.Errorf("expected an empty slice, got nil") } if len(result) != 0 { t.Errorf("expected an empty slice, got %d items", len(result)) } } func TestUpdateArticleDelegatesEachOperation(t *testing.T) { newRepo := func() *mockArticleRepo { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1, Title: "Existing"} return repo } t.Run("UpdateBody_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) result, err := uc.UpdateBody(testCtx, 1, "New body", article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 || result.Body != "New body" { t.Errorf("unexpected result: %+v", result) } }) t.Run("UpdateBody_id9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) _, err := uc.UpdateBody(testCtx, 9, "New body", article.BodyFormatHTML) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) t.Run("UpdateIsOnline_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) result, err := uc.UpdateIsOnline(testCtx, 1, true, article.BodyFormatHTML) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 || !result.IsOnline { t.Errorf("unexpected result: %+v", result) } }) t.Run("UpdateIsOnline_id9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder) _, err := uc.UpdateIsOnline(testCtx, 9, true, article.BodyFormatHTML) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) } func TestDeleteArticleReportsMissingArticle(t *testing.T) { t.Run("id_1", func(t *testing.T) { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1} uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder) err := uc.DeleteArticle(testCtx, 1) if err != nil { t.Errorf("expected no error, got %v", err) } }) t.Run("id_9", func(t *testing.T) { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1} uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder) err := uc.DeleteArticle(testCtx, 9) if !errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected article.ErrArticleNotFound, got %v", err) } }) } func TestDeleteArticlePropagatesRepositoryFailure(t *testing.T) { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1} repo.fail.failNext("Delete") uc := useCases.NewDeleteArticleUseCase(repo, discardRecorder) err := uc.DeleteArticle(testCtx, 1) if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected repository failure, got %v", err) } } func TestCreateImageDelegatesContentTypeCheck(t *testing.T) { cases := []struct { contentType string valid bool }{ {"image/png", true}, {"image/jpeg", true}, {"image/webp", true}, {"image/svg+xml", true}, {"image/gif", false}, } for _, tc := range cases { t.Run(strings.ReplaceAll(tc.contentType, "/", "_"), func(t *testing.T) { repo := newMockImageRepo() uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder) content := []byte("fake-image-data") if tc.valid { content = validBytesFor(t, tc.contentType) } result, err := uc.CreateImage(testCtx, imageDomain.ImageInput{ Content: bytes.NewReader(content), Filename: "test.img", ContentType: tc.contentType, }) if tc.valid { if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID == 0 { t.Errorf("expected repository-assigned id, got 0") } } else { if err == nil { t.Errorf("expected an unsupported-content-type error, got nil") } if len(repo.images) != 0 { t.Errorf("expected no image stored, got %d", len(repo.images)) } } }) } } func TestCreateImagePropagatesRepositoryFailure(t *testing.T) { repo := newMockImageRepo() repo.fail.failNext("Create") uc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder) _, err := uc.CreateImage(testCtx, imageDomain.ImageInput{ Content: bytes.NewReader(pngBytes(t, 3, 2)), Filename: "test.png", ContentType: "image/png", }) if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected repository failure, got %v", err) } } func TestDeleteImageRequiresExistence(t *testing.T) { t.Run("id_1", func(t *testing.T) { repo := newMockImageRepo() repo.images[1] = imageDomain.Image{ID: 1} uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), newMockVariantFileRepo(), discardRecorder) err := uc.Delete(testCtx, 1) if err != nil { t.Fatalf("expected no error, got %v", err) } if _, exists := repo.images[1]; exists { t.Errorf("expected the image to be removed from the fake") } }) t.Run("id_9", func(t *testing.T) { repo := newMockImageRepo() repo.images[1] = imageDomain.Image{ID: 1} uc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), newMockVariantFileRepo(), discardRecorder) err := uc.Delete(testCtx, 9) if !errors.Is(err, imageDomain.ErrImageNotFound) { t.Errorf("expected image.ErrImageNotFound, got %v", err) } }) } func TestCreateUserAssignsVerificationCode(t *testing.T) { repo := newMockUserRepo() uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder) before := time.Now() result, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{ Username: "alice", Password: "hunter2example", Email: "alice@example.com", }) after := time.Now() if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID == 0 { t.Errorf("expected repository-assigned id, got 0") } if result.IsVerified { t.Errorf("expected is_verified false") } if result.VerificationCode == "" { t.Errorf("expected a generated verification code") } if result.PasswordResetCode != "" { t.Errorf("expected empty password_reset_code, got %q", result.PasswordResetCode) } minExpiration := before.Add(2 * time.Hour).Add(-time.Minute) maxExpiration := after.Add(2 * time.Hour).Add(time.Minute) if result.VerificationExpiration.Before(minExpiration) || result.VerificationExpiration.After(maxExpiration) { t.Errorf("expected verification_expiration about 2 hours from now, got %v", result.VerificationExpiration) } second, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{ Username: "bob", Password: "hunter2example", Email: "bob@example.com", }) if err != nil { t.Fatalf("expected no error, got %v", err) } if second.VerificationCode == result.VerificationCode { t.Errorf("expected a distinct verification code from the previous call") } } func TestCreateUserPropagatesRepositoryFailure(t *testing.T) { repo := newMockUserRepo() repo.fail.failNext("Create") uc := useCases.NewCreateUserUseCase(repo, &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder) _, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{ Username: "alice", Password: "hunter2example", Email: "alice@example.com", }) if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected repository failure, got %v", err) } } func TestGetUserReadsByIdUsernameOrEmail(t *testing.T) { newRepo := func() *mockUserRepo { repo := newMockUserRepo() repo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com"} return repo } t.Run("GetUser_1", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) result, err := uc.GetUser(1) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected user id 1, got %d", result.ID) } }) t.Run("GetUser_9", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) _, err := uc.GetUser(9) if !errors.Is(err, user.ErrUserNotFound) { t.Errorf("expected user.ErrUserNotFound, got %v", err) } }) t.Run("GetUserByUsername_alice", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) result, err := uc.GetUserByUsername("alice") if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected user id 1, got %d", result.ID) } }) t.Run("GetUserByUsername_bob", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) _, err := uc.GetUserByUsername("bob") if !errors.Is(err, user.ErrUserNotFound) { t.Errorf("expected user.ErrUserNotFound, got %v", err) } }) t.Run("GetUserByEmail_alice", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) result, err := uc.GetUserByEmail("alice@example.com") if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 { t.Errorf("expected user id 1, got %d", result.ID) } }) t.Run("GetUserByEmail_bob", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetUserUseCase(repo) _, err := uc.GetUserByEmail("bob@example.com") if !errors.Is(err, user.ErrUserNotFound) { t.Errorf("expected user.ErrUserNotFound, got %v", err) } }) } func TestListUsersReturnsEveryUser(t *testing.T) { repo := newMockUserRepo() repo.users[1] = user.User{ID: 1} repo.users[2] = user.User{ID: 2} uc := useCases.NewListUsersUseCase(repo) result := uc.ListUsers() if len(result) != 2 { t.Fatalf("expected 2 users, got %d", len(result)) } ids := map[uint32]bool{result[0].ID: true, result[1].ID: true} if !ids[1] || !ids[2] { t.Errorf("expected users with ids 1 and 2, got %+v", result) } } func TestSendMailForwardsArgumentsVerbatim(t *testing.T) { cases := []struct { receiver string template string }{ {"alice@example.com", "verify-email"}, {"bob@example.com", "password-reset"}, } for _, tc := range cases { t.Run(tc.receiver+"_"+tc.template, func(t *testing.T) { repo := newMockMailRepo() uc := useCases.NewSendMailUseCase(repo) data := map[string]string{"key": "value"} err := uc.SendMail(tc.receiver, tc.template, data) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(repo.SentEmails) != 1 { t.Fatalf("expected exactly one recorded send, got %d", len(repo.SentEmails)) } sent := repo.SentEmails[0] if sent.Address != tc.receiver || sent.Template != tc.template { t.Errorf("unexpected recorded send: %+v", sent) } if !reflect.DeepEqual(sent.Data, data) { t.Errorf("expected recorded data %+v, got %+v", data, sent.Data) } }) } } func TestSendMailPropagatesRepositoryFailureWithoutRecording(t *testing.T) { repo := newMockMailRepo() repo.fail.failNext("Send") uc := useCases.NewSendMailUseCase(repo) err := uc.SendMail("carol@example.com", "welcome", nil) if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected repository failure, got %v", err) } if len(repo.SentEmails) != 0 { t.Errorf("expected no send recorded, got %d", len(repo.SentEmails)) } }