vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Name branches after the account, and labels after the display
474a812d199a109ba894124cd6f26be12c73285d · Keenan Nemetz · 16d ago · parent ade6107e
Name branches after the account, and labels after the display
A contributor's folder was `refs/heads/contrib/<account-id>/`, which told a
reader nothing without decoding a ULID. It is now the account name,
refs/heads/contrib/<name>/<branch>
and the name is therefore fixed at creation: renaming an account would strand
every ref under its old folder, so `vole admin user rename` is gone and the
users row itself — kept after retirement — is what stops a name being reissued.
What people see is now a separate, mutable display name, validated by the same
rules wherever it is set (CLI, remote admin, registration, `/account`) and shown
on pages, approvals, listings and refusals. Nothing is authorized by it and
nothing on disk is keyed by it: metadata authors stay account names because the
push boundary resolves them for ownership, and approvals stay account ids.
`user_names`, `Snapshot.Reserved` and `Account.Former` are dropped: with a fixed
name there are no former names to remember, and the UNIQUE column already does
the reserving. Opening an older roster adds the `display` column and drops the
alias table in place; contributor refs written under the old id namespace are
left where they are, readable, and landable by a maintainer.
forge/identity/identity_test.go +138−63
| @@ -49,7 +49,7 @@ func mustCreate(t *testing.T, s *Store, name string) *Account { | |||
| 49 | 49 | func mustCreateBy(t *testing.T, s *Store, name, actor string) *Account { | |
| 50 | 50 | t.Helper() | |
| 51 | 51 | line, _ := pubLine(t) | |
| 52 | a, err := s.Create(name, line, actor) | ||
| 52 | a, err := s.Create(name, "", line, actor) | ||
| 53 | 53 | if err != nil { | |
| 54 | 54 | t.Fatalf("create %s: %v", name, err) | |
| 55 | 55 | } | |
| @@ -75,7 +75,7 @@ func TestCreateAccountWithKeyAndAddKey(t *testing.T) { | |||
| 75 | 75 | t.Parallel() | |
| 76 | 76 | s := newStore(t) | |
| 77 | 77 | line, wantFP := pubLine(t) | |
| 78 | a, err := s.Create("keenan", line, "test") | ||
| 78 | a, err := s.Create("keenan", "", line, "test") | ||
| 79 | 79 | if err != nil { | |
| 80 | 80 | t.Fatal(err) | |
| 81 | 81 | } | |
| @@ -111,7 +111,7 @@ func TestCreateRequiresAKeyAndDoesNotReserveTheName(t *testing.T) { | |||
| 111 | 111 | t.Parallel() | |
| 112 | 112 | s := newStore(t) | |
| 113 | 113 | for _, bad := range []string{"", "not an SSH key"} { | |
| 114 | if _, err := s.Create("alice", bad, "test"); err == nil { | ||
| 114 | if _, err := s.Create("alice", "", bad, "test"); err == nil { | ||
| 115 | 115 | t.Fatalf("Create accepted key %q", bad) | |
| 116 | 116 | } | |
| 117 | 117 | } | |
| @@ -119,7 +119,7 @@ func TestCreateRequiresAKeyAndDoesNotReserveTheName(t *testing.T) { | |||
| 119 | 119 | t.Fatal("a failed create left an account behind") | |
| 120 | 120 | } | |
| 121 | 121 | line, _ := pubLine(t) | |
| 122 | a, err := s.Create("alice", line, "test") | ||
| 122 | a, err := s.Create("alice", "", line, "test") | ||
| 123 | 123 | if err != nil { | |
| 124 | 124 | t.Fatalf("the failed attempts reserved the name: %v", err) | |
| 125 | 125 | } | |
| @@ -134,10 +134,10 @@ func TestCreateRejectsAKeyAlreadyRegistered(t *testing.T) { | |||
| 134 | 134 | t.Parallel() | |
| 135 | 135 | s := newStore(t) | |
| 136 | 136 | line, _ := pubLine(t) | |
| 137 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 137 | if _, err := s.Create("alice", "", line, "test"); err != nil { | ||
| 138 | 138 | t.Fatal(err) | |
| 139 | 139 | } | |
| 140 | if _, err := s.Create("bob", line, "test"); err == nil { | ||
| 140 | if _, err := s.Create("bob", "", line, "test"); err == nil { | ||
| 141 | 141 | t.Fatal("a registered key was accepted for a second account") | |
| 142 | 142 | } else if !strings.Contains(err.Error(), "already registered") { | |
| 143 | 143 | t.Fatalf("unclear error: %v", err) | |
| @@ -146,7 +146,7 @@ func TestCreateRejectsAKeyAlreadyRegistered(t *testing.T) { | |||
| 146 | 146 | t.Fatal("the duplicate-key failure left an account behind") | |
| 147 | 147 | } | |
| 148 | 148 | fresh, _ := pubLine(t) | |
| 149 | if _, err := s.Create("bob", fresh, "test"); err != nil { | ||
| 149 | if _, err := s.Create("bob", "", fresh, "test"); err != nil { | ||
| 150 | 150 | t.Fatalf("the duplicate-key failure reserved bob's name: %v", err) | |
| 151 | 151 | } | |
| 152 | 152 | } | |
| @@ -155,7 +155,7 @@ func TestMultipleKeysPerAccount(t *testing.T) { | |||
| 155 | 155 | t.Parallel() | |
| 156 | 156 | s := newStore(t) | |
| 157 | 157 | line, _ := pubLine(t) | |
| 158 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 158 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 159 | 159 | t.Fatal(err) | |
| 160 | 160 | } | |
| 161 | 161 | var fps []string | |
| @@ -188,11 +188,11 @@ func TestDuplicateKeyAcrossAccountsRejected(t *testing.T) { | |||
| 188 | 188 | t.Parallel() | |
| 189 | 189 | s := newStore(t) | |
| 190 | 190 | line, _ := pubLine(t) | |
| 191 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 191 | if _, err := s.Create("alice", "", line, "test"); err != nil { | ||
| 192 | 192 | t.Fatal(err) | |
| 193 | 193 | } | |
| 194 | 194 | bobLine, _ := pubLine(t) | |
| 195 | if _, err := s.Create("bob", bobLine, "test"); err != nil { | ||
| 195 | if _, err := s.Create("bob", "", bobLine, "test"); err != nil { | ||
| 196 | 196 | t.Fatal(err) | |
| 197 | 197 | } | |
| 198 | 198 | if _, err := s.AddKey("bob", line, "test"); err == nil { | |
| @@ -260,7 +260,7 @@ func TestRemoveKey(t *testing.T) { | |||
| 260 | 260 | s := newStore(t) | |
| 261 | 261 | keepLine, keepFP := pubLine(t) | |
| 262 | 262 | dropLine, dropFP := pubLine(t) | |
| 263 | if _, err := s.Create("keenan", keepLine, "test"); err != nil { | ||
| 263 | if _, err := s.Create("keenan", "", keepLine, "test"); err != nil { | ||
| 264 | 264 | t.Fatal(err) | |
| 265 | 265 | } | |
| 266 | 266 | if _, err := s.AddKey("keenan", dropLine, "test"); err != nil { | |
| @@ -283,7 +283,7 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 283 | 283 | t.Parallel() | |
| 284 | 284 | s := newStore(t) | |
| 285 | 285 | line, fp := pubLine(t) | |
| 286 | a, err := s.Create("wilbur", line, "test") | ||
| 286 | a, err := s.Create("wilbur", "", line, "test") | ||
| 287 | 287 | if err != nil { | |
| 288 | 288 | t.Fatal(err) | |
| 289 | 289 | } | |
| @@ -297,7 +297,7 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 297 | 297 | t.Fatal("a retired name still resolves") | |
| 298 | 298 | } | |
| 299 | 299 | reissue, _ := pubLine(t) | |
| 300 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 300 | if _, err := s.Create("wilbur", "", reissue, "test"); err == nil { | ||
| 301 | 301 | t.Fatalf("the retired name was reissued to a new account") | |
| 302 | 302 | } else if !strings.Contains(err.Error(), "cannot be reused") { | |
| 303 | 303 | t.Fatalf("unclear error: %v", err) | |
| @@ -308,48 +308,82 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 308 | 308 | } | |
| 309 | 309 | } | |
| 310 | 310 | ||
| 311 | // TestRenameKeepsTheIdAndReservesBothNames: the id is what ACLs and approvals | ||
| 312 | // hold, so a rename must not disturb it, and neither name may be reused. | ||
| 313 | func TestRenameKeepsTheIdAndReservesBothNames(t *testing.T) { | ||
| 311 | // TestDisplayNameIsALabelAndTheNameIsFixed: the display name is what people | ||
| 312 | // read, and it may change; the account name is what refs and metadata files were | ||
| 313 | // written with, so it may not. | ||
| 314 | func TestDisplayNameIsALabelAndTheNameIsFixed(t *testing.T) { | ||
| 314 | 315 | t.Parallel() | |
| 315 | 316 | s := newStore(t) | |
| 316 | 317 | line, fp := pubLine(t) | |
| 317 | a, err := s.Create("wilbur", line, "test") | ||
| 318 | a, err := s.Create("wilbur", "Wilbur Wright", line, "test") | ||
| 318 | 319 | if err != nil { | |
| 319 | 320 | t.Fatal(err) | |
| 320 | 321 | } | |
| 321 | renamed, err := s.Rename("wilbur", "robert", "test") | ||
| 322 | if a.Label() != "Wilbur Wright" { | ||
| 323 | t.Fatalf("Label = %q, want the display name", a.Label()) | ||
| 324 | } | ||
| 325 | // The display name is not an identity: only the fixed name resolves. | ||
| 326 | if _, ok := s.ResolveID("Wilbur Wright"); ok { | ||
| 327 | t.Fatal("a display name resolved as an account name") | ||
| 328 | } | ||
| 329 | if got, ok := s.ResolveID("wilbur"); !ok || got != a.ID { | ||
| 330 | t.Fatalf("ResolveID(wilbur) = %q,%v; want %s", got, ok, a.ID) | ||
| 331 | } | ||
| 332 | |||
| 333 | changed, err := s.SetDisplay("wilbur", "Wilbur", "test") | ||
| 322 | 334 | if err != nil { | |
| 323 | 335 | t.Fatal(err) | |
| 324 | 336 | } | |
| 325 | if renamed.ID != a.ID { | ||
| 326 | t.Fatalf("rename changed the id: %s -> %s", a.ID, renamed.ID) | ||
| 337 | if changed.ID != a.ID || changed.Name != "wilbur" { | ||
| 338 | t.Fatalf("SetDisplay touched the name: %+v", changed) | ||
| 339 | } | ||
| 340 | if changed.Label() != "Wilbur" { | ||
| 341 | t.Fatalf("Label after SetDisplay = %q", changed.Label()) | ||
| 327 | 342 | } | |
| 328 | 343 | if got, _, ok := s.Lookup(fp); !ok || got != a.ID { | |
| 329 | t.Fatalf("Lookup after rename = %q,%v; want %s", got, ok, a.ID) | ||
| 344 | t.Fatalf("Lookup after SetDisplay = %q,%v; want %s", got, ok, a.ID) | ||
| 330 | 345 | } | |
| 331 | if _, ok := s.ResolveID("wilbur"); ok { | ||
| 332 | t.Fatal("the old name still resolves to an active account") | ||
| 346 | |||
| 347 | // Clearing the display falls back to the name. | ||
| 348 | cleared, err := s.SetDisplay(a.ID, "", "test") | ||
| 349 | if err != nil { | ||
| 350 | t.Fatal(err) | ||
| 333 | 351 | } | |
| 334 | if got, ok := s.ResolveID("robert"); !ok || got != a.ID { | ||
| 335 | t.Fatalf("new name does not resolve: %q,%v", got, ok) | ||
| 352 | if cleared.Display != "" || cleared.Label() != "wilbur" { | ||
| 353 | t.Fatalf("cleared display = %q, label %q", cleared.Display, cleared.Label()) | ||
| 336 | 354 | } | |
| 337 | // Both names stay reserved. | ||
| 355 | |||
| 356 | // The name cannot be taken by a second account, and the API that used to | ||
| 357 | // move it is gone. | ||
| 338 | 358 | reissue, _ := pubLine(t) | |
| 339 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 340 | t.Fatal("the former name was handed to a new account") | ||
| 359 | if _, err := s.Create("wilbur", "", reissue, "test"); err == nil { | ||
| 360 | t.Fatal("a second account took the name") | ||
| 341 | 361 | } | |
| 342 | otherLine, _ := pubLine(t) | ||
| 343 | other, err := s.Create("dana", otherLine, "test") | ||
| 344 | if err != nil { | ||
| 362 | if _, err := s.SetDisplay("nobody", "x", "test"); err == nil { | ||
| 363 | t.Fatal("SetDisplay accepted an unknown account") | ||
| 364 | } | ||
| 365 | } | ||
| 366 | |||
| 367 | func TestInvalidDisplaysRefused(t *testing.T) { | ||
| 368 | t.Parallel() | ||
| 369 | s := newStore(t) | ||
| 370 | line, _ := pubLine(t) | ||
| 371 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 345 | 372 | t.Fatal(err) | |
| 346 | 373 | } | |
| 347 | if _, err := s.Rename(other.ID, "wilbur", "test"); err == nil { | ||
| 348 | t.Fatal("another account took the former name") | ||
| 374 | for _, display := range []string{"two\nlines", "tab\there", strings.Repeat("x", maxDisplayRunes+1)} { | ||
| 375 | if err := ValidateDisplay(display); err == nil { | ||
| 376 | t.Fatalf("ValidateDisplay accepted %q", display) | ||
| 377 | } | ||
| 378 | if _, err := s.SetDisplay("keenan", display, "test"); err == nil { | ||
| 379 | t.Fatalf("SetDisplay accepted %q", display) | ||
| 380 | } | ||
| 349 | 381 | } | |
| 350 | // The old name still identifies the same account for display. | ||
| 351 | if got, err := s.Find("wilbur"); err != nil || got.ID != a.ID { | ||
| 352 | t.Fatalf("Find(former name) = %v, %v", got, err) | ||
| 382 | // Spaces, capitals and non-Latin scripts are exactly what a label is for. | ||
| 383 | for _, display := range []string{"Keenan O'Brien", "田中 健", " padded "} { | ||
| 384 | if _, err := s.SetDisplay("keenan", display, "test"); err != nil { | ||
| 385 | t.Fatalf("SetDisplay(%q): %v", display, err) | ||
| 386 | } | ||
| 353 | 387 | } | |
| 354 | 388 | } | |
| 355 | 389 | ||
| @@ -358,7 +392,7 @@ func TestInvalidNamesRefused(t *testing.T) { | |||
| 358 | 392 | s := newStore(t) | |
| 359 | 393 | line, _ := pubLine(t) | |
| 360 | 394 | for _, name := range []string{"Keenan", "has space", "a", "with/slash", "trailing."} { | |
| 361 | if _, err := s.Create(name, line, "test"); err == nil { | ||
| 395 | if _, err := s.Create(name, "", line, "test"); err == nil { | ||
| 362 | 396 | t.Fatalf("%q accepted as a name", name) | |
| 363 | 397 | } | |
| 364 | 398 | } | |
| @@ -374,7 +408,7 @@ func TestConcurrentAddsAllLand(t *testing.T) { | |||
| 374 | 408 | for i := range lines { | |
| 375 | 409 | lines[i], _ = pubLine(t) | |
| 376 | 410 | } | |
| 377 | if _, err := s.Create("keenan", lines[0], "test"); err != nil { | ||
| 411 | if _, err := s.Create("keenan", "", lines[0], "test"); err != nil { | ||
| 378 | 412 | t.Fatal(err) | |
| 379 | 413 | } | |
| 380 | 414 | var wg sync.WaitGroup | |
| @@ -404,17 +438,17 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 404 | 438 | t.Parallel() | |
| 405 | 439 | s := newStore(t) | |
| 406 | 440 | line, fp := pubLine(t) | |
| 407 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 441 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 408 | 442 | t.Fatal(err) | |
| 409 | 443 | } | |
| 410 | 444 | second, secondFP := pubLine(t) | |
| 411 | 445 | if _, err := s.AddKey("keenan", second, "test"); err != nil { | |
| 412 | 446 | t.Fatal(err) | |
| 413 | 447 | } | |
| 414 | if _, err := s.Rename("keenan", "robert", "test"); err != nil { | ||
| 448 | if _, err := s.SetDisplay("keenan", "Keenan", "test"); err != nil { | ||
| 415 | 449 | t.Fatal(err) | |
| 416 | 450 | } | |
| 417 | if err := s.RemoveKey("robert", secondFP, "test"); err != nil { | ||
| 451 | if err := s.RemoveKey("keenan", secondFP, "test"); err != nil { | ||
| 418 | 452 | t.Fatal(err) | |
| 419 | 453 | } | |
| 420 | 454 | data, err := os.ReadFile(s.AuditPath) | |
| @@ -422,13 +456,69 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 422 | 456 | t.Fatal(err) | |
| 423 | 457 | } | |
| 424 | 458 | log := string(data) | |
| 425 | for _, want := range []string{"identity.create", "identity.add-key", "identity.rename", "identity.remove-key", fp, secondFP} { | ||
| 459 | for _, want := range []string{"identity.create", "identity.add-key", "identity.display", "identity.remove-key", fp, secondFP} { | ||
| 426 | 460 | if !strings.Contains(log, want) { | |
| 427 | 461 | t.Fatalf("audit log missing %q:\n%s", want, log) | |
| 428 | 462 | } | |
| 429 | 463 | } | |
| 430 | 464 | } | |
| 431 | 465 | ||
| 466 | // TestRosterUpgradesFromTheAliasTable: rosters written before names were fixed | ||
| 467 | // kept a user_names alias table and no display column. Opening one must add the | ||
| 468 | // column, keep every account readable, and drop the alias table rather than | ||
| 469 | // leave a list of former names nothing resolves. | ||
| 470 | func TestRosterUpgradesFromTheAliasTable(t *testing.T) { | ||
| 471 | t.Parallel() | ||
| 472 | path := filepath.Join(t.TempDir(), "users.db") | ||
| 473 | db, err := openDB(path) | ||
| 474 | if err != nil { | ||
| 475 | t.Fatal(err) | ||
| 476 | } | ||
| 477 | if _, err := db.Exec(` | ||
| 478 | CREATE TABLE users ( | ||
| 479 | id TEXT PRIMARY KEY, | ||
| 480 | name TEXT NOT NULL UNIQUE, | ||
| 481 | status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','retired')), | ||
| 482 | created INTEGER NOT NULL | ||
| 483 | ); | ||
| 484 | CREATE TABLE user_names ( | ||
| 485 | name TEXT PRIMARY KEY, | ||
| 486 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE | ||
| 487 | ); | ||
| 488 | INSERT INTO users(id, name, status, created) VALUES('01M5JR2JK29KKMAA1T9MDNTPBP', 'wilbur', 'active', 0); | ||
| 489 | INSERT INTO user_names(name, user_id) VALUES('wilbur', '01M5JR2JK29KKMAA1T9MDNTPBP'); | ||
| 490 | INSERT INTO user_names(name, user_id) VALUES('robert', '01M5JR2JK29KKMAA1T9MDNTPBP');`); err != nil { | ||
| 491 | t.Fatal(err) | ||
| 492 | } | ||
| 493 | if err := db.Close(); err != nil { | ||
| 494 | t.Fatal(err) | ||
| 495 | } | ||
| 496 | |||
| 497 | s, err := Open(path) | ||
| 498 | if err != nil { | ||
| 499 | t.Fatal(err) | ||
| 500 | } | ||
| 501 | defer s.Close() | ||
| 502 | a, err := s.Find("wilbur") | ||
| 503 | if err != nil { | ||
| 504 | t.Fatal(err) | ||
| 505 | } | ||
| 506 | if a.Display != "" || a.Label() != "wilbur" { | ||
| 507 | t.Fatalf("upgraded account display = %q label = %q", a.Display, a.Label()) | ||
| 508 | } | ||
| 509 | // The former name was only ever an alias, so it no longer resolves. | ||
| 510 | if _, err := s.Find("robert"); err == nil { | ||
| 511 | t.Fatal("an entry from the dropped alias table still resolves") | ||
| 512 | } | ||
| 513 | var n int | ||
| 514 | if err := s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE name='user_names'`).Scan(&n); err != nil { | ||
| 515 | t.Fatal(err) | ||
| 516 | } | ||
| 517 | if n != 0 { | ||
| 518 | t.Fatal("the user_names table survived the upgrade") | ||
| 519 | } | ||
| 520 | } | ||
| 521 | |||
| 432 | 522 | // TestRosterIsOneDatabaseFile proves the backup story: the roster is a single | |
| 433 | 523 | // SQLite file, and Backup takes a consistent copy of it without a system repo or | |
| 434 | 524 | // a directory of key files beside it. | |
| @@ -436,13 +526,10 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 436 | 526 | t.Parallel() | |
| 437 | 527 | s := newStore(t) | |
| 438 | 528 | line, fp := pubLine(t) | |
| 439 | a, err := s.Create("keenan", line, "test") | ||
| 529 | a, err := s.Create("keenan", "Keenan O'Brien", line, "test") | ||
| 440 | 530 | if err != nil { | |
| 441 | 531 | t.Fatal(err) | |
| 442 | 532 | } | |
| 443 | if _, err := s.Rename(a.ID, "robert", "test"); err != nil { | ||
| 444 | t.Fatal(err) | ||
| 445 | } | ||
| 446 | 533 | ||
| 447 | 534 | copied := filepath.Join(t.TempDir(), "users.db") | |
| 448 | 535 | if err := s.Backup(copied); err != nil { | |
| @@ -453,14 +540,14 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 453 | 540 | t.Fatal(err) | |
| 454 | 541 | } | |
| 455 | 542 | defer other.Close() | |
| 456 | if got, ok := other.ResolveID("robert"); !ok || got != a.ID { | ||
| 457 | t.Fatalf("copied roster resolves robert to %q,%v; want %s", got, ok, a.ID) | ||
| 543 | if got, ok := other.ResolveID("keenan"); !ok || got != a.ID { | ||
| 544 | t.Fatalf("copied roster resolves keenan to %q,%v; want %s", got, ok, a.ID) | ||
| 458 | 545 | } | |
| 459 | 546 | if _, _, ok := other.Lookup(fp); !ok { | |
| 460 | 547 | t.Fatal("copied roster lost the key") | |
| 461 | 548 | } | |
| 462 | if want := []string{"keenan"}; !equal(other.Snapshot().ByID[a.ID].Former, want) { | ||
| 463 | t.Fatalf("copied roster former names = %v, want %v", other.Snapshot().ByID[a.ID].Former, want) | ||
| 549 | if got := other.Label(a.ID); got != "Keenan O'Brien" { | ||
| 550 | t.Fatalf("copied roster display = %q", got) | ||
| 464 | 551 | } | |
| 465 | 552 | ||
| 466 | 553 | for _, absent := range []string{"system.git", "trusted_keys"} { | |
| @@ -470,18 +557,6 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 470 | 557 | } | |
| 471 | 558 | } | |
| 472 | 559 | ||
| 473 | func equal(got, want []string) bool { | ||
| 474 | if len(got) != len(want) { | ||
| 475 | return false | ||
| 476 | } | ||
| 477 | for i := range got { | ||
| 478 | if got[i] != want[i] { | ||
| 479 | return false | ||
| 480 | } | ||
| 481 | } | ||
| 482 | return true | ||
| 483 | } | ||
| 484 | |||
| 485 | 560 | func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |
| 486 | 561 | t.Parallel() | |
| 487 | 562 | s := newStore(t) | |
| @@ -506,7 +581,7 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 506 | 581 | t.Fatalf("admin audit line does not name the actor:\n%s", log) | |
| 507 | 582 | } | |
| 508 | 583 | bobKey, _ := pubLine(t) | |
| 509 | if _, err := s.Create("bob", bobKey, "operator"); err != nil { | ||
| 584 | if _, err := s.Create("bob", "", bobKey, "operator"); err != nil { | ||
| 510 | 585 | t.Fatal(err) | |
| 511 | 586 | } | |
| 512 | 587 | if _, err := s.SetAdmin("bob", true, "operator"); err != nil { | |