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>
162 lines
4.7 KiB
Go
162 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
|
)
|
|
|
|
func (s *Store) createAuthUser(ctx context.Context, u *AuthUser) error {
|
|
u.ID = bson.NewObjectID()
|
|
u.CreatedAt = time.Now()
|
|
_, err := s.db.Collection("finance_users").InsertOne(ctx, u)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) findAuthUserByEmail(ctx context.Context, email string) (*AuthUser, error) {
|
|
var u AuthUser
|
|
err := s.db.Collection("finance_users").FindOne(ctx, bson.M{"email": email}).Decode(&u)
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, nil
|
|
}
|
|
return &u, err
|
|
}
|
|
|
|
func (s *Store) findAuthUserByID(ctx context.Context, userID string) (*AuthUser, error) {
|
|
oid, err := bson.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
var u AuthUser
|
|
err = s.db.Collection("finance_users").FindOne(ctx, bson.M{"_id": oid}).Decode(&u)
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, nil
|
|
}
|
|
return &u, err
|
|
}
|
|
|
|
func (s *Store) findAuthUserByProvider(ctx context.Context, provider, providerID string) (*AuthUser, error) {
|
|
var u AuthUser
|
|
err := s.db.Collection("finance_users").FindOne(ctx, bson.M{
|
|
"provider": provider,
|
|
"provider_id": providerID,
|
|
}).Decode(&u)
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, nil
|
|
}
|
|
return &u, err
|
|
}
|
|
|
|
func (s *Store) createAuthSession(ctx context.Context, sess *AuthSession) error {
|
|
sess.ID = bson.NewObjectID()
|
|
sess.CreatedAt = time.Now()
|
|
_, err := s.db.Collection("finance_sessions").InsertOne(ctx, sess)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) getAuthSession(ctx context.Context, id string) (*AuthSession, error) {
|
|
oid, err := bson.ObjectIDFromHex(id)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
var sess AuthSession
|
|
err = s.db.Collection("finance_sessions").FindOne(ctx, bson.M{"_id": oid}).Decode(&sess)
|
|
if err == mongo.ErrNoDocuments {
|
|
return nil, nil
|
|
}
|
|
return &sess, err
|
|
}
|
|
|
|
func (s *Store) deleteAuthSession(ctx context.Context, id string) error {
|
|
oid, err := bson.ObjectIDFromHex(id)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
_, err = s.db.Collection("finance_sessions").DeleteOne(ctx, bson.M{"_id": oid})
|
|
return err
|
|
}
|
|
|
|
func (s *Store) getSessionsByUserID(ctx context.Context, userID string) ([]AuthSession, error) {
|
|
oid, err := bson.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
cur, err := s.db.Collection("finance_sessions").Find(ctx,
|
|
bson.M{"user_id": oid},
|
|
options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sessions []AuthSession
|
|
if err := cur.All(ctx, &sessions); err != nil {
|
|
return nil, err
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
func (s *Store) deleteSessionForUser(ctx context.Context, sessionID, userID string) error {
|
|
sid, err := bson.ObjectIDFromHex(sessionID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
uid, err := bson.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
_, err = s.db.Collection("finance_sessions").DeleteOne(ctx, bson.M{"_id": sid, "user_id": uid})
|
|
return err
|
|
}
|
|
|
|
// deleteAllUserData purges every record belonging to userID across all collections,
|
|
// then deletes the user account itself. Irreversible.
|
|
func (s *Store) deleteAllUserData(ctx context.Context, userID string) error {
|
|
uid, err := bson.ObjectIDFromHex(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 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", 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}}, // AuthSession.UserID is bson.ObjectID
|
|
}
|
|
for _, c := range collections {
|
|
if _, err := s.db.Collection(c.name).DeleteMany(ctx, c.filter); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = s.db.Collection("finance_users").DeleteOne(ctx, bson.M{"_id": uid})
|
|
return err
|
|
}
|
|
|
|
func (s *Store) ensureAuthIndexes(ctx context.Context) {
|
|
s.db.Collection("finance_users").Indexes().CreateOne(ctx, mongo.IndexModel{
|
|
Keys: bson.D{{Key: "email", Value: 1}},
|
|
Options: options.Index().SetUnique(true).SetSparse(true),
|
|
})
|
|
s.db.Collection("finance_sessions").Indexes().CreateOne(ctx, mongo.IndexModel{
|
|
Keys: bson.D{{Key: "expires_at", Value: 1}},
|
|
Options: options.Index().SetExpireAfterSeconds(0),
|
|
})
|
|
}
|