18858: Adds duplicated email check & tests.
[arvados.git] / tools / sync-users / sync-users_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "fmt"
9         "io/ioutil"
10         "os"
11         "regexp"
12         "strings"
13         "testing"
14
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         . "gopkg.in/check.v1"
17 )
18
19 // Gocheck boilerplate
20 func Test(t *testing.T) {
21         TestingT(t)
22 }
23
24 type TestSuite struct {
25         cfg   *ConfigParams
26         ac    *arvados.Client
27         users map[string]arvados.User
28 }
29
30 func (s *TestSuite) SetUpTest(c *C) {
31         s.ac = arvados.NewClientFromEnv()
32         u, err := s.ac.CurrentUser()
33         c.Assert(err, IsNil)
34         c.Assert(u.IsAdmin, Equals, true)
35
36         s.users = make(map[string]arvados.User)
37         ul := arvados.UserList{}
38         s.ac.RequestAndDecode(&ul, "GET", "/arvados/v1/users", nil, arvados.ResourceListParams{})
39         c.Assert(ul.ItemsAvailable, Not(Equals), 0)
40         s.users = make(map[string]arvados.User)
41         for _, u := range ul.Items {
42                 s.users[u.UUID] = u
43         }
44
45         // Set up command config
46         os.Args = []string{"cmd", "somefile.csv"}
47         config, err := GetConfig()
48         c.Assert(err, IsNil)
49         s.cfg = &config
50 }
51
52 func (s *TestSuite) TearDownTest(c *C) {
53         var dst interface{}
54         // Reset database to fixture state after every test run.
55         err := s.cfg.Client.RequestAndDecode(&dst, "POST", "/database/reset", nil, nil)
56         c.Assert(err, IsNil)
57 }
58
59 var _ = Suite(&TestSuite{})
60
61 // MakeTempCSVFile creates a temp file with data as comma separated values
62 func MakeTempCSVFile(data [][]string) (f *os.File, err error) {
63         f, err = ioutil.TempFile("", "test_sync_users")
64         if err != nil {
65                 return
66         }
67         for _, line := range data {
68                 fmt.Fprintf(f, "%s\n", strings.Join(line, ","))
69         }
70         err = f.Close()
71         return
72 }
73
74 func ListUsers(ac *arvados.Client) ([]arvados.User, error) {
75         var ul arvados.UserList
76         err := ac.RequestAndDecode(&ul, "GET", "/arvados/v1/users", nil, arvados.ResourceListParams{})
77         if err != nil {
78                 return nil, err
79         }
80         return ul.Items, nil
81 }
82
83 func (s *TestSuite) TestParseFlagsWithoutPositionalArgument(c *C) {
84         os.Args = []string{"cmd", "-verbose"}
85         err := ParseFlags(&ConfigParams{})
86         c.Assert(err, NotNil)
87 }
88
89 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
90         cfg := ConfigParams{}
91         os.Args = []string{"cmd", "/tmp/somefile.csv"}
92         err := ParseFlags(&cfg)
93         c.Assert(err, IsNil)
94         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
95         c.Assert(cfg.Verbose, Equals, false)
96         c.Assert(cfg.DeactivateUnlisted, Equals, false)
97 }
98
99 func (s *TestSuite) TestParseFlagsWithOptionalFlags(c *C) {
100         cfg := ConfigParams{}
101         os.Args = []string{"cmd", "-verbose", "-deactivate-unlisted", "/tmp/somefile.csv"}
102         err := ParseFlags(&cfg)
103         c.Assert(err, IsNil)
104         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
105         c.Assert(cfg.Verbose, Equals, true)
106         c.Assert(cfg.DeactivateUnlisted, Equals, true)
107 }
108
109 func (s *TestSuite) TestGetConfig(c *C) {
110         os.Args = []string{"cmd", "/tmp/somefile.csv"}
111         cfg, err := GetConfig()
112         c.Assert(err, IsNil)
113         c.Assert(cfg.AnonUserUUID, Not(Equals), "")
114         c.Assert(cfg.SysUserUUID, Not(Equals), "")
115         c.Assert(cfg.CurrentUser, Not(Equals), "")
116         c.Assert(cfg.ClusterID, Not(Equals), "")
117         c.Assert(cfg.Client, NotNil)
118 }
119
120 func (s *TestSuite) TestFailOnEmptyFields(c *C) {
121         records := [][]string{
122                 {"", "first-name", "last-name", "1", "0"},
123                 {"user@example", "", "last-name", "1", "0"},
124                 {"user@example", "first-name", "", "1", "0"},
125                 {"user@example", "first-name", "last-name", "", "0"},
126                 {"user@example", "first-name", "last-name", "1", ""},
127         }
128         for _, record := range records {
129                 data := [][]string{record}
130                 tmpfile, err := MakeTempCSVFile(data)
131                 c.Assert(err, IsNil)
132                 defer os.Remove(tmpfile.Name())
133                 s.cfg.Path = tmpfile.Name()
134                 err = doMain(s.cfg)
135                 c.Assert(err, NotNil)
136                 c.Assert(err, ErrorMatches, ".*fields cannot be empty.*")
137         }
138 }
139
140 func (s *TestSuite) TestIgnoreSpaces(c *C) {
141         // Make sure users aren't already there from fixtures
142         for _, user := range s.users {
143                 e := user.Email
144                 found := e == "user1@example.com" || e == "user2@example.com" || e == "user3@example.com"
145                 c.Assert(found, Equals, false)
146         }
147         // Use CSV data with leading/trailing whitespaces, confirm that they get ignored
148         data := [][]string{
149                 {" user1@example.com", "  Example", "   User1", "1", "0"},
150                 {"user2@example.com ", "Example  ", "User2   ", "1", "0"},
151                 {" user3@example.com ", "  Example  ", "   User3   ", "1", "0"},
152         }
153         tmpfile, err := MakeTempCSVFile(data)
154         c.Assert(err, IsNil)
155         defer os.Remove(tmpfile.Name())
156         s.cfg.Path = tmpfile.Name()
157         err = doMain(s.cfg)
158         c.Assert(err, IsNil)
159         users, err := ListUsers(s.cfg.Client)
160         c.Assert(err, IsNil)
161         for _, userNr := range []int{1, 2, 3} {
162                 found := false
163                 for _, user := range users {
164                         if user.Email == fmt.Sprintf("user%d@example.com", userNr) &&
165                                 user.LastName == fmt.Sprintf("User%d", userNr) &&
166                                 user.FirstName == "Example" && user.IsActive == true {
167                                 found = true
168                                 break
169                         }
170                 }
171                 c.Assert(found, Equals, true)
172         }
173 }
174
175 // Error out when records have != 5 records
176 func (s *TestSuite) TestWrongNumberOfFields(c *C) {
177         for _, testCase := range [][][]string{
178                 {{"user1@example.com", "Example", "User1", "1"}},
179                 {{"user1@example.com", "Example", "User1", "1", "0", "extra data"}},
180         } {
181                 tmpfile, err := MakeTempCSVFile(testCase)
182                 c.Assert(err, IsNil)
183                 defer os.Remove(tmpfile.Name())
184                 s.cfg.Path = tmpfile.Name()
185                 err = doMain(s.cfg)
186                 c.Assert(err, NotNil)
187                 c.Assert(err, ErrorMatches, ".*expected 5 fields, found.*")
188         }
189 }
190
191 // Error out when records have incorrect data types
192 func (s *TestSuite) TestWrongDataFields(c *C) {
193         for _, testCase := range [][][]string{
194                 {{"user1@example.com", "Example", "User1", "yep", "0"}},
195                 {{"user1@example.com", "Example", "User1", "1", "nope"}},
196         } {
197                 tmpfile, err := MakeTempCSVFile(testCase)
198                 c.Assert(err, IsNil)
199                 defer os.Remove(tmpfile.Name())
200                 s.cfg.Path = tmpfile.Name()
201                 err = doMain(s.cfg)
202                 c.Assert(err, NotNil)
203                 c.Assert(err, ErrorMatches, ".*parsing error at line.*[active|admin] status not recognized.*")
204         }
205 }
206
207 // Activate and deactivate users
208 func (s *TestSuite) TestUserCreationAndUpdate(c *C) {
209         testCases := []struct {
210                 Email     string
211                 FirstName string
212                 LastName  string
213                 Active    bool
214                 Admin     bool
215         }{{
216                 Email:     "user1@example.com",
217                 FirstName: "Example",
218                 LastName:  "User1",
219                 Active:    true,
220                 Admin:     false,
221         }, {
222                 Email:     "admin1@example.com",
223                 FirstName: "Example",
224                 LastName:  "Admin1",
225                 Active:    true,
226                 Admin:     true,
227         }}
228         // Make sure users aren't already there from fixtures
229         for _, user := range s.users {
230                 e := user.Email
231                 found := e == testCases[0].Email || e == testCases[1].Email
232                 c.Assert(found, Equals, false)
233         }
234         // User creation
235         data := [][]string{
236                 {testCases[0].Email, testCases[0].FirstName, testCases[0].LastName, fmt.Sprintf("%t", testCases[0].Active), fmt.Sprintf("%t", testCases[0].Admin)},
237                 {testCases[1].Email, testCases[1].FirstName, testCases[1].LastName, fmt.Sprintf("%t", testCases[1].Active), fmt.Sprintf("%t", testCases[1].Admin)},
238         }
239         tmpfile, err := MakeTempCSVFile(data)
240         c.Assert(err, IsNil)
241         defer os.Remove(tmpfile.Name())
242         s.cfg.Path = tmpfile.Name()
243         err = doMain(s.cfg)
244         c.Assert(err, IsNil)
245
246         users, err := ListUsers(s.cfg.Client)
247         c.Assert(err, IsNil)
248         for _, tc := range testCases {
249                 var foundUser arvados.User
250                 for _, user := range users {
251                         if user.Email == tc.Email {
252                                 foundUser = user
253                                 break
254                         }
255                 }
256                 c.Assert(foundUser, NotNil)
257                 c.Logf("Checking recently created user %q", foundUser.Email)
258                 c.Assert(foundUser.FirstName, Equals, tc.FirstName)
259                 c.Assert(foundUser.LastName, Equals, tc.LastName)
260                 c.Assert(foundUser.IsActive, Equals, true)
261                 c.Assert(foundUser.IsAdmin, Equals, tc.Admin)
262         }
263         // User deactivation
264         testCases[0].Active = false
265         testCases[1].Active = false
266         data = [][]string{
267                 {testCases[0].Email, testCases[0].FirstName, testCases[0].LastName, fmt.Sprintf("%t", testCases[0].Active), fmt.Sprintf("%t", testCases[0].Admin)},
268                 {testCases[1].Email, testCases[1].FirstName, testCases[1].LastName, fmt.Sprintf("%t", testCases[1].Active), fmt.Sprintf("%t", testCases[1].Admin)},
269         }
270         tmpfile, err = MakeTempCSVFile(data)
271         c.Assert(err, IsNil)
272         defer os.Remove(tmpfile.Name())
273         s.cfg.Path = tmpfile.Name()
274         err = doMain(s.cfg)
275         c.Assert(err, IsNil)
276
277         users, err = ListUsers(s.cfg.Client)
278         c.Assert(err, IsNil)
279         for _, tc := range testCases {
280                 var foundUser arvados.User
281                 for _, user := range users {
282                         if user.Email == tc.Email {
283                                 foundUser = user
284                                 break
285                         }
286                 }
287                 c.Assert(foundUser, NotNil)
288                 c.Logf("Checking recently deactivated user %q", foundUser.Email)
289                 c.Assert(foundUser.FirstName, Equals, tc.FirstName)
290                 c.Assert(foundUser.LastName, Equals, tc.LastName)
291                 c.Assert(foundUser.IsActive, Equals, false)
292                 c.Assert(foundUser.IsAdmin, Equals, false) // inactive users cannot be admins
293         }
294 }
295
296 func (s *TestSuite) TestDeactivateUnlisted(c *C) {
297         localUserUuidRegex := regexp.MustCompile(fmt.Sprintf("^%s-tpzed-[0-9a-z]{15}$", s.cfg.ClusterID))
298         users, err := ListUsers(s.cfg.Client)
299         c.Assert(err, IsNil)
300         previouslyActiveUsers := 0
301         for _, u := range users {
302                 if u.UUID == fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID) && !u.IsActive {
303                         // Make sure the anonymous user is active for this test
304                         var au arvados.User
305                         err := UpdateUser(s.cfg.Client, u.UUID, &au, map[string]string{"is_active": "true"})
306                         c.Assert(err, IsNil)
307                         c.Assert(au.IsActive, Equals, true)
308                 }
309                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
310                         previouslyActiveUsers++
311                 }
312         }
313         // At least 3 active users: System root, Anonymous and the current user.
314         // Other active users should exist from fixture.
315         c.Logf("Initial active users count: %d", previouslyActiveUsers)
316         c.Assert(previouslyActiveUsers > 3, Equals, true)
317
318         s.cfg.DeactivateUnlisted = true
319         s.cfg.Verbose = true
320         data := [][]string{
321                 {"user1@example.com", "Example", "User1", "0", "0"},
322         }
323         tmpfile, err := MakeTempCSVFile(data)
324         c.Assert(err, IsNil)
325         defer os.Remove(tmpfile.Name())
326         s.cfg.Path = tmpfile.Name()
327         err = doMain(s.cfg)
328         c.Assert(err, IsNil)
329
330         users, err = ListUsers(s.cfg.Client)
331         c.Assert(err, IsNil)
332         currentlyActiveUsers := 0
333         acceptableActiveUUIDs := map[string]bool{
334                 fmt.Sprintf("%s-tpzed-000000000000000", s.cfg.ClusterID): true,
335                 fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID): true,
336                 s.cfg.CurrentUser.UUID: true,
337         }
338         remainingActiveUUIDs := map[string]bool{}
339         seenUserEmails := map[string]bool{}
340         for _, u := range users {
341                 if _, ok := seenUserEmails[u.Email]; ok {
342                         c.Errorf("Duplicated email address %q in user list (probably from fixtures). This test requires unique email addresses.", u.Email)
343                 }
344                 seenUserEmails[u.Email] = true
345                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
346                         c.Logf("Found remaining active user %q (%s)", u.Email, u.UUID)
347                         _, ok := acceptableActiveUUIDs[u.UUID]
348                         c.Assert(ok, Equals, true)
349                         remainingActiveUUIDs[u.UUID] = true
350                         currentlyActiveUsers++
351                 }
352         }
353         // 3 active users remaining: System root, Anonymous and the current user.
354         c.Logf("Active local users remaining: %v", remainingActiveUUIDs)
355         c.Assert(currentlyActiveUsers, Equals, 3)
356 }
357
358 func (s *TestSuite) TestFailOnDuplicatedEmails(c *C) {
359         for i := range []int{1, 2} {
360                 isAdmin := i == 2
361                 err := CreateUser(s.cfg.Client, &arvados.User{}, map[string]string{
362                         "email":      "somedupedemail@example.com",
363                         "first_name": fmt.Sprintf("Duped %d", i),
364                         "username":   fmt.Sprintf("dupedemail%d", i),
365                         "last_name":  "User",
366                         "is_active":  "true",
367                         "is_admin":   fmt.Sprintf("%t", isAdmin),
368                 })
369                 c.Assert(err, IsNil)
370         }
371         s.cfg.Verbose = true
372         data := [][]string{
373                 {"user1@example.com", "Example", "User1", "0", "0"},
374         }
375         tmpfile, err := MakeTempCSVFile(data)
376         c.Assert(err, IsNil)
377         defer os.Remove(tmpfile.Name())
378         s.cfg.Path = tmpfile.Name()
379         err = doMain(s.cfg)
380         c.Assert(err, NotNil)
381         c.Assert(err, ErrorMatches, "skipped.*duplicated email address.*")
382 }