From 06e4b33167e84d429af5657b523973296e18b5f5 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 2 Oct 2025 21:03:00 +0200 Subject: [PATCH] fix: properly handle NULL UserName in database operations Add scanSignature helper to convert sql.NullString to string type. Update Create method to insert NULL for empty UserName values. Fix integration tests to work with string type instead of pointer. --- .../infrastructure/database/repository.go | 123 +++++++----------- .../database/repository_constraints_test.go | 6 +- 2 files changed, 49 insertions(+), 80 deletions(-) diff --git a/internal/infrastructure/database/repository.go b/internal/infrastructure/database/repository.go index d5d0e89..af90762 100644 --- a/internal/infrastructure/database/repository.go +++ b/internal/infrastructure/database/repository.go @@ -18,6 +18,37 @@ func NewSignatureRepository(db *sql.DB) *SignatureRepository { return &SignatureRepository{db: db} } +// scanSignature scans a row into a Signature, handling NULL values properly +func scanSignature(scanner interface { + Scan(dest ...interface{}) error +}, signature *models.Signature) error { + var userName sql.NullString + err := scanner.Scan( + &signature.ID, + &signature.DocID, + &signature.UserSub, + &signature.UserEmail, + &userName, + &signature.SignedAtUTC, + &signature.PayloadHash, + &signature.Signature, + &signature.Nonce, + &signature.CreatedAt, + &signature.Referer, + &signature.PrevHash, + ) + if err != nil { + return err + } + // Convert sql.NullString to string (empty string if NULL) + if userName.Valid { + signature.UserName = userName.String + } else { + signature.UserName = "" + } + return nil +} + func (r *SignatureRepository) Create(ctx context.Context, signature *models.Signature) error { query := ` INSERT INTO signatures (doc_id, user_sub, user_email, user_name, signed_at, payload_hash, signature, nonce, referer, prev_hash) @@ -25,12 +56,18 @@ func (r *SignatureRepository) Create(ctx context.Context, signature *models.Sign RETURNING id, created_at ` + // Convert empty string to NULL for user_name + var userName sql.NullString + if signature.UserName != "" { + userName = sql.NullString{String: signature.UserName, Valid: true} + } + err := r.db.QueryRowContext( ctx, query, signature.DocID, signature.UserSub, signature.UserEmail, - signature.UserName, + userName, signature.SignedAtUTC, signature.PayloadHash, signature.Signature, @@ -49,25 +86,12 @@ func (r *SignatureRepository) Create(ctx context.Context, signature *models.Sign func (r *SignatureRepository) GetByDocAndUser(ctx context.Context, docID, userSub string) (*models.Signature, error) { query := ` SELECT id, doc_id, user_sub, user_email, user_name, signed_at, payload_hash, signature, nonce, created_at, referer, prev_hash - FROM signatures + FROM signatures WHERE doc_id = $1 AND user_sub = $2 ` signature := &models.Signature{} - err := r.db.QueryRowContext(ctx, query, docID, userSub).Scan( - &signature.ID, - &signature.DocID, - &signature.UserSub, - &signature.UserEmail, - &signature.UserName, - &signature.SignedAtUTC, - &signature.PayloadHash, - &signature.Signature, - &signature.Nonce, - &signature.CreatedAt, - &signature.Referer, - &signature.PrevHash, - ) + err := scanSignature(r.db.QueryRowContext(ctx, query, docID, userSub), signature) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -98,21 +122,7 @@ func (r *SignatureRepository) GetByDoc(ctx context.Context, docID string) ([]*mo var signatures []*models.Signature for rows.Next() { signature := &models.Signature{} - err := rows.Scan( - &signature.ID, - &signature.DocID, - &signature.UserSub, - &signature.UserEmail, - &signature.UserName, - &signature.SignedAtUTC, - &signature.PayloadHash, - &signature.Signature, - &signature.Nonce, - &signature.CreatedAt, - &signature.Referer, - &signature.PrevHash, - ) - if err != nil { + if err := scanSignature(rows, signature); err != nil { continue } signatures = append(signatures, signature) @@ -140,21 +150,7 @@ func (r *SignatureRepository) GetByUser(ctx context.Context, userSub string) ([] var signatures []*models.Signature for rows.Next() { signature := &models.Signature{} - err := rows.Scan( - &signature.ID, - &signature.DocID, - &signature.UserSub, - &signature.UserEmail, - &signature.UserName, - &signature.SignedAtUTC, - &signature.PayloadHash, - &signature.Signature, - &signature.Nonce, - &signature.CreatedAt, - &signature.Referer, - &signature.PrevHash, - ) - if err != nil { + if err := scanSignature(rows, signature); err != nil { continue } signatures = append(signatures, signature) @@ -195,26 +191,13 @@ func (r *SignatureRepository) CheckUserSignatureStatus(ctx context.Context, docI func (r *SignatureRepository) GetLastSignature(ctx context.Context) (*models.Signature, error) { query := ` SELECT id, doc_id, user_sub, user_email, user_name, signed_at, payload_hash, signature, nonce, created_at, referer, prev_hash - FROM signatures - ORDER BY id DESC + FROM signatures + ORDER BY id DESC LIMIT 1 ` signature := &models.Signature{} - err := r.db.QueryRowContext(ctx, query).Scan( - &signature.ID, - &signature.DocID, - &signature.UserSub, - &signature.UserEmail, - &signature.UserName, - &signature.SignedAtUTC, - &signature.PayloadHash, - &signature.Signature, - &signature.Nonce, - &signature.CreatedAt, - &signature.Referer, - &signature.PrevHash, - ) + err := scanSignature(r.db.QueryRowContext(ctx, query), signature) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -243,21 +226,7 @@ func (r *SignatureRepository) GetAllSignaturesOrdered(ctx context.Context) ([]*m var signatures []*models.Signature for rows.Next() { signature := &models.Signature{} - err := rows.Scan( - &signature.ID, - &signature.DocID, - &signature.UserSub, - &signature.UserEmail, - &signature.UserName, - &signature.SignedAtUTC, - &signature.PayloadHash, - &signature.Signature, - &signature.Nonce, - &signature.CreatedAt, - &signature.Referer, - &signature.PrevHash, - ) - if err != nil { + if err := scanSignature(rows, signature); err != nil { continue } signatures = append(signatures, signature) diff --git a/internal/infrastructure/database/repository_constraints_test.go b/internal/infrastructure/database/repository_constraints_test.go index 2ec12d9..198a0ec 100644 --- a/internal/infrastructure/database/repository_constraints_test.go +++ b/internal/infrastructure/database/repository_constraints_test.go @@ -62,7 +62,7 @@ func TestRepository_DatabaseConstraints_Integration(t *testing.T) { { name: "valid signature with nulls", modifyFn: func(s *models.Signature) { - s.UserName = nil + s.UserName = "" s.Referer = nil s.PrevHash = nil }, @@ -403,7 +403,7 @@ func TestRepository_EdgeCases_Integration(t *testing.T) { // Test with empty strings for nullable fields sig := factory.CreateValidSignature() emptyString := "" - sig.UserName = &emptyString + sig.UserName = emptyString sig.Referer = &emptyString sig.PrevHash = &emptyString @@ -418,7 +418,7 @@ func TestRepository_EdgeCases_Integration(t *testing.T) { } // Verify empty strings are preserved (not converted to NULL) - if result.UserName == nil || *result.UserName != "" { + if result.UserName != "" { t.Error("Empty string UserName not preserved") } if result.Referer == nil || *result.Referer != "" {