18858: Tidies up testing code.
[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 }
102
103 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
104         cfg := ConfigParams{}
105         os.Args = []string{"cmd", "/tmp/somefile.csv"}
106         err := ParseFlags(&cfg)
107         c.Assert(err, IsNil)
108         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
109         c.Assert(cfg.Verbose, Equals, false)
110         c.Assert(cfg.DeactivateUnlisted, Equals, false)
111 }
112
113 func (s *TestSuite) TestParseFlagsWithOptionalFlags(c *C) {
114         cfg := ConfigParams{}
115         os.Args = []string{"cmd", "-verbose", "-deactivate-unlisted", "/tmp/somefile.csv"}
116         err := ParseFlags(&cfg)
117         c.Assert(err, IsNil)
118         c.Assert(cfg.Path, Equals, "/tmp/somefile.csv")
119         c.Assert(cfg.Verbose, Equals, true)
120         c.Assert(cfg.DeactivateUnlisted, Equals, true)
121 }
122
123 func (s *TestSuite) TestGetConfig(c *C) {
124         os.Args = []string{"cmd", "/tmp/somefile.csv"}
125         cfg, err := GetConfig()
126         c.Assert(err, IsNil)
127         c.Assert(cfg.AnonUserUUID, Not(Equals), "")
128         c.Assert(cfg.SysUserUUID, Not(Equals), "")
129         c.Assert(cfg.CurrentUser, Not(Equals), "")
130         c.Assert(cfg.ClusterID, Not(Equals), "")
131         c.Assert(cfg.Client, NotNil)
132 }
133
134 func (s *TestSuite) TestFailOnEmptyFields(c *C) {
135         records := [][]string{
136                 {"", "first-name", "last-name", "1", "0"},
137                 {"user@example", "", "last-name", "1", "0"},
138                 {"user@example", "first-name", "", "1", "0"},
139                 {"user@example", "first-name", "last-name", "", "0"},
140                 {"user@example", "first-name", "last-name", "1", ""},
141         }
142         for _, record := range records {
143                 data := [][]string{record}
144                 tmpfile, err := MakeTempCSVFile(data)
145                 c.Assert(err, IsNil)
146                 defer os.Remove(tmpfile.Name())
147                 s.cfg.Path = tmpfile.Name()
148                 err = doMain(s.cfg)
149                 c.Assert(err, NotNil)
150                 c.Assert(err, ErrorMatches, ".*fields cannot be empty.*")
151         }
152 }
153
154 func (s *TestSuite) TestIgnoreSpaces(c *C) {
155         // Make sure users aren't already there from fixtures
156         for _, user := range s.users {
157                 e := user.Email
158                 found := e == "user1@example.com" || e == "user2@example.com" || e == "user3@example.com"
159                 c.Assert(found, Equals, false)
160         }
161         // Use CSV data with leading/trailing whitespaces, confirm that they get ignored
162         data := [][]string{
163                 {" user1@example.com", "  Example", "   User1", "1", "0"},
164                 {"user2@example.com ", "Example  ", "User2   ", "1", "0"},
165                 {" user3@example.com ", "  Example  ", "   User3   ", "1", "0"},
166         }
167         tmpfile, err := MakeTempCSVFile(data)
168         c.Assert(err, IsNil)
169         defer os.Remove(tmpfile.Name())
170         s.cfg.Path = tmpfile.Name()
171         err = doMain(s.cfg)
172         c.Assert(err, IsNil)
173         users, err := ListUsers(s.cfg.Client)
174         c.Assert(err, IsNil)
175         for _, userNr := range []int{1, 2, 3} {
176                 found := false
177                 for _, user := range users {
178                         if user.Email == fmt.Sprintf("user%d@example.com", userNr) &&
179                                 user.LastName == fmt.Sprintf("User%d", userNr) &&
180                                 user.FirstName == "Example" && user.IsActive == true {
181                                 found = true
182                                 break
183                         }
184                 }
185                 c.Assert(found, Equals, true)
186         }
187 }
188
189 // Error out when records have != 5 records
190 func (s *TestSuite) TestWrongNumberOfFields(c *C) {
191         for _, testCase := range [][][]string{
192                 {{"user1@example.com", "Example", "User1", "1"}},
193                 {{"user1@example.com", "Example", "User1", "1", "0", "extra data"}},
194         } {
195                 tmpfile, err := MakeTempCSVFile(testCase)
196                 c.Assert(err, IsNil)
197                 defer os.Remove(tmpfile.Name())
198                 s.cfg.Path = tmpfile.Name()
199                 err = doMain(s.cfg)
200                 c.Assert(err, NotNil)
201                 c.Assert(err, ErrorMatches, ".*expected 5 fields, found.*")
202         }
203 }
204
205 // Error out when records have incorrect data types
206 func (s *TestSuite) TestWrongDataFields(c *C) {
207         for _, testCase := range [][][]string{
208                 {{"user1@example.com", "Example", "User1", "yep", "0"}},
209                 {{"user1@example.com", "Example", "User1", "1", "nope"}},
210         } {
211                 tmpfile, err := MakeTempCSVFile(testCase)
212                 c.Assert(err, IsNil)
213                 defer os.Remove(tmpfile.Name())
214                 s.cfg.Path = tmpfile.Name()
215                 err = doMain(s.cfg)
216                 c.Assert(err, NotNil)
217                 c.Assert(err, ErrorMatches, ".*parsing error at line.*[active|admin] status not recognized.*")
218         }
219 }
220
221 // Activate and deactivate users
222 func (s *TestSuite) TestUserCreationAndUpdate(c *C) {
223         testCases := []userRecord{{
224                 UserID:    "user1@example.com",
225                 FirstName: "Example",
226                 LastName:  "User1",
227                 Active:    true,
228                 Admin:     false,
229         }, {
230                 UserID:    "admin1@example.com",
231                 FirstName: "Example",
232                 LastName:  "Admin1",
233                 Active:    true,
234                 Admin:     true,
235         }}
236         // Make sure users aren't already there from fixtures
237         for _, user := range s.users {
238                 e := user.Email
239                 found := false
240                 for _, r := range testCases {
241                         if e == r.UserID {
242                                 found = true
243                                 break
244                         }
245                 }
246                 c.Assert(found, Equals, false)
247         }
248         // User creation
249         tmpfile, err := MakeTempCSVFile(RecordsToStrings(testCases))
250         c.Assert(err, IsNil)
251         defer os.Remove(tmpfile.Name())
252         s.cfg.Path = tmpfile.Name()
253         err = doMain(s.cfg)
254         c.Assert(err, IsNil)
255
256         users, err := ListUsers(s.cfg.Client)
257         c.Assert(err, IsNil)
258         for _, tc := range testCases {
259                 var foundUser arvados.User
260                 for _, user := range users {
261                         if user.Email == tc.UserID {
262                                 foundUser = user
263                                 break
264                         }
265                 }
266                 c.Assert(foundUser, NotNil)
267                 c.Logf("Checking recently created user %q", foundUser.Email)
268                 c.Assert(foundUser.FirstName, Equals, tc.FirstName)
269                 c.Assert(foundUser.LastName, Equals, tc.LastName)
270                 c.Assert(foundUser.IsActive, Equals, true)
271                 c.Assert(foundUser.IsAdmin, Equals, tc.Admin)
272         }
273         // User deactivation
274         for idx := range testCases {
275                 testCases[idx].Active = false
276         }
277         tmpfile, err = MakeTempCSVFile(RecordsToStrings(testCases))
278         c.Assert(err, IsNil)
279         defer os.Remove(tmpfile.Name())
280         s.cfg.Path = tmpfile.Name()
281         err = doMain(s.cfg)
282         c.Assert(err, IsNil)
283
284         users, err = ListUsers(s.cfg.Client)
285         c.Assert(err, IsNil)
286         for _, tc := range testCases {
287                 var foundUser arvados.User
288                 for _, user := range users {
289                         if user.Email == tc.UserID {
290                                 foundUser = user
291                                 break
292                         }
293                 }
294                 c.Assert(foundUser, NotNil)
295                 c.Logf("Checking recently deactivated user %q", foundUser.Email)
296                 c.Assert(foundUser.FirstName, Equals, tc.FirstName)
297                 c.Assert(foundUser.LastName, Equals, tc.LastName)
298                 c.Assert(foundUser.IsActive, Equals, false)
299                 c.Assert(foundUser.IsAdmin, Equals, false) // inactive users cannot be admins
300         }
301 }
302
303 func (s *TestSuite) TestDeactivateUnlisted(c *C) {
304         localUserUuidRegex := regexp.MustCompile(fmt.Sprintf("^%s-tpzed-[0-9a-z]{15}$", s.cfg.ClusterID))
305         users, err := ListUsers(s.cfg.Client)
306         c.Assert(err, IsNil)
307         previouslyActiveUsers := 0
308         for _, u := range users {
309                 if u.UUID == fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID) && !u.IsActive {
310                         // Make sure the anonymous user is active for this test
311                         var au arvados.User
312                         err := UpdateUser(s.cfg.Client, u.UUID, &au, map[string]string{"is_active": "true"})
313                         c.Assert(err, IsNil)
314                         c.Assert(au.IsActive, Equals, true)
315                 }
316                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
317                         previouslyActiveUsers++
318                 }
319         }
320         // At least 3 active users: System root, Anonymous and the current user.
321         // Other active users should exist from fixture.
322         c.Logf("Initial active users count: %d", previouslyActiveUsers)
323         c.Assert(previouslyActiveUsers > 3, Equals, true)
324
325         s.cfg.DeactivateUnlisted = true
326         s.cfg.Verbose = true
327         data := [][]string{
328                 {"user1@example.com", "Example", "User1", "0", "0"},
329         }
330         tmpfile, err := MakeTempCSVFile(data)
331         c.Assert(err, IsNil)
332         defer os.Remove(tmpfile.Name())
333         s.cfg.Path = tmpfile.Name()
334         err = doMain(s.cfg)
335         c.Assert(err, IsNil)
336
337         users, err = ListUsers(s.cfg.Client)
338         c.Assert(err, IsNil)
339         currentlyActiveUsers := 0
340         acceptableActiveUUIDs := map[string]bool{
341                 fmt.Sprintf("%s-tpzed-000000000000000", s.cfg.ClusterID): true,
342                 fmt.Sprintf("%s-tpzed-anonymouspublic", s.cfg.ClusterID): true,
343                 s.cfg.CurrentUser.UUID: true,
344         }
345         remainingActiveUUIDs := map[string]bool{}
346         seenUserEmails := map[string]bool{}
347         for _, u := range users {
348                 if _, ok := seenUserEmails[u.Email]; ok {
349                         c.Errorf("Duplicated email address %q in user list (probably from fixtures). This test requires unique email addresses.", u.Email)
350                 }
351                 seenUserEmails[u.Email] = true
352                 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
353                         c.Logf("Found remaining active user %q (%s)", u.Email, u.UUID)
354                         _, ok := acceptableActiveUUIDs[u.UUID]
355                         c.Assert(ok, Equals, true)
356                         remainingActiveUUIDs[u.UUID] = true
357                         currentlyActiveUsers++
358                 }
359         }
360         // 3 active users remaining: System root, Anonymous and the current user.
361         c.Logf("Active local users remaining: %v", remainingActiveUUIDs)
362         c.Assert(currentlyActiveUsers, Equals, 3)
363 }
364
365 func (s *TestSuite) TestFailOnDuplicatedEmails(c *C) {
366         for i := range []int{1, 2} {
367                 isAdmin := i == 2
368                 err := CreateUser(s.cfg.Client, &arvados.User{}, map[string]string{
369                         "email":      "somedupedemail@example.com",
370                         "first_name": fmt.Sprintf("Duped %d", i),
371                         "username":   fmt.Sprintf("dupedemail%d", i),
372                         "last_name":  "User",
373                         "is_active":  "true",
374                         "is_admin":   fmt.Sprintf("%t", isAdmin),
375                 })
376                 c.Assert(err, IsNil)
377         }
378         s.cfg.Verbose = true
379         data := [][]string{
380                 {"user1@example.com", "Example", "User1", "0", "0"},
381         }
382         tmpfile, err := MakeTempCSVFile(data)
383         c.Assert(err, IsNil)
384         defer os.Remove(tmpfile.Name())
385         s.cfg.Path = tmpfile.Name()
386         err = doMain(s.cfg)
387         c.Assert(err, NotNil)
388         c.Assert(err, ErrorMatches, "skipped.*duplicated email address.*")
389 }