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
10 changes: 10 additions & 0 deletions src/app/core/shared/bitstream.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {Bundle} from './bundle.model';
import { ChildHALResource } from './child-hal-resource.model';
import { BITSTREAM_CHECKSUM } from './bitstream-checksum.resource';
import { BitstreamChecksum } from './bitstream-checksum.model';
import { AccessStatusObject } from '../../shared/object-collection/shared/badges/access-status-badge/access-status.model';
import { ACCESS_STATUS } from '../../shared/object-collection/shared/badges/access-status-badge/access-status.resource-type';

// Store number if the bitstream is stored in the both stores (S3 and local)
export const SYNCHRONIZED_STORES_NUMBER = 77;
Expand Down Expand Up @@ -56,6 +58,7 @@ export class Bitstream extends DSpaceObject implements ChildHALResource {
content: HALLink;
thumbnail: HALLink;
checksum: HALLink;
accessStatus: HALLink;
};

/**
Expand Down Expand Up @@ -85,6 +88,13 @@ export class Bitstream extends DSpaceObject implements ChildHALResource {
@link(BITSTREAM_CHECKSUM)
checksum?: Observable<RemoteData<BitstreamChecksum>>;

/**
* The access status for this Bitstream
* Will be undefined unless the access status {@link HALLink} has been resolved.
*/
@link(ACCESS_STATUS)
accessStatus?: Observable<RemoteData<AccessStatusObject>>;

getParentLinkKey(): keyof this['_links'] {
return 'format';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<span *ngIf="!(canDownload$ |async)" class="pr-1"><i class="fas fa-lock"></i></span>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
<ds-themed-access-status-badge [object]="bitstream"></ds-themed-access-status-badge>

<ng-template #content>
<ng-content></ng-content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { FileDownloadLinkComponent } from './file-download-link.component';
import { Bitstream } from '../../core/shared/bitstream.model';
import { By } from '@angular/platform-browser';
Expand Down Expand Up @@ -42,7 +43,8 @@ describe('FileDownloadLinkComponent', () => {
declarations: [FileDownloadLinkComponent, RouterLinkDirectiveStub],
providers: [
{provide: AuthorizationDataService, useValue: authorizationService},
]
],
schemas: [NO_ERRORS_SCHEMA],
})
.compileComponents();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<ng-container *ngIf="showAccessStatus">
<span *ngIf="accessStatus$ | async as accessStatus">
<span [class]="'badge badge-secondary access-status-list-element-badge ' + accessStatusClass">{{ accessStatus | translate }}</span>
</span>
<ng-container *ngIf="accessStatus$ | async as accessStatus">
<span *ngIf="embargoDate$ | async as embargoDate; else noEmbargoDate"
[class]="'badge badge-secondary access-status-list-element-badge ' + accessStatusClass">{{ accessStatus | translate: { date: embargoDate } }}</span>
<ng-template #noEmbargoDate>
<span [class]="'badge badge-secondary access-status-list-element-badge ' + accessStatusClass">{{ accessStatus | translate }}</span>
</ng-template>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Item } from '../../../../../core/shared/item.model';
import { Bitstream } from '../../../../../core/shared/bitstream.model';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatePipe } from '../../../../utils/truncate.pipe';
Expand All @@ -7,10 +8,11 @@ import { AccessStatusBadgeComponent } from './access-status-badge.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
import { By } from '@angular/platform-browser';
import { AccessStatusObject } from './access-status.model';
import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service';
import { LinkService } from 'src/app/core/cache/builders/link.service';
import { environment } from 'src/environments/environment';
import { EMPTY } from 'rxjs';

describe('ItemAccessStatusBadgeComponent', () => {
describe('AccessStatusBadgeComponent', () => {
let component: AccessStatusBadgeComponent;
let fixture: ComponentFixture<AccessStatusBadgeComponent>;

Expand All @@ -20,9 +22,10 @@ describe('ItemAccessStatusBadgeComponent', () => {
let embargoStatus: AccessStatusObject;
let restrictedStatus: AccessStatusObject;

let accessStatusDataService: AccessStatusDataService;
let linkService: LinkService;

let item: Item;
let bitstream: Bitstream;

function init() {
unknownStatus = Object.assign(new AccessStatusObject(), {
Expand All @@ -38,20 +41,38 @@ describe('ItemAccessStatusBadgeComponent', () => {
});

embargoStatus = Object.assign(new AccessStatusObject(), {
status: 'embargo'
status: 'embargo',
embargoDate: '2050-01-01'
});

restrictedStatus = Object.assign(new AccessStatusObject(), {
status: 'restricted'
});

accessStatusDataService = jasmine.createSpyObj('accessStatusDataService', {
findAccessStatusFor: createSuccessfulRemoteDataObject$(unknownStatus)
linkService = jasmine.createSpyObj('linkService', ['resolveLink']);
// Mirror LinkService.resolveLink's real behavior for a missing + optional link:
// it synchronously attaches EMPTY to the model rather than leaving it undefined
// (and never touches _links.accessStatus directly - see link.service.ts).
(linkService.resolveLink as jasmine.Spy).and.callFake((model: any, linkToFollow: any) => {
model[linkToFollow.name] = EMPTY;
return model;
});

item = Object.assign(new Item(), {
uuid: 'item-uuid',
type: 'item'
type: 'item',
accessStatus: createSuccessfulRemoteDataObject$(unknownStatus)
});

// A bitstream as it looks BEFORE the backend exposes the accessStatus link
// (i.e. today, on this branch's target backend): no accessStatus key in
// _links, and the accessStatus property itself never populated.
bitstream = Object.assign(new Bitstream(), {
uuid: 'bitstream-uuid',
type: 'bitstream',
_links: {
self: { href: 'obj-selflink' }
}
});
}

Expand All @@ -61,18 +82,20 @@ describe('ItemAccessStatusBadgeComponent', () => {
declarations: [AccessStatusBadgeComponent, TruncatePipe],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{provide: AccessStatusDataService, useValue: accessStatusDataService}
{ provide: LinkService, useValue: linkService }
]
}).compileComponents();
}

function initFixtureAndComponent() {
function initFixtureAndComponent(object: Item | Bitstream) {
environment.item.showAccessStatuses = true;
environment.item.bitstream.showAccessStatuses = true;
fixture = TestBed.createComponent(AccessStatusBadgeComponent);
component = fixture.componentInstance;
component.object = item;
component.object = object;
fixture.detectChanges();
environment.item.showAccessStatuses = false;
environment.item.bitstream.showAccessStatuses = false;
}

function lookForAccessStatusBadge(status: string) {
Expand All @@ -86,79 +109,146 @@ describe('ItemAccessStatusBadgeComponent', () => {
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
initFixtureAndComponent(item);
});
it('should init the component', () => {
expect(component).toBeTruthy();
});
});

describe('When the findAccessStatusFor method returns unknown', () => {
beforeEach(waitForAsync(() => {
init();
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
});
it('should show the unknown badge', () => {
lookForAccessStatusBadge('unknown');
describe('for an Item', () => {
describe('when the access status is unknown', () => {
beforeEach(waitForAsync(() => {
init();
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(item);
});
it('should show the unknown badge', () => {
lookForAccessStatusBadge('unknown');
});
});

describe('when the access status is metadata.only', () => {
beforeEach(waitForAsync(() => {
init();
item.accessStatus = createSuccessfulRemoteDataObject$(metadataOnlyStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(item);
});
it('should show the metadata only badge', () => {
lookForAccessStatusBadge('metadata.only');
});
});

describe('when the access status is open.access', () => {
beforeEach(waitForAsync(() => {
init();
item.accessStatus = createSuccessfulRemoteDataObject$(openAccessStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(item);
});
it('should show the open access badge', () => {
lookForAccessStatusBadge('open.access');
});
});

describe('when the access status is embargo', () => {
beforeEach(waitForAsync(() => {
init();
item.accessStatus = createSuccessfulRemoteDataObject$(embargoStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(item);
});
it('should show the embargo badge', () => {
lookForAccessStatusBadge('embargo');
});
});

describe('when the access status is restricted', () => {
beforeEach(waitForAsync(() => {
init();
item.accessStatus = createSuccessfulRemoteDataObject$(restrictedStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(item);
});
it('should show the restricted badge', () => {
lookForAccessStatusBadge('restricted');
});
});
});

describe('When the findAccessStatusFor method returns metadata.only', () => {
beforeEach(waitForAsync(() => {
init();
(accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus));
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
});
it('should show the metadata only badge', () => {
lookForAccessStatusBadge('metadata.only');
});
});

describe('When the findAccessStatusFor method returns open.access', () => {
beforeEach(waitForAsync(() => {
init();
(accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus));
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
});
it('should show the open access badge', () => {
lookForAccessStatusBadge('open.access');
});
});

describe('When the findAccessStatusFor method returns embargo', () => {
beforeEach(waitForAsync(() => {
init();
(accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus));
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
});
it('should show the embargo badge', () => {
lookForAccessStatusBadge('embargo');
});
});

describe('When the findAccessStatusFor method returns restricted', () => {
beforeEach(waitForAsync(() => {
init();
(accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus));
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent();
});
it('should show the restricted badge', () => {
lookForAccessStatusBadge('restricted');
describe('for a Bitstream', () => {
describe('when the bitstream is embargoed', () => {
beforeEach(waitForAsync(() => {
init();
bitstream.accessStatus = createSuccessfulRemoteDataObject$(embargoStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(bitstream);
});
it('should show the embargo badge with the embargo date', () => {
const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge.nativeElement.textContent).toContain('embargo.listelement.badge');
});
});

describe('when the bitstream is open access (no embargo date)', () => {
beforeEach(waitForAsync(() => {
init();
bitstream.accessStatus = createSuccessfulRemoteDataObject$(openAccessStatus);
initTestBed();
}));
beforeEach(() => {
initFixtureAndComponent(bitstream);
});
it('should not show a badge', () => {
const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge).toBeNull();
});
});

describe('when the backend does not expose the accessStatus link yet (pre-DSpace#1377)', () => {
// Regression test: findBitstreamAccessStatusFor() used to read
// bitstream._links.accessStatus.href synchronously, which threw a
// TypeError (uncaught by any catchError, since it happened before the
// Observable pipe was even constructed) and broke the whole file-list
// render. The component must now fail closed instead: no crash, no
// badge, using a bitstream that has NO accessStatus link and NO
// pre-resolved accessStatus property, exactly like a real bitstream
// from this branch's current (pre-backend-PR) REST API.
beforeEach(waitForAsync(() => {
init();
initTestBed();
}));

it('should not throw when initializing with a bitstream lacking the accessStatus link', () => {
expect(() => initFixtureAndComponent(bitstream)).not.toThrow();
});

it('should render the file list without a badge', () => {
initFixtureAndComponent(bitstream);
const badge = fixture.debugElement.query(By.css('span.badge'));
expect(badge).toBeNull();
});

it('should ask the LinkService to resolve the link as optional', () => {
initFixtureAndComponent(bitstream);
expect(linkService.resolveLink).toHaveBeenCalledWith(
bitstream,
jasmine.objectContaining({ name: 'accessStatus', isOptional: true }),
);
});
});
});
});
Loading
Loading