fix(iam): tolerate nullable db-backed role fields

This commit is contained in:
Your Name
2026-04-20 16:14:12 +08:00
parent 566169687a
commit a109a6836f
2 changed files with 107 additions and 20 deletions

View File

@@ -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})
}

View File

@@ -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)
}
}