package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/sundynix/pets-be/internal/config" "github.com/sundynix/pets-be/internal/database" "github.com/sundynix/pets-be/internal/model" ) func TestTaskCompletionCannotBeUndone(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = httptest.NewRequest(http.MethodPut, "/completion", bytes.NewBufferString(`{"date":"2026-10-05","completed":false}`)) api := New(nil, nil, &config.Config{}) api.setTaskCompletion(c) if rec.Code != http.StatusBadRequest { t.Fatalf("undo status %d", rec.Code) } } // All fixtures and writes are rolled back, including inner batch transactions. func TestTaskCompletionPostgres(t *testing.T) { if os.Getenv("PETS_ADMIN_INTEGRATION") != "1" { t.Skip("PostgreSQL integration disabled") } cfg, err := config.Load() if err != nil { t.Fatal(err) } db, err := database.Open(cfg.Postgres, true) if err != nil { t.Fatal(err) } sqlDB, _ := db.DB() defer sqlDB.Close() tx := db.Begin() if tx.Error != nil { t.Fatal(tx.Error) } defer tx.Rollback() user := model.User{OpenID: "completion-test:" + uuid.NewString(), Nickname: "测试家长"} if err := tx.Create(&user).Error; err != nil { t.Fatal(err) } pet := model.Pet{UserID: user.ID, Name: "测试毛孩", Species: "dog", Gender: "male"} if err := tx.Create(&pet).Error; err != nil { t.Fatal(err) } tasks := []model.CareTask{{UserID: user.ID, PetID: pet.ID, Title: "喝水", Category: "water", TimeOfDay: "08:00", RepeatRule: "daily"}, {UserID: user.ID, PetID: pet.ID, Title: "喂食", Category: "food", TimeOfDay: "09:00", RepeatRule: "daily"}} if err := tx.Create(&tasks).Error; err != nil { t.Fatal(err) } api := New(tx, nil, cfg) call := func(ids []string) int { t.Helper() body, _ := json.Marshal(gin.H{"date": "2026-10-05", "task_ids": ids}) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("actor_id", user.ID) c.Request = httptest.NewRequest(http.MethodPut, "/tasks/completions", bytes.NewReader(body)) api.completeTasks(c) return rec.Code } if status := call([]string{tasks[0].ID, uuid.NewString()}); status != 400 { t.Fatalf("invalid batch status %d", status) } var count int64 tx.Model(&model.TaskCompletion{}).Where("user_id = ?", user.ID).Count(&count) if count != 0 { t.Fatal("invalid batch partially completed") } ids := []string{tasks[0].ID, tasks[1].ID} for range 2 { if status := call(ids); status != 200 { t.Fatalf("batch status %d", status) } } tx.Model(&model.TaskCompletion{}).Where("user_id = ?", user.ID).Count(&count) if count != 2 { t.Fatalf("retry duplicated completions: %d", count) } }