18858: Don't immediately exit on existing accounts with empty user IDs.
[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         users, err := ListUsers(s.cfg.Client)
353         c.Assert(err, IsNil)
354         previouslyActiveUsers := 0
355         for _, u := range users {
356                 if u.UUID == fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID) && !u.IsActive {
357                         // Make sure the anonymous user is active for this test
358                         var au arvados.User
359                         err := UpdateUser(s.cfg.Client, u.UUID, &au, map[string]string{"is_active": "true"})
360                         c.Assert(err, IsNil)
361                         c.Assert(au.IsActive, Equals, true)
362                 }
363                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
364                         previouslyActiveUsers++
365                 }
366         }
367         // At least 3 active users: System root, Anonymous and the current user.
368         // Other active users should exist from fixture.
369         c.Logf("Initial active users count: %d", previouslyActiveUsers)
370         c.Assert(previouslyActiveUsers > 3, Equals, true)
371
372         s.cfg.DeactivateUnlisted = true
373         s.cfg.Verbose = true
374         data := [][]string{
375                 {"user1@example.com", "Example", "User1", "0", "0"},
376         }
377         tmpfile, err := MakeTempCSVFile(data)
378         c.Assert(err, IsNil)
379         defer os.Remove(tmpfile.Name())
380         s.cfg.Path = tmpfile.Name()
381         err = doMain(s.cfg)
382         c.Assert(err, IsNil)
383
384         users, err = ListUsers(s.cfg.Client)
385         c.Assert(err, IsNil)
386         currentlyActiveUsers := 0
387         acceptableActiveUUIDs := map[string]bool{
388                 fmt.Sprintf("%s-tpzed-000000000000000", s.cfg.ClusterID): true,
389                 fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID): true,
390                 s.cfg.CurrentUser.UUID: true,
391         }
392         remainingActiveUUIDs := map[string]bool{}
393         seenUserEmails := map[string]bool{}
394         for _, u := range users {
395                 if _, ok := seenUserEmails[u.Email]; ok {
396                         c.Errorf("Duplicated email address %q in user list (probably from fixtures). This test requires unique email addresses.", u.Email)
397                 }
398                 seenUserEmails[u.Email] = true
399                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
400                         c.Logf("Found remaining active user %q (%s)", u.Email, u.UUID)
401                         _, ok := acceptableActiveUUIDs[u.UUID]
402                         c.Assert(ok, Equals, true)
403                         remainingActiveUUIDs[u.UUID] = true
404                         currentlyActiveUsers++
405                 }
406         }
407         // 3 active users remaining: System root, Anonymous and the current user.
408         c.Logf("Active local users remaining: %v", remainingActiveUUIDs)
409         c.Assert(currentlyActiveUsers, Equals, 3)
410 }
411
412 func (s *TestSuite) TestFailOnDuplicatedEmails(c *C) {
413         for i := range []int{1, 2} {
414                 isAdmin := i == 2
415                 err := CreateUser(s.cfg.Client, &arvados.User{}, map[string]string{
416                         "email":      "somedupedemail@example.com",
417                         "first_name": fmt.Sprintf("Duped %d", i),
418                         "username":   fmt.Sprintf("dupedemail%d", i),
419                         "last_name":  "User",
420                         "is_active":  "true",
421                         "is_admin":   fmt.Sprintf("%t", isAdmin),
422                 })
423                 c.Assert(err, IsNil)
424         }
425         s.cfg.Verbose = true
426         data := [][]string{
427                 {"user1@example.com", "Example", "User1", "0", "0"},
428         }
429         tmpfile, err := MakeTempCSVFile(data)
430         c.Assert(err, IsNil)
431         defer os.Remove(tmpfile.Name())
432         s.cfg.Path = tmpfile.Name()
433         err = doMain(s.cfg)
434         c.Assert(err, NotNil)
435         c.Assert(err, ErrorMatches, "skipped.*duplicated email address.*")
436 }
437
438 func (s *TestSuite) TestFailOnEmptyUsernames(c *C) {
439         for i := range []int{1, 2} {
440                 var user arvados.User
441                 err := CreateUser(s.cfg.Client, &user, map[string]string{
442                         "email":      fmt.Sprintf("johndoe%d@example.com", i),
443                         "username":   "",
444                         "first_name": "John",
445                         "last_name":  "Doe",
446                         "is_active":  "true",
447                         "is_admin":   "false",
448                 })
449                 c.Assert(err, IsNil)
450                 c.Assert(user.Username, Equals, fmt.Sprintf("johndoe%d", i))
451                 if i == 1 {
452                         err = UpdateUser(s.cfg.Client, user.UUID, &user, map[string]string{
453                                 "username": "",
454                         })
455                         c.Assert(err, IsNil)
456                         c.Assert(user.Username, Equals, "")
457                 }
458         }
459
460         s.cfg.Verbose = true
461         data := [][]string{
462                 {"johndoe0", "John", "Doe", "0", "0"},
463         }
464         tmpfile, err := MakeTempCSVFile(data)
465         c.Assert(err, IsNil)
466         defer os.Remove(tmpfile.Name())
467         s.cfg.Path = tmpfile.Name()
468         s.cfg.UserID = "username"
469         err = doMain(s.cfg)
470         c.Assert(err, NotNil)
471         c.Assert(err, ErrorMatches, "skipped 1 user account.*with empty username.*")
472 }