Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests) - Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests) - Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests) - Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests) - Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests) - Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,3 +231,472 @@ func TestSuggestionService_MultipleConditionsAllMustMatch(t *testing.T) {
|
||||
assert.InDelta(t, expectedScore, resp.Suggestions[0].RelevanceScore, 0.01)
|
||||
assert.Len(t, resp.Suggestions[0].MatchReasons, 3)
|
||||
}
|
||||
|
||||
// === Malformed conditions JSON treated as universal ===
|
||||
|
||||
func TestSuggestionService_MalformedConditions(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "Test House")
|
||||
|
||||
// Create template with malformed JSON conditions
|
||||
tmpl := &models.TaskTemplate{
|
||||
Title: "Bad JSON Template",
|
||||
IsActive: true,
|
||||
Conditions: json.RawMessage(`{bad json`),
|
||||
}
|
||||
err := service.db.Create(tmpl).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
// Should be treated as universal
|
||||
assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore)
|
||||
}
|
||||
|
||||
// === Null conditions JSON treated as universal ===
|
||||
|
||||
func TestSuggestionService_NullConditions(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "Test House")
|
||||
|
||||
tmpl := &models.TaskTemplate{
|
||||
Title: "Null Conditions",
|
||||
IsActive: true,
|
||||
Conditions: json.RawMessage(`null`),
|
||||
}
|
||||
err := service.db.Create(tmpl).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Equal(t, baseUniversalScore, resp.Suggestions[0].RelevanceScore)
|
||||
}
|
||||
|
||||
// === Template with property_type condition ===
|
||||
|
||||
func TestSuggestionService_PropertyTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
// Create a property type
|
||||
propType := &models.ResidenceType{Name: "House"}
|
||||
err := service.db.Create(propType).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "My House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
PropertyTypeID: &propType.ID,
|
||||
}
|
||||
err = service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
// Reload with PropertyType preloaded
|
||||
err = service.db.Preload("PropertyType").First(residence, residence.ID).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "House Task", map[string]interface{}{
|
||||
"property_type": "House",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "property_type:House")
|
||||
}
|
||||
|
||||
// === CalculateProfileCompleteness with fully filled profile ===
|
||||
|
||||
func TestCalculateProfileCompleteness_FullProfile(t *testing.T) {
|
||||
ht := "gas_furnace"
|
||||
ct := "central_ac"
|
||||
wht := "tank_gas"
|
||||
rt := "asphalt_shingle"
|
||||
et := "brick"
|
||||
fp := "hardwood"
|
||||
lt := "lawn"
|
||||
|
||||
residence := &models.Residence{
|
||||
HeatingType: &ht,
|
||||
CoolingType: &ct,
|
||||
WaterHeaterType: &wht,
|
||||
RoofType: &rt,
|
||||
HasPool: true,
|
||||
HasSprinklerSystem: true,
|
||||
HasSeptic: true,
|
||||
HasFireplace: true,
|
||||
HasGarage: true,
|
||||
HasBasement: true,
|
||||
HasAttic: true,
|
||||
ExteriorType: &et,
|
||||
FlooringPrimary: &fp,
|
||||
LandscapingType: <,
|
||||
}
|
||||
|
||||
completeness := CalculateProfileCompleteness(residence)
|
||||
assert.Equal(t, 1.0, completeness)
|
||||
}
|
||||
|
||||
// === CalculateProfileCompleteness with empty profile ===
|
||||
|
||||
func TestCalculateProfileCompleteness_EmptyProfile(t *testing.T) {
|
||||
residence := &models.Residence{}
|
||||
completeness := CalculateProfileCompleteness(residence)
|
||||
assert.Equal(t, 0.0, completeness)
|
||||
}
|
||||
|
||||
// === Score capped at 1.0 ===
|
||||
|
||||
func TestSuggestionService_ScoreCappedAtOne(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
ht := "gas_furnace"
|
||||
ct := "central_ac"
|
||||
wht := "tank_gas"
|
||||
rt := "asphalt_shingle"
|
||||
et := "brick"
|
||||
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Full House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
HeatingType: &ht,
|
||||
CoolingType: &ct,
|
||||
WaterHeaterType: &wht,
|
||||
RoofType: &rt,
|
||||
ExteriorType: &et,
|
||||
HasPool: true,
|
||||
HasFireplace: true,
|
||||
HasGarage: true,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// Template that matches many fields — score should be capped at 1.0
|
||||
createTemplateWithConditions(t, service, "Super Match", map[string]interface{}{
|
||||
"heating_type": "gas_furnace",
|
||||
"cooling_type": "central_ac",
|
||||
"water_heater_type": "tank_gas",
|
||||
"roof_type": "asphalt_shingle",
|
||||
"exterior_type": "brick",
|
||||
"has_pool": true,
|
||||
"has_fireplace": true,
|
||||
"has_garage": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.LessOrEqual(t, resp.Suggestions[0].RelevanceScore, 1.0)
|
||||
}
|
||||
|
||||
// === Inactive templates are excluded ===
|
||||
|
||||
func TestSuggestionService_InactiveTemplateExcluded(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "Test House")
|
||||
|
||||
// Create inactive template via raw SQL to bypass GORM default:true on is_active
|
||||
err := service.db.Exec("INSERT INTO task_tasktemplate (title, is_active, conditions, created_at, updated_at) VALUES ('Inactive Task', false, '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)").Error
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
// === Template excluded when requires sprinkler but residence doesn't have it ===
|
||||
|
||||
func TestSuggestionService_ExcludedWhenSprinklerRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "No Sprinkler House")
|
||||
|
||||
createTemplateWithConditions(t, service, "Sprinkler Maintenance", map[string]interface{}{
|
||||
"has_sprinkler_system": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
// === All bool field exclusions ===
|
||||
|
||||
func TestSuggestionService_ExcludedWhenSepticRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "City House")
|
||||
|
||||
createTemplateWithConditions(t, service, "Septic Pump", map[string]interface{}{
|
||||
"has_septic": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
func TestSuggestionService_ExcludedWhenFireplaceRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "No Fireplace")
|
||||
|
||||
createTemplateWithConditions(t, service, "Chimney Sweep", map[string]interface{}{
|
||||
"has_fireplace": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
func TestSuggestionService_ExcludedWhenGarageRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "No Garage")
|
||||
|
||||
createTemplateWithConditions(t, service, "Garage Door Service", map[string]interface{}{
|
||||
"has_garage": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
func TestSuggestionService_ExcludedWhenBasementRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "Slab Home")
|
||||
|
||||
createTemplateWithConditions(t, service, "Basement Waterproofing", map[string]interface{}{
|
||||
"has_basement": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
func TestSuggestionService_ExcludedWhenAtticRequired(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, service.db, user.ID, "Flat Roof")
|
||||
|
||||
createTemplateWithConditions(t, service, "Attic Insulation", map[string]interface{}{
|
||||
"has_attic": true,
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Suggestions, 0)
|
||||
}
|
||||
|
||||
// === String field matches for all remaining types ===
|
||||
|
||||
func TestSuggestionService_CoolingTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
coolingType := "central_ac"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Cool House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
CoolingType: &coolingType,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "AC Filter", map[string]interface{}{
|
||||
"cooling_type": "central_ac",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "cooling_type:central_ac")
|
||||
}
|
||||
|
||||
func TestSuggestionService_WaterHeaterTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
wht := "tank_gas"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Hot Water House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
WaterHeaterType: &wht,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "Flush Water Heater", map[string]interface{}{
|
||||
"water_heater_type": "tank_gas",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "water_heater_type:tank_gas")
|
||||
}
|
||||
|
||||
func TestSuggestionService_ExteriorTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
et := "brick"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Brick House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
ExteriorType: &et,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "Pressure Wash Brick", map[string]interface{}{
|
||||
"exterior_type": "brick",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "exterior_type:brick")
|
||||
}
|
||||
|
||||
func TestSuggestionService_FlooringPrimaryMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
fp := "hardwood"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Hardwood House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
FlooringPrimary: &fp,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "Refinish Floors", map[string]interface{}{
|
||||
"flooring_primary": "hardwood",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "flooring_primary:hardwood")
|
||||
}
|
||||
|
||||
func TestSuggestionService_LandscapingTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
lt := "lawn"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Lawn House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
LandscapingType: <,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "Fertilize Lawn", map[string]interface{}{
|
||||
"landscaping_type": "lawn",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "landscaping_type:lawn")
|
||||
}
|
||||
|
||||
func TestSuggestionService_RoofTypeMatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
rt := "asphalt_shingle"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Shingle House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
RoofType: &rt,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
createTemplateWithConditions(t, service, "Inspect Roof", map[string]interface{}{
|
||||
"roof_type": "asphalt_shingle",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "roof_type:asphalt_shingle")
|
||||
}
|
||||
|
||||
// === Mismatch on string field — no score for that field ===
|
||||
|
||||
func TestSuggestionService_HeatingTypeMismatch(t *testing.T) {
|
||||
service := setupSuggestionService(t)
|
||||
user := testutil.CreateTestUser(t, service.db, "owner", "owner@test.com", "Password123")
|
||||
|
||||
heatingType := "electric_furnace"
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Electric House",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
HeatingType: &heatingType,
|
||||
}
|
||||
err := service.db.Create(residence).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// Template wants gas_furnace but residence has electric_furnace
|
||||
createTemplateWithConditions(t, service, "Gas Furnace Service", map[string]interface{}{
|
||||
"heating_type": "gas_furnace",
|
||||
})
|
||||
|
||||
resp, err := service.GetSuggestions(residence.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Suggestions, 1)
|
||||
// Should still be included but with partial_profile (no match, no exclude)
|
||||
assert.Contains(t, resp.Suggestions[0].MatchReasons, "partial_profile")
|
||||
}
|
||||
|
||||
// === templateConditions.isEmpty ===
|
||||
|
||||
func TestTemplateConditions_IsEmpty(t *testing.T) {
|
||||
cond := &templateConditions{}
|
||||
assert.True(t, cond.isEmpty())
|
||||
|
||||
ht := "gas"
|
||||
cond2 := &templateConditions{HeatingType: &ht}
|
||||
assert.False(t, cond2.isEmpty())
|
||||
|
||||
pool := true
|
||||
cond3 := &templateConditions{HasPool: &pool}
|
||||
assert.False(t, cond3.isEmpty())
|
||||
|
||||
pt := "House"
|
||||
cond4 := &templateConditions{PropertyType: &pt}
|
||||
assert.False(t, cond4.isEmpty())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user