Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions src/app/core/resolves/lean-folder-resolve.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { TestBed } from '@angular/core/testing';
import * as Testing from '@root/test/testbedConfig';
import { cloneDeep } from 'lodash';
import { Router } from '@angular/router';

import { LeanFolderResolveService } from '@core/resolves/lean-folder-resolve.service';
import { ApiService } from '@shared/services/api/api.service';
import { AccountService } from '@shared/services/account/account.service';
import { FolderResponse } from '@shared/services/api/folder.repo';
import { FolderVO } from '@models/index';
import {
MessageDisplayOptions,
MessageService,
} from '@shared/services/message/message.service';

const buildFolderResponse = (folderData: Record<string, unknown>) =>
new FolderResponse({
isSuccessful: true,
Results: [{ data: [{ FolderVO: { ChildItemVOs: [], ...folderData } }] }],
});

describe('LeanFolderResolveService', () => {
let service: LeanFolderResolveService;
let api: ApiService;
let accountService: AccountService;
let message: MessageService;
let router: Router;

beforeEach(() => {
const config = cloneDeep(Testing.BASE_TEST_CONFIG);
config.providers.push(LeanFolderResolveService);
TestBed.configureTestingModule(config);

service = TestBed.inject(LeanFolderResolveService);
api = TestBed.inject(ApiService);
accountService = TestBed.inject(AccountService);
message = TestBed.inject(MessageService);
router = TestBed.inject(Router);

spyOn(accountService, 'getRootFolder').and.returnValue(
new FolderVO({
ChildItemVOs: [
new FolderVO({
folderId: '11',
type: 'type.folder.root.private',
archiveNbr: '0001-0001',
}),
new FolderVO({
folderId: '22',
type: 'type.folder.root.app',
archiveNbr: '0001-0002',
}),
],
}),
);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should load My Files by default', async () => {
const getSpy = spyOn(
api.folder,
'getWithChildrenByIdentifier',
).and.resolveTo(buildFolderResponse({ displayName: 'My Files' }));

const result = await service.resolve(
{ params: {} } as any,
{ url: '/private' } as any,
);

expect(getSpy).toHaveBeenCalled();
expect(getSpy.calls.mostRecent().args[0].folderId).toBe('11');
expect(result.displayName).toBe('My Files');
});

it('should load the apps folder on /apps', async () => {
const getSpy = spyOn(
api.folder,
'getWithChildrenByIdentifier',
).and.resolveTo(buildFolderResponse({ displayName: 'Apps' }));

await service.resolve({ params: {} } as any, { url: '/apps' } as any);

expect(getSpy.calls.mostRecent().args[0].folderId).toBe('22');
});

it('should pass the route identifiers through for a deep link', async () => {
const getSpy = spyOn(
api.folder,
'getWithChildrenByIdentifier',
).and.resolveTo(buildFolderResponse({ displayName: 'Deep Linked' }));

const result = await service.resolve(
{ params: { archiveNbr: '0001-0005', folderLinkId: '99' } } as any,
{ url: '/view/timeline/0001-0005/99' } as any,
);

const requestedFolder = getSpy.calls.mostRecent().args[0];

expect(requestedFolder.archiveNbr).toBe('0001-0005');
expect(requestedFolder.folder_linkId).toBe('99' as any);
expect(requestedFolder.folderId).toBeUndefined();
expect(result.displayName).toBe('Deep Linked');
});

it('should splice share crumbs onto a shared record without calling the API', async () => {
const getSpy = spyOn(api.folder, 'getWithChildrenByIdentifier');
const sharedRecord = { displayName: 'A shared photo' };

const result = await service.resolve(
{
params: {},
parent: {
data: {
sharePreviewVO: { FolderVO: null, RecordVO: sharedRecord },
currentFolder: new FolderVO({
pathAsText: ['My Files'],
pathAsArchiveNbr: ['0001-0001'],
pathAsFolder_linkId: [11],
}),
},
},
} as any,
{ url: '/share/abc123/view/timeline' } as any,
);

expect(getSpy).not.toHaveBeenCalled();
expect(result.pathAsText).toEqual(['Shares', 'Record', 'My Files']);
expect(result.pathAsArchiveNbr).toEqual([
'0000-0000',
'0000-0000',
'0001-0001',
]);

expect(result.pathAsFolder_linkId).toEqual([0, 0, 11]);
expect(result.ChildItemVOs).toEqual([sharedRecord] as any);
});

it('should surface the server message when the load fails', async () => {
spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith(
new FolderResponse({
isSuccessful: false,
Results: [{ message: ['Test Error'] }],
}),
);
spyOn(accountService, 'logOut').and.resolveTo(null);
spyOn(router, 'navigate');
let displayedErrorMessage: string;
spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => {
displayedErrorMessage = data.message;
});

await expectAsync(
service.resolve({ params: {} } as any, { url: '/private' } as any),
).toBeRejected();

expect(displayedErrorMessage).toBe('Test Error');
});

it('should log out when a root folder fails to load', async () => {
spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith(
new Error('Network down'),
);
const logOutSpy = spyOn(accountService, 'logOut').and.resolveTo(null);
spyOn(router, 'navigate');
spyOn(message, 'showError');

await expectAsync(
service.resolve({ params: {} } as any, { url: '/private' } as any),
).toBeRejected();

expect(logOutSpy).toHaveBeenCalled();
});

it('should fall back to a generic message for a raw error', async () => {
spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith(
new Error('Network down'),
);
spyOn(accountService, 'logOut').and.resolveTo(null);
spyOn(router, 'navigate');
let displayedErrorMessage: string;
spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => {
displayedErrorMessage = data.message;
});

await expectAsync(
service.resolve({ params: {} } as any, { url: '/private' } as any),
).toBeRejected();

expect(displayedErrorMessage).toBe('error.generic.internal');
});

it('should redirect rather than throw when a deep link fails', async () => {
spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith(
new Error('Network down'),
);
const navigateSpy = spyOn(router, 'navigate');
spyOn(message, 'showError');

await expectAsync(
service.resolve(
{ params: { archiveNbr: '0001-0005', folderLinkId: '99' } } as any,
{ url: '/view/timeline/0001-0005/99' } as any,
),
).toBeRejectedWith(false);

expect(navigateSpy).toHaveBeenCalledWith(['/private']);
});
});
69 changes: 32 additions & 37 deletions src/app/core/resolves/lean-folder-resolve.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import {
RouterStateSnapshot,
Router,
} from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { find, cloneDeep } from 'lodash';
import { ApiService } from '@shared/services/api/api.service';
import { AccountService } from '@shared/services/account/account.service';
import { MessageService } from '@shared/services/message/message.service';

import { FolderResponse } from '@shared/services/api/index.repo';
import { getFolderErrorMessage } from '@shared/utilities/folder-error-message';

import { FolderVO } from '@root/app/models';

Expand All @@ -24,10 +22,10 @@ export class LeanFolderResolveService {
private router: Router,
) {}

resolve(
async resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<any> | Promise<any> {
): Promise<any> {
let targetFolder;

if (route.params.archiveNbr && route.params.folderLinkId) {
Expand All @@ -51,7 +49,7 @@ export class LeanFolderResolveService {
folder.pathAsText.unshift('Shares', 'Record');
folder.pathAsFolder_linkId.unshift(0, 0);
folder.ChildItemVOs = [sharedRecord];
return Promise.resolve(folder);
return folder;
}
} else {
const myFiles = find(this.accountService.getRootFolder().ChildItemVOs, {
Expand All @@ -60,38 +58,35 @@ export class LeanFolderResolveService {
targetFolder = new FolderVO(myFiles);
}

return this.api.folder
.navigateLean(targetFolder)
.pipe(
map((response: FolderResponse) => {
if (!response.isSuccessful) {
throw response;
}
try {
const folderResponse =
await this.api.folder.getWithChildrenByIdentifier(targetFolder);

return response.getFolderVO(true);
}),
)
.toPromise()
.catch(async (response: FolderResponse) => {
this.message.showError({
message: response.getMessage(),
translate: true,
});
if (targetFolder.type.includes('root')) {
this.accountService
.logOut()
.then(() => {
this.router.navigate(['/login']);
})
.catch(() => {
this.router.navigate(['/login']);
});
} else if (state.url.includes('apps')) {
this.router.navigate(['/apps']);
} else {
this.router.navigate(['/private']);
}
return await Promise.reject(false);
if (!folderResponse.isSuccessful) {
throw folderResponse;
}

return folderResponse.getFolderVO(true);
} catch (error) {
this.message.showError({
message: getFolderErrorMessage(error),
translate: true,
});
if (targetFolder.type?.includes('root')) {
this.accountService
.logOut()
.then(() => {
this.router.navigate(['/login']);
})
.catch(() => {
this.router.navigate(['/login']);
});
} else if (state.url.includes('apps')) {
this.router.navigate(['/apps']);
} else {
this.router.navigate(['/private']);
}
return await Promise.reject(false);
}
}
}
69 changes: 69 additions & 0 deletions src/app/models/access-role.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE,
getAccessRoleFromArchiveMembershipRole,
getAccessAsEnum,
type ArchiveMembershipRoleType,
} from './access-role';

describe('getAccessRoleFromArchiveMembershipRole', () => {
const expectedTranslations: Array<[ArchiveMembershipRoleType, string]> = [
['contributor', 'access.role.contributor'],
['curator', 'access.role.curator'],
['editor', 'access.role.editor'],
['manager', 'access.role.manager'],
['owner', 'access.role.owner'],
['viewer', 'access.role.viewer'],
];

expectedTranslations.forEach(
([archiveMembershipRole, expectedAccessRole]) => {
it(`should translate ${archiveMembershipRole}`, () => {
expect(
getAccessRoleFromArchiveMembershipRole(archiveMembershipRole),
).toBe(expectedAccessRole);
});
},
);

it('should cover every role Stela can send', () => {
expect(Object.keys(ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE).length).toBe(
expectedTranslations.length,
);
});

it('should produce roles that getAccessAsEnum understands', () => {
expectedTranslations.forEach(([archiveMembershipRole]) => {
const accessRole = getAccessRoleFromArchiveMembershipRole(
archiveMembershipRole,
);

expect(getAccessAsEnum(accessRole)).toBeDefined();
});
});

it('should translate manager to manager, not the curator that PERMISSIONS_LEVEL_TO_ACCESS_ROLE maps it to', () => {
expect(getAccessRoleFromArchiveMembershipRole('manager')).toBe(
'access.role.manager',
);
});

it('should return undefined rather than guess at an unknown role', () => {
expect(
getAccessRoleFromArchiveMembershipRole(
'wizard' as ArchiveMembershipRoleType,
),
).toBeUndefined();
});

it('should return undefined when no role is given', () => {
expect(getAccessRoleFromArchiveMembershipRole(undefined)).toBeUndefined();
});

it('should return undefined for an inherited object property name', () => {
expect(
getAccessRoleFromArchiveMembershipRole(
'toString' as ArchiveMembershipRoleType,
),
).toBeUndefined();
});
});
Loading
Loading