fix(iam): tolerate nullable db-backed role fields
This commit is contained in:
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user