github.com/cs3org/reva/v2@v2.27.7/pkg/appauth/manager/json/json_test.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package json 20 21 import ( 22 "bytes" 23 "context" 24 "encoding/json" 25 "io" 26 "os" 27 "reflect" 28 "testing" 29 "time" 30 31 "bou.ke/monkey" 32 apppb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1" 33 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 34 typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 35 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 36 "github.com/gdexlab/go-render/render" 37 "github.com/google/go-cmp/cmp" 38 "github.com/sethvargo/go-password/password" 39 "golang.org/x/crypto/bcrypt" 40 "google.golang.org/protobuf/testing/protocmp" 41 ) 42 43 func TestNewManager(t *testing.T) { 44 userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}, Username: "Test User"} 45 46 // temp directory where are stored tests config files 47 tempDir := createTempDir(t, "jsonappauth_test") 48 defer os.RemoveAll(tempDir) 49 50 jsonCorruptedFile := createTempFile(t, tempDir, "corrupted.json") 51 defer jsonCorruptedFile.Close() 52 jsonEmptyFile := createTempFile(t, tempDir, "empty.json") 53 defer jsonEmptyFile.Close() 54 jsonOkFile := createTempFile(t, tempDir, "ok.json") 55 defer jsonOkFile.Close() 56 57 hashToken, _ := bcrypt.GenerateFromPassword([]byte("1234"), 10) 58 59 dummyData := map[string]map[string]*apppb.AppPassword{ 60 userTest.GetId().String(): { 61 string(hashToken): { 62 Password: string(hashToken), 63 TokenScope: nil, 64 Label: "label", 65 User: userTest.GetId(), 66 Expiration: nil, 67 Ctime: &typespb.Timestamp{Seconds: 0}, 68 Utime: &typespb.Timestamp{Seconds: 0}, 69 }, 70 }} 71 72 dummyDataJSON, _ := json.Marshal(dummyData) 73 74 // fill temp file with tests data 75 fill(t, jsonCorruptedFile, `[{`) 76 fill(t, jsonEmptyFile, "") 77 fill(t, jsonOkFile, string(dummyDataJSON)) 78 79 testCases := []struct { 80 description string 81 configMap map[string]interface{} 82 expected *jsonManager 83 }{ 84 { 85 description: "New appauth manager from corrupted state file", 86 configMap: map[string]interface{}{ 87 "file": jsonCorruptedFile.Name(), 88 "token_strength": 10, 89 }, 90 expected: nil, // nil == error 91 }, 92 { 93 description: "New appauth manager from empty state file", 94 configMap: map[string]interface{}{ 95 "file": jsonEmptyFile.Name(), 96 "token_strength": 10, 97 "password_hash_cost": 12, 98 }, 99 expected: &jsonManager{ 100 config: &config{ 101 File: jsonEmptyFile.Name(), 102 TokenStrength: 10, 103 PasswordHashCost: 12, 104 }, 105 passwords: map[string]map[string]*apppb.AppPassword{}, 106 }, 107 }, 108 { 109 description: "New appauth manager from state file", 110 configMap: map[string]interface{}{ 111 "file": jsonOkFile.Name(), 112 "token_strength": 10, 113 "password_hash_cost": 10, 114 }, 115 expected: &jsonManager{ 116 config: &config{ 117 File: jsonOkFile.Name(), 118 TokenStrength: 10, 119 PasswordHashCost: 10, 120 }, 121 passwords: dummyData, 122 }, 123 }, 124 } 125 126 for _, test := range testCases { 127 t.Run(test.description, func(t *testing.T) { 128 manager, err := New(test.configMap) 129 if test.expected == nil { 130 if err == nil { 131 t.Fatalf("no error (but we expected one) while get manager") 132 } else { 133 t.Skip() 134 } 135 } 136 if !reflect.DeepEqual(test.expected.config, manager.(*jsonManager).config) { 137 t.Fatalf("appauth differ: expected=%v got=%v", render.AsCode(test.expected), render.AsCode(manager)) 138 } 139 140 comparePasswords(t, test.expected.passwords, manager.(*jsonManager).passwords) 141 }) 142 } 143 144 } 145 146 func TestGenerateAppPassword(t *testing.T) { 147 userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}, Username: "Test User"} 148 ctx := ctxpkg.ContextSetUser(context.Background(), userTest) 149 tempDir := createTempDir(t, "jsonappauth_test") 150 defer os.RemoveAll(tempDir) 151 152 nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) 153 patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) 154 now := now() 155 token := "1234" 156 patchPasswordGenerate := monkey.Patch(password.Generate, func(int, int, int, bool, bool) (string, error) { return token, nil }) 157 defer patchNow.Unpatch() 158 defer patchPasswordGenerate.Unpatch() 159 160 generateFromPassword := monkey.Patch(bcrypt.GenerateFromPassword, func(pw []byte, n int) ([]byte, error) { 161 return append([]byte("hash:"), pw...), nil 162 }) 163 defer generateFromPassword.Restore() 164 hashTokenXXXX, _ := bcrypt.GenerateFromPassword([]byte("XXXX"), 11) 165 hashToken1234, _ := bcrypt.GenerateFromPassword([]byte(token), 11) 166 167 dummyData := map[string]map[string]*apppb.AppPassword{ 168 userpb.User{Id: &userpb.UserId{Idp: "1"}, Username: "Test User1"}.Id.String(): { 169 string(hashTokenXXXX): { 170 Password: string(hashTokenXXXX), 171 Label: "", 172 User: &userpb.UserId{Idp: "1"}, 173 Ctime: now, 174 Utime: now, 175 }, 176 }, 177 } 178 179 dummyDataJSON, _ := json.Marshal(dummyData) 180 181 testCases := []struct { 182 description string 183 prevStateJSON string 184 expected *apppb.AppPassword 185 expectedState map[string]map[string]*apppb.AppPassword 186 }{ 187 { 188 description: "GenerateAppPassword with empty state", 189 prevStateJSON: `{}`, 190 expected: &apppb.AppPassword{ 191 Password: token, 192 TokenScope: nil, 193 Label: "label", 194 User: userTest.GetId(), 195 Expiration: nil, 196 Ctime: now, 197 Utime: now, 198 }, 199 expectedState: map[string]map[string]*apppb.AppPassword{ 200 userTest.GetId().String(): { 201 string(hashToken1234): { 202 Password: string(hashToken1234), 203 TokenScope: nil, 204 Label: "label", 205 User: userTest.GetId(), 206 Expiration: nil, 207 Ctime: now, 208 Utime: now, 209 }, 210 }, 211 }, 212 }, 213 { 214 description: "GenerateAppPassword with not empty state", 215 prevStateJSON: string(dummyDataJSON), 216 expected: &apppb.AppPassword{ 217 Password: token, 218 TokenScope: nil, 219 Label: "label", 220 User: userTest.GetId(), 221 Expiration: nil, 222 Ctime: now, 223 Utime: now, 224 }, 225 expectedState: concatMaps(map[string]map[string]*apppb.AppPassword{ 226 userTest.GetId().String(): { 227 string(hashToken1234): { 228 Password: string(hashToken1234), 229 TokenScope: nil, 230 Label: "label", 231 User: userTest.GetId(), 232 Expiration: nil, 233 Ctime: now, 234 Utime: now, 235 }, 236 }}, 237 dummyData), 238 }, 239 } 240 241 for _, test := range testCases { 242 t.Run(test.description, func(t *testing.T) { 243 // initialize temp file with `prevStateJSON` content 244 tmpFile := createTempFile(t, tempDir, "test.json") 245 defer tmpFile.Close() 246 fill(t, tmpFile, test.prevStateJSON) 247 manager, err := New(map[string]interface{}{ 248 "file": tmpFile.Name(), 249 "token_strength": len(token), 250 "password_hash_cost": 11, 251 }) 252 if err != nil { 253 t.Fatal("error creating manager:", err) 254 } 255 256 pw, err := manager.GenerateAppPassword(ctx, nil, "label", nil) 257 if err != nil { 258 t.Fatal("error generating password:", err) 259 } 260 261 // test state in memory 262 263 if !cmp.Equal(pw, test.expected, protocmp.Transform()) { 264 t.Fatalf("apppassword differ: expected=%v got=%v", test.expected, pw) 265 } 266 267 comparePasswords(t, manager.(*jsonManager).passwords, test.expectedState) 268 269 // test saved json 270 271 _, err = tmpFile.Seek(0, 0) 272 if err != nil { 273 t.Fatal(err) 274 } 275 data, err := io.ReadAll(tmpFile) 276 if err != nil { 277 t.Fatalf("error reading file %s: %v", tmpFile.Name(), err) 278 } 279 280 var jsonState map[string]map[string]*apppb.AppPassword 281 err = json.Unmarshal(data, &jsonState) 282 if err != nil { 283 t.Fatalf("error decoding json: %v", err) 284 } 285 286 comparePasswords(t, jsonState, test.expectedState) 287 }) 288 } 289 290 } 291 292 func TestListAppPasswords(t *testing.T) { 293 user0Test := &userpb.User{Id: &userpb.UserId{Idp: "0"}} 294 user1Test := &userpb.User{Id: &userpb.UserId{Idp: "1"}} 295 ctx := ctxpkg.ContextSetUser(context.Background(), user0Test) 296 tempDir := createTempDir(t, "jsonappauth_test") 297 defer os.RemoveAll(tempDir) 298 299 nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) 300 patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) 301 defer patchNow.Unpatch() 302 now := now() 303 304 token := "hash:1234" 305 306 dummyDataUser0 := map[string]map[string]*apppb.AppPassword{ 307 user0Test.GetId().String(): { 308 token: { 309 Password: token, 310 TokenScope: nil, 311 Label: "label", 312 User: user0Test.GetId(), 313 Expiration: nil, 314 Ctime: now, 315 Utime: now, 316 }, 317 }} 318 319 dummyDataUserExpired := map[string]map[string]*apppb.AppPassword{ 320 user0Test.GetId().String(): { 321 token: { 322 Password: token, 323 TokenScope: nil, 324 Label: "label", 325 User: user0Test.GetId(), 326 Expiration: &typespb.Timestamp{ 327 Seconds: 100, 328 }, 329 Ctime: now, 330 Utime: now, 331 }, 332 }} 333 334 dummyDataUser0JSON, _ := json.Marshal(dummyDataUser0) 335 dummyDataUserExpiredJSON, _ := json.Marshal(dummyDataUserExpired) 336 337 dummyDataUser1 := map[string]map[string]*apppb.AppPassword{ 338 user1Test.GetId().String(): { 339 "XXXX": { 340 Password: "XXXX", 341 TokenScope: nil, 342 Label: "label", 343 User: user1Test.GetId(), 344 Expiration: nil, 345 Ctime: now, 346 Utime: now, 347 }, 348 }} 349 350 dummyDataTwoUsersJSON, _ := json.Marshal(concatMaps(dummyDataUser0, dummyDataUser1)) 351 352 testCases := []struct { 353 description string 354 stateJSON string 355 expectedState []*apppb.AppPassword 356 }{ 357 { 358 description: "ListAppPasswords with empty state", 359 stateJSON: `{}`, 360 expectedState: make([]*apppb.AppPassword, 0), 361 }, 362 { 363 description: "ListAppPasswords with not json state file", 364 stateJSON: "", 365 expectedState: make([]*apppb.AppPassword, 0), 366 }, 367 { 368 description: "ListAppPasswords with not empty state (only one user)", 369 stateJSON: string(dummyDataUser0JSON), 370 expectedState: []*apppb.AppPassword{ 371 dummyDataUser0[user0Test.GetId().String()][token], 372 }, 373 }, 374 { 375 description: "ListAppPasswords with not empty state with expired password (only one user)", 376 stateJSON: string(dummyDataUserExpiredJSON), 377 expectedState: []*apppb.AppPassword{ 378 dummyDataUserExpired[user0Test.GetId().String()][token], 379 }, 380 }, 381 { 382 description: "ListAppPasswords with not empty state (different users)", 383 stateJSON: string(dummyDataTwoUsersJSON), 384 expectedState: []*apppb.AppPassword{ 385 dummyDataUser0[user0Test.GetId().String()][token], 386 }, 387 }, 388 } 389 390 for _, test := range testCases { 391 t.Run(test.description, func(t *testing.T) { 392 // initialize temp file with `state_json` content 393 tmpFile := createTempFile(t, tempDir, "test.json") 394 defer tmpFile.Close() 395 if test.stateJSON != "" { 396 fill(t, tmpFile, test.stateJSON) 397 } 398 manager, err := New(map[string]interface{}{ 399 "file": tmpFile.Name(), 400 "token_strength": len(token), 401 }) 402 if err != nil { 403 t.Fatal("error creating manager:", err) 404 } 405 406 pws, err := manager.ListAppPasswords(ctx) 407 if err != nil { 408 t.Fatal("error listing passwords:", err) 409 } 410 411 if len(pws) != len(test.expectedState) { 412 t.Fatalf("list passwords differ: expected=%v got=%v", test.expectedState, pws) 413 } 414 415 cmp.Equal(pws, test.expectedState, protocmp.Transform()) 416 }) 417 } 418 419 } 420 421 func TestInvalidateAppPassword(t *testing.T) { 422 userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}} 423 ctx := ctxpkg.ContextSetUser(context.Background(), userTest) 424 tempDir := createTempDir(t, "jsonappauth_test") 425 defer os.RemoveAll(tempDir) 426 427 nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) 428 patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) 429 now := now() 430 defer patchNow.Unpatch() 431 432 token := "hash:1234" 433 434 dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ 435 userTest.GetId().String(): { 436 token: { 437 Password: token, 438 TokenScope: nil, 439 Label: "label", 440 User: userTest.GetId(), 441 Expiration: nil, 442 Ctime: now, 443 Utime: now, 444 }, 445 }} 446 447 dummyDataUser1TokenJSON, _ := json.Marshal(dummyDataUser1Token) 448 449 dummyDataUser2Token := map[string]map[string]*apppb.AppPassword{ 450 userTest.GetId().String(): { 451 token: { 452 Password: token, 453 TokenScope: nil, 454 Label: "label", 455 User: userTest.GetId(), 456 Expiration: nil, 457 Ctime: now, 458 Utime: now, 459 }, 460 "hash:XXXX": { 461 Password: "hash:XXXX", 462 TokenScope: nil, 463 Label: "label", 464 User: userTest.GetId(), 465 Expiration: nil, 466 Ctime: now, 467 Utime: now, 468 }, 469 }} 470 471 dummyDataUser2TokenJSON, _ := json.Marshal(dummyDataUser2Token) 472 473 testCases := []struct { 474 description string 475 stateJSON string 476 password string 477 expectedState map[string]map[string]*apppb.AppPassword 478 }{ 479 { 480 description: "InvalidateAppPassword with empty state", 481 stateJSON: `{}`, 482 password: "TOKEN_NOT_EXISTS", 483 expectedState: nil, 484 }, 485 { 486 description: "InvalidateAppPassword with not empty state and token does not exist", 487 stateJSON: string(dummyDataUser1TokenJSON), 488 password: "TOKEN_NOT_EXISTS", 489 expectedState: nil, 490 }, 491 { 492 description: "InvalidateAppPassword with not empty state and token exists", 493 stateJSON: string(dummyDataUser1TokenJSON), 494 password: token, 495 expectedState: map[string]map[string]*apppb.AppPassword{}, 496 }, 497 { 498 description: "InvalidateAppPassword with user that has more than 1 token", 499 stateJSON: string(dummyDataUser2TokenJSON), 500 password: token, 501 expectedState: map[string]map[string]*apppb.AppPassword{ 502 userTest.GetId().String(): { 503 "hash:XXXX": { 504 Password: "hash:XXXX", 505 TokenScope: nil, 506 Label: "label", 507 User: userTest.GetId(), 508 Expiration: nil, 509 Ctime: now, 510 Utime: now, 511 }, 512 }, 513 }, 514 }, 515 } 516 517 for _, test := range testCases { 518 t.Run(test.description, func(t *testing.T) { 519 // initialize temp file with `state_json` content 520 tmpFile := createTempFile(t, tempDir, "test.json") 521 fill(t, tmpFile, test.stateJSON) 522 manager, err := New(map[string]interface{}{ 523 "file": tmpFile.Name(), 524 "token_strength": 4, 525 }) 526 if err != nil { 527 t.Fatal("error creating manager:", err) 528 } 529 530 err = manager.InvalidateAppPassword(ctx, test.password) 531 if test.expectedState == nil { 532 if err == nil { 533 t.Fatalf("no error (but we expected one) while get manager") 534 } else { 535 t.Skip() 536 } 537 } 538 comparePasswords(t, test.expectedState, manager.(*jsonManager).passwords) 539 }) 540 } 541 542 } 543 544 func TestGetAppPassword(t *testing.T) { 545 userTest := &userpb.User{Id: &userpb.UserId{Idp: "0"}} 546 ctx := ctxpkg.ContextSetUser(context.Background(), userTest) 547 tempDir := createTempDir(t, "jsonappauth_test") 548 defer os.RemoveAll(tempDir) 549 550 nowFixed := time.Date(2021, time.May, 21, 12, 21, 0, 0, time.UTC) 551 patchNow := monkey.Patch(time.Now, func() time.Time { return nowFixed }) 552 defer patchNow.Unpatch() 553 554 now := now() 555 token := "1234" 556 557 generateFromPassword := monkey.Patch(bcrypt.GenerateFromPassword, func(pw []byte, n int) ([]byte, error) { 558 return append([]byte("hash:"), pw...), nil 559 }) 560 compareHashAndPassword := monkey.Patch(bcrypt.CompareHashAndPassword, func(hash, pw []byte) error { 561 hashPw, _ := bcrypt.GenerateFromPassword(pw, 0) 562 if bytes.Equal(hashPw, hash) { 563 return nil 564 } 565 return bcrypt.ErrMismatchedHashAndPassword 566 }) 567 defer generateFromPassword.Restore() 568 defer compareHashAndPassword.Restore() 569 hashToken1234, _ := bcrypt.GenerateFromPassword([]byte(token), 11) 570 571 dummyDataUser1Token := map[string]map[string]*apppb.AppPassword{ 572 userTest.GetId().String(): { 573 string(hashToken1234): { 574 Password: string(hashToken1234), 575 TokenScope: nil, 576 Label: "label", 577 User: userTest.GetId(), 578 Expiration: nil, 579 Ctime: now, 580 Utime: now, 581 }, 582 }} 583 584 dummyDataUserExpired := map[string]map[string]*apppb.AppPassword{ 585 userTest.GetId().String(): { 586 string(hashToken1234): { 587 Password: string(hashToken1234), 588 TokenScope: nil, 589 Label: "label", 590 User: userTest.GetId(), 591 Expiration: &typespb.Timestamp{ 592 Seconds: 100, 593 }, 594 Ctime: now, 595 Utime: now, 596 }, 597 }} 598 599 dummyDataUserFutureExpiration := map[string]map[string]*apppb.AppPassword{ 600 userTest.GetId().String(): { 601 string(hashToken1234): { 602 Password: string(hashToken1234), 603 TokenScope: nil, 604 Label: "label", 605 User: userTest.GetId(), 606 Expiration: &typespb.Timestamp{ 607 Seconds: uint64(time.Now().Unix()) + 3600, 608 }, 609 Ctime: now, 610 Utime: now, 611 }, 612 }} 613 614 dummyDataUser1TokenJSON, _ := json.Marshal(dummyDataUser1Token) 615 dummyDataUserExpiredJSON, _ := json.Marshal(dummyDataUserExpired) 616 dummyDataUserFutureExpirationJSON, _ := json.Marshal(dummyDataUserFutureExpiration) 617 618 dummyDataDifferentUserToken := map[string]map[string]*apppb.AppPassword{ 619 "OTHER_USER_ID": { 620 string(hashToken1234): { 621 Password: string(hashToken1234), 622 TokenScope: nil, 623 Label: "label", 624 User: &userpb.UserId{Idp: "OTHER_USER_ID"}, 625 Expiration: nil, 626 Ctime: now, 627 Utime: now, 628 }, 629 }} 630 631 dummyDataDifferentUserTokenJSON, _ := json.Marshal(dummyDataDifferentUserToken) 632 633 testCases := []struct { 634 description string 635 stateJSON string 636 password string 637 expectedState *apppb.AppPassword 638 }{ 639 { 640 description: "GetAppPassword with token that does not exist", 641 stateJSON: string(dummyDataUser1TokenJSON), 642 password: "TOKEN_NOT_EXISTS", 643 expectedState: nil, 644 }, 645 { 646 description: "GetAppPassword with expired token", 647 stateJSON: string(dummyDataUserExpiredJSON), 648 password: "1234", 649 expectedState: nil, 650 }, 651 { 652 description: "GetAppPassword with token with expiration set in the future", 653 stateJSON: string(dummyDataUserFutureExpirationJSON), 654 password: "1234", 655 expectedState: dummyDataUserFutureExpiration[userTest.GetId().String()][string(hashToken1234)], 656 }, 657 { 658 description: "GetAppPassword with token that exists but different user", 659 stateJSON: string(dummyDataDifferentUserTokenJSON), 660 password: "1234", 661 expectedState: nil, 662 }, 663 { 664 description: "GetAppPassword with token that exists owned by user", 665 stateJSON: string(dummyDataUser1TokenJSON), 666 password: "1234", 667 expectedState: dummyDataUser1Token[userTest.GetId().String()][string(hashToken1234)], 668 }, 669 } 670 671 for _, test := range testCases { 672 t.Run(test.description, func(t *testing.T) { 673 // initialize temp file with `state_json` content 674 tmpFile := createTempFile(t, tempDir, "test.json") 675 fill(t, tmpFile, test.stateJSON) 676 manager, err := New(map[string]interface{}{ 677 "file": tmpFile.Name(), 678 "token_strength": 4, 679 }) 680 if err != nil { 681 t.Fatal("error creating manager:", err) 682 } 683 684 pw, err := manager.GetAppPassword(ctx, userTest.GetId(), test.password) 685 if test.expectedState == nil { 686 if err == nil { 687 t.Fatalf("no error (but we expected one) while get manager") 688 } else { 689 t.Skip() 690 } 691 } 692 if !cmp.Equal(test.expectedState, pw, protocmp.Transform()) { 693 t.Fatalf("apppauth state differ: expected=%v got=%v", test.expectedState, pw) 694 } 695 696 }) 697 } 698 } 699 700 func createTempDir(t *testing.T, name string) string { 701 tempDir, err := os.MkdirTemp("", name) 702 if err != nil { 703 t.Fatalf("error while creating temp dir: %v", err) 704 } 705 return tempDir 706 } 707 708 func createTempFile(t *testing.T, tempDir string, name string) *os.File { 709 tempFile, err := os.CreateTemp(tempDir, name) 710 if err != nil { 711 t.Fatalf("error while creating temp file: %v", err) 712 } 713 return tempFile 714 } 715 716 func fill(t *testing.T, file *os.File, data string) { 717 _, err := file.WriteString(data) 718 if err != nil { 719 t.Fatalf("error while writing to file: %v", err) 720 } 721 } 722 723 func concatMaps(maps ...map[string]map[string]*apppb.AppPassword) map[string]map[string]*apppb.AppPassword { 724 res := make(map[string]map[string]*apppb.AppPassword) 725 for _, m := range maps { 726 for k := range m { 727 res[k] = m[k] 728 } 729 } 730 return res 731 } 732 733 func comparePasswords(t *testing.T, expected, got map[string]map[string]*apppb.AppPassword) { 734 if !cmp.Equal(expected, got, protocmp.Transform()) { 735 t.Fatalf("passwords differ: expected=%v got=%v", expected, got) 736 } 737 }