Skip to content

Commit adfa80e

Browse files
Add AI-generated tests for BUILD, user_role_sync.go (#1117)
Co-authored-by: testbot[bot] <testbot[bot]@users.noreply.github.com>
1 parent d5cb40c commit adfa80e

2 files changed

Lines changed: 329 additions & 0 deletions

File tree

src/utils/roles/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ go_test(
5252
name = "roles_integration_test",
5353
srcs = [
5454
"roles_integration_test.go",
55+
"user_role_sync_integration_test.go",
5556
],
5657
gotags = ["integration"],
5758
tags = [
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
//go:build integration
2+
3+
/*
4+
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
SPDX-License-Identifier: Apache-2.0
19+
*/
20+
21+
package roles_test
22+
23+
import (
24+
"context"
25+
"strings"
26+
"testing"
27+
28+
"go.corp.nvidia.com/osmo/tests/common/database"
29+
"go.corp.nvidia.com/osmo/utils/roles"
30+
)
31+
32+
// insertRoleWithSyncMode writes a roles row with the given sync_mode and an
33+
// empty policies list. The roles table has a NOT NULL DEFAULT for policies,
34+
// so the minimal insert is enough for the role-sync tests below.
35+
func insertRoleWithSyncMode(t *testing.T, fixture *database.PostgresFixture,
36+
name, syncMode string) {
37+
t.Helper()
38+
fixture.ExecSQL(t,
39+
`INSERT INTO roles (name, sync_mode) VALUES ($1, $2)`, name, syncMode)
40+
}
41+
42+
// insertRoleMapping creates an external→internal role mapping row.
43+
func insertRoleMapping(t *testing.T, fixture *database.PostgresFixture,
44+
roleName, externalRole string) {
45+
t.Helper()
46+
fixture.ExecSQL(t,
47+
`INSERT INTO role_external_mappings (role_name, external_role)
48+
VALUES ($1, $2)`, roleName, externalRole)
49+
}
50+
51+
// insertUser creates a users row directly (bypassing SyncUserRoles' upsert) so
52+
// pre-existing role assignments can be set up before the function under test
53+
// runs.
54+
func insertUser(t *testing.T, fixture *database.PostgresFixture, userID string) {
55+
t.Helper()
56+
fixture.ExecSQL(t,
57+
`INSERT INTO users (id, created_by) VALUES ($1, $1)`, userID)
58+
}
59+
60+
// insertUserRole writes a user_roles row directly.
61+
func insertUserRole(t *testing.T, fixture *database.PostgresFixture,
62+
userID, roleName string) {
63+
t.Helper()
64+
fixture.ExecSQL(t,
65+
`INSERT INTO user_roles (user_id, role_name) VALUES ($1, $2)`,
66+
userID, roleName)
67+
}
68+
69+
// readUserRoleNames returns the role names currently assigned to userID,
70+
// ordered by role_name for deterministic comparison.
71+
func readUserRoleNames(t *testing.T, fixture *database.PostgresFixture,
72+
userID string) []string {
73+
t.Helper()
74+
rows, err := fixture.Pool.Query(context.Background(),
75+
`SELECT role_name FROM user_roles WHERE user_id = $1 ORDER BY role_name`,
76+
userID)
77+
if err != nil {
78+
t.Fatalf("failed to query user_roles: %v", err)
79+
}
80+
defer rows.Close()
81+
82+
var names []string
83+
for rows.Next() {
84+
var name string
85+
if scanErr := rows.Scan(&name); scanErr != nil {
86+
t.Fatalf("scan failed: %v", scanErr)
87+
}
88+
names = append(names, name)
89+
}
90+
return names
91+
}
92+
93+
// containsString reports whether slice contains target. Helper kept outside
94+
// test bodies to avoid in-test loops.
95+
func containsString(slice []string, target string) bool {
96+
for _, item := range slice {
97+
if item == target {
98+
return true
99+
}
100+
}
101+
return false
102+
}
103+
104+
// TestSyncUserRoles_Integration_EmptyUserName covers the early-return guard
105+
// for empty user names: SyncUserRoles must return (nil, nil) without writing
106+
// to or reading from the database.
107+
func TestSyncUserRoles_Integration_EmptyUserName(t *testing.T) {
108+
fixture := database.StartPostgresWithSchema(t)
109+
110+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
111+
"", []string{"idp-admins"}, silentLogger())
112+
if err != nil {
113+
t.Fatalf("expected nil error for empty userName, got: %v", err)
114+
}
115+
if result != nil {
116+
t.Errorf("expected nil result for empty userName, got: %v", result)
117+
}
118+
}
119+
120+
// TestSyncUserRoles_Integration_ImportMode_AddsMappedRole covers the success
121+
// path that exercises upsertUser, the syncAndReturnRoles SQL, the "added"
122+
// change_type branch, and the logger.Info emit (because Added is non-empty).
123+
// The role row pre-exists in the roles table; the user row is created by
124+
// upsertUser inside SyncUserRoles.
125+
func TestSyncUserRoles_Integration_ImportMode_AddsMappedRole(t *testing.T) {
126+
fixture := database.StartPostgresWithSchema(t)
127+
128+
insertRoleWithSyncMode(t, fixture, "osmo-admin", roles.SyncModeImport)
129+
insertRoleMapping(t, fixture, "osmo-admin", "idp-admins")
130+
131+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
132+
"alice", []string{"idp-admins"}, silentLogger())
133+
if err != nil {
134+
t.Fatalf("unexpected error: %v", err)
135+
}
136+
137+
if !containsString(result, "osmo-admin") {
138+
t.Errorf("expected returned roles to contain %q, got: %v",
139+
"osmo-admin", result)
140+
}
141+
142+
stored := readUserRoleNames(t, fixture, "alice")
143+
if !containsString(stored, "osmo-admin") {
144+
t.Errorf("expected user_roles to contain %q after sync, got: %v",
145+
"osmo-admin", stored)
146+
}
147+
}
148+
149+
// TestSyncUserRoles_Integration_ForceMode_RemovesUnmappedRole covers the
150+
// "force" sync semantics that strip a role when its external mapping is
151+
// absent from the request, and exercises the "removed" change_type branch
152+
// of the row-scan switch.
153+
func TestSyncUserRoles_Integration_ForceMode_RemovesUnmappedRole(t *testing.T) {
154+
fixture := database.StartPostgresWithSchema(t)
155+
156+
insertRoleWithSyncMode(t, fixture, "osmo-eng", roles.SyncModeForce)
157+
insertRoleMapping(t, fixture, "osmo-eng", "idp-eng")
158+
insertUser(t, fixture, "bob")
159+
insertUserRole(t, fixture, "bob", "osmo-eng")
160+
161+
// External roles do NOT include "idp-eng", so force-mode removes osmo-eng.
162+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
163+
"bob", []string{"idp-other"}, silentLogger())
164+
if err != nil {
165+
t.Fatalf("unexpected error: %v", err)
166+
}
167+
168+
if containsString(result, "osmo-eng") {
169+
t.Errorf("expected returned roles NOT to contain force-removed %q, got: %v",
170+
"osmo-eng", result)
171+
}
172+
173+
stored := readUserRoleNames(t, fixture, "bob")
174+
if containsString(stored, "osmo-eng") {
175+
t.Errorf("expected user_roles NOT to contain %q after force sync, got: %v",
176+
"osmo-eng", stored)
177+
}
178+
}
179+
180+
// TestSyncUserRoles_Integration_ImportMode_DoesNotRemoveExisting covers the
181+
// "import" semantics: an existing role is preserved even when its external
182+
// mapping is absent. The role flows through the union as change_type
183+
// "existing" and is returned in RoleNames without being deleted.
184+
func TestSyncUserRoles_Integration_ImportMode_DoesNotRemoveExisting(t *testing.T) {
185+
fixture := database.StartPostgresWithSchema(t)
186+
187+
insertRoleWithSyncMode(t, fixture, "osmo-viewer", roles.SyncModeImport)
188+
insertRoleMapping(t, fixture, "osmo-viewer", "idp-viewers")
189+
insertUser(t, fixture, "carol")
190+
insertUserRole(t, fixture, "carol", "osmo-viewer")
191+
192+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
193+
"carol", []string{"idp-other"}, silentLogger())
194+
if err != nil {
195+
t.Fatalf("unexpected error: %v", err)
196+
}
197+
198+
if !containsString(result, "osmo-viewer") {
199+
t.Errorf("expected import-mode role to be retained, got: %v", result)
200+
}
201+
202+
stored := readUserRoleNames(t, fixture, "carol")
203+
if !containsString(stored, "osmo-viewer") {
204+
t.Errorf("expected user_roles to still contain %q, got: %v",
205+
"osmo-viewer", stored)
206+
}
207+
}
208+
209+
// TestSyncUserRoles_Integration_IgnoreMode_NotAddedEvenWhenMapped covers the
210+
// SQL filter "WHERE r.sync_mode != $3" (where $3 is SyncModeIgnore): roles
211+
// configured with sync_mode='ignore' are entirely outside the sync set, so
212+
// even a matching external role does not cause assignment.
213+
func TestSyncUserRoles_Integration_IgnoreMode_NotAddedEvenWhenMapped(t *testing.T) {
214+
fixture := database.StartPostgresWithSchema(t)
215+
216+
insertRoleWithSyncMode(t, fixture, "osmo-static", roles.SyncModeIgnore)
217+
insertRoleMapping(t, fixture, "osmo-static", "idp-static")
218+
219+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
220+
"dave", []string{"idp-static"}, silentLogger())
221+
if err != nil {
222+
t.Fatalf("unexpected error: %v", err)
223+
}
224+
225+
if containsString(result, "osmo-static") {
226+
t.Errorf("expected ignore-mode role NOT to be added, got: %v", result)
227+
}
228+
229+
stored := readUserRoleNames(t, fixture, "dave")
230+
if containsString(stored, "osmo-static") {
231+
t.Errorf("expected user_roles NOT to contain ignore-mode role, got: %v",
232+
stored)
233+
}
234+
}
235+
236+
// TestSyncUserRoles_Integration_NilExternalRoles covers the
237+
// "if len(externalRoles) == 0 { externalRoles = []string{} }" normalization:
238+
// passing nil must not crash and must produce a usable text[] for the SQL
239+
// driver. With force-mode roles defined but no current assignments, no
240+
// inserts or deletes are performed.
241+
func TestSyncUserRoles_Integration_NilExternalRoles(t *testing.T) {
242+
fixture := database.StartPostgresWithSchema(t)
243+
244+
insertRoleWithSyncMode(t, fixture, "osmo-keep", roles.SyncModeForce)
245+
insertRoleMapping(t, fixture, "osmo-keep", "idp-keep")
246+
247+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
248+
"eve", nil, silentLogger())
249+
if err != nil {
250+
t.Fatalf("unexpected error for nil externalRoles: %v", err)
251+
}
252+
if len(result) != 0 {
253+
t.Errorf("expected empty result for new user with no external roles, got: %v",
254+
result)
255+
}
256+
}
257+
258+
// TestSyncUserRoles_Integration_NoChanges_ReturnsExistingRole covers the
259+
// "existing" change_type branch where the user already has the role, the
260+
// external mapping still matches, and neither Added nor Removed is populated.
261+
// This is the path where the logger.Info emit is skipped.
262+
func TestSyncUserRoles_Integration_NoChanges_ReturnsExistingRole(t *testing.T) {
263+
fixture := database.StartPostgresWithSchema(t)
264+
265+
insertRoleWithSyncMode(t, fixture, "osmo-stable", roles.SyncModeImport)
266+
insertRoleMapping(t, fixture, "osmo-stable", "idp-stable")
267+
insertUser(t, fixture, "frank")
268+
insertUserRole(t, fixture, "frank", "osmo-stable")
269+
270+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
271+
"frank", []string{"idp-stable"}, silentLogger())
272+
if err != nil {
273+
t.Fatalf("unexpected error: %v", err)
274+
}
275+
276+
if !containsString(result, "osmo-stable") {
277+
t.Errorf("expected returned roles to contain pre-existing %q, got: %v",
278+
"osmo-stable", result)
279+
}
280+
}
281+
282+
// TestSyncUserRoles_Integration_CanceledContext_UpsertError covers the error
283+
// wrap "upsert user: <err>" returned by SyncUserRoles when upsertUser's
284+
// Pool().Exec call fails. A canceled context fails the Exec immediately.
285+
func TestSyncUserRoles_Integration_CanceledContext_UpsertError(t *testing.T) {
286+
fixture := database.StartPostgresWithSchema(t)
287+
288+
ctx, cancel := context.WithCancel(context.Background())
289+
cancel()
290+
291+
result, err := roles.SyncUserRoles(ctx, fixture.Client,
292+
"user-x", []string{"idp"}, silentLogger())
293+
if err == nil {
294+
t.Fatalf("expected error from canceled context, got nil")
295+
}
296+
if !strings.Contains(err.Error(), "upsert user") {
297+
t.Errorf("expected error wrapped with %q, got: %v", "upsert user", err)
298+
}
299+
if result != nil {
300+
t.Errorf("expected nil result on error, got: %v", result)
301+
}
302+
}
303+
304+
// TestSyncUserRoles_Integration_SyncQueryError covers the error wrap
305+
// "sync user roles: <err>" returned by SyncUserRoles when the
306+
// syncAndReturnRoles query fails. Dropping the roles table after the user
307+
// upsert succeeds makes the sync query reference a missing table; upsertUser
308+
// (which only touches users) still succeeds, so the second error path runs.
309+
func TestSyncUserRoles_Integration_SyncQueryError(t *testing.T) {
310+
fixture := database.StartPostgresWithSchema(t)
311+
312+
// CASCADE also drops user_roles and role_external_mappings (both FK into
313+
// roles), so syncAndReturnRoles' query references missing tables.
314+
fixture.ExecSQL(t, `DROP TABLE roles CASCADE`)
315+
316+
result, err := roles.SyncUserRoles(context.Background(), fixture.Client,
317+
"user-y", []string{"idp"}, silentLogger())
318+
if err == nil {
319+
t.Fatalf("expected error after dropping roles table, got nil")
320+
}
321+
if !strings.Contains(err.Error(), "sync user roles") {
322+
t.Errorf("expected error wrapped with %q, got: %v",
323+
"sync user roles", err)
324+
}
325+
if result != nil {
326+
t.Errorf("expected nil result on error, got: %v", result)
327+
}
328+
}

0 commit comments

Comments
 (0)