diff --git a/supply-api/internal/iam/repository/iam_repository.go b/supply-api/internal/iam/repository/iam_repository.go index 1dea3c70..bcbf7426 100644 --- a/supply-api/internal/iam/repository/iam_repository.go +++ b/supply-api/internal/iam/repository/iam_repository.go @@ -69,6 +69,20 @@ func newPostgresIAMRepositoryWithDB(db iamDB) *PostgresIAMRepository { return &PostgresIAMRepository{pool: db} } +func iamStringOrEmpty(value *string) string { + if value == nil { + return "" + } + return *value +} + +func iamInt64OrZero(value *int64) int64 { + if value == nil { + return 0 + } + return *value +} + // Ensure interfaces var _ IAMRepository = (*PostgresIAMRepository)(nil) @@ -126,11 +140,11 @@ func (r *PostgresIAMRepository) GetRoleByCode(ctx context.Context, code string) var role model.Role var parentID *int64 - var createdIP, updatedIP *string + var requestID, createdIP, updatedIP *string err := r.pool.QueryRow(ctx, query, code).Scan( &role.ID, &role.Code, &role.Name, &role.Type, &parentID, &role.Level, - &role.Description, &role.IsActive, &role.RequestID, &createdIP, &updatedIP, + &role.Description, &role.IsActive, &requestID, &createdIP, &updatedIP, &role.Version, &role.CreatedAt, &role.UpdatedAt, ) if err != nil { @@ -141,12 +155,9 @@ func (r *PostgresIAMRepository) GetRoleByCode(ctx context.Context, code string) } role.ParentRoleID = parentID - if createdIP != nil { - role.CreatedIP = *createdIP - } - if updatedIP != nil { - role.UpdatedIP = *updatedIP - } + role.RequestID = iamStringOrEmpty(requestID) + role.CreatedIP = iamStringOrEmpty(createdIP) + role.UpdatedIP = iamStringOrEmpty(updatedIP) return &role, nil } @@ -217,11 +228,11 @@ func (r *PostgresIAMRepository) ListRoles(ctx context.Context, roleType string) for rows.Next() { var role model.Role var parentID *int64 - var createdIP, updatedIP *string + var requestID, createdIP, updatedIP *string err := rows.Scan( &role.ID, &role.Code, &role.Name, &role.Type, &parentID, &role.Level, - &role.Description, &role.IsActive, &role.RequestID, &createdIP, &updatedIP, + &role.Description, &role.IsActive, &requestID, &createdIP, &updatedIP, &role.Version, &role.CreatedAt, &role.UpdatedAt, ) if err != nil { @@ -229,12 +240,9 @@ func (r *PostgresIAMRepository) ListRoles(ctx context.Context, roleType string) } role.ParentRoleID = parentID - if createdIP != nil { - role.CreatedIP = *createdIP - } - if updatedIP != nil { - role.UpdatedIP = *updatedIP - } + role.RequestID = iamStringOrEmpty(requestID) + role.CreatedIP = iamStringOrEmpty(createdIP) + role.UpdatedIP = iamStringOrEmpty(updatedIP) roles = append(roles, &role) } @@ -266,9 +274,10 @@ func (r *PostgresIAMRepository) GetScopeByCode(ctx context.Context, code string) ` var scope model.Scope + var requestID *string err := r.pool.QueryRow(ctx, query, code).Scan( &scope.ID, &scope.Code, &scope.Name, &scope.Description, &scope.Type, - &scope.IsActive, &scope.RequestID, &scope.Version, &scope.CreatedAt, &scope.UpdatedAt, + &scope.IsActive, &requestID, &scope.Version, &scope.CreatedAt, &scope.UpdatedAt, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -277,6 +286,7 @@ func (r *PostgresIAMRepository) GetScopeByCode(ctx context.Context, code string) return nil, fmt.Errorf("failed to get scope: %w", err) } + scope.RequestID = iamStringOrEmpty(requestID) return &scope, nil } @@ -296,13 +306,15 @@ func (r *PostgresIAMRepository) ListScopes(ctx context.Context) ([]*model.Scope, var scopes []*model.Scope for rows.Next() { var scope model.Scope + var requestID *string err := rows.Scan( &scope.ID, &scope.Code, &scope.Name, &scope.Description, &scope.Type, - &scope.IsActive, &scope.RequestID, &scope.Version, &scope.CreatedAt, &scope.UpdatedAt, + &scope.IsActive, &requestID, &scope.Version, &scope.CreatedAt, &scope.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan scope: %w", err) } + scope.RequestID = iamStringOrEmpty(requestID) scopes = append(scopes, &scope) } @@ -480,10 +492,15 @@ func (r *PostgresIAMRepository) GetUserRoles(ctx context.Context, userID int64) for rows.Next() { var ur model.UserRoleMapping var roleCode string - err := rows.Scan(&ur.ID, &ur.UserID, &roleCode, &ur.TenantID, &ur.IsActive, &ur.GrantedBy, &ur.ExpiresAt, &ur.RequestID, &ur.CreatedAt, &ur.UpdatedAt) + var tenantID, grantedBy *int64 + var requestID *string + err := rows.Scan(&ur.ID, &ur.UserID, &roleCode, &tenantID, &ur.IsActive, &grantedBy, &ur.ExpiresAt, &requestID, &ur.CreatedAt, &ur.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to scan user role: %w", err) } + ur.TenantID = iamInt64OrZero(tenantID) + ur.GrantedBy = iamInt64OrZero(grantedBy) + ur.RequestID = iamStringOrEmpty(requestID) userRoles = append(userRoles, &ur) } @@ -510,10 +527,15 @@ func (r *PostgresIAMRepository) GetUserRolesWithCode(ctx context.Context, userID for rows.Next() { var ur model.UserRoleMapping var roleCode string - err := rows.Scan(&ur.ID, &ur.UserID, &roleCode, &ur.TenantID, &ur.IsActive, &ur.GrantedBy, &ur.ExpiresAt, &ur.RequestID, &ur.CreatedAt, &ur.UpdatedAt) + var tenantID, grantedBy *int64 + var requestID *string + err := rows.Scan(&ur.ID, &ur.UserID, &roleCode, &tenantID, &ur.IsActive, &grantedBy, &ur.ExpiresAt, &requestID, &ur.CreatedAt, &ur.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to scan user role: %w", err) } + ur.TenantID = iamInt64OrZero(tenantID) + ur.GrantedBy = iamInt64OrZero(grantedBy) + ur.RequestID = iamStringOrEmpty(requestID) userRoles = append(userRoles, &UserRoleWithCode{UserRoleMapping: &ur, RoleCode: roleCode}) } diff --git a/supply-api/internal/iam/repository/repository_test.go b/supply-api/internal/iam/repository/repository_test.go index 68017d8c..36fcd312 100644 --- a/supply-api/internal/iam/repository/repository_test.go +++ b/supply-api/internal/iam/repository/repository_test.go @@ -3,6 +3,7 @@ package repository import ( "context" "errors" + "fmt" "strings" "testing" "time" @@ -91,12 +92,24 @@ func assignIAMScan(dest []any, values []any) error { for i, value := range values { switch d := dest[i].(type) { case *int64: + if value == nil { + return fmt.Errorf("cannot scan NULL into *int64") + } *d = value.(int64) case *string: + if value == nil { + return fmt.Errorf("cannot scan NULL into *string") + } *d = value.(string) case *int: + if value == nil { + return fmt.Errorf("cannot scan NULL into *int") + } *d = value.(int) case *bool: + if value == nil { + return fmt.Errorf("cannot scan NULL into *bool") + } *d = value.(bool) case **int64: if value == nil { @@ -216,3 +229,55 @@ func TestRevokeRoleReturnsErrUserRoleNotFoundWhenNothingIsUpdated(t *testing.T) t.Fatalf("unexpected revoke args: %#v", db.execArgs) } } + +func TestListRolesAcceptsNullRequestID(t *testing.T) { + now := time.Unix(1710000000, 0).UTC() + db := &stubIAMDB{ + queryRows: &stubIAMRows{ + rows: [][]any{ + {int64(1), "viewer", "Viewer", model.RoleTypePlatform, nil, 10, "readonly", true, nil, nil, nil, 1, now, now}, + }, + }, + } + repo := newPostgresIAMRepositoryWithDB(db) + + roles, err := repo.ListRoles(context.Background(), "") + if err != nil { + t.Fatalf("ListRoles() error = %v", err) + } + if len(roles) != 1 { + t.Fatalf("role count = %d, want 1", len(roles)) + } + if roles[0].RequestID != "" { + t.Fatalf("expected null request_id to map to empty string, got %q", roles[0].RequestID) + } +} + +func TestGetUserRolesWithCodeAcceptsNullableTenantAndGrantFields(t *testing.T) { + now := time.Unix(1710000000, 0).UTC() + db := &stubIAMDB{ + queryRows: &stubIAMRows{ + rows: [][]any{ + {int64(11), int64(7), "viewer", nil, true, nil, nil, nil, now, now}, + }, + }, + } + repo := newPostgresIAMRepositoryWithDB(db) + + roles, err := repo.GetUserRolesWithCode(context.Background(), 7) + if err != nil { + t.Fatalf("GetUserRolesWithCode() error = %v", err) + } + if len(roles) != 1 { + t.Fatalf("role count = %d, want 1", len(roles)) + } + if roles[0].TenantID != 0 { + t.Fatalf("expected null tenant_id to map to 0, got %d", roles[0].TenantID) + } + if roles[0].GrantedBy != 0 { + t.Fatalf("expected null granted_by to map to 0, got %d", roles[0].GrantedBy) + } + if roles[0].RequestID != "" { + t.Fatalf("expected null request_id to map to empty string, got %q", roles[0].RequestID) + } +}