To gitea and beyond, let's go(-yco)

This commit is contained in:
2025-11-10 19:12:09 +01:00
parent 8f6133392d
commit 71a031342b
245 changed files with 83994 additions and 0 deletions

View File

@@ -0,0 +1,918 @@
package services
import (
"errors"
"fmt"
"strings"
"sync"
"testing"
"gorm.io/gorm"
"goyco/internal/database"
"goyco/internal/repositories"
)
type mockVoteRepo struct {
votes map[uint]*database.Vote
byUserPost map[string]*database.Vote
byVoteHash map[string]*database.Vote
nextID uint
createErr error
getByUserAndPostErr error
getByVoteHashErr error
getByPostIDErr error
updateErr error
deleteErr error
createCalls int
updateCalls int
deleteCalls int
mu sync.RWMutex
}
func newMockVoteRepo() *mockVoteRepo {
return &mockVoteRepo{
votes: make(map[uint]*database.Vote),
byUserPost: make(map[string]*database.Vote),
byVoteHash: make(map[string]*database.Vote),
nextID: 1,
}
}
func (m *mockVoteRepo) Create(vote *database.Vote) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.createErr != nil {
return m.createErr
}
var key string
if vote.UserID != nil {
key = m.key(*vote.UserID, vote.PostID)
} else if vote.VoteHash != nil {
key = *vote.VoteHash
} else {
return errors.New("vote must have either user_id or vote_hash")
}
if existingVote, exists := m.byUserPost[key]; exists {
existingVote.Type = vote.Type
existingVote.UpdatedAt = vote.UpdatedAt
vote.ID = existingVote.ID
return nil
}
vote.ID = m.nextID
m.nextID++
voteCopy := *vote
m.votes[vote.ID] = &voteCopy
m.byUserPost[key] = &voteCopy
if vote.VoteHash != nil {
m.byVoteHash[*vote.VoteHash] = &voteCopy
}
m.createCalls++
return nil
}
func (m *mockVoteRepo) CreateOrUpdate(vote *database.Vote) error {
return m.Create(vote)
}
func (m *mockVoteRepo) GetByID(id uint) (*database.Vote, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if vote, ok := m.votes[id]; ok {
voteCopy := *vote
return &voteCopy, nil
}
return nil, gorm.ErrRecordNotFound
}
func (m *mockVoteRepo) GetByUserAndPost(userID, postID uint) (*database.Vote, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.getByUserAndPostErr != nil {
return nil, m.getByUserAndPostErr
}
key := m.key(userID, postID)
if vote, ok := m.byUserPost[key]; ok {
voteCopy := *vote
return &voteCopy, nil
}
return nil, gorm.ErrRecordNotFound
}
func (m *mockVoteRepo) GetByVoteHash(voteHash string) (*database.Vote, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.getByVoteHashErr != nil {
return nil, m.getByVoteHashErr
}
if vote, ok := m.byVoteHash[voteHash]; ok {
voteCopy := *vote
return &voteCopy, nil
}
return nil, gorm.ErrRecordNotFound
}
func (m *mockVoteRepo) GetByPostID(postID uint) ([]database.Vote, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.getByPostIDErr != nil {
return nil, m.getByPostIDErr
}
var votes []database.Vote
for _, vote := range m.votes {
if vote.PostID == postID {
votes = append(votes, *vote)
}
}
return votes, nil
}
func (m *mockVoteRepo) GetByUserID(userID uint) ([]database.Vote, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var votes []database.Vote
for _, vote := range m.votes {
if vote.UserID != nil && *vote.UserID == userID {
votes = append(votes, *vote)
}
}
return votes, nil
}
func (m *mockVoteRepo) Update(vote *database.Vote) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.updateErr != nil {
return m.updateErr
}
if _, ok := m.votes[vote.ID]; !ok {
return gorm.ErrRecordNotFound
}
voteCopy := *vote
m.votes[vote.ID] = &voteCopy
var key string
if vote.UserID != nil {
key = m.key(*vote.UserID, vote.PostID)
m.byUserPost[key] = &voteCopy
}
if vote.VoteHash != nil {
m.byVoteHash[*vote.VoteHash] = &voteCopy
}
m.updateCalls++
return nil
}
func (m *mockVoteRepo) Delete(id uint) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.deleteErr != nil {
return m.deleteErr
}
vote, ok := m.votes[id]
if !ok {
return gorm.ErrRecordNotFound
}
delete(m.votes, id)
var key string
if vote.UserID != nil {
key = m.key(*vote.UserID, vote.PostID)
delete(m.byUserPost, key)
}
if vote.VoteHash != nil {
delete(m.byVoteHash, *vote.VoteHash)
}
m.deleteCalls++
return nil
}
func (m *mockVoteRepo) Count() (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return int64(len(m.votes)), nil
}
func (m *mockVoteRepo) CountByPostID(postID uint) (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
count := int64(0)
for _, vote := range m.votes {
if vote.PostID == postID {
count++
}
}
return count, nil
}
func (m *mockVoteRepo) CountByUserID(userID uint) (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
count := int64(0)
for _, vote := range m.votes {
if vote.UserID != nil && *vote.UserID == userID {
count++
}
}
return count, nil
}
func (m *mockVoteRepo) WithTx(tx *gorm.DB) repositories.VoteRepository {
return m
}
func (m *mockVoteRepo) key(userID, postID uint) string {
return fmt.Sprintf("%d:%d", userID, postID)
}
type mockPostRepo struct {
posts map[uint]*database.Post
nextID uint
getErr error
updateErr error
mu sync.RWMutex
}
func newMockPostRepo() *mockPostRepo {
return &mockPostRepo{
posts: make(map[uint]*database.Post),
nextID: 1,
}
}
func (m *mockPostRepo) GetByID(id uint) (*database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.getErr != nil {
return nil, m.getErr
}
if post, ok := m.posts[id]; ok {
postCopy := *post
return &postCopy, nil
}
return nil, gorm.ErrRecordNotFound
}
func (m *mockPostRepo) Update(post *database.Post) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.updateErr != nil {
return m.updateErr
}
if _, ok := m.posts[post.ID]; !ok {
return gorm.ErrRecordNotFound
}
postCopy := *post
m.posts[post.ID] = &postCopy
return nil
}
func (m *mockPostRepo) Create(post *database.Post) error {
m.mu.Lock()
defer m.mu.Unlock()
post.ID = m.nextID
m.nextID++
postCopy := *post
m.posts[post.ID] = &postCopy
return nil
}
func (m *mockPostRepo) GetByURL(url string) (*database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, post := range m.posts {
if post.URL == url {
postCopy := *post
return &postCopy, nil
}
}
return nil, gorm.ErrRecordNotFound
}
func (m *mockPostRepo) GetByAuthorID(authorID uint) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
for _, post := range m.posts {
if post.AuthorID != nil && *post.AuthorID == authorID {
posts = append(posts, *post)
}
}
return posts, nil
}
func (m *mockPostRepo) GetAll(limit, offset int) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
count := 0
for _, post := range m.posts {
if count >= offset && count < offset+limit {
posts = append(posts, *post)
}
count++
}
return posts, nil
}
func (m *mockPostRepo) Count() (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return int64(len(m.posts)), nil
}
func (m *mockPostRepo) Delete(id uint) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.posts[id]; !ok {
return gorm.ErrRecordNotFound
}
delete(m.posts, id)
return nil
}
func (m *mockPostRepo) GetByUserID(userID uint, limit, offset int) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
count := 0
for _, post := range m.posts {
if post.AuthorID != nil && *post.AuthorID == userID {
if count >= offset && count < offset+limit {
posts = append(posts, *post)
}
count++
}
}
return posts, nil
}
func (m *mockPostRepo) CountByUserID(userID uint) (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
count := int64(0)
for _, post := range m.posts {
if post.AuthorID != nil && *post.AuthorID == userID {
count++
}
}
return count, nil
}
func (m *mockPostRepo) GetTopPosts(limit int) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
count := 0
for _, post := range m.posts {
if count < limit {
posts = append(posts, *post)
count++
}
}
return posts, nil
}
func (m *mockPostRepo) GetNewestPosts(limit int) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
count := 0
for _, post := range m.posts {
if count < limit {
posts = append(posts, *post)
count++
}
}
return posts, nil
}
func (m *mockPostRepo) Search(query string, limit, offset int) ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
count := 0
for _, post := range m.posts {
if strings.Contains(strings.ToLower(post.Title), strings.ToLower(query)) {
if count >= offset && count < offset+limit {
posts = append(posts, *post)
}
count++
}
}
return posts, nil
}
func (m *mockPostRepo) GetPostsByDeletedUsers() ([]database.Post, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var posts []database.Post
for _, post := range m.posts {
if post.AuthorID == nil {
posts = append(posts, *post)
}
}
return posts, nil
}
func (m *mockPostRepo) HardDeletePostsByDeletedUsers() (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
count := int64(0)
for id, post := range m.posts {
if post.AuthorID == nil {
delete(m.posts, id)
count++
}
}
return count, nil
}
func (m *mockPostRepo) HardDeleteAll() (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
count := int64(len(m.posts))
m.posts = make(map[uint]*database.Post)
return count, nil
}
func (m *mockPostRepo) WithTx(tx *gorm.DB) repositories.PostRepository {
return m
}
func TestVoteService_CastVote_Authenticated(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
post := &database.Post{
ID: 1,
Title: "Test Post",
URL: "https://example.com",
AuthorID: &[]uint{1}[0],
UpVotes: 0,
DownVotes: 0,
Score: 0,
}
postRepo.posts[1] = post
req := VoteRequest{
UserID: 1,
PostID: 1,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
result, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Type != database.VoteUp {
t.Errorf("Expected vote type 'up', got '%v'", result.Type)
}
if result.UpVotes != 1 {
t.Errorf("Expected up votes to be 1, got %d", result.UpVotes)
}
if result.DownVotes != 0 {
t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
}
if result.Score != 1 {
t.Errorf("Expected score to be 1, got %d", result.Score)
}
if result.IsUnauthenticated {
t.Error("Expected IsUnauthenticated to be false for authenticated vote")
}
}
func TestVoteService_CastVote_Unauthenticated(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
post := &database.Post{
ID: 1,
Title: "Test Post",
URL: "https://example.com",
AuthorID: &[]uint{1}[0],
UpVotes: 0,
DownVotes: 0,
Score: 0,
}
postRepo.posts[1] = post
req := VoteRequest{
UserID: 0,
PostID: 1,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
result, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Type != database.VoteUp {
t.Errorf("Expected vote type 'up', got '%v'", result.Type)
}
if result.UpVotes != 1 {
t.Errorf("Expected up votes to be 1, got %d", result.UpVotes)
}
if result.DownVotes != 0 {
t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
}
if result.Score != 1 {
t.Errorf("Expected score to be 1, got %d", result.Score)
}
if !result.IsUnauthenticated {
t.Error("Expected IsUnauthenticated to be true for unauthenticated vote")
}
}
func TestVoteService_CastVote_UpdateExisting(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
post := &database.Post{
ID: 1,
Title: "Test Post",
URL: "https://example.com",
AuthorID: &[]uint{1}[0],
UpVotes: 0,
DownVotes: 0,
Score: 0,
}
postRepo.posts[1] = post
req := VoteRequest{
UserID: 1,
PostID: 1,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
_, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
req.Type = database.VoteDown
result, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.UpVotes != 0 {
t.Errorf("Expected up votes to be 0, got %d", result.UpVotes)
}
if result.DownVotes != 1 {
t.Errorf("Expected down votes to be 1, got %d", result.DownVotes)
}
if result.Score != -1 {
t.Errorf("Expected score to be -1, got %d", result.Score)
}
}
func TestVoteService_CastVote_RemoveVote(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
post := &database.Post{
ID: 1,
Title: "Test Post",
URL: "https://example.com",
AuthorID: &[]uint{1}[0],
UpVotes: 0,
DownVotes: 0,
Score: 0,
}
postRepo.posts[1] = post
req := VoteRequest{
UserID: 1,
PostID: 1,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
_, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
req.Type = database.VoteNone
result, err := service.CastVote(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.UpVotes != 0 {
t.Errorf("Expected up votes to be 0, got %d", result.UpVotes)
}
if result.DownVotes != 0 {
t.Errorf("Expected down votes to be 0, got %d", result.DownVotes)
}
if result.Score != 0 {
t.Errorf("Expected score to be 0, got %d", result.Score)
}
}
func TestVoteService_GetUserVote_Authenticated(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
userID := uint(1)
vote := &database.Vote{
ID: 1,
UserID: &userID,
PostID: 1,
Type: database.VoteUp,
}
voteRepo.votes[1] = vote
voteRepo.byUserPost["1:1"] = vote
result, err := service.GetUserVote(1, 1, "127.0.0.1", "test-agent")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected vote, got nil")
}
if result.Type != database.VoteUp {
t.Errorf("Expected vote type 'up', got '%v'", result.Type)
}
if result.UserID == nil || *result.UserID != 1 {
t.Errorf("Expected user ID 1, got %v", result.UserID)
}
}
func TestVoteService_GetUserVote_Unauthenticated(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
voteHash := service.GenerateVoteHash("127.0.0.1", "test-agent", 1)
vote := &database.Vote{
ID: 1,
UserID: nil,
PostID: 1,
Type: database.VoteUp,
VoteHash: &voteHash,
}
voteRepo.votes[1] = vote
voteRepo.byVoteHash[voteHash] = vote
result, err := service.GetUserVote(0, 1, "127.0.0.1", "test-agent")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected vote, got nil")
}
if result.Type != database.VoteUp {
t.Errorf("Expected vote type 'up', got '%v'", result.Type)
}
if result.UserID != nil {
t.Error("Expected UserID to be nil for unauthenticated vote")
}
if result.VoteHash == nil || *result.VoteHash != voteHash {
t.Errorf("Expected vote hash '%s', got %v", voteHash, result.VoteHash)
}
}
func TestVoteService_GetPostVotes(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
userID1 := uint(1)
userID2 := uint(2)
voteHash := "test-hash"
vote1 := &database.Vote{
ID: 1,
UserID: &userID1,
PostID: 1,
Type: database.VoteUp,
}
vote2 := &database.Vote{
ID: 2,
UserID: &userID2,
PostID: 1,
Type: database.VoteDown,
}
vote3 := &database.Vote{
ID: 3,
UserID: nil,
PostID: 1,
Type: database.VoteUp,
VoteHash: &voteHash,
}
voteRepo.votes[1] = vote1
voteRepo.votes[2] = vote2
voteRepo.votes[3] = vote3
votes, err := service.GetPostVotes(1)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(votes) != 3 {
t.Errorf("Expected 3 votes, got %d", len(votes))
}
hasAuthenticated := false
hasUnauthenticated := false
for _, vote := range votes {
if vote.UserID != nil {
hasAuthenticated = true
}
if vote.VoteHash != nil {
hasUnauthenticated = true
}
}
if !hasAuthenticated {
t.Error("Expected to find authenticated votes")
}
if !hasUnauthenticated {
t.Error("Expected to find unauthenticated votes")
}
}
func TestVoteService_GetVoteStatistics(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
userID1 := uint(1)
userID2 := uint(2)
voteHash := "test-hash"
vote1 := &database.Vote{
ID: 1,
UserID: &userID1,
PostID: 1,
Type: database.VoteUp,
}
vote2 := &database.Vote{
ID: 2,
UserID: &userID2,
PostID: 1,
Type: database.VoteDown,
}
vote3 := &database.Vote{
ID: 3,
UserID: nil,
PostID: 1,
Type: database.VoteUp,
VoteHash: &voteHash,
}
voteRepo.votes[1] = vote1
voteRepo.votes[2] = vote2
voteRepo.votes[3] = vote3
authenticatedCount, anonymousCount, err := service.GetVoteStatistics()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if authenticatedCount != 3 {
t.Errorf("Expected total count to be 3, got %d", authenticatedCount)
}
if anonymousCount != 0 {
t.Errorf("Expected unauthenticated count to be 0, got %d", anonymousCount)
}
}
func TestVoteService_Validation(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
req := VoteRequest{
UserID: 1,
PostID: 0,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
_, err := service.CastVote(req)
if err == nil {
t.Error("Expected error for missing post ID")
}
req.PostID = 1
req.Type = "invalid"
_, err = service.CastVote(req)
if err == nil {
t.Error("Expected error for invalid vote type")
}
}
func TestVoteService_PostNotFound(t *testing.T) {
voteRepo := newMockVoteRepo()
postRepo := newMockPostRepo()
service := NewVoteService(voteRepo, postRepo, nil)
req := VoteRequest{
UserID: 1,
PostID: 999,
Type: database.VoteUp,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
}
_, err := service.CastVote(req)
if err == nil {
t.Error("Expected error for non-existent post")
}
if !strings.Contains(err.Error(), "post not found") {
t.Errorf("Expected 'post not found' error, got %v", err)
}
}