Files
Gardomatic/internal/storage/postgres/task_generation_integration_test.go
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

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)
}
}