Skip to content
Merged
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);
expect(requestedFolder.folderId).toBeUndefined();
Comment thread
aasandei-vsp marked this conversation as resolved.
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']);
});
});
72 changes: 34 additions & 38 deletions src/app/core/resolves/lean-folder-resolve.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ 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';
import { toFolderLinkId } from '@shared/services/api/folder.repo';

@Injectable()
export class LeanFolderResolveService {
Expand All @@ -24,16 +23,16 @@ 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) {
targetFolder = new FolderVO({
archiveNbr: route.params.archiveNbr,
folder_linkId: route.params.folderLinkId,
folder_linkId: toFolderLinkId(route.params.folderLinkId),
});
} else if (state.url === '/apps') {
const apps = find(this.accountService.getRootFolder().ChildItemVOs, {
Expand All @@ -51,7 +50,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 +59,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);
}
}
}
15 changes: 15 additions & 0 deletions src/app/models/access-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ export function getAccessRoleFromArchiveMembershipRole(
return ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE[archiveMembershipRole];
}

/**
* Spread into VO data so that a role Stela did not send -- or one we cannot
* translate -- leaves no accessRole field behind at all, letting permission
* checks keep using the role the v1 endpoints supplied.
*/
export function getOptionalAccessRoleField(
archiveMembershipRole: ArchiveMembershipRoleType | undefined,
): { accessRole?: AccessRoleType } {
const accessRole = getAccessRoleFromArchiveMembershipRole(
archiveMembershipRole,
);

return accessRole ? { accessRole } : {};
}

// Mapping for share link permissions. Note the stela share link API
// mistakenly returns "manager" where it should use "curator" -- see
// https://github.com/PermanentOrg/stela/issues/540
Expand Down
Loading
Loading