1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
15 "git.arvados.org/arvados.git/sdk/go/arvados"
19 // Gocheck boilerplate
20 func Test(t *testing.T) {
24 type TestSuite struct {
27 users map[string]arvados.User
30 func (s *TestSuite) SetUpTest(c *C) {
31 s.ac = arvados.NewClientFromEnv()
32 u, err := s.ac.CurrentUser()
34 c.Assert(u.IsAdmin, Equals, true)
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 {
45 // Set up command config
46 os.Args = []string{"cmd", "somefile.csv"}
47 config, err := GetConfig()
52 func (s *TestSuite) TearDownTest(c *C) {
54 // Reset database to fixture state after every test run.
55 err := s.cfg.Client.RequestAndDecode(&dst, "POST", "/database/reset", nil, nil)
59 var _ = Suite(&TestSuite{})
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")
67 for _, line := range data {
68 fmt.Fprintf(f, "%s\n", strings.Join(line, ","))
74 // RecordsToStrings formats the input data suitable for MakeTempCSVFile
75 func RecordsToStrings(records []userRecord) [][]string {
77 for _, u := range records {
78 data = append(data, []string{
82 fmt.Sprintf("%t", u.Active),
83 fmt.Sprintf("%t", u.Admin)})
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{})
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.*")
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:.*")
111 func (s *TestSuite) TestParseFlagsWithPositionalArgument(c *C) {
112 cfg := ConfigParams{}
113 os.Args = []string{"cmd", "/tmp/somefile.csv"}
114 err := ParseFlags(&cfg)
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)
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)
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)
135 func (s *TestSuite) TestGetConfig(c *C) {
136 os.Args = []string{"cmd", "/tmp/somefile.csv"}
137 cfg, err := GetConfig()
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)
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", ""},
154 for _, record := range records {
155 data := [][]string{record}
156 tmpfile, err := MakeTempCSVFile(data)
158 defer os.Remove(tmpfile.Name())
159 s.cfg.Path = tmpfile.Name()
161 c.Assert(err, NotNil)
162 c.Assert(err, ErrorMatches, ".*fields cannot be empty.*")
166 func (s *TestSuite) TestIgnoreSpaces(c *C) {
167 // Make sure users aren't already there from fixtures
168 for _, user := range s.users {
170 found := e == "user1@example.com" || e == "user2@example.com" || e == "user3@example.com"
171 c.Assert(found, Equals, false)
173 // Use CSV data with leading/trailing whitespaces, confirm that they get ignored
175 {" user1@example.com", " Example", " User1", "1", "0"},
176 {"user2@example.com ", "Example ", "User2 ", "1", "0"},
177 {" user3@example.com ", " Example ", " User3 ", "1", "0"},
179 tmpfile, err := MakeTempCSVFile(data)
181 defer os.Remove(tmpfile.Name())
182 s.cfg.Path = tmpfile.Name()
185 users, err := ListUsers(s.cfg.Client)
187 for _, userNr := range []int{1, 2, 3} {
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 {
197 c.Assert(found, Equals, true)
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"}},
207 tmpfile, err := MakeTempCSVFile(testCase)
209 defer os.Remove(tmpfile.Name())
210 s.cfg.Path = tmpfile.Name()
212 c.Assert(err, NotNil)
213 c.Assert(err, ErrorMatches, ".*expected 5 fields, found.*")
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"}},
223 tmpfile, err := MakeTempCSVFile(testCase)
225 defer os.Remove(tmpfile.Name())
226 s.cfg.Path = tmpfile.Name()
228 c.Assert(err, NotNil)
229 c.Assert(err, ErrorMatches, ".*parsing error at line.*[active|admin] status not recognized.*")
233 // Create, activate and deactivate users
234 func (s *TestSuite) TestUserCreationAndUpdate(c *C) {
235 for _, tc := range []string{"email", "username"} {
239 uIDSuffix = "@example.com"
242 records := []userRecord{{
243 UserID: fmt.Sprintf("%suser1%s", uIDPrefix, uIDSuffix),
244 FirstName: "Example",
249 UserID: fmt.Sprintf("%suser2%s", uIDPrefix, uIDSuffix),
250 FirstName: "Example",
252 Active: false, // initially inactive
255 UserID: fmt.Sprintf("%sadmin1%s", uIDPrefix, uIDSuffix),
256 FirstName: "Example",
261 UserID: fmt.Sprintf("%sadmin2%s", uIDPrefix, uIDSuffix),
262 FirstName: "Example",
264 Active: false, // initially inactive
267 // Make sure users aren't already there from fixtures
268 for _, user := range s.users {
269 uID, err := GetUserID(user, s.cfg.UserID)
272 for _, r := range records {
278 c.Assert(found, Equals, false)
281 tmpfile, err := MakeTempCSVFile(RecordsToStrings(records))
283 defer os.Remove(tmpfile.Name())
284 s.cfg.Path = tmpfile.Name()
288 users, err := ListUsers(s.cfg.Client)
290 for _, r := range records {
291 var foundUser arvados.User
292 for _, user := range users {
293 uID, err := GetUserID(user, s.cfg.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),
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))
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"
321 tmpfile, err = MakeTempCSVFile(RecordsToStrings(records))
323 defer os.Remove(tmpfile.Name())
324 s.cfg.Path = tmpfile.Name()
328 users, err = ListUsers(s.cfg.Client)
330 for _, r := range records {
331 var foundUser arvados.User
332 for _, user := range users {
333 uID, err := GetUserID(user, s.cfg.UserID)
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))
350 func (s *TestSuite) TestDeactivateUnlisted(c *C) {
351 localUserUuidRegex := regexp.MustCompile(fmt.Sprintf("^%s-tpzed-[0-9a-z]{15}$", s.cfg.ClusterID))
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),
364 c.Assert(newUser.IsActive, Equals, true)
366 user1 = newUser // for later confirmation
370 users, err := ListUsers(s.cfg.Client)
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
377 err := UpdateUser(s.cfg.Client, u.UUID, &au, map[string]string{"is_active": "true"})
379 c.Assert(au.IsActive, Equals, true)
381 if localUserUuidRegex.MatchString(u.UUID) && u.IsActive {
382 previouslyActiveUsers++
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)
390 // Here we omit user2@example.com from the CSV file.
392 {"user1@example.com", "Example", "User1", "1", "0"},
394 tmpfile, err := MakeTempCSVFile(data)
396 defer os.Remove(tmpfile.Name())
398 s.cfg.DeactivateUnlisted = true
400 s.cfg.Path = tmpfile.Name()
404 users, err = ListUsers(s.cfg.Client)
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,
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)
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++
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)
434 func (s *TestSuite) TestFailOnDuplicatedEmails(c *C) {
435 for i := range []int{1, 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),
443 "is_admin": fmt.Sprintf("%t", isAdmin),
449 {"user1@example.com", "Example", "User1", "0", "0"},
451 tmpfile, err := MakeTempCSVFile(data)
453 defer os.Remove(tmpfile.Name())
454 s.cfg.Path = tmpfile.Name()
456 c.Assert(err, NotNil)
457 c.Assert(err, ErrorMatches, "skipped.*duplicated email address.*")
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),
466 "first_name": "John",
472 c.Assert(user.Username, Equals, fmt.Sprintf("johndoe%d", i))
474 err = UpdateUser(s.cfg.Client, user.UUID, &user, map[string]string{
478 c.Assert(user.Username, Equals, "")
484 {"johndoe0", "John", "Doe", "0", "0"},
486 tmpfile, err := MakeTempCSVFile(data)
488 defer os.Remove(tmpfile.Name())
489 s.cfg.Path = tmpfile.Name()
490 s.cfg.UserID = "username"
492 c.Assert(err, NotNil)
493 c.Assert(err, ErrorMatches, "skipped 1 user account.*with empty username.*")
496 func (s *TestSuite) TestFailOnDupedUsernameAndCaseInsensitiveMatching(c *C) {
497 for _, i := range []int{1, 2} {
498 var user arvados.User
499 emailPrefix := "john"
503 err := CreateUser(s.cfg.Client, &user, map[string]string{
504 "email": fmt.Sprintf("%sdoe@example.com", emailPrefix),
506 "first_name": "John",
512 c.Assert(user.Username, Equals, fmt.Sprintf("%sdoe", emailPrefix))
517 {"johndoe", "John", "Doe", "0", "0"},
519 tmpfile, err := MakeTempCSVFile(data)
521 defer os.Remove(tmpfile.Name())
522 s.cfg.Path = tmpfile.Name()
523 s.cfg.UserID = "username"
524 s.cfg.CaseInsensitive = true
526 c.Assert(err, NotNil)
527 c.Assert(err, ErrorMatches, "case insensitive collision for username.*between.*and.*")
530 func (s *TestSuite) TestSuccessOnUsernameAndCaseSensitiveMatching(c *C) {
531 for _, i := range []int{1, 2} {
532 var user arvados.User
533 emailPrefix := "john"
537 err := CreateUser(s.cfg.Client, &user, map[string]string{
538 "email": fmt.Sprintf("%sdoe@example.com", emailPrefix),
540 "first_name": "John",
546 c.Assert(user.Username, Equals, fmt.Sprintf("%sdoe", emailPrefix))
551 {"johndoe", "John", "Doe", "0", "0"},
553 tmpfile, err := MakeTempCSVFile(data)
555 defer os.Remove(tmpfile.Name())
556 s.cfg.Path = tmpfile.Name()
557 s.cfg.UserID = "username"
558 s.cfg.CaseInsensitive = false