82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package postgres
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gardomatic.kleiax.de/internal/storage"
|
|
)
|
|
|
|
func TestGeneratedTaskInsertIsIdempotentUnderConcurrency(t *testing.T) {
|
|
dsn := os.Getenv("GARDOMATIC_TEST_DB_DSN")
|
|
if dsn == "" {
|
|
t.Skip("set GARDOMATIC_TEST_DB_DSN to run PostgreSQL integration tests")
|
|
}
|
|
db, err := sql.Open("postgres", dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
if err := db.Ping(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var userID, gardenID, speciesID, plantID, templateID int
|
|
if err := db.QueryRow(`INSERT INTO users (name, email, password_hash, activated) VALUES ('Generator integration', $1, 'hash', true) RETURNING id`, "generator-"+time.Now().Format("150405.000000000")+"@example.com").Scan(&userID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.QueryRow(`INSERT INTO gardens (name) VALUES ('Generator integration') RETURNING id`).Scan(&gardenID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = db.Exec(`DELETE FROM gardens WHERE id = $1`, gardenID)
|
|
_, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID)
|
|
})
|
|
if err := db.QueryRow(`INSERT INTO species (garden_id, common_name) VALUES ($1, 'Parallelrose') RETURNING id`, gardenID).Scan(&speciesID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.QueryRow(`INSERT INTO plants (garden_id, species_id, name) VALUES ($1, $2, 'Rose') RETURNING id`, gardenID, speciesID).Scan(&plantID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.QueryRow(`INSERT INTO species_task_templates (species_id, title, trigger_type, month_from, month_to) VALUES ($1, 'Schneiden', 'month_of_year', 2, 3) RETURNING id`, speciesID).Scan(&templateID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
generatedFor := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
|
|
dueStart, dueEnd := generatedFor, time.Date(2026, 3, 31, 23, 59, 59, 0, time.UTC)
|
|
model := TaskModel{DB: db}
|
|
const workers = 12
|
|
results := make(chan error, workers)
|
|
var wg sync.WaitGroup
|
|
for range workers {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := model.Insert(storage.Task{GardenID: gardenID, PlantID: &plantID, TemplateID: &templateID, Title: "Schneiden", DueAtStart: &dueStart, DueAtEnd: &dueEnd, GeneratedFor: &generatedFor, CreatedBy: userID})
|
|
results <- err
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(results)
|
|
successes, conflicts := 0, 0
|
|
for err := range results {
|
|
if err == nil {
|
|
successes++
|
|
} else if errors.Is(err, storage.ErrConflict) {
|
|
conflicts++
|
|
} else {
|
|
t.Fatalf("insert error: %v", err)
|
|
}
|
|
}
|
|
if successes != 1 || conflicts != workers-1 {
|
|
t.Fatalf("successes=%d conflicts=%d", successes, conflicts)
|
|
}
|
|
var count int
|
|
if err := db.QueryRow(`SELECT count(*) FROM tasks WHERE plant_id = $1 AND template_id = $2 AND generated_for = $3`, plantID, templateID, generatedFor).Scan(&count); err != nil || count != 1 {
|
|
t.Fatalf("count=%d err=%v", count, err)
|
|
}
|
|
}
|