Merge branch '20735-update-deps'
[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 // RecordsToStrings formats the input data suitable for MakeTempCSVFile
75 func RecordsToStrings(records []userRecord) [][]string {
76         data := [][]string{}
77         for _, u := range records {
78                 data = append(data, []string{
79                         u.UserID,
80                         u.FirstName,
81                         u.LastName,
82                         fmt.Sprintf("%t", u.Active),
83                         fmt.Sprintf("%t", u.Admin)})
84         }
85         return data
86 }
87
88 func ListUsers(ac *arvados.Client) ([]arvados.User, error) {
89         var ul arvados.UserList
90         err := ac.RequestAndDecode(&ul, "GET", "/arvados/v1/users", nil, arvados.ResourceListParams{})
91         if err != nil {
92                 return nil, err
93         }
94         return ul.Items, nil
95 }
96
97 func (s *TestSuite) TestParseFlagsWithoutPositionalArgument(c *C) {
98         os.Args = []string{"cmd", "-verbose"}
99         err := ParseFlags(&ConfigParams{})
100         c.Assert(err, NotNil)
101         c.Assert(err, ErrorMatches, ".*please provide a path to an input file.*")
102 }
103
104 func (s *TestSuite) TestParseFlagsWrongUserID(c *C) {
105         os.Args = []string{"cmd", "-user-id", "nickname", "/tmp/somefile.csv"}
106         err := ParseFlags(&ConfigParams{})
107         c.Assert(err, NotNil)
108         c.Assert(err, ErrorMatches, ".*user ID must be one of:.*")
109 }
110
111 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
112         cfg := ConfigParams{}
113         os.Args = []string{"cmd", "/tmp/somefile.csv"}
114         err := ParseFlags(&cfg)
115         c.Assert(err, IsNil)
116         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
117         c.Assert(cfg.Verbose, Equals, false)
118         c.Assert(cfg.DeactivateUnlisted, Equals, false)
119         c.Assert(cfg.UserID, Equals, "email")
120         c.Assert(cfg.CaseInsensitive, Equals, true)
121 }
122
123 func (s *TestSuite) TestParseFlagsWithOptionalFlags(c *C) {
124         cfg := ConfigParams{}
125         os.Args = []string{"cmd", "-verbose", "-deactivate-unlisted", "-user-id", "username", "/tmp/somefile.csv"}
126         err := ParseFlags(&cfg)
127         c.Assert(err, IsNil)
128         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
129         c.Assert(cfg.Verbose, Equals, true)
130         c.Assert(cfg.DeactivateUnlisted, Equals, true)
131         c.Assert(cfg.UserID, Equals, "username")
132         c.Assert(cfg.CaseInsensitive, Equals, false)
133 }
134
135 func (s *TestSuite) TestGetConfig(c *C) {
136         os.Args = []string{"cmd", "/tmp/somefile.csv"}
137         cfg, err := GetConfig()
138         c.Assert(err, IsNil)
139         c.Assert(cfg.AnonUserUUID, Not(Equals), "")
140         c.Assert(cfg.SysUserUUID, Not(Equals), "")
141         c.Assert(cfg.CurrentUser, Not(Equals), "")
142         c.Assert(cfg.ClusterID, Not(Equals), "")
143         c.Assert(cfg.Client, NotNil)
144 }
145
146 func (s *TestSuite) TestFailOnEmptyFields(c *C) {
147         records := [][]string{
148                 {"", "first-name", "last-name", "1", "0"},
149                 {"user@example", "", "last-name", "1", "0"},
150                 {"user@example", "first-name", "", "1", "0"},
151                 {"user@example", "first-name", "last-name", "", "0"},
152                 {"user@example", "first-name", "last-name", "1", ""},
153         }
154         for _, record := range records {
155                 data := [][]string{record}
156                 tmpfile, err := MakeTempCSVFile(data)
157                 c.Assert(err, IsNil)
158                 defer os.Remove(tmpfile.Name())
159                 s.cfg.Path = tmpfile.Name()
160                 err = doMain(s.cfg)
161                 c.Assert(err, NotNil)
162                 c.Assert(err, ErrorMatches, ".*fields cannot be empty.*")
163         }
164 }
165
166 func (s *TestSuite) TestIgnoreSpaces(c *C) {
167         // Make sure users aren't already there from fixtures
168         for _, user := range s.users {
169                 e := user.Email
170                 found := e == "user1@example.com" || e == "user2@example.com" || e == "user3@example.com"
171                 c.Assert(found, Equals, false)
172         }
173         // Use CSV data with leading/trailing whitespaces, confirm that they get ignored
174         data := [][]string{
175                 {" user1@example.com", "  Example", "   User1", "1", "0"},
176                 {"user2@example.com ", "Example  ", "User2   ", "1", "0"},
177                 {" user3@example.com ", "  Example  ", "   User3   ", "1", "0"},
178         }
179         tmpfile, err := MakeTempCSVFile(data)
180         c.Assert(err, IsNil)
181         defer os.Remove(tmpfile.Name())
182         s.cfg.Path = tmpfile.Name()
183         err = doMain(s.cfg)
184         c.Assert(err, IsNil)
185         users, err := ListUsers(s.cfg.Client)
186         c.Assert(err, IsNil)
187         for _, userNr := range []int{1, 2, 3} {
188                 found := false
189                 for _, user := range users {
190                         if user.Email == fmt.Sprintf("user%d@example.com", userNr) &&
191                                 user.LastName == fmt.Sprintf("User%d", userNr) &&
192                                 user.FirstName == "Example" && user.IsActive == true {
193                                 found = true
194                                 break
195                         }
196                 }
197                 c.Assert(found, Equals, true)
198         }
199 }
200
201 // Error out when records have != 5 records
202 func (s *TestSuite) TestWrongNumberOfFields(c *C) {
203         for _, testCase := range [][][]string{
204                 {{"user1@example.com", "Example", "User1", "1"}},
205                 {{"user1@example.com", "Example", "User1", "1", "0", "extra data"}},
206         } {
207                 tmpfile, err := MakeTempCSVFile(testCase)
208                 c.Assert(err, IsNil)
209                 defer os.Remove(tmpfile.Name())
210                 s.cfg.Path = tmpfile.Name()
211                 err = doMain(s.cfg)
212                 c.Assert(err, NotNil)
213                 c.Assert(err, ErrorMatches, ".*expected 5 fields, found.*")
214         }
215 }
216
217 // Error out when records have incorrect data types
218 func (s *TestSuite) TestWrongDataFields(c *C) {
219         for _, testCase := range [][][]string{
220                 {{"user1@example.com", "Example", "User1", "yep", "0"}},
221                 {{"user1@example.com", "Example", "User1", "1", "nope"}},
222         } {
223                 tmpfile, err := MakeTempCSVFile(testCase)
224                 c.Assert(err, IsNil)
225                 defer os.Remove(tmpfile.Name())
226                 s.cfg.Path = tmpfile.Name()
227                 err = doMain(s.cfg)
228                 c.Assert(err, NotNil)
229                 c.Assert(err, ErrorMatches, ".*parsing error at line.*[active|admin] status not recognized.*")
230         }
231 }
232
233 // Create, activate and deactivate users
234 func (s *TestSuite) TestUserCreationAndUpdate(c *C) {
235         for _, tc := range []string{"email", "username"} {
236                 uIDPrefix := tc
237                 uIDSuffix := ""
238                 if tc == "email" {
239                         uIDSuffix = "@example.com"
240                 }
241                 s.cfg.UserID = tc
242                 records := []userRecord{{
243                         UserID:    fmt.Sprintf("%suser1%s", uIDPrefix, uIDSuffix),
244                         FirstName: "Example",
245                         LastName:  "User1",
246                         Active:    true,
247                         Admin:     false,
248                 }, {
249                         UserID:    fmt.Sprintf("%suser2%s", uIDPrefix, uIDSuffix),
250                         FirstName: "Example",
251                         LastName:  "User2",
252                         Active:    false, // initially inactive
253                         Admin:     false,
254                 }, {
255                         UserID:    fmt.Sprintf("%sadmin1%s", uIDPrefix, uIDSuffix),
256                         FirstName: "Example",
257                         LastName:  "Admin1",
258                         Active:    true,
259                         Admin:     true,
260                 }, {
261                         UserID:    fmt.Sprintf("%sadmin2%s", uIDPrefix, uIDSuffix),
262                         FirstName: "Example",
263                         LastName:  "Admin2",
264                         Active:    false, // initially inactive
265                         Admin:     true,
266                 }}
267                 // Make sure users aren't already there from fixtures
268                 for _, user := range s.users {
269                         uID, err := GetUserID(user, s.cfg.UserID)
270                         c.Assert(err, IsNil)
271                         found := false
272                         for _, r := range records {
273                                 if uID == r.UserID {
274                                         found = true
275                                         break
276                                 }
277                         }
278                         c.Assert(found, Equals, false)
279                 }
280                 // User creation
281                 tmpfile, err := MakeTempCSVFile(RecordsToStrings(records))
282                 c.Assert(err, IsNil)
283                 defer os.Remove(tmpfile.Name())
284                 s.cfg.Path = tmpfile.Name()
285                 err = doMain(s.cfg)
286                 c.Assert(err, IsNil)
287
288                 users, err := ListUsers(s.cfg.Client)
289                 c.Assert(err, IsNil)
290                 for _, r := range records {
291                         var foundUser arvados.User
292                         for _, user := range users {
293                                 uID, err := GetUserID(user, s.cfg.UserID)
294                                 c.Assert(err, IsNil)
295                                 if uID == r.UserID {
296                                         // Add an @example.com email if missing
297                                         // (to avoid database reset errors)
298                                         if tc == "username" && user.Email == "" {
299                                                 err := UpdateUser(s.cfg.Client, user.UUID, &user, map[string]string{
300                                                         "email": fmt.Sprintf("%s@example.com", user.Username),
301                                                 })
302                                                 c.Assert(err, IsNil)
303                                         }
304                                         foundUser = user
305                                         break
306                                 }
307                         }
308                         c.Assert(foundUser, NotNil)
309                         c.Logf("Checking creation for user %q", r.UserID)
310                         c.Assert(foundUser.FirstName, Equals, r.FirstName)
311                         c.Assert(foundUser.LastName, Equals, r.LastName)
312                         c.Assert(foundUser.IsActive, Equals, r.Active)
313                         c.Assert(foundUser.IsAdmin, Equals, (r.Active && r.Admin))
314                 }
315                 // User update
316                 for idx := range records {
317                         records[idx].Active = !records[idx].Active
318                         records[idx].FirstName = records[idx].FirstName + "Updated"
319                         records[idx].LastName = records[idx].LastName + "Updated"
320                 }
321                 tmpfile, err = MakeTempCSVFile(RecordsToStrings(records))
322                 c.Assert(err, IsNil)
323                 defer os.Remove(tmpfile.Name())
324                 s.cfg.Path = tmpfile.Name()
325                 err = doMain(s.cfg)
326                 c.Assert(err, IsNil)
327
328                 users, err = ListUsers(s.cfg.Client)
329                 c.Assert(err, IsNil)
330                 for _, r := range records {
331                         var foundUser arvados.User
332                         for _, user := range users {
333                                 uID, err := GetUserID(user, s.cfg.UserID)
334                                 c.Assert(err, IsNil)
335                                 if uID == r.UserID {
336                                         foundUser = user
337                                         break
338                                 }
339                         }
340                         c.Assert(foundUser, NotNil)
341                         c.Logf("Checking update for user %q", r.UserID)
342                         c.Assert(foundUser.FirstName, Equals, r.FirstName)
343                         c.Assert(foundUser.LastName, Equals, r.LastName)
344                         c.Assert(foundUser.IsActive, Equals, r.Active)
345                         c.Assert(foundUser.IsAdmin, Equals, (r.Active && r.Admin))
346                 }
347         }
348 }
349
350 func (s *TestSuite) TestDeactivateUnlisted(c *C) {
351         localUserUuidRegex := regexp.MustCompile(fmt.Sprintf("^%s-tpzed-[0-9a-z]{15}$", s.cfg.ClusterID))
352
353         var user1 arvados.User
354         for _, nr := range []int{1, 2} {
355                 var newUser arvados.User
356                 err := CreateUser(s.cfg.Client, &newUser, map[string]string{
357                         "email":      fmt.Sprintf("user%d@example.com", nr),
358                         "first_name": "Example",
359                         "last_name":  fmt.Sprintf("User%d", nr),
360                         "is_active":  "true",
361                         "is_admin":   "false",
362                 })
363                 c.Assert(err, IsNil)
364                 c.Assert(newUser.IsActive, Equals, true)
365                 if nr == 1 {
366                         user1 = newUser // for later confirmation
367                 }
368         }
369
370         users, err := ListUsers(s.cfg.Client)
371         c.Assert(err, IsNil)
372         previouslyActiveUsers := 0
373         for _, u := range users {
374                 if u.UUID == fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID) && !u.IsActive {
375                         // Make sure the anonymous user is active for this test
376                         var au arvados.User
377                         err := UpdateUser(s.cfg.Client, u.UUID, &au, map[string]string{"is_active": "true"})
378                         c.Assert(err, IsNil)
379                         c.Assert(au.IsActive, Equals, true)
380                 }
381                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
382                         previouslyActiveUsers++
383                 }
384         }
385         // Active users: System root, Anonymous, current user and the
386         // ones just created (other active users may exist from fixture).
387         c.Logf("Initial active users count: %d", previouslyActiveUsers)
388         c.Assert(previouslyActiveUsers > 5, Equals, true)
389
390         // Here we omit user2@example.com from the CSV file.
391         data := [][]string{
392                 {"user1@example.com", "Example", "User1", "1", "0"},
393         }
394         tmpfile, err := MakeTempCSVFile(data)
395         c.Assert(err, IsNil)
396         defer os.Remove(tmpfile.Name())
397
398         s.cfg.DeactivateUnlisted = true
399         s.cfg.Verbose = true
400         s.cfg.Path = tmpfile.Name()
401         err = doMain(s.cfg)
402         c.Assert(err, IsNil)
403
404         users, err = ListUsers(s.cfg.Client)
405         c.Assert(err, IsNil)
406         currentlyActiveUsers := 0
407         acceptableActiveUUIDs := map[string]bool{
408                 fmt.Sprintf("%s-tpzed-000000000000000", s.cfg.ClusterID): true,
409                 fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID): true,
410                 s.cfg.CurrentUser.UUID: true,
411                 user1.UUID:             true,
412         }
413         remainingActiveUUIDs := map[string]bool{}
414         seenUserEmails := map[string]bool{}
415         for _, u := range users {
416                 if _, ok := seenUserEmails[u.Email]; ok {
417                         c.Errorf("Duplicated email address %q in user list (probably from fixtures). This test requires unique email addresses.", u.Email)
418                 }
419                 seenUserEmails[u.Email] = true
420                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
421                         c.Logf("Found remaining active user %q (%s)", u.Email, u.UUID)
422                         _, ok := acceptableActiveUUIDs[u.UUID]
423                         c.Assert(ok, Equals, true)
424                         remainingActiveUUIDs[u.UUID] = true
425                         currentlyActiveUsers++
426                 }
427         }
428         // 4 active users remaining: System root, Anonymous, the current user
429         // and user1@example.com
430         c.Logf("Active local users remaining: %v", remainingActiveUUIDs)
431         c.Assert(currentlyActiveUsers, Equals, 4)
432 }
433
434 func (s *TestSuite) TestFailOnDuplicatedEmails(c *C) {
435         for i := range []int{1, 2} {
436                 isAdmin := i == 2
437                 err := CreateUser(s.cfg.Client, &arvados.User{}, map[string]string{
438                         "email":      "somedupedemail@example.com",
439                         "first_name": fmt.Sprintf("Duped %d", i),
440                         "username":   fmt.Sprintf("dupedemail%d", i),
441                         "last_name":  "User",
442                         "is_active":  "true",
443                         "is_admin":   fmt.Sprintf("%t", isAdmin),
444                 })
445                 c.Assert(err, IsNil)
446         }
447         s.cfg.Verbose = true
448         data := [][]string{
449                 {"user1@example.com", "Example", "User1", "0", "0"},
450         }
451         tmpfile, err := MakeTempCSVFile(data)
452         c.Assert(err, IsNil)
453         defer os.Remove(tmpfile.Name())
454         s.cfg.Path = tmpfile.Name()
455         err = doMain(s.cfg)
456         c.Assert(err, NotNil)
457         c.Assert(err, ErrorMatches, "skipped.*duplicated email address.*")
458 }
459
460 func (s *TestSuite) TestFailOnEmptyUsernames(c *C) {
461         for i := range []int{1, 2} {
462                 var user arvados.User
463                 err := CreateUser(s.cfg.Client, &user, map[string]string{
464                         "email":      fmt.Sprintf("johndoe%d@example.com", i),
465                         "username":   "",
466                         "first_name": "John",
467                         "last_name":  "Doe",
468                         "is_active":  "true",
469                         "is_admin":   "false",
470                 })
471                 c.Assert(err, IsNil)
472                 c.Assert(user.Username, Equals, fmt.Sprintf("johndoe%d", i))
473                 if i == 1 {
474                         err = UpdateUser(s.cfg.Client, user.UUID, &user, map[string]string{
475                                 "username": "",
476                         })
477                         c.Assert(err, IsNil)
478                         c.Assert(user.Username, Equals, "")
479                 }
480         }
481
482         s.cfg.Verbose = true
483         data := [][]string{
484                 {"johndoe0", "John", "Doe", "0", "0"},
485         }
486         tmpfile, err := MakeTempCSVFile(data)
487         c.Assert(err, IsNil)
488         defer os.Remove(tmpfile.Name())
489         s.cfg.Path = tmpfile.Name()
490         s.cfg.UserID = "username"
491         err = doMain(s.cfg)
492         c.Assert(err, NotNil)
493         c.Assert(err, ErrorMatches, "skipped 1 user account.*with empty username.*")
494 }
495
496 func (s *TestSuite) TestFailOnDupedUsernameAndCaseInsensitiveMatching(c *C) {
497         for _, i := range []int{1, 2} {
498                 var user arvados.User
499                 emailPrefix := "john"
500                 if i == 1 {
501                         emailPrefix = "JOHN"
502                 }
503                 err := CreateUser(s.cfg.Client, &user, map[string]string{
504                         "email":      fmt.Sprintf("%sdoe@example.com", emailPrefix),
505                         "username":   "",
506                         "first_name": "John",
507                         "last_name":  "Doe",
508                         "is_active":  "true",
509                         "is_admin":   "false",
510                 })
511                 c.Assert(err, IsNil)
512                 c.Assert(user.Username, Equals, fmt.Sprintf("%sdoe", emailPrefix))
513         }
514
515         s.cfg.Verbose = true
516         data := [][]string{
517                 {"johndoe", "John", "Doe", "0", "0"},
518         }
519         tmpfile, err := MakeTempCSVFile(data)
520         c.Assert(err, IsNil)
521         defer os.Remove(tmpfile.Name())
522         s.cfg.Path = tmpfile.Name()
523         s.cfg.UserID = "username"
524         s.cfg.CaseInsensitive = true
525         err = doMain(s.cfg)
526         c.Assert(err, NotNil)
527         c.Assert(err, ErrorMatches, "case insensitive collision for username.*between.*and.*")
528 }
529
530 func (s *TestSuite) TestSuccessOnUsernameAndCaseSensitiveMatching(c *C) {
531         for _, i := range []int{1, 2} {
532                 var user arvados.User
533                 emailPrefix := "john"
534                 if i == 1 {
535                         emailPrefix = "JOHN"
536                 }
537                 err := CreateUser(s.cfg.Client, &user, map[string]string{
538                         "email":      fmt.Sprintf("%sdoe@example.com", emailPrefix),
539                         "username":   "",
540                         "first_name": "John",
541                         "last_name":  "Doe",
542                         "is_active":  "true",
543                         "is_admin":   "false",
544                 })
545                 c.Assert(err, IsNil)
546                 c.Assert(user.Username, Equals, fmt.Sprintf("%sdoe", emailPrefix))
547         }
548
549         s.cfg.Verbose = true
550         data := [][]string{
551                 {"johndoe", "John", "Doe", "0", "0"},
552         }
553         tmpfile, err := MakeTempCSVFile(data)
554         c.Assert(err, IsNil)
555         defer os.Remove(tmpfile.Name())
556         s.cfg.Path = tmpfile.Name()
557         s.cfg.UserID = "username"
558         s.cfg.CaseInsensitive = false
559         err = doMain(s.cfg)
560         c.Assert(err, IsNil)
561 }