package test import ( "RenewCMS/internal/domain/article" imageDomain "RenewCMS/internal/domain/image" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "errors" "fmt" "reflect" "strings" "testing" "time" "gorm.io/gorm" ) 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) title := strings.Repeat("a", tc.length) result, err := uc.CreateArticle(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) _, err := uc.CreateArticle(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) result, err := uc.GetArticle(1) 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) result, err := uc.GetArticle(2) 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) _, err := uc.GetArticle(9) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("GetOnlineArticle_1", func(t *testing.T) { repo := newRepo() uc := useCases.NewGetArticleUseCase(repo) result, err := uc.GetOnlineArticle(1) 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) _, err := uc.GetOnlineArticle(2) 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) _, err := uc.GetOnlineArticle(9) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } if errors.Is(err, article.ErrArticleNotFound) { t.Errorf("expected the raw not-found error, not article.ErrArticleNotFound") } }) } 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) 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) _, err := uc.GetArticleByName("Unknown Title") if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, 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) result, err := uc.ListOnlineArticles() 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) result, err := uc.ListAllArticles() 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) result, err := uc.ListOnlineArticles() 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"} repo.images[1] = &imageDomain.Image{ID: 1, Path: "/static/one.png"} return repo } t.Run("UpdateBody_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo) result, err := uc.UpdateBody(1, "New body") 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) _, err := uc.UpdateBody(9, "New body") if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("UpdateIsOnline_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo) result, err := uc.UpdateIsOnline(1, true) 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) _, err := uc.UpdateIsOnline(9, true) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("AddImage_article1_image1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo) err := uc.AddImage(1, 1) if err != nil { t.Fatalf("expected no error, got %v", err) } updated, err := repo.Get(1) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(updated.Images) != 1 || updated.Images[0].ID != 1 { t.Errorf("expected image 1 attached, got %+v", updated.Images) } }) t.Run("AddImage_article9_image1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo) err := uc.AddImage(9, 1) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("AddImage_article1_image9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateArticleUseCase(repo) err := uc.AddImage(1, 9) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) } func TestDeleteArticleSucceedsSilently(t *testing.T) { for _, id := range []uint32{1, 9} { t.Run(fmt.Sprintf("id_%d", id), func(t *testing.T) { repo := newMockArticleRepo() repo.articles[1] = article.Article{ID: 1} uc := useCases.NewDeleteArticleUseCase(repo) err := uc.DeleteArticle(id) if err != nil { t.Errorf("expected no error, 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) err := uc.DeleteArticle(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) result, err := uc.CreateImage(imageDomain.ImageInput{ Content: strings.NewReader("fake-image-data"), 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) _, err := uc.CreateImage(imageDomain.ImageInput{ Content: strings.NewReader("data"), 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.NewDeleteImageUseCase(repo) err := uc.DeleteImage(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.NewDeleteImageUseCase(repo) err := uc.DeleteImage(9) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) } func TestCreateUserAssignsVerificationCode(t *testing.T) { repo := newMockUserRepo() uc := useCases.NewCreateUserUseCase(repo) before := time.Now() result, err := uc.CreateUser(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(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) _, err := uc.CreateUser(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, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, 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, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, 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, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, 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 TestUpdateUserDelegatesEachOperation(t *testing.T) { newRepo := func() *mockUserRepo { repo := newMockUserRepo() repo.users[1] = user.User{ID: 1} return repo } t.Run("UpdateVerificationStatus_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) result, err := uc.UpdateVerificationStatus(1, true) if err != nil { t.Fatalf("expected no error, got %v", err) } if result.ID != 1 || !result.IsVerified { t.Errorf("unexpected result: %+v", result) } }) t.Run("UpdateVerificationStatus_id9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) _, err := uc.UpdateVerificationStatus(9, true) if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("UpdatePassword_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) result, err := uc.UpdatePassword(1, "newpassword123") 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("UpdatePassword_id9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) _, err := uc.UpdatePassword(9, "newpassword123") if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) t.Run("UpdatePasswordResetCode_id1", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) result, err := uc.UpdatePasswordResetCode(1, "reset-code-123") 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("UpdatePasswordResetCode_id9", func(t *testing.T) { repo := newRepo() uc := useCases.NewUpdateUserUseCase(repo) _, err := uc.UpdatePasswordResetCode(9, "reset-code-123") if !errors.Is(err, gorm.ErrRecordNotFound) { t.Errorf("expected gorm.ErrRecordNotFound, got %v", err) } }) } func TestDeleteUserSucceedsSilently(t *testing.T) { for _, id := range []uint32{1, 9} { t.Run(fmt.Sprintf("id_%d", id), func(t *testing.T) { repo := newMockUserRepo() repo.users[1] = user.User{ID: 1} uc := useCases.NewDeleteUserUseCase(repo) err := uc.DeleteUser(id) if err != nil { t.Errorf("expected no error, got %v", err) } }) } } 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)) } }