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
238 changes: 196 additions & 42 deletions src/app/file-browser/components/publish/publish.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountService } from '@shared/services/account/account.service';
import { FolderVO, RecordVO } from '@models/index';
import { FolderResponse } from '@shared/services/api/folder.repo';
import { Observable } from 'rxjs';
import { MessageService } from '@shared/services/message/message.service';
import { EventService } from '@shared/services/event/event.service';
import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog';
Expand All @@ -12,50 +11,80 @@ import { ArchiveVO } from '../../../models/archive-vo';
import { ApiService } from '../../../shared/services/api/api.service';
import { PublishComponent } from './publish.component';

const mockAccountService = {
getArchive: () => {
const archive = new ArchiveVO({ accessRole: 'access.role.viewer' });
return archive;
},
getRootFolder: () => ({
ChildItemVOs: [],
}),
refreshAccountDebounced: () => {},
};
const MOCK_PUBLIC_ROOT = new FolderVO({
folderId: '123456',
folder_linkId: 55,
archiveNbr: '0001-0000',
type: 'type.folder.root.public',
});

// Shaped the way getWithChildren returns it: a v1-style envelope whose child items
// are the converted Stela folders.
function publicRootResponseWithChildren(
children: Array<Record<string, unknown>>,
): FolderResponse {
return new FolderResponse({
isSuccessful: true,
isSystemUp: true,
Results: [
{
data: [
{
FolderVO: {
...MOCK_PUBLIC_ROOT,
ChildItemVOs: children,
},
},
],
},
],
});
}

class MockDialogRef {
close() {}
}

const mockApiService = {
folder: {
copy: async (
folderVOs: FolderVO[],
destination: FolderVO,
): Promise<FolderResponse> => await Promise.resolve(new FolderResponse({})),
navigateLean: (folder: FolderVO): Observable<FolderResponse> =>
new Observable<FolderResponse>(),
},
publish: {
getInternetArchiveLink: async () => ({
getPublishIaVO: () => null,
}),
publishToInternetArchive: async () => ({
getPublishIaVO: () => null,
}),
},
record: {
copy: async () => ({
getRecordVO: () => new RecordVO({}),
}),
},
};

describe('PublishComponent', () => {
let component: PublishComponent;
let fixture: ComponentFixture<PublishComponent>;
let mockApiService: any;
let mockAccountService: any;
let showErrorSpy: jasmine.Spy;

beforeEach(async () => {
mockAccountService = {
getArchive: () => new ArchiveVO({ accessRole: 'access.role.owner' }),
getRootFolder: () => ({ ChildItemVOs: [MOCK_PUBLIC_ROOT] }),
refreshAccountDebounced: () => {},
};

mockApiService = {
folder: {
copy: jasmine
.createSpy('copy')
.and.resolveTo(new FolderResponse({ isSuccessful: true })),
getWithChildren: jasmine
.createSpy('getWithChildren')
.and.resolveTo(publicRootResponseWithChildren([])),
},
publish: {
getInternetArchiveLink: async () => ({
getPublishIaVO: () => null,
}),
publishToInternetArchive: async () => ({
getPublishIaVO: () => null,
}),
},
record: {
copy: async () => ({
getRecordVO: () => new RecordVO({}),
}),
},
};

showErrorSpy = jasmine.createSpy('showError');

await TestBed.configureTestingModule({
declarations: [PublishComponent],
providers: [
Expand All @@ -71,15 +100,11 @@ describe('PublishComponent', () => {
EventService,
{
provide: MessageService,
useValue: {
showError: () => {},
},
useValue: { showError: showErrorSpy },
},
{
provide: Router,
useValue: {
navigate: () => {},
},
useValue: { navigate: () => {} },
},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
Expand All @@ -94,7 +119,11 @@ describe('PublishComponent', () => {
expect(component).toBeTruthy();
});

it('should disaple the public to internet archive button if the user does not have the correct access role', () => {
it('should disable the public to internet archive button if the user does not have the correct access role', () => {
mockAccountService.getArchive = () =>
new ArchiveVO({ accessRole: 'access.role.viewer' });
fixture = TestBed.createComponent(PublishComponent);
component = fixture.componentInstance;
component.publicItem = new RecordVO({ recordId: 1 });
component.publishIa = null;
component.publicLink = null;
Expand All @@ -105,4 +134,129 @@ describe('PublishComponent', () => {

expect(button.disabled).toBeTruthy();
});

describe('publishing a folder', () => {
beforeEach(() => {
component.sourceItem = new FolderVO({
folderId: '900',
archiveNbr: '0002-0001',
folder_linkId: 12,
displayName: 'Trip to Iceland',
type: 'type.folder.private',
});
});

it('should load the public root through getWithChildren', async () => {
mockApiService.folder.getWithChildren.and.resolveTo(
publicRootResponseWithChildren([
{
folderId: '901',
archiveNbr: '0001-0002',
folder_linkId: 71,
displayName: 'Trip to Iceland',
type: 'type.folder.public',
updatedDT: '2026-08-10T10:00:00Z',
ChildItemVOs: [],
},
]),
);

await component.publishItem();

expect(mockApiService.folder.getWithChildren).toHaveBeenCalledWith([
MOCK_PUBLIC_ROOT,
]);

expect(mockApiService.folder.getWithChildren).toHaveBeenCalledTimes(1);
});

it('should pick the most recently updated folder matching the source name', async () => {
mockApiService.folder.getWithChildren.and.resolveTo(
publicRootResponseWithChildren([
{
folderId: '901',
archiveNbr: '0001-0002',
folder_linkId: 71,
displayName: 'Trip to Iceland',
type: 'type.folder.public',
updatedDT: '2026-01-01T10:00:00Z',
ChildItemVOs: [],
},
{
folderId: '902',
archiveNbr: '0001-0003',
folder_linkId: 72,
displayName: 'Trip to Iceland',
type: 'type.folder.public',
updatedDT: '2026-08-10T10:00:00Z',
ChildItemVOs: [],
},
]),
);

await component.publishItem();

// The newer of the two copies. This asserts the selection given a mapped
// updatedDT; that the Stela conversion actually populates it is covered
// in folder.repo.spec.ts.
expect(component.publicItem.folder_linkId).toBe(72);
expect(component.publicLink).toContain('0001-0003');
});

it('should ignore child records when looking for the copy', async () => {
mockApiService.folder.getWithChildren.and.resolveTo(
publicRootResponseWithChildren([
{
recordId: '500',
archiveNbr: '0001-0009',
folder_linkId: 80,
displayName: 'Trip to Iceland',
updatedDT: '2026-08-10T12:00:00Z',
},
{
folderId: '901',
archiveNbr: '0001-0002',
folder_linkId: 71,
displayName: 'Trip to Iceland',
type: 'type.folder.public',
updatedDT: '2026-08-10T10:00:00Z',
ChildItemVOs: [],
},
]),
);

await component.publishItem();

expect(component.publicItem instanceof FolderVO).toBeTrue();
expect(component.publicItem.folder_linkId).toBe(71);
});

it('should surface a generic error when the request rejects without a message', async () => {
mockApiService.folder.getWithChildren.and.rejectWith(
new Error('500 from the server'),
);

await component.publishItem();

expect(showErrorSpy).toHaveBeenCalledWith({
message: 'error.generic.internal',
translate: true,
});

expect(component.waiting).toBeFalse();
});

it('should keep showing the server message when one is available', async () => {
mockApiService.folder.copy.and.rejectWith({
getMessage: () => 'warning.record.copy_status',
});

await component.publishItem();

expect(showErrorSpy).toHaveBeenCalledWith({
message:
'Sorry, this record cannot be copied or published until processing completes.',
});
});
});
});
15 changes: 11 additions & 4 deletions src/app/file-browser/components/publish/publish.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { PublicLinkPipe } from '@shared/pipes/public-link.pipe';
import { AccountService } from '@shared/services/account/account.service';
import { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service';
import { EVENTS } from '@shared/services/google-analytics/events';
import { FolderResponse } from '@shared/services/api/index.repo';
import { PublicRoutePipe } from '@shared/pipes/public-route.pipe';
import { Router } from '@angular/router';
import { PublishIaData } from '@models/publish-ia-vo';
Expand Down Expand Up @@ -80,9 +79,9 @@ export class PublishComponent {
let tries = 0;
while (!this.publicItem && tries < 10) {
tries += 1;
const publicRootResponse = (await this.api.folder
.navigateLean(publicRoot)
.toPromise()) as FolderResponse;
const publicRootResponse = await this.api.folder.getWithChildren([
publicRoot,
]);
const publicRootFull = publicRootResponse.getFolderVO(true);
const publicFolders: FolderVO[] = publicRootFull.ChildItemVOs.filter(
(i) => i instanceof FolderVO,
Expand Down Expand Up @@ -125,6 +124,14 @@ export class PublishComponent {
message: err.getMessage(),
});
}
} else {
// getWithChildren rejects with the raw HTTP error rather than a
// FolderResponse, so there is no server message to read. Without
// this branch the failure would be swallowed silently.
this.messageService.showError({
message: 'error.generic.internal',
translate: true,
});
}
} finally {
this.waiting = false;
Expand Down
44 changes: 44 additions & 0 deletions src/app/shared/services/api/folder.repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,50 @@ describe('Folder repo', () => {
});
});

describe('folder timestamps', () => {
it('should map createdAt and updatedAt onto the FolderVO', async () => {
const folderVO = new FolderVO({ folderId: 123 });

httpV2Spy.get.and.returnValue(of([{ items: [mockStelaFolder] }]));

const result = await folderRepo.getStelaFolderVOs([folderVO]);
const folder = result.getFolderVOs()[0];

expect(folder.createdDT).toBe('2024-01-01T00:00:00Z');
expect(folder.updatedDT).toBe('2024-01-02T00:00:00Z');
});

it('should map timestamps onto child folders too, so callers can pick the most recent one', async () => {
const olderChild = {
...mockStelaFolder,
folderId: '900',
displayName: 'Older',
updatedAt: '2024-03-01T00:00:00Z',
};
const newerChild = {
...mockStelaFolder,
folderId: '901',
displayName: 'Newer',
updatedAt: '2024-05-01T00:00:00Z',
};

httpV2Spy.get.and.returnValues(
of([{ items: [mockStelaFolder] }]),
of([{ items: [olderChild, newerChild] }]),
);

const result = await folderRepo.getWithChildren([
new FolderVO({ folderId: 123 }),
]);
const children = result.getFolderVO(true).ChildItemVOs;

expect(children.map((child) => child.updatedDT)).toEqual([
'2024-03-01T00:00:00Z',
'2024-05-01T00:00:00Z',
]);
});
});

describe('getStelaFolderVOs', () => {
it('should fetch single folder and return FolderResponse', async () => {
const folderVO = new FolderVO({ folderId: 123 });
Expand Down
5 changes: 5 additions & 0 deletions src/app/shared/services/api/folder.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ const convertStelaFolderToFolderVO = (stelaFolder: StelaFolder): FolderVO => {
displayTime: stelaFolder.displayTime,
derivedDT: stelaFolder.displayTimestamp,
derivedEndDT: stelaFolder.displayEndTimestamp,
// Stela names these createdAt / updatedAt. Records already map them; folders
// did not, so anything picking the most recently updated folder was comparing
// undefined values.
createdDT: stelaFolder.createdAt,
updatedDT: stelaFolder.updatedAt,
note: '',
description: stelaFolder.description,
sort: stelaFolder.sort,
Expand Down
Loading