// NOTE: These tests use a testableService that reimplements service logic // with mock repositories. This means tests can pass even if the real service // diverges. For full coverage, consider refactoring services to use repository // interfaces so the real service can be tested with mock repositories injected. package announcement import ( "fmt" "testing" "time" "gorm.io/gorm" ) // --------------------------------------------------------------------------- // Mock repository — implements the same methods that Service calls on *Repository. // We embed it into a real *Repository via a wrapper approach. // Since Service uses concrete *Repository, we create a repositoryInterface and // a testableService that mirrors Service but uses the interface. // --------------------------------------------------------------------------- type repositoryInterface interface { FindAll() ([]Announcement, error) FindByID(id uint) (*Announcement, error) Create(a *Announcement) error Save(a *Announcement) error Delete(id uint) error } type testableService struct { repo repositoryInterface } func (s *testableService) GetAll() ([]Announcement, error) { return s.repo.FindAll() } func (s *testableService) Create(title, content string) (*Announcement, error) { a := &Announcement{Title: title, Content: content} return a, s.repo.Create(a) } func (s *testableService) Update(id uint, title, content string) (*Announcement, error) { a, err := s.repo.FindByID(id) if err != nil { return nil, fmt.Errorf("공지사항을 찾을 수 없습니다") } if title != "" { a.Title = title } if content != "" { a.Content = content } return a, s.repo.Save(a) } func (s *testableService) Delete(id uint) error { if _, err := s.repo.FindByID(id); err != nil { return fmt.Errorf("공지사항을 찾을 수 없습니다") } return s.repo.Delete(id) } // --------------------------------------------------------------------------- // Mock implementation // --------------------------------------------------------------------------- type mockRepo struct { announcements map[uint]*Announcement nextID uint findAllErr error createErr error saveErr error deleteErr error } func newMockRepo() *mockRepo { return &mockRepo{ announcements: make(map[uint]*Announcement), nextID: 1, } } func (m *mockRepo) FindAll() ([]Announcement, error) { if m.findAllErr != nil { return nil, m.findAllErr } result := make([]Announcement, 0, len(m.announcements)) for _, a := range m.announcements { result = append(result, *a) } return result, nil } func (m *mockRepo) FindByID(id uint) (*Announcement, error) { a, ok := m.announcements[id] if !ok { return nil, gorm.ErrRecordNotFound } // Return a copy so mutations don't affect the store until Save is called cp := *a return &cp, nil } func (m *mockRepo) Create(a *Announcement) error { if m.createErr != nil { return m.createErr } a.ID = m.nextID a.CreatedAt = time.Now() a.UpdatedAt = time.Now() m.nextID++ stored := *a m.announcements[a.ID] = &stored return nil } func (m *mockRepo) Save(a *Announcement) error { if m.saveErr != nil { return m.saveErr } a.UpdatedAt = time.Now() stored := *a m.announcements[a.ID] = &stored return nil } func (m *mockRepo) Delete(id uint) error { if m.deleteErr != nil { return m.deleteErr } delete(m.announcements, id) return nil } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- func TestGetAll_ReturnsAnnouncements(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} // Empty at first list, err := svc.GetAll() if err != nil { t.Fatalf("GetAll failed: %v", err) } if len(list) != 0 { t.Errorf("expected 0 announcements, got %d", len(list)) } // Add some _, _ = svc.Create("Title 1", "Content 1") _, _ = svc.Create("Title 2", "Content 2") list, err = svc.GetAll() if err != nil { t.Fatalf("GetAll failed: %v", err) } if len(list) != 2 { t.Errorf("expected 2 announcements, got %d", len(list)) } } func TestGetAll_ReturnsError(t *testing.T) { repo := newMockRepo() repo.findAllErr = fmt.Errorf("db connection error") svc := &testableService{repo: repo} _, err := svc.GetAll() if err == nil { t.Error("expected error from GetAll, got nil") } } func TestCreate_Success(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} a, err := svc.Create("Test Title", "Test Content") if err != nil { t.Fatalf("Create failed: %v", err) } if a.Title != "Test Title" { t.Errorf("Title = %q, want %q", a.Title, "Test Title") } if a.Content != "Test Content" { t.Errorf("Content = %q, want %q", a.Content, "Test Content") } if a.ID == 0 { t.Error("expected non-zero ID after Create") } } func TestCreate_EmptyTitle(t *testing.T) { // The current service does not validate title presence — it delegates to the DB. // This test documents that behavior: an empty title goes through to the repo. repo := newMockRepo() svc := &testableService{repo: repo} a, err := svc.Create("", "Some content") if err != nil { t.Fatalf("Create failed: %v", err) } if a.Title != "" { t.Errorf("Title = %q, want empty", a.Title) } } func TestCreate_RepoError(t *testing.T) { repo := newMockRepo() repo.createErr = fmt.Errorf("insert failed") svc := &testableService{repo: repo} _, err := svc.Create("Title", "Content") if err == nil { t.Error("expected error when repo returns error, got nil") } } func TestUpdate_Success(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} created, _ := svc.Create("Original Title", "Original Content") updated, err := svc.Update(created.ID, "New Title", "New Content") if err != nil { t.Fatalf("Update failed: %v", err) } if updated.Title != "New Title" { t.Errorf("Title = %q, want %q", updated.Title, "New Title") } if updated.Content != "New Content" { t.Errorf("Content = %q, want %q", updated.Content, "New Content") } } func TestUpdate_PartialUpdate(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} created, _ := svc.Create("Original Title", "Original Content") // Update only title (empty content means keep existing) updated, err := svc.Update(created.ID, "New Title", "") if err != nil { t.Fatalf("Update failed: %v", err) } if updated.Title != "New Title" { t.Errorf("Title = %q, want %q", updated.Title, "New Title") } if updated.Content != "Original Content" { t.Errorf("Content = %q, want %q (should be unchanged)", updated.Content, "Original Content") } } func TestUpdate_NotFound(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} _, err := svc.Update(999, "Title", "Content") if err == nil { t.Error("expected error updating non-existent announcement, got nil") } } func TestDelete_Success(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} created, _ := svc.Create("To Delete", "Content") err := svc.Delete(created.ID) if err != nil { t.Fatalf("Delete failed: %v", err) } // Verify it's gone list, _ := svc.GetAll() if len(list) != 0 { t.Errorf("expected 0 announcements after delete, got %d", len(list)) } } func TestDelete_NotFound(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} err := svc.Delete(999) if err == nil { t.Error("expected error deleting non-existent announcement, got nil") } } func TestDelete_RepoError(t *testing.T) { repo := newMockRepo() svc := &testableService{repo: repo} created, _ := svc.Create("Title", "Content") repo.deleteErr = fmt.Errorf("delete failed") err := svc.Delete(created.ID) if err == nil { t.Error("expected error when repo delete fails, got nil") } }