604 lines
13 KiB
Go
604 lines
13 KiB
Go
package database
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
func newTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
dbName := "file:memdb_" + t.Name() + "?mode=memory&cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
|
|
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to test database: %v", err)
|
|
}
|
|
err = db.AutoMigrate(
|
|
&User{},
|
|
&Post{},
|
|
&Vote{},
|
|
&AccountDeletionRequest{},
|
|
&RefreshToken{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to migrate database: %v", err)
|
|
}
|
|
|
|
if execErr := db.Exec("PRAGMA busy_timeout = 5000").Error; execErr != nil {
|
|
t.Fatalf("Failed to configure busy timeout: %v", execErr)
|
|
}
|
|
if execErr := db.Exec("PRAGMA foreign_keys = ON").Error; execErr != nil {
|
|
t.Fatalf("Failed to enable foreign keys: %v", execErr)
|
|
}
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("Failed to access SQL DB: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
|
return db
|
|
}
|
|
|
|
func createTestUser(t *testing.T, db *gorm.DB) *User {
|
|
t.Helper()
|
|
|
|
uniqueID := time.Now().UnixNano()
|
|
user := &User{
|
|
Username: fmt.Sprintf("testuser%d", uniqueID),
|
|
Email: fmt.Sprintf("test%d@example.com", uniqueID),
|
|
Password: "hashedpassword123",
|
|
EmailVerified: true,
|
|
}
|
|
if err := db.Create(user).Error; err != nil {
|
|
t.Fatalf("Failed to create test user: %v", err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func createTestPost(t *testing.T, db *gorm.DB, authorID uint) *Post {
|
|
t.Helper()
|
|
post := &Post{
|
|
Title: "Test Post " + t.Name(),
|
|
URL: "https://example.com/test" + t.Name(),
|
|
Content: "Test content",
|
|
AuthorID: &authorID,
|
|
}
|
|
if err := db.Create(post).Error; err != nil {
|
|
t.Fatalf("Failed to create test post: %v", err)
|
|
}
|
|
return post
|
|
}
|
|
|
|
func TestUser_Model(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("create_user", func(t *testing.T) {
|
|
user := &User{
|
|
Username: "testuser",
|
|
Email: "test@example.com",
|
|
Password: "hashedpassword",
|
|
EmailVerified: true,
|
|
}
|
|
|
|
if err := db.Create(user).Error; err != nil {
|
|
t.Fatalf("Failed to create user: %v", err)
|
|
}
|
|
|
|
if user.ID == 0 {
|
|
t.Error("Expected user ID to be set")
|
|
}
|
|
|
|
if user.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
|
|
if user.UpdatedAt.IsZero() {
|
|
t.Error("Expected UpdatedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("user_constraints", func(t *testing.T) {
|
|
|
|
user1 := &User{
|
|
Username: "duplicate",
|
|
Email: "user1@example.com",
|
|
Password: "hashedpassword",
|
|
EmailVerified: true,
|
|
}
|
|
|
|
user2 := &User{
|
|
Username: "duplicate",
|
|
Email: "user2@example.com",
|
|
Password: "hashedpassword",
|
|
EmailVerified: true,
|
|
}
|
|
|
|
if err := db.Create(user1).Error; err != nil {
|
|
t.Fatalf("Failed to create first user: %v", err)
|
|
}
|
|
|
|
if err := db.Create(user2).Error; err == nil {
|
|
t.Error("Expected error when creating user with duplicate username")
|
|
}
|
|
|
|
user3 := &User{
|
|
Username: "unique",
|
|
Email: "user1@example.com",
|
|
Password: "hashedpassword",
|
|
EmailVerified: true,
|
|
}
|
|
|
|
if err := db.Create(user3).Error; err == nil {
|
|
t.Error("Expected error when creating user with duplicate email")
|
|
}
|
|
})
|
|
|
|
t.Run("user_relationships", func(t *testing.T) {
|
|
user := &User{
|
|
Username: "author",
|
|
Email: "author@example.com",
|
|
Password: "hashedpassword",
|
|
EmailVerified: true,
|
|
}
|
|
|
|
if err := db.Create(user).Error; err != nil {
|
|
t.Fatalf("Failed to create user: %v", err)
|
|
}
|
|
|
|
post1 := &Post{
|
|
Title: "Post 1",
|
|
URL: "https://example.com/1",
|
|
Content: "Content 1",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
post2 := &Post{
|
|
Title: "Post 2",
|
|
URL: "https://example.com/2",
|
|
Content: "Content 2",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
if err := db.Create(post1).Error; err != nil {
|
|
t.Fatalf("Failed to create post 1: %v", err)
|
|
}
|
|
|
|
if err := db.Create(post2).Error; err != nil {
|
|
t.Fatalf("Failed to create post 2: %v", err)
|
|
}
|
|
|
|
var foundUser User
|
|
if err := db.Preload("Posts").First(&foundUser, user.ID).Error; err != nil {
|
|
t.Fatalf("Failed to load user with posts: %v", err)
|
|
}
|
|
|
|
if len(foundUser.Posts) != 2 {
|
|
t.Errorf("Expected 2 posts, got %d", len(foundUser.Posts))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPost_Model(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("create_post", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
post := &Post{
|
|
Title: "Test Post",
|
|
URL: "https://example.com/test",
|
|
Content: "Test content",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
if err := db.Create(post).Error; err != nil {
|
|
t.Fatalf("Failed to create post: %v", err)
|
|
}
|
|
|
|
if post.ID == 0 {
|
|
t.Error("Expected post ID to be set")
|
|
}
|
|
|
|
if post.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
|
|
if post.UpdatedAt.IsZero() {
|
|
t.Error("Expected UpdatedAt to be set")
|
|
}
|
|
|
|
if post.UpVotes != 0 {
|
|
t.Error("Expected UpVotes to be 0 by default")
|
|
}
|
|
|
|
if post.DownVotes != 0 {
|
|
t.Error("Expected DownVotes to be 0 by default")
|
|
}
|
|
|
|
if post.Score != 0 {
|
|
t.Error("Expected Score to be 0 by default")
|
|
}
|
|
})
|
|
|
|
t.Run("post_constraints", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
post1 := &Post{
|
|
Title: "Post 1",
|
|
URL: "https://example.com/unique",
|
|
Content: "Content 1",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
post2 := &Post{
|
|
Title: "Post 2",
|
|
URL: "https://example.com/unique",
|
|
Content: "Content 2",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
if err := db.Create(post1).Error; err != nil {
|
|
t.Fatalf("Failed to create first post: %v", err)
|
|
}
|
|
|
|
if err := db.Create(post2).Error; err == nil {
|
|
t.Error("Expected error when creating post with duplicate URL")
|
|
}
|
|
})
|
|
|
|
t.Run("post_relationships", func(t *testing.T) {
|
|
user1 := createTestUser(t, db)
|
|
user2 := createTestUser(t, db)
|
|
|
|
post := createTestPost(t, db, user1.ID)
|
|
|
|
vote1 := &Vote{
|
|
UserID: &user1.ID,
|
|
PostID: post.ID,
|
|
Type: VoteUp,
|
|
}
|
|
|
|
vote2 := &Vote{
|
|
UserID: &user2.ID,
|
|
PostID: post.ID,
|
|
Type: VoteDown,
|
|
}
|
|
|
|
if err := db.Create(vote1).Error; err != nil {
|
|
t.Fatalf("Failed to create vote 1: %v", err)
|
|
}
|
|
|
|
if err := db.Create(vote2).Error; err != nil {
|
|
t.Fatalf("Failed to create vote 2: %v", err)
|
|
}
|
|
|
|
var foundPost Post
|
|
if err := db.Preload("Votes").First(&foundPost, post.ID).Error; err != nil {
|
|
t.Fatalf("Failed to load post with votes: %v", err)
|
|
}
|
|
|
|
if len(foundPost.Votes) != 2 {
|
|
t.Errorf("Expected 2 votes, got %d", len(foundPost.Votes))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVote_Model(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("create_vote", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
post := createTestPost(t, db, user.ID)
|
|
|
|
vote := &Vote{
|
|
UserID: &user.ID,
|
|
PostID: post.ID,
|
|
Type: VoteUp,
|
|
}
|
|
|
|
if err := db.Create(vote).Error; err != nil {
|
|
t.Fatalf("Failed to create vote: %v", err)
|
|
}
|
|
|
|
if vote.ID == 0 {
|
|
t.Error("Expected vote ID to be set")
|
|
}
|
|
|
|
if vote.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
|
|
if vote.UpdatedAt.IsZero() {
|
|
t.Error("Expected UpdatedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("vote_constraints", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
post := createTestPost(t, db, user.ID)
|
|
|
|
vote1 := &Vote{
|
|
UserID: &user.ID,
|
|
PostID: post.ID,
|
|
Type: VoteUp,
|
|
}
|
|
|
|
vote2 := &Vote{
|
|
UserID: &user.ID,
|
|
PostID: post.ID,
|
|
Type: VoteDown,
|
|
}
|
|
|
|
if err := db.Create(vote1).Error; err != nil {
|
|
t.Fatalf("Failed to create first vote: %v", err)
|
|
}
|
|
|
|
if err := db.Create(vote2).Error; err == nil {
|
|
t.Error("Expected error when creating vote with duplicate user-post combination")
|
|
}
|
|
})
|
|
|
|
t.Run("vote_types", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
voteTypes := []VoteType{VoteUp, VoteDown, VoteNone}
|
|
|
|
for i, voteType := range voteTypes {
|
|
|
|
post := &Post{
|
|
Title: "Test Post " + string(rune(i)),
|
|
URL: "https://example.com/test" + string(rune(i)),
|
|
Content: "Test content",
|
|
AuthorID: &user.ID,
|
|
}
|
|
|
|
if err := db.Create(post).Error; err != nil {
|
|
t.Fatalf("Failed to create post %d: %v", i, err)
|
|
}
|
|
|
|
vote := &Vote{
|
|
UserID: &user.ID,
|
|
PostID: post.ID,
|
|
Type: voteType,
|
|
}
|
|
|
|
if err := db.Create(vote).Error; err != nil {
|
|
t.Fatalf("Failed to create vote with type %s: %v", voteType, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("vote_relationships", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
post := createTestPost(t, db, user.ID)
|
|
|
|
vote := &Vote{
|
|
UserID: &user.ID,
|
|
PostID: post.ID,
|
|
Type: VoteUp,
|
|
}
|
|
|
|
if err := db.Create(vote).Error; err != nil {
|
|
t.Fatalf("Failed to create vote: %v", err)
|
|
}
|
|
|
|
var foundVote Vote
|
|
if err := db.Preload("User").Preload("Post").First(&foundVote, vote.ID).Error; err != nil {
|
|
t.Fatalf("Failed to load vote with relationships: %v", err)
|
|
}
|
|
|
|
if foundVote.User.ID != user.ID {
|
|
t.Error("Expected vote to be associated with correct user")
|
|
}
|
|
|
|
if foundVote.Post.ID != post.ID {
|
|
t.Error("Expected vote to be associated with correct post")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRefreshToken_Model(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("create_refresh_token", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
token := &RefreshToken{
|
|
UserID: user.ID,
|
|
TokenHash: "hashedtoken123",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
if err := db.Create(token).Error; err != nil {
|
|
t.Fatalf("Failed to create refresh token: %v", err)
|
|
}
|
|
|
|
if token.ID == 0 {
|
|
t.Error("Expected token ID to be set")
|
|
}
|
|
|
|
if token.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
|
|
if token.UpdatedAt.IsZero() {
|
|
t.Error("Expected UpdatedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("refresh_token_constraints", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
token1 := &RefreshToken{
|
|
UserID: user.ID,
|
|
TokenHash: "uniquehash",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
token2 := &RefreshToken{
|
|
UserID: user.ID,
|
|
TokenHash: "uniquehash",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
if err := db.Create(token1).Error; err != nil {
|
|
t.Fatalf("Failed to create first token: %v", err)
|
|
}
|
|
|
|
if err := db.Create(token2).Error; err == nil {
|
|
t.Error("Expected error when creating token with duplicate hash")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAccountDeletionRequest_Model(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("create_account_deletion_request", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
request := &AccountDeletionRequest{
|
|
UserID: user.ID,
|
|
TokenHash: "deletiontoken123",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
if err := db.Create(request).Error; err != nil {
|
|
t.Fatalf("Failed to create account deletion request: %v", err)
|
|
}
|
|
|
|
if request.ID == 0 {
|
|
t.Error("Expected request ID to be set")
|
|
}
|
|
|
|
if request.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("account_deletion_request_constraints", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
request1 := &AccountDeletionRequest{
|
|
UserID: user.ID,
|
|
TokenHash: "token1",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
request2 := &AccountDeletionRequest{
|
|
UserID: user.ID,
|
|
TokenHash: "token2",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
if err := db.Create(request1).Error; err != nil {
|
|
t.Fatalf("Failed to create first request: %v", err)
|
|
}
|
|
|
|
if err := db.Create(request2).Error; err == nil {
|
|
t.Error("Expected error when creating request with duplicate user")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVoteType_Constants(t *testing.T) {
|
|
t.Run("vote_type_constants", func(t *testing.T) {
|
|
if VoteUp != "up" {
|
|
t.Errorf("Expected VoteUp to be 'up', got '%s'", VoteUp)
|
|
}
|
|
|
|
if VoteDown != "down" {
|
|
t.Errorf("Expected VoteDown to be 'down', got '%s'", VoteDown)
|
|
}
|
|
|
|
if VoteNone != "none" {
|
|
t.Errorf("Expected VoteNone to be 'none', got '%s'", VoteNone)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestModel_SoftDelete(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}()
|
|
|
|
t.Run("user_soft_delete", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
|
|
if err := db.Delete(user).Error; err != nil {
|
|
t.Fatalf("Failed to soft delete user: %v", err)
|
|
}
|
|
|
|
var foundUser User
|
|
if err := db.First(&foundUser, user.ID).Error; err == nil {
|
|
t.Error("Expected user to be soft deleted")
|
|
}
|
|
|
|
if err := db.Unscoped().First(&foundUser, user.ID).Error; err != nil {
|
|
t.Fatalf("Expected to find soft deleted user with Unscoped: %v", err)
|
|
}
|
|
|
|
if foundUser.DeletedAt.Time.IsZero() {
|
|
t.Error("Expected DeletedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("post_soft_delete", func(t *testing.T) {
|
|
user := createTestUser(t, db)
|
|
post := createTestPost(t, db, user.ID)
|
|
|
|
if err := db.Delete(post).Error; err != nil {
|
|
t.Fatalf("Failed to soft delete post: %v", err)
|
|
}
|
|
|
|
var foundPost Post
|
|
if err := db.First(&foundPost, post.ID).Error; err == nil {
|
|
t.Error("Expected post to be soft deleted")
|
|
}
|
|
|
|
if err := db.Unscoped().First(&foundPost, post.ID).Error; err != nil {
|
|
t.Fatalf("Expected to find soft deleted post with Unscoped: %v", err)
|
|
}
|
|
|
|
if foundPost.DeletedAt.Time.IsZero() {
|
|
t.Error("Expected DeletedAt to be set")
|
|
}
|
|
})
|
|
}
|