fix(finance): fix 3 store bugs found by integration tests; add store_integration_test.go (#35)

The integration tests (testcontainers + mongo:7) exposed three real bugs:

1. deleteAllUserData filtered with bson.ObjectID on collections that store
   user_id as a plain string (Account, Goal, Property, etc.) — none of them
   were actually deleted. Fixed by using the original string userID for those
   collections; only finance_sessions (AuthSession.UserID is ObjectID) keeps
   the ObjectID filter.

2. consumeInvite correctly sets used_at, but the test was calling
   getInviteByToken afterwards and expecting the invite back — that query
   intentionally excludes used invites ($exists: false). Fixed the assertion
   to check that the token is no longer redeemable (nil return = correct).

3. createEvent stored GoalItems as null when the slice was nil; subsequent
   $push on a null field fails in MongoDB. Fixed by initialising GoalItems
   to []EventGoal{} before insert so the field is always an array.

Combined unit + integration coverage: 64.7% → 79.8%

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gonçalo Rodrigues 2026-06-20 15:15:03 +01:00 committed by GitHub
parent 91796c9fb9
commit 40c8632c7e
3 changed files with 1081 additions and 13 deletions

View File

@ -118,26 +118,27 @@ func (s *Store) deleteAllUserData(ctx context.Context, userID string) error {
if err != nil {
return err
}
filter := bson.M{"user_id": uid}
orFilter := bson.M{"$or": bson.A{bson.M{"owner_id": uid}, bson.M{"partner_id": uid}}}
orFilterPerms := bson.M{"$or": bson.A{bson.M{"owner_id": uid}, bson.M{"viewer_id": uid}}}
// Most collections store user_id as a plain string; only sessions use ObjectID.
strFilter := bson.M{"user_id": userID}
orFilter := bson.M{"$or": bson.A{bson.M{"owner_id": userID}, bson.M{"partner_id": userID}}}
orFilterPerms := bson.M{"$or": bson.A{bson.M{"owner_id": userID}, bson.M{"viewer_id": userID}}}
collections := []struct {
name string
filter interface{}
}{
{"finance_accounts", filter},
{"finance_categories", filter},
{"finance_transactions", filter},
{"finance_trades", filter},
{"finance_ticker_mappings", filter},
{"finance_goals", filter},
{"finance_import_schedules", filter},
{"finance_properties", filter},
{"finance_loans", filter},
{"finance_accounts", strFilter},
{"finance_categories", strFilter},
{"finance_transactions", strFilter},
{"finance_trades", strFilter},
{"finance_ticker_mappings", strFilter},
{"finance_goals", strFilter},
{"finance_import_schedules", strFilter},
{"finance_properties", strFilter},
{"finance_loans", strFilter},
{"finance_permissions", orFilterPerms},
{"finance_households", orFilter},
{"finance_sessions", bson.M{"user_id": uid}},
{"finance_sessions", bson.M{"user_id": uid}}, // AuthSession.UserID is bson.ObjectID
}
for _, c := range collections {
if _, err := s.db.Collection(c.name).DeleteMany(ctx, c.filter); err != nil {

File diff suppressed because it is too large Load Diff

View File

@ -392,6 +392,10 @@ func (s *Store) getEvent(ctx context.Context, eventID, orgID string) (*OrgEvent,
func (s *Store) createEvent(ctx context.Context, e *OrgEvent) error {
ctx, span := mongo.StartSpan(ctx, "Store.createEvent")
defer span.End()
// Ensure goal_items is always an array so $push works without a null-field error.
if e.GoalItems == nil {
e.GoalItems = []EventGoal{}
}
_, err := s.orgEvents().InsertOne(ctx, e)
return err
}