cozy-sharing 33.3.4 → 33.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -411,6 +411,305 @@ describe('updateDocumentPermissions', function () {
411
411
  }, _callee7);
412
412
  })));
413
413
  });
414
+ describe('fetchSharedDriveSharingLinks', function () {
415
+ var PERM_DRIVE_FILE = {
416
+ type: 'io.cozy.permissions',
417
+ id: 'perm_drive_file',
418
+ attributes: {
419
+ type: 'share',
420
+ permissions: {
421
+ rule0: {
422
+ type: 'io.cozy.files',
423
+ verbs: ['GET'],
424
+ values: ['file_in_drive']
425
+ }
426
+ },
427
+ shortcodes: {
428
+ code: 'shortcode123'
429
+ }
430
+ }
431
+ };
432
+ var driveFile = {
433
+ _id: 'file_in_drive',
434
+ id: 'file_in_drive',
435
+ driveId: 'drive_123'
436
+ };
437
+ it('fetches shared drive sharing links by file id', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8() {
438
+ var mockFindLinksByIds, mockClient, provider, result, permissions;
439
+ return _regeneratorRuntime.wrap(function _callee8$(_context8) {
440
+ while (1) switch (_context8.prev = _context8.next) {
441
+ case 0:
442
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
443
+ data: [PERM_DRIVE_FILE]
444
+ });
445
+ mockClient = createMockClient({});
446
+ mockClient.collection = jest.fn().mockReturnValue({
447
+ findLinksByIds: mockFindLinksByIds
448
+ });
449
+ provider = new SharingProvider({
450
+ client: mockClient
451
+ });
452
+ provider.state = reducer();
453
+ provider.dispatch = jest.fn(function (action) {
454
+ provider.state = reducer(provider.state, action);
455
+ });
456
+ _context8.next = 8;
457
+ return provider.fetchSharedDriveSharingLinks(driveFile);
458
+
459
+ case 8:
460
+ result = _context8.sent;
461
+ expect(mockClient.collection).toHaveBeenCalledWith('io.cozy.permissions', {
462
+ driveId: 'drive_123'
463
+ });
464
+ expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive']);
465
+ expect(provider.dispatch).toHaveBeenCalled();
466
+ permissions = getDocumentPermissions(provider.state, 'file_in_drive');
467
+ expect(permissions).toHaveLength(1);
468
+ expect(permissions[0].id).toBe(PERM_DRIVE_FILE.id);
469
+ expect(result).toEqual([PERM_DRIVE_FILE]);
470
+
471
+ case 16:
472
+ case "end":
473
+ return _context8.stop();
474
+ }
475
+ }, _callee8);
476
+ })));
477
+ it('returns an empty array and skips the API call for non-shared-drive documents', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee9() {
478
+ var mockClient, findLinksByIds, provider, result;
479
+ return _regeneratorRuntime.wrap(function _callee9$(_context9) {
480
+ while (1) switch (_context9.prev = _context9.next) {
481
+ case 0:
482
+ mockClient = createMockClient({});
483
+ findLinksByIds = jest.fn();
484
+ mockClient.collection = jest.fn().mockReturnValue({
485
+ findLinksByIds: findLinksByIds
486
+ });
487
+ provider = new SharingProvider({
488
+ client: mockClient
489
+ });
490
+ provider.dispatch = jest.fn();
491
+ _context9.next = 7;
492
+ return provider.fetchSharedDriveSharingLinks({
493
+ _id: 'regular_file'
494
+ });
495
+
496
+ case 7:
497
+ result = _context9.sent;
498
+ expect(result).toEqual([]);
499
+ expect(findLinksByIds).not.toHaveBeenCalled();
500
+ expect(provider.dispatch).not.toHaveBeenCalled();
501
+
502
+ case 11:
503
+ case "end":
504
+ return _context9.stop();
505
+ }
506
+ }, _callee9);
507
+ })));
508
+ it('uses document.id as a fallback when _id is missing', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
509
+ var mockFindLinksByIds, mockClient, provider;
510
+ return _regeneratorRuntime.wrap(function _callee10$(_context10) {
511
+ while (1) switch (_context10.prev = _context10.next) {
512
+ case 0:
513
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
514
+ data: [PERM_DRIVE_FILE]
515
+ });
516
+ mockClient = createMockClient({});
517
+ mockClient.collection = jest.fn().mockReturnValue({
518
+ findLinksByIds: mockFindLinksByIds
519
+ });
520
+ provider = new SharingProvider({
521
+ client: mockClient
522
+ });
523
+ provider.state = reducer();
524
+ provider.dispatch = jest.fn(function (action) {
525
+ provider.state = reducer(provider.state, action);
526
+ });
527
+ _context10.next = 8;
528
+ return provider.fetchSharedDriveSharingLinks({
529
+ id: 'file_in_drive',
530
+ driveId: 'drive_123'
531
+ });
532
+
533
+ case 8:
534
+ expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive']);
535
+
536
+ case 9:
537
+ case "end":
538
+ return _context10.stop();
539
+ }
540
+ }, _callee10);
541
+ })));
542
+ it('does not dispatch when the response has no data', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
543
+ var mockFindLinksByIds, mockClient, provider, result;
544
+ return _regeneratorRuntime.wrap(function _callee11$(_context11) {
545
+ while (1) switch (_context11.prev = _context11.next) {
546
+ case 0:
547
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
548
+ data: []
549
+ });
550
+ mockClient = createMockClient({});
551
+ mockClient.collection = jest.fn().mockReturnValue({
552
+ findLinksByIds: mockFindLinksByIds
553
+ });
554
+ provider = new SharingProvider({
555
+ client: mockClient
556
+ });
557
+ provider.dispatch = jest.fn();
558
+ _context11.next = 7;
559
+ return provider.fetchSharedDriveSharingLinks(driveFile);
560
+
561
+ case 7:
562
+ result = _context11.sent;
563
+ expect(result).toEqual([]);
564
+ expect(provider.dispatch).not.toHaveBeenCalled();
565
+
566
+ case 10:
567
+ case "end":
568
+ return _context11.stop();
569
+ }
570
+ }, _callee11);
571
+ })));
572
+ });
573
+ describe('shareByLink shared drive 409 recovery', function () {
574
+ var PERM_DRIVE_FILE = {
575
+ type: 'io.cozy.permissions',
576
+ id: 'perm_drive_file',
577
+ attributes: {
578
+ type: 'share',
579
+ permissions: {
580
+ rule0: {
581
+ type: 'io.cozy.files',
582
+ verbs: ['GET'],
583
+ values: ['file_in_drive']
584
+ }
585
+ },
586
+ shortcodes: {
587
+ code: 'shortcode123'
588
+ }
589
+ }
590
+ };
591
+ var driveFile = {
592
+ _id: 'file_in_drive',
593
+ id: 'file_in_drive',
594
+ driveId: 'drive_123'
595
+ };
596
+ it('falls back to fetching existing links when create returns 409', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
597
+ var conflict, mockCreateSharingLink, mockFindLinksByIds, mockClient, provider, resp;
598
+ return _regeneratorRuntime.wrap(function _callee12$(_context12) {
599
+ while (1) switch (_context12.prev = _context12.next) {
600
+ case 0:
601
+ conflict = Object.assign(new Error('Conflict'), {
602
+ status: 409
603
+ });
604
+ mockCreateSharingLink = jest.fn().mockRejectedValue(conflict);
605
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
606
+ data: [PERM_DRIVE_FILE]
607
+ });
608
+ mockClient = createMockClient({});
609
+ mockClient.collection = jest.fn().mockReturnValue({
610
+ createSharingLink: mockCreateSharingLink,
611
+ findLinksByIds: mockFindLinksByIds
612
+ });
613
+ provider = new SharingProvider({
614
+ client: mockClient
615
+ });
616
+ provider.state = reducer();
617
+ provider.dispatch = jest.fn(function (action) {
618
+ provider.state = reducer(provider.state, action);
619
+ });
620
+ _context12.next = 10;
621
+ return provider.shareByLink(driveFile, {
622
+ verbs: ['GET']
623
+ });
624
+
625
+ case 10:
626
+ resp = _context12.sent;
627
+ expect(mockCreateSharingLink).toHaveBeenCalled();
628
+ expect(mockFindLinksByIds).toHaveBeenCalledWith(['file_in_drive']);
629
+ expect(resp.data.id).toBe(PERM_DRIVE_FILE.id);
630
+
631
+ case 14:
632
+ case "end":
633
+ return _context12.stop();
634
+ }
635
+ }, _callee12);
636
+ })));
637
+ it('does not duplicate existing links when 409 recovery refetches them', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
638
+ var conflict, mockCreateSharingLink, mockFindLinksByIds, mockClient, provider, resp, permissions;
639
+ return _regeneratorRuntime.wrap(function _callee13$(_context13) {
640
+ while (1) switch (_context13.prev = _context13.next) {
641
+ case 0:
642
+ conflict = Object.assign(new Error('Conflict'), {
643
+ status: 409
644
+ });
645
+ mockCreateSharingLink = jest.fn().mockRejectedValue(conflict);
646
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
647
+ data: [PERM_DRIVE_FILE]
648
+ });
649
+ mockClient = createMockClient({});
650
+ mockClient.collection = jest.fn().mockReturnValue({
651
+ createSharingLink: mockCreateSharingLink,
652
+ findLinksByIds: mockFindLinksByIds
653
+ });
654
+ provider = new SharingProvider({
655
+ client: mockClient
656
+ });
657
+ provider.state = reducer(undefined, addSharingLink(PERM_DRIVE_FILE));
658
+ provider.dispatch = jest.fn(function (action) {
659
+ provider.state = reducer(provider.state, action);
660
+ });
661
+ _context13.next = 10;
662
+ return provider.shareByLink(driveFile, {
663
+ verbs: ['GET']
664
+ });
665
+
666
+ case 10:
667
+ resp = _context13.sent;
668
+ permissions = getDocumentPermissions(provider.state, 'file_in_drive');
669
+ expect(resp.data.id).toBe(PERM_DRIVE_FILE.id);
670
+ expect(provider.dispatch).not.toHaveBeenCalled();
671
+ expect(permissions).toHaveLength(1);
672
+
673
+ case 15:
674
+ case "end":
675
+ return _context13.stop();
676
+ }
677
+ }, _callee13);
678
+ })));
679
+ it('rethrows the original error when 409 happens and no link is found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
680
+ var conflict, mockCreateSharingLink, mockFindLinksByIds, mockClient, provider;
681
+ return _regeneratorRuntime.wrap(function _callee14$(_context14) {
682
+ while (1) switch (_context14.prev = _context14.next) {
683
+ case 0:
684
+ conflict = Object.assign(new Error('Conflict'), {
685
+ status: 409
686
+ });
687
+ mockCreateSharingLink = jest.fn().mockRejectedValue(conflict);
688
+ mockFindLinksByIds = jest.fn().mockResolvedValue({
689
+ data: []
690
+ });
691
+ mockClient = createMockClient({});
692
+ mockClient.collection = jest.fn().mockReturnValue({
693
+ createSharingLink: mockCreateSharingLink,
694
+ findLinksByIds: mockFindLinksByIds
695
+ });
696
+ provider = new SharingProvider({
697
+ client: mockClient
698
+ });
699
+ provider.state = reducer();
700
+ provider.dispatch = jest.fn();
701
+ _context14.next = 10;
702
+ return expect(provider.shareByLink(driveFile, {
703
+ verbs: ['GET']
704
+ })).rejects.toBe(conflict);
705
+
706
+ case 10:
707
+ case "end":
708
+ return _context14.stop();
709
+ }
710
+ }, _callee14);
711
+ })));
712
+ });
414
713
  describe('updateSharingMemberType', function () {
415
714
  var mockSharing = {
416
715
  id: 'sharing-123',
@@ -447,37 +746,37 @@ describe('updateSharingMemberType', function () {
447
746
  };
448
747
  jest.spyOn(instance, 'setState');
449
748
  });
450
- it('should throw when sharing is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8() {
451
- return _regeneratorRuntime.wrap(function _callee8$(_context8) {
452
- while (1) switch (_context8.prev = _context8.next) {
749
+ it('should throw when sharing is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
750
+ return _regeneratorRuntime.wrap(function _callee15$(_context15) {
751
+ while (1) switch (_context15.prev = _context15.next) {
453
752
  case 0:
454
- _context8.next = 2;
753
+ _context15.next = 2;
455
754
  return expect(instance.updateSharingMemberType('unknown-id', 1, 'one-way')).rejects.toThrow('Sharing not found');
456
755
 
457
756
  case 2:
458
757
  case "end":
459
- return _context8.stop();
758
+ return _context15.stop();
460
759
  }
461
- }, _callee8);
760
+ }, _callee15);
462
761
  })));
463
- it('should throw when member is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee9() {
464
- return _regeneratorRuntime.wrap(function _callee9$(_context9) {
465
- while (1) switch (_context9.prev = _context9.next) {
762
+ it('should throw when member is not found', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee16() {
763
+ return _regeneratorRuntime.wrap(function _callee16$(_context16) {
764
+ while (1) switch (_context16.prev = _context16.next) {
466
765
  case 0:
467
- _context9.next = 2;
766
+ _context16.next = 2;
468
767
  return expect(instance.updateSharingMemberType('sharing-123', 99, 'one-way')).rejects.toThrow('Member not found');
469
768
 
470
769
  case 2:
471
770
  case "end":
472
- return _context9.stop();
771
+ return _context16.stop();
473
772
  }
474
- }, _callee9);
773
+ }, _callee16);
475
774
  })));
476
- it('should call setReadOnly when switching to one-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10() {
477
- return _regeneratorRuntime.wrap(function _callee10$(_context10) {
478
- while (1) switch (_context10.prev = _context10.next) {
775
+ it('should call setReadOnly when switching to one-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee17() {
776
+ return _regeneratorRuntime.wrap(function _callee17$(_context17) {
777
+ while (1) switch (_context17.prev = _context17.next) {
479
778
  case 0:
480
- _context10.next = 2;
779
+ _context17.next = 2;
481
780
  return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
482
781
 
483
782
  case 2:
@@ -486,15 +785,15 @@ describe('updateSharingMemberType', function () {
486
785
 
487
786
  case 4:
488
787
  case "end":
489
- return _context10.stop();
788
+ return _context17.stop();
490
789
  }
491
- }, _callee10);
790
+ }, _callee17);
492
791
  })));
493
- it('should call setReadWrite when switching to two-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11() {
494
- return _regeneratorRuntime.wrap(function _callee11$(_context11) {
495
- while (1) switch (_context11.prev = _context11.next) {
792
+ it('should call setReadWrite when switching to two-way', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee18() {
793
+ return _regeneratorRuntime.wrap(function _callee18$(_context18) {
794
+ while (1) switch (_context18.prev = _context18.next) {
496
795
  case 0:
497
- _context11.next = 2;
796
+ _context18.next = 2;
498
797
  return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
499
798
 
500
799
  case 2:
@@ -503,15 +802,15 @@ describe('updateSharingMemberType', function () {
503
802
 
504
803
  case 4:
505
804
  case "end":
506
- return _context11.stop();
805
+ return _context18.stop();
507
806
  }
508
- }, _callee11);
807
+ }, _callee18);
509
808
  })));
510
- it('should not dispatch a state update — realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee12() {
511
- return _regeneratorRuntime.wrap(function _callee12$(_context12) {
512
- while (1) switch (_context12.prev = _context12.next) {
809
+ it('should not dispatch a state update — realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee19() {
810
+ return _regeneratorRuntime.wrap(function _callee19$(_context19) {
811
+ while (1) switch (_context19.prev = _context19.next) {
513
812
  case 0:
514
- _context12.next = 2;
813
+ _context19.next = 2;
515
814
  return instance.updateSharingMemberType('sharing-123', 1, 'one-way');
516
815
 
517
816
  case 2:
@@ -519,15 +818,15 @@ describe('updateSharingMemberType', function () {
519
818
 
520
819
  case 3:
521
820
  case "end":
522
- return _context12.stop();
821
+ return _context19.stop();
523
822
  }
524
- }, _callee12);
823
+ }, _callee19);
525
824
  })));
526
- it('should not dispatch a state update for two-way — realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
527
- return _regeneratorRuntime.wrap(function _callee13$(_context13) {
528
- while (1) switch (_context13.prev = _context13.next) {
825
+ it('should not dispatch a state update for two-way — realtime refreshes the store', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee20() {
826
+ return _regeneratorRuntime.wrap(function _callee20$(_context20) {
827
+ while (1) switch (_context20.prev = _context20.next) {
529
828
  case 0:
530
- _context13.next = 2;
829
+ _context20.next = 2;
531
830
  return instance.updateSharingMemberType('sharing-123', 1, 'two-way');
532
831
 
533
832
  case 2:
@@ -535,44 +834,44 @@ describe('updateSharingMemberType', function () {
535
834
 
536
835
  case 3:
537
836
  case "end":
538
- return _context13.stop();
837
+ return _context20.stop();
539
838
  }
540
- }, _callee13);
839
+ }, _callee20);
541
840
  })));
542
- it('should rethrow error if setReadOnly fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
543
- return _regeneratorRuntime.wrap(function _callee14$(_context14) {
544
- while (1) switch (_context14.prev = _context14.next) {
841
+ it('should rethrow error if setReadOnly fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee21() {
842
+ return _regeneratorRuntime.wrap(function _callee21$(_context21) {
843
+ while (1) switch (_context21.prev = _context21.next) {
545
844
  case 0:
546
845
  instance.sharingCol.setReadOnly.mockRejectedValue(new Error('Network error'));
547
- _context14.next = 3;
846
+ _context21.next = 3;
548
847
  return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
549
848
 
550
849
  case 3:
551
850
  case "end":
552
- return _context14.stop();
851
+ return _context21.stop();
553
852
  }
554
- }, _callee14);
853
+ }, _callee21);
555
854
  })));
556
- it('should rethrow error if setReadWrite fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
557
- return _regeneratorRuntime.wrap(function _callee15$(_context15) {
558
- while (1) switch (_context15.prev = _context15.next) {
855
+ it('should rethrow error if setReadWrite fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee22() {
856
+ return _regeneratorRuntime.wrap(function _callee22$(_context22) {
857
+ while (1) switch (_context22.prev = _context22.next) {
559
858
  case 0:
560
859
  instance.sharingCol.setReadWrite.mockRejectedValue(new Error('Network error'));
561
- _context15.next = 3;
860
+ _context22.next = 3;
562
861
  return expect(instance.updateSharingMemberType('sharing-123', 1, 'two-way')).rejects.toThrow('Network error');
563
862
 
564
863
  case 3:
565
864
  case "end":
566
- return _context15.stop();
865
+ return _context22.stop();
567
866
  }
568
- }, _callee15);
867
+ }, _callee22);
569
868
  })));
570
- it('should not dispatch state update if API call fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee16() {
571
- return _regeneratorRuntime.wrap(function _callee16$(_context16) {
572
- while (1) switch (_context16.prev = _context16.next) {
869
+ it('should not dispatch state update if API call fails', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee23() {
870
+ return _regeneratorRuntime.wrap(function _callee23$(_context23) {
871
+ while (1) switch (_context23.prev = _context23.next) {
573
872
  case 0:
574
873
  instance.sharingCol.setReadOnly.mockRejectedValue(new Error('Network error'));
575
- _context16.next = 3;
874
+ _context23.next = 3;
576
875
  return expect(instance.updateSharingMemberType('sharing-123', 1, 'one-way')).rejects.toThrow('Network error');
577
876
 
578
877
  case 3:
@@ -580,9 +879,9 @@ describe('updateSharingMemberType', function () {
580
879
 
581
880
  case 4:
582
881
  case "end":
583
- return _context16.stop();
882
+ return _context23.stop();
584
883
  }
585
- }, _callee16);
884
+ }, _callee23);
586
885
  })));
587
886
  }); // TODO Convert with react-testing-library
588
887
  // describe('hasWriteAccess', () => {
@@ -1,4 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
3
  import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
4
  var _excluded = ["recipient"];
4
5
 
@@ -6,7 +7,7 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
6
7
 
7
8
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
8
9
 
9
- import React from 'react';
10
+ import React, { useState } from 'react';
10
11
  import { useClient } from 'cozy-client';
11
12
  import Avatar from 'cozy-ui/transpiled/react/Avatar';
12
13
  import logger from '../../logger';
@@ -16,11 +17,19 @@ var MemberAvatar = function MemberAvatar(_ref) {
16
17
  var recipient = _ref.recipient,
17
18
  rest = _objectWithoutProperties(_ref, _excluded);
18
19
 
19
- var client = useClient(); // There are cases when due to apparent memory leaks, the recipient does not exist
20
+ var client = useClient(); // Remember which image url failed to load so we can fall back to the
21
+ // initials. Storing the url (instead of a boolean) means a new url is
22
+ // retried automatically, without an effect to reset the error state.
23
+
24
+ var _useState = useState(null),
25
+ _useState2 = _slicedToArray(_useState, 2),
26
+ erroredSrc = _useState2[0],
27
+ setErroredSrc = _useState2[1]; // There are cases when due to apparent memory leaks, the recipient does not exist
20
28
  // This will trigger an unhanded error in the Avatar component and crash the app
21
29
  // At the moment, we are not sure where is the root cause of this cascading undefined props
22
30
  // Nevertheless, we can prevent the crash by returning null if the recipient is undefined
23
31
 
32
+
24
33
  if (!recipient) {
25
34
  // eslint-disable-next-line
26
35
  logger.warn('RecipientAvatar: recipient is missing, see props:', _objectSpread({
@@ -40,11 +49,18 @@ var MemberAvatar = function MemberAvatar(_ref) {
40
49
  */
41
50
 
42
51
 
43
- var image = recipient.avatarPath && recipient.status ? "".concat(client.options.uri).concat(recipient.avatarPath, "?v=").concat(recipient.status) : null;
44
- return /*#__PURE__*/React.createElement(Avatar, rest, image ? /*#__PURE__*/React.createElement("img", {
52
+ var image = recipient.avatarPath && recipient.status ? "".concat(client.options.uri).concat(recipient.avatarPath).concat(recipient.avatarPath.includes('?') ? '&' : '?', "v=").concat(recipient.status) : null; // The avatar can become unreachable (eg. the related sharing is revoked
53
+ // while its members are still rendered), in which case the image request
54
+ // fails. Without a fallback the browser shows a broken-image icon, so we
55
+ // render the initials instead.
56
+
57
+ return /*#__PURE__*/React.createElement(Avatar, rest, image && erroredSrc !== image ? /*#__PURE__*/React.createElement("img", {
45
58
  width: "100%",
46
59
  height: "100%",
47
- src: image
60
+ src: image,
61
+ onError: function onError() {
62
+ return setErroredSrc(image);
63
+ }
48
64
  }) : getInitials(recipient));
49
65
  };
50
66
 
@@ -0,0 +1,77 @@
1
+ import { render, fireEvent } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { createMockClient } from 'cozy-client';
4
+ import { MemberAvatar } from './MemberAvatar';
5
+ import AppLike from '../../../test/AppLike';
6
+ describe('MemberAvatar', function () {
7
+ var client = createMockClient({});
8
+ client.options = {
9
+ uri: 'http://cozy.example.com'
10
+ };
11
+
12
+ var setup = function setup(recipient) {
13
+ return render( /*#__PURE__*/React.createElement(AppLike, {
14
+ client: client
15
+ }, /*#__PURE__*/React.createElement(MemberAvatar, {
16
+ recipient: recipient
17
+ })));
18
+ };
19
+
20
+ it('renders nothing when the recipient is missing', function () {
21
+ var _setup = setup(undefined),
22
+ container = _setup.container; // The component returns null; only the provider wrappers remain, with no
23
+ // avatar rendered inside.
24
+
25
+
26
+ expect(container.querySelector('[class*="MuiAvatar-root"]')).toBeNull();
27
+ expect(container.querySelector('img')).toBeNull();
28
+ });
29
+ it('renders the avatar image when the recipient has an avatarPath and a status', function () {
30
+ var _setup2 = setup({
31
+ public_name: 'Bob',
32
+ status: 'owner',
33
+ avatarPath: '/sharings/123/recipients/0/avatar'
34
+ }),
35
+ container = _setup2.container;
36
+
37
+ var img = container.querySelector('img');
38
+ expect(img).toBeTruthy();
39
+ expect(img.getAttribute('src')).toBe('http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner');
40
+ });
41
+ it('appends the cache-busting param with & when avatarPath already has a query string', function () {
42
+ var _setup3 = setup({
43
+ public_name: 'Bob',
44
+ status: 'owner',
45
+ avatarPath: '/public/avatar?fallback=initials'
46
+ }),
47
+ container = _setup3.container;
48
+
49
+ expect(container.querySelector('img').getAttribute('src')).toBe('http://cozy.example.com/public/avatar?fallback=initials&v=owner');
50
+ });
51
+ it('renders the initials when there is no avatarPath', function () {
52
+ var _setup4 = setup({
53
+ public_name: 'Bob',
54
+ status: 'owner'
55
+ }),
56
+ container = _setup4.container,
57
+ getByText = _setup4.getByText;
58
+
59
+ expect(container.querySelector('img')).toBeNull();
60
+ expect(getByText('B')).toBeTruthy();
61
+ });
62
+ it('falls back to the initials when the avatar image fails to load', function () {
63
+ var _setup5 = setup({
64
+ public_name: 'Bob',
65
+ status: 'owner',
66
+ avatarPath: '/sharings/123/recipients/0/avatar'
67
+ }),
68
+ container = _setup5.container,
69
+ getByText = _setup5.getByText;
70
+
71
+ var img = container.querySelector('img');
72
+ expect(img).toBeTruthy();
73
+ fireEvent.error(img);
74
+ expect(container.querySelector('img')).toBeNull();
75
+ expect(getByText('B')).toBeTruthy();
76
+ });
77
+ });