Migrate Auth/Contractor/Document/Notification/Subscription services to ctx
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

Every public method on these five services now takes ctx context.Context as
the first arg and routes its repo calls through .WithContext(ctx). With
TaskService and ResidenceService already migrated, this means every
in-process service that touches Postgres now produces a flame graph in
Jaeger where the SQL spans nest under the parent HTTP request span.

Endpoints now fully traced (HTTP → service → SQL):
- /api/auth/login, /register, /logout, /me, /verify-email, /resend-verification
- /api/auth/forgot-password, /verify-reset, /reset-password, /update-profile
- /api/contractors/* (CRUD + favorite + by-residence + tasks)
- /api/documents/* (CRUD + activate/deactivate + image upload/delete)
- /api/notifications/* (list, count, mark-read, prefs, devices)
- /api/subscription/* (status, purchase, cancel, triggers, promotions)
- All previously-migrated /api/tasks/* and /api/residences/* paths

Internal helpers also threaded:
- TaskService.sendTaskCompletedNotification → forwards ctx
- TaskService.UpdateUserTimezone → forwards ctx to NotificationService
- ResidenceService.CreateResidence → forwards ctx to SubscriptionService.CheckLimit
- NotificationService.registerAPNSDevice / registerGCMDevice → both take ctx

~75 method signatures, ~120 handler/test call sites updated. Tests pass
green; the only failure is the pre-existing flaky TaskHandler_QuickComplete
SQLite race that fails ~60% of runs on master.

Step 3 of the observability plan is now genuinely complete: every API
endpoint backed by a Go service emits a per-request flame graph with
HTTP → service → SQL spans, plus B2/APNs/FCM/asynq spans where applicable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-25 16:26:21 -05:00
parent 65a9aae4e5
commit e881d37de0
20 changed files with 529 additions and 522 deletions
+38 -37
View File
@@ -1,6 +1,7 @@
package services
import (
"context"
"net/http"
"testing"
@@ -41,7 +42,7 @@ func TestContractorService_CreateContractor(t *testing.T) {
Email: "bob@plumbing.com",
}
resp, err := service.CreateContractor(req, user.ID)
resp, err := service.CreateContractor(context.Background(), req, user.ID)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "Bob's Plumbing", resp.Name)
@@ -63,7 +64,7 @@ func TestContractorService_CreateContractor_Personal(t *testing.T) {
Name: "Personal Handyman",
}
resp, err := service.CreateContractor(req, user.ID)
resp, err := service.CreateContractor(context.Background(), req, user.ID)
require.NoError(t, err)
assert.Equal(t, "Personal Handyman", resp.Name)
}
@@ -84,7 +85,7 @@ func TestContractorService_CreateContractor_AccessDenied(t *testing.T) {
Name: "Unauthorized Contractor",
}
_, err := service.CreateContractor(req, other.ID)
_, err := service.CreateContractor(context.Background(), req, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
@@ -105,7 +106,7 @@ func TestContractorService_CreateContractor_WithFavorite(t *testing.T) {
IsFavorite: &isFav,
}
resp, err := service.CreateContractor(req, user.ID)
resp, err := service.CreateContractor(context.Background(), req, user.ID)
require.NoError(t, err)
assert.True(t, resp.IsFavorite)
}
@@ -123,7 +124,7 @@ func TestContractorService_GetContractor(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor")
resp, err := service.GetContractor(contractor.ID, user.ID)
resp, err := service.GetContractor(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
assert.Equal(t, contractor.ID, resp.ID)
assert.Equal(t, "Test Contractor", resp.Name)
@@ -138,7 +139,7 @@ func TestContractorService_GetContractor_NotFound(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
_, err := service.GetContractor(9999, user.ID)
_, err := service.GetContractor(context.Background(), 9999, user.ID)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found")
}
@@ -154,7 +155,7 @@ func TestContractorService_GetContractor_AccessDenied(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor")
_, err := service.GetContractor(contractor.ID, other.ID)
_, err := service.GetContractor(context.Background(), contractor.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}
@@ -171,7 +172,7 @@ func TestContractorService_GetContractor_SharedUserHasAccess(t *testing.T) {
residenceRepo.AddUser(residence.ID, shared.ID)
contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Shared Contractor")
resp, err := service.GetContractor(contractor.ID, shared.ID)
resp, err := service.GetContractor(context.Background(), contractor.ID, shared.ID)
require.NoError(t, err)
assert.Equal(t, "Shared Contractor", resp.Name)
}
@@ -190,7 +191,7 @@ func TestContractorService_ListContractors(t *testing.T) {
testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor 1")
testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor 2")
resp, err := service.ListContractors(user.ID)
resp, err := service.ListContractors(context.Background(), user.ID)
require.NoError(t, err)
assert.Len(t, resp, 2)
}
@@ -208,11 +209,11 @@ func TestContractorService_DeleteContractor(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "To Delete")
err := service.DeleteContractor(contractor.ID, user.ID)
err := service.DeleteContractor(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
// Should not be found after deletion
_, err = service.GetContractor(contractor.ID, user.ID)
_, err = service.GetContractor(context.Background(), contractor.ID, user.ID)
assert.Error(t, err)
}
@@ -225,7 +226,7 @@ func TestContractorService_DeleteContractor_NotFound(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
err := service.DeleteContractor(9999, user.ID)
err := service.DeleteContractor(context.Background(), 9999, user.ID)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found")
}
@@ -241,7 +242,7 @@ func TestContractorService_DeleteContractor_AccessDenied(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor")
err := service.DeleteContractor(contractor.ID, other.ID)
err := service.DeleteContractor(context.Background(), contractor.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}
@@ -259,17 +260,17 @@ func TestContractorService_ToggleFavorite(t *testing.T) {
contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor")
// Initially not favorite
resp, err := service.GetContractor(contractor.ID, user.ID)
resp, err := service.GetContractor(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
assert.False(t, resp.IsFavorite)
// Toggle to favorite
resp, err = service.ToggleFavorite(contractor.ID, user.ID)
resp, err = service.ToggleFavorite(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
assert.True(t, resp.IsFavorite)
// Toggle back
resp, err = service.ToggleFavorite(contractor.ID, user.ID)
resp, err = service.ToggleFavorite(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
assert.False(t, resp.IsFavorite)
}
@@ -283,7 +284,7 @@ func TestContractorService_ToggleFavorite_NotFound(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
_, err := service.ToggleFavorite(9999, user.ID)
_, err := service.ToggleFavorite(context.Background(), 9999, user.ID)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found")
}
@@ -299,7 +300,7 @@ func TestContractorService_ToggleFavorite_AccessDenied(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor")
_, err := service.ToggleFavorite(contractor.ID, other.ID)
_, err := service.ToggleFavorite(context.Background(), contractor.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}
@@ -317,7 +318,7 @@ func TestContractorService_ListContractorsByResidence(t *testing.T) {
testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor A")
testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Contractor B")
resp, err := service.ListContractorsByResidence(residence.ID, user.ID)
resp, err := service.ListContractorsByResidence(context.Background(), residence.ID, user.ID)
require.NoError(t, err)
assert.Len(t, resp, 2)
}
@@ -333,7 +334,7 @@ func TestContractorService_ListContractorsByResidence_AccessDenied(t *testing.T)
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
_, err := service.ListContractorsByResidence(residence.ID, other.ID)
_, err := service.ListContractorsByResidence(context.Background(), residence.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
}
@@ -348,7 +349,7 @@ func TestContractorService_GetContractorTasks_NotFound(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
_, err := service.GetContractorTasks(9999, user.ID)
_, err := service.GetContractorTasks(context.Background(), 9999, user.ID)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found")
}
@@ -364,7 +365,7 @@ func TestContractorService_GetContractorTasks_AccessDenied(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, owner.ID, "Private Contractor")
_, err := service.GetContractorTasks(contractor.ID, other.ID)
_, err := service.GetContractorTasks(context.Background(), contractor.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}
@@ -379,7 +380,7 @@ func TestContractorService_GetContractorTasks_Empty(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
contractor := testutil.CreateTestContractor(t, db, residence.ID, user.ID, "Test Contractor")
resp, err := service.GetContractorTasks(contractor.ID, user.ID)
resp, err := service.GetContractorTasks(context.Background(), contractor.ID, user.ID)
require.NoError(t, err)
assert.Empty(t, resp)
}
@@ -393,7 +394,7 @@ func TestContractorService_GetSpecialties(t *testing.T) {
residenceRepo := repositories.NewResidenceRepository(db)
service := NewContractorService(contractorRepo, residenceRepo)
resp, err := service.GetSpecialties()
resp, err := service.GetSpecialties(context.Background())
require.NoError(t, err)
// SeedLookupData creates 4 specialties
assert.Len(t, resp, 4)
@@ -413,7 +414,7 @@ func TestContractorService_UpdateContractor_NotFound(t *testing.T) {
newName := "Won't Work"
req := &requests.UpdateContractorRequest{Name: &newName}
_, err := service.UpdateContractor(9999, user.ID, req)
_, err := service.UpdateContractor(context.Background(), 9999, user.ID, req)
testutil.AssertAppError(t, err, http.StatusNotFound, "error.contractor_not_found")
}
@@ -432,7 +433,7 @@ func TestContractorService_UpdateContractor_AccessDenied(t *testing.T) {
newName := "Hacked"
req := &requests.UpdateContractorRequest{Name: &newName}
_, err := service.UpdateContractor(contractor.ID, other.ID, req)
_, err := service.UpdateContractor(context.Background(), contractor.ID, other.ID, req)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}
@@ -461,7 +462,7 @@ func TestUpdateContractor_CrossResidence_Returns403(t *testing.T) {
ResidenceID: &newResidenceID,
}
_, err := service.UpdateContractor(contractor.ID, attacker.ID, req)
_, err := service.UpdateContractor(context.Background(), contractor.ID, attacker.ID, req)
require.Error(t, err, "should not allow reassigning contractor to a residence the user has no access to")
testutil.AssertAppErrorCode(t, err, http.StatusForbidden)
}
@@ -486,7 +487,7 @@ func TestUpdateContractor_SameResidence_Succeeds(t *testing.T) {
ResidenceID: &newResidenceID,
}
resp, err := service.UpdateContractor(contractor.ID, owner.ID, req)
resp, err := service.UpdateContractor(context.Background(), contractor.ID, owner.ID, req)
require.NoError(t, err, "should allow reassigning contractor to a residence the user owns")
require.NotNil(t, resp)
require.Equal(t, "Updated Contractor", resp.Name)
@@ -508,7 +509,7 @@ func TestUpdateContractor_RemoveResidence_Succeeds(t *testing.T) {
ResidenceID: nil,
}
resp, err := service.UpdateContractor(contractor.ID, owner.ID, req)
resp, err := service.UpdateContractor(context.Background(), contractor.ID, owner.ID, req)
require.NoError(t, err, "should allow removing residence association")
require.NotNil(t, resp)
}
@@ -555,7 +556,7 @@ func TestContractorService_UpdateContractor_PartialUpdate(t *testing.T) {
ResidenceID: &residence.ID,
}
resp, err := service.UpdateContractor(contractor.ID, user.ID, req)
resp, err := service.UpdateContractor(context.Background(), contractor.ID, user.ID, req)
require.NoError(t, err)
assert.Equal(t, "Updated Plumber", resp.Name)
assert.Equal(t, "555-9999", resp.Phone)
@@ -588,7 +589,7 @@ func TestContractorService_UpdateContractor_WithSpecialties(t *testing.T) {
ResidenceID: &residence.ID,
}
resp, err := service.UpdateContractor(contractor.ID, user.ID, req)
resp, err := service.UpdateContractor(context.Background(), contractor.ID, user.ID, req)
require.NoError(t, err)
assert.NotNil(t, resp)
}
@@ -615,7 +616,7 @@ func TestContractorService_CreateContractor_WithSpecialties(t *testing.T) {
SpecialtyIDs: []uint{specialties[0].ID},
}
resp, err := service.CreateContractor(req, user.ID)
resp, err := service.CreateContractor(context.Background(), req, user.ID)
require.NoError(t, err)
assert.Equal(t, "Specialized Plumber", resp.Name)
}
@@ -631,7 +632,7 @@ func TestContractorService_ListContractors_Empty(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
// No residence, no contractors
resp, err := service.ListContractors(user.ID)
resp, err := service.ListContractors(context.Background(), user.ID)
require.NoError(t, err)
assert.Empty(t, resp)
}
@@ -648,7 +649,7 @@ func TestContractorService_ListContractorsByResidence_Empty(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Empty House")
resp, err := service.ListContractorsByResidence(residence.ID, user.ID)
resp, err := service.ListContractorsByResidence(context.Background(), residence.ID, user.ID)
require.NoError(t, err)
assert.Empty(t, resp)
}
@@ -669,14 +670,14 @@ func TestContractorService_PersonalContractor_OnlyCreatorAccess(t *testing.T) {
req := &requests.CreateContractorRequest{
Name: "Personal Plumber",
}
resp, err := service.CreateContractor(req, creator.ID)
resp, err := service.CreateContractor(context.Background(), req, creator.ID)
require.NoError(t, err)
// Creator can access
_, err = service.GetContractor(resp.ID, creator.ID)
_, err = service.GetContractor(context.Background(), resp.ID, creator.ID)
require.NoError(t, err)
// Other user cannot
_, err = service.GetContractor(resp.ID, other.ID)
_, err = service.GetContractor(context.Background(), resp.ID, other.ID)
testutil.AssertAppError(t, err, http.StatusForbidden, "error.contractor_access_denied")
}