Skip to content

Commit c7faf08

Browse files
committed
Add tests for user and group deletion functionality
Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
1 parent b83a058 commit c7faf08

6 files changed

Lines changed: 486 additions & 0 deletions

File tree

cmd/authctl/group/delete_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package group_test
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/canonical/authd/internal/testutils"
11+
"github.com/stretchr/testify/require"
12+
"google.golang.org/grpc/codes"
13+
)
14+
15+
func TestGroupDeleteCommand(t *testing.T) {
16+
daemonSocket := testutils.StartAuthd(t, daemonPath,
17+
testutils.WithGroupFile(filepath.Join("testdata", "empty.group")),
18+
testutils.WithPreviousDBState("multiple_users_and_groups"),
19+
testutils.WithCurrentUserAsRoot,
20+
)
21+
22+
err := os.Setenv("AUTHD_SOCKET", daemonSocket)
23+
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")
24+
25+
tests := map[string]struct {
26+
args []string
27+
stdin string
28+
authdUnavailable bool
29+
30+
expectedExitCode int
31+
}{
32+
"Delete_group_success": {
33+
args: []string{"delete", "--yes", "group1"},
34+
expectedExitCode: 0,
35+
},
36+
37+
"Confirmation_prompt_accepted_with_y": {
38+
args: []string{"delete", "group2"},
39+
stdin: "y\n",
40+
expectedExitCode: 0,
41+
},
42+
"Confirmation_prompt_accepted_with_yes": {
43+
args: []string{"delete", "group3"},
44+
stdin: "yes\n",
45+
expectedExitCode: 0,
46+
},
47+
"Confirmation_prompt_accepted_case_insensitively": {
48+
args: []string{"delete", "group4"},
49+
stdin: "YES\n",
50+
expectedExitCode: 0,
51+
},
52+
"Confirmation_prompt_aborted_with_n": {
53+
args: []string{"delete", "group1"},
54+
stdin: "n\n",
55+
expectedExitCode: 0,
56+
},
57+
"Confirmation_prompt_aborted_with_empty_input": {
58+
args: []string{"delete", "group1"},
59+
stdin: "\n",
60+
expectedExitCode: 0,
61+
},
62+
63+
"Error_when_group_does_not_exist": {
64+
args: []string{"delete", "--yes", "nonexistent"},
65+
expectedExitCode: int(codes.NotFound),
66+
},
67+
"Error_when_authd_is_unavailable": {
68+
args: []string{"delete", "--yes", "group1"},
69+
authdUnavailable: true,
70+
expectedExitCode: int(codes.Unavailable),
71+
},
72+
}
73+
74+
for name, tc := range tests {
75+
t.Run(name, func(t *testing.T) {
76+
if tc.authdUnavailable {
77+
origValue := os.Getenv("AUTHD_SOCKET")
78+
err := os.Setenv("AUTHD_SOCKET", "/non-existent")
79+
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")
80+
t.Cleanup(func() {
81+
err := os.Setenv("AUTHD_SOCKET", origValue)
82+
require.NoError(t, err, "Failed to restore AUTHD_SOCKET environment variable")
83+
})
84+
}
85+
86+
//nolint:gosec // G204 it's safe to use exec.Command with a variable here
87+
cmd := exec.Command(authctlPath, append([]string{"group"}, tc.args...)...)
88+
if tc.stdin != "" {
89+
cmd.Stdin = strings.NewReader(tc.stdin)
90+
}
91+
testutils.CheckCommand(t, cmd, tc.expectedExitCode)
92+
})
93+
}
94+
}

cmd/authctl/user/delete_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package user_test
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/canonical/authd/internal/testutils"
11+
"github.com/stretchr/testify/require"
12+
"google.golang.org/grpc/codes"
13+
)
14+
15+
const homeBasePath = "/tmp/authd-delete-cmd-test/home"
16+
17+
func TestUserDeleteCommand(t *testing.T) {
18+
daemonSocket := testutils.StartAuthd(t, daemonPath,
19+
testutils.WithGroupFile(filepath.Join("testdata", "empty.group")),
20+
testutils.WithPreviousDBState("multiple_users_and_groups_with_tmp_home"),
21+
testutils.WithCurrentUserAsRoot,
22+
)
23+
t.Cleanup(func() { _ = os.RemoveAll(homeBasePath) })
24+
25+
err := os.Setenv("AUTHD_SOCKET", daemonSocket)
26+
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")
27+
28+
tests := map[string]struct {
29+
args []string
30+
stdin string
31+
authdUnavailable bool
32+
33+
createHomeDir bool
34+
wantHomeDirRemoved bool
35+
36+
expectedExitCode int
37+
}{
38+
"Delete_user_success": {
39+
args: []string{"delete", "--yes", "user1@example.com"},
40+
expectedExitCode: 0,
41+
},
42+
43+
"Confirmation_prompt_accepted_with_y": {
44+
args: []string{"delete", "user2@example.com"},
45+
stdin: "y\n",
46+
expectedExitCode: 0,
47+
},
48+
"Confirmation_prompt_accepted_with_yes": {
49+
args: []string{"delete", "user3@example.com"},
50+
stdin: "yes\n",
51+
expectedExitCode: 0,
52+
},
53+
"Confirmation_prompt_accepted_case_insensitively": {
54+
args: []string{"delete", "user4@example.com"},
55+
stdin: "YES\n",
56+
expectedExitCode: 0,
57+
},
58+
"Confirmation_prompt_aborted_with_n": {
59+
args: []string{"delete", "user1@example.com"},
60+
stdin: "n\n",
61+
expectedExitCode: 0,
62+
},
63+
"Confirmation_prompt_aborted_with_empty_input": {
64+
args: []string{"delete", "user1@example.com"},
65+
stdin: "\n",
66+
expectedExitCode: 0,
67+
},
68+
69+
"Delete_with_remove_flag_removes_home_dir": {
70+
args: []string{"delete", "--yes", "--remove", "user5@example.com"},
71+
createHomeDir: true,
72+
wantHomeDirRemoved: true,
73+
expectedExitCode: 0,
74+
},
75+
"Delete_without_remove_flag_keeps_home_dir": {
76+
args: []string{"delete", "--yes", "user6@example.com"},
77+
createHomeDir: true,
78+
expectedExitCode: 0,
79+
},
80+
"Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist": {
81+
args: []string{"delete", "--yes", "--remove", "user7@example.com"},
82+
wantHomeDirRemoved: true,
83+
expectedExitCode: 0,
84+
},
85+
86+
"Error_when_user_does_not_exist": {
87+
args: []string{"delete", "--yes", "nonexistent@example.com"},
88+
expectedExitCode: int(codes.NotFound),
89+
},
90+
"Error_when_authd_is_unavailable": {
91+
args: []string{"delete", "--yes", "user1@example.com"},
92+
authdUnavailable: true,
93+
expectedExitCode: int(codes.Unavailable),
94+
},
95+
}
96+
97+
for name, tc := range tests {
98+
t.Run(name, func(t *testing.T) {
99+
if tc.authdUnavailable {
100+
origValue := os.Getenv("AUTHD_SOCKET")
101+
err := os.Setenv("AUTHD_SOCKET", "/non-existent")
102+
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")
103+
t.Cleanup(func() {
104+
err := os.Setenv("AUTHD_SOCKET", origValue)
105+
require.NoError(t, err, "Failed to restore AUTHD_SOCKET environment variable")
106+
})
107+
}
108+
109+
// Extract the username from the last element of args
110+
username := tc.args[len(tc.args)-1]
111+
homeDir := filepath.Join(homeBasePath, username)
112+
113+
if tc.createHomeDir {
114+
err := os.MkdirAll(homeDir, 0o700)
115+
require.NoError(t, err, "Setup: failed to create home directory %q", homeDir)
116+
t.Cleanup(func() { _ = os.RemoveAll(homeDir) })
117+
}
118+
119+
//nolint:gosec // G204 it's safe to use exec.Command with a variable here
120+
cmd := exec.Command(authctlPath, append([]string{"user"}, tc.args...)...)
121+
if tc.stdin != "" {
122+
cmd.Stdin = strings.NewReader(tc.stdin)
123+
}
124+
testutils.CheckCommand(t, cmd, tc.expectedExitCode)
125+
126+
if tc.wantHomeDirRemoved {
127+
require.NoDirExists(t, homeDir, "Home directory %q should have been removed", homeDir)
128+
} else if tc.createHomeDir {
129+
require.DirExists(t, homeDir, "Home directory %q should still exist", homeDir)
130+
}
131+
})
132+
}
133+
}

internal/brokers/broker_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,33 @@ func TestUserPreCheck(t *testing.T) {
346346
}
347347
}
348348

349+
func TestDeleteUser(t *testing.T) {
350+
t.Parallel()
351+
352+
b := newBrokerForTests(t, "", "")
353+
354+
tests := map[string]struct {
355+
username string
356+
357+
wantErr bool
358+
}{
359+
"Successfully_delete_user": {username: "user1@example.com"},
360+
"Error_when_broker_returns_error": {username: "delete_error@example.com", wantErr: true},
361+
}
362+
for name, tc := range tests {
363+
t.Run(name, func(t *testing.T) {
364+
t.Parallel()
365+
366+
err := b.DeleteUser(context.Background(), tc.username)
367+
if tc.wantErr {
368+
require.Error(t, err, "DeleteUser should return an error, but did not")
369+
return
370+
}
371+
require.NoError(t, err, "DeleteUser should not return an error, but did")
372+
})
373+
}
374+
}
375+
349376
func newBrokerForTests(t *testing.T, cfgDir, brokerCfg string) (b brokers.Broker) {
350377
t.Helper()
351378

internal/services/user/user_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,85 @@ func TestSetGroupID(t *testing.T) {
423423
}
424424
}
425425

426+
func TestDeleteUser(t *testing.T) {
427+
tests := map[string]struct {
428+
sourceDB string
429+
username string
430+
currentUserNotRoot bool
431+
432+
wantErr bool
433+
}{
434+
"Successfully_delete_user": {username: "user1@example.com"},
435+
"Successfully_delete_user_with_uppercase": {username: "USER1@EXAMPLE.COM"},
436+
437+
"Error_when_username_is_empty": {wantErr: true},
438+
"Error_when_user_does_not_exist": {username: "doesnotexist@example.com", wantErr: true},
439+
"Error_when_not_root": {username: "user1@example.com", currentUserNotRoot: true, wantErr: true},
440+
"Error_when_broker_fails_to_delete": {username: "delete_error@example.com", wantErr: true},
441+
"Error_when_broker_not_found": {sourceDB: "default.db.yaml", username: "user1@example.com", wantErr: true},
442+
}
443+
for name, tc := range tests {
444+
t.Run(name, func(t *testing.T) {
445+
if !tc.wantErr {
446+
userslocking.Z_ForTests_OverrideLockingWithCleanup(t)
447+
}
448+
449+
dbFile := tc.sourceDB
450+
if dbFile == "" {
451+
dbFile = "delete-user.db.yaml"
452+
}
453+
454+
client, m := newUserServiceClient(t, dbFile, tc.currentUserNotRoot)
455+
456+
_, err := client.DeleteUser(context.Background(), &authd.DeleteUserRequest{Name: tc.username})
457+
if tc.wantErr {
458+
require.Error(t, err, "DeleteUser should return an error, but did not")
459+
return
460+
}
461+
require.NoError(t, err, "DeleteUser should not return an error, but did")
462+
463+
dbContent, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m))
464+
require.NoError(t, err, "Setup: failed to dump database for comparing")
465+
golden.CheckOrUpdate(t, dbContent)
466+
})
467+
}
468+
}
469+
470+
func TestDeleteGroup(t *testing.T) {
471+
tests := map[string]struct {
472+
sourceDB string
473+
474+
groupname string
475+
currentUserNotRoot bool
476+
477+
wantErr bool
478+
}{
479+
"Successfully_delete_group": {groupname: "commongroup"},
480+
"Successfully_delete_group_with_uppercase": {groupname: "COMMONGROUP"},
481+
482+
"Error_when_groupname_is_empty": {wantErr: true},
483+
"Error_when_group_does_not_exist": {groupname: "doesnotexist", wantErr: true},
484+
"Error_when_not_root": {groupname: "commongroup", currentUserNotRoot: true, wantErr: true},
485+
"Error_when_group_is_primary_group_of_an_existing_user": {groupname: "group1", wantErr: true},
486+
}
487+
for name, tc := range tests {
488+
t.Run(name, func(t *testing.T) {
489+
client, m := newUserServiceClient(t, tc.sourceDB, tc.currentUserNotRoot)
490+
491+
_, err := client.DeleteGroup(context.Background(), &authd.DeleteGroupRequest{Name: tc.groupname})
492+
if tc.wantErr {
493+
require.Error(t, err, "DeleteGroup should return an error, but did not")
494+
return
495+
}
496+
require.NoError(t, err, "DeleteGroup should not return an error, but did")
497+
498+
dbContent, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m))
499+
require.NoError(t, err, "Setup: failed to dump database for comparing")
500+
golden.CheckOrUpdate(t, dbContent)
501+
})
502+
}
503+
}
504+
426505
// newUserServiceClient returns a new gRPC client for the CLI service.
427506
func newUserServiceClient(t *testing.T, dbFile string, currentUserNotRoot ...bool) (client authd.UserServiceClient, userManager *users.Manager) {
428507
t.Helper()

internal/users/db/db_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,46 @@ func TestDeleteUser(t *testing.T) {
10661066
}
10671067
}
10681068

1069+
func TestDeleteGroup(t *testing.T) {
1070+
t.Parallel()
1071+
1072+
tests := map[string]struct {
1073+
dbFile string
1074+
gid uint32
1075+
1076+
wantErr bool
1077+
wantErrType error
1078+
}{
1079+
"Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user": {dbFile: "one_user_and_group", gid: 11111},
1080+
"Deleting_shared_group_removes_only_that_group_and_its_memberships": {dbFile: "multiple_users_and_groups", gid: 99999},
1081+
1082+
"Error_on_nonexistent_group": {gid: 11111, wantErrType: db.NoDataFoundError{}},
1083+
}
1084+
for name, tc := range tests {
1085+
t.Run(name, func(t *testing.T) {
1086+
t.Parallel()
1087+
1088+
c := initDB(t, tc.dbFile)
1089+
1090+
err := c.DeleteGroup(tc.gid)
1091+
log.Debugf(context.Background(), "DeleteGroup error: %v", err)
1092+
if tc.wantErr {
1093+
require.Error(t, err, "DeleteGroup should return an error but didn't")
1094+
return
1095+
}
1096+
if tc.wantErrType != nil {
1097+
require.ErrorIs(t, err, tc.wantErrType, "DeleteGroup should return expected error")
1098+
return
1099+
}
1100+
require.NoError(t, err)
1101+
1102+
got, err := db.Z_ForTests_DumpNormalizedYAML(c)
1103+
require.NoError(t, err)
1104+
golden.CheckOrUpdate(t, got)
1105+
})
1106+
}
1107+
}
1108+
10691109
// TestBackwardCompatibilityAndMigrations covers loading legacy schemas (e.g., v2 with INT ugid)
10701110
// and migrating older schemas (e.g., v1 without 'locked' column) to the latest schema.
10711111
func TestBackwardCompatibilityAndMigrations(t *testing.T) {

0 commit comments

Comments
 (0)