Files
honeyDueAPI/internal/repositories/task_template_repo_test.go
Trey t 237c6b84ee
Some checks failed
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Onboarding: template backlink, bulk-create endpoint, climate-region scoring
Clients that send users through a multi-task onboarding step no longer
loop N POST /api/tasks/ calls and no longer create "orphan" tasks with
no reference to the TaskTemplate they came from.

Task model
- New task_template_id column + GORM FK (migration 000016)
- CreateTaskRequest.template_id, TaskResponse.template_id
- task_service.CreateTask persists the backlink

Bulk endpoint
- POST /api/tasks/bulk/ — 1-50 tasks in a single transaction,
  returns every created row + TotalSummary. Single residence access
  check, per-entry residence_id is overridden with batch value
- task_handler.BulkCreateTasks + task_service.BulkCreateTasks using
  db.Transaction; task_repo.CreateTx + FindByIDTx helpers

Climate-region scoring
- templateConditions gains ClimateRegionID; suggestion_service scores
  residence.PostalCode -> ZipToState -> GetClimateRegionIDByState against
  the template's conditions JSON (no penalty on mismatch / unknown ZIP)
- regionMatchBonus 0.35, totalProfileFields 14 -> 15
- Standalone GET /api/tasks/templates/by-region/ removed; legacy
  task_tasktemplate_regions many-to-many dropped (migration 000017).
  Region affinity now lives entirely in the template's conditions JSON

Tests
- +11 cases across task_service_test, task_handler_test, suggestion_
  service_test: template_id persistence, bulk rollback + cap + auth,
  region match / mismatch / no-ZIP / unknown-ZIP / stacks-with-others

Docs
- docs/openapi.yaml: /tasks/bulk/ + BulkCreateTasks schemas, template_id
  on TaskResponse + CreateTaskRequest, /templates/by-region/ removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:23:57 -05:00

210 lines
6.6 KiB
Go

package repositories
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/testutil"
)
func TestTaskTemplateRepository_Create(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskTemplateRepository(db)
var cat models.TaskCategory
require.NoError(t, db.First(&cat).Error)
var freq models.TaskFrequency
require.NoError(t, db.First(&freq).Error)
template := &models.TaskTemplate{
Title: "Change HVAC Filter",
Description: "Replace the HVAC air filter",
CategoryID: &cat.ID,
FrequencyID: &freq.ID,
IsActive: true,
Tags: "hvac,filter,maintenance",
}
err := repo.Create(template)
require.NoError(t, err)
assert.NotZero(t, template.ID)
}
func TestTaskTemplateRepository_GetAll(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskTemplateRepository(db)
// Create active and inactive templates
t1 := &models.TaskTemplate{Title: "Active Template", IsActive: true}
t2 := &models.TaskTemplate{Title: "Inactive Template", IsActive: false}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
templates, err := repo.GetAll()
require.NoError(t, err)
assert.Len(t, templates, 1) // Only active
assert.Equal(t, "Active Template", templates[0].Title)
}
func TestTaskTemplateRepository_GetByCategory(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskTemplateRepository(db)
var cat models.TaskCategory
require.NoError(t, db.First(&cat).Error)
t1 := &models.TaskTemplate{Title: "Cat Template", CategoryID: &cat.ID, IsActive: true}
t2 := &models.TaskTemplate{Title: "No Cat Template", IsActive: true}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
templates, err := repo.GetByCategory(cat.ID)
require.NoError(t, err)
assert.Len(t, templates, 1)
assert.Equal(t, "Cat Template", templates[0].Title)
}
func TestTaskTemplateRepository_Search(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
t1 := &models.TaskTemplate{Title: "Change HVAC Filter", Tags: "hvac,filter", IsActive: true}
t2 := &models.TaskTemplate{Title: "Fix Faucet", Tags: "plumbing", IsActive: true}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
// Search by title
results, err := repo.Search("hvac")
require.NoError(t, err)
assert.Len(t, results, 1)
assert.Equal(t, "Change HVAC Filter", results[0].Title)
// Search by tag
results, err = repo.Search("plumbing")
require.NoError(t, err)
assert.Len(t, results, 1)
assert.Equal(t, "Fix Faucet", results[0].Title)
// No match
results, err = repo.Search("nonexistent")
require.NoError(t, err)
assert.Empty(t, results)
}
func TestTaskTemplateRepository_GetByID(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
tmpl := &models.TaskTemplate{Title: "Test Template", IsActive: true}
require.NoError(t, db.Create(tmpl).Error)
found, err := repo.GetByID(tmpl.ID)
require.NoError(t, err)
assert.Equal(t, "Test Template", found.Title)
}
func TestTaskTemplateRepository_GetByID_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
_, err := repo.GetByID(9999)
assert.Error(t, err)
}
func TestTaskTemplateRepository_Update(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
tmpl := &models.TaskTemplate{Title: "Original", IsActive: true}
require.NoError(t, db.Create(tmpl).Error)
tmpl.Title = "Updated"
err := repo.Update(tmpl)
require.NoError(t, err)
found, err := repo.GetByID(tmpl.ID)
require.NoError(t, err)
assert.Equal(t, "Updated", found.Title)
}
func TestTaskTemplateRepository_Delete(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
tmpl := &models.TaskTemplate{Title: "To Delete", IsActive: true}
require.NoError(t, db.Create(tmpl).Error)
err := repo.Delete(tmpl.ID)
require.NoError(t, err)
_, err = repo.GetByID(tmpl.ID)
assert.Error(t, err)
}
func TestTaskTemplateRepository_GetAllIncludingInactive(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
t1 := &models.TaskTemplate{Title: "Active", IsActive: true}
t2 := &models.TaskTemplate{Title: "Inactive", IsActive: false}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
templates, err := repo.GetAllIncludingInactive()
require.NoError(t, err)
assert.Len(t, templates, 2) // Both active and inactive
}
func TestTaskTemplateRepository_Count(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskTemplateRepository(db)
t1 := &models.TaskTemplate{Title: "Active 1", IsActive: true}
t2 := &models.TaskTemplate{Title: "Active 2", IsActive: true}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
// Use raw SQL for inactive record to avoid GORM default:true overriding IsActive=false
require.NoError(t, db.Exec(
"INSERT INTO task_tasktemplate (title, is_active, display_order, created_at, updated_at) VALUES (?, ?, ?, datetime('now'), datetime('now'))",
"Inactive", false, 0,
).Error)
count, err := repo.Count()
require.NoError(t, err)
assert.Equal(t, int64(2), count) // Only active
}
func TestTaskTemplateRepository_GetGroupedByCategory(t *testing.T) {
t.Skip("requires PostgreSQL: SQLite cannot scan jsonb default into json.RawMessage")
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskTemplateRepository(db)
var cat models.TaskCategory
require.NoError(t, db.First(&cat).Error)
t1 := &models.TaskTemplate{Title: "Categorized", CategoryID: &cat.ID, IsActive: true}
t2 := &models.TaskTemplate{Title: "Uncategorized", IsActive: true}
require.NoError(t, db.Create(t1).Error)
require.NoError(t, db.Create(t2).Error)
grouped, err := repo.GetGroupedByCategory()
require.NoError(t, err)
assert.GreaterOrEqual(t, len(grouped), 2) // At least the category + "Uncategorized"
// Uncategorized should have the template without category
assert.Len(t, grouped["Uncategorized"], 1)
assert.Equal(t, "Uncategorized", grouped["Uncategorized"][0].Title)
}