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
241 changes: 200 additions & 41 deletions src/app/file-browser/components/publish/publish.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,89 @@ 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 { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the migration to getWithChildren or was it a "we found this issue while here" addition?

If the latter, let's make it a separate commit.

import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog';
import { Router } from '@angular/router';
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 PUBLIC_ROOT = new FolderVO({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this object magical / meaningful / must have these specific values?

If not, let's call this MOCK_PUBLIC_ROOT to make it clear that these are mocked values, and maybe even have the folderId clearly mocked (e.g. 123456) so that there's no confusion.

folderId: '140683',
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: {
...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: [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 +101,15 @@ describe('PublishComponent', () => {
EventService,
{
provide: MessageService,
useValue: {
showError: () => {},
},
useValue: { showError: showErrorSpy },
},
{
provide: Router,
useValue: {
navigate: () => {},
},
useValue: { navigate: () => {} },
},
{
provide: GoogleAnalyticsService,
useValue: { sendEvent: () => {} },
},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
Expand All @@ -95,6 +125,10 @@ describe('PublishComponent', () => {
});

it('should disaple 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 +139,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([
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 @@ -288,6 +288,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 @@ -138,6 +138,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