diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.html b/src/app/item-page/access-by-token/item-access-by-token-page.component.html new file mode 100644 index 0000000000..a99f01858b --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.html @@ -0,0 +1,8 @@ +
+
+ + +
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.scss b/src/app/item-page/access-by-token/item-access-by-token-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts b/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts new file mode 100644 index 0000000000..e270b34669 --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.spec.ts @@ -0,0 +1,299 @@ +import { KeyValuePipe } from '@angular/common'; +import { PLATFORM_ID } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Store } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + of as observableOf, +} from 'rxjs'; + +import { getForbiddenRoute } from '../../app-routing-paths'; +import { AuthService } from '../../core/auth/auth.service'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { Item } from '../../core/shared/item.model'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { ItemAccessByTokenPageComponent } from './item-access-by-token-page.component'; +import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component'; + +describe('ItemAccessByTokenPageComponent', () => { + let component: ItemAccessByTokenPageComponent; + let fixture: ComponentFixture; + let itemRequestService: jasmine.SpyObj; + let router: jasmine.SpyObj; + let authorizationService: AuthorizationDataService; + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(false), + }); + let signpostingDataService: SignpostingDataService; + + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test', + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test', + }; + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink, mocklink2]), + }); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '', + }); + + const mockItem = Object.assign(new Item(), { + uuid: 'test-item-uuid', + id: 'test-item-id', + metadata: { + 'dspace.entity.type': [{ + value: 'Publication', + language: 'en', + place: 0, + authority: null, + confidence: -1, + }], + }, + _links: { + self: { href: 'obj-selflink' }, + }, + }); + + const mockItemRequest = Object.assign(new ItemRequest(), { + token: 'valid-token', + accessToken: 'valid-token', + itemId: mockItem.uuid, + }); + + const queryParams = { accessToken: 'valid-token' }; + const mockActivatedRoute = { + queryParams: new BehaviorSubject(queryParams), + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem), + }), + params: observableOf({ itemId: mockItem.uuid, queryParams: [ { accessToken: 'valid-token' } ] }), + children: [], + }; + itemRequestService = jasmine.createSpyObj('ItemRequestDataService', { + getSanitizedRequestByAccessToken: observableOf(createSuccessfulRemoteDataObject(mockItemRequest)), + }); + router = jasmine.createSpyObj('Router', ['navigateByUrl'], { + events: observableOf([]), + }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + KeyValuePipe, + ], + providers: [ + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: ItemDataService, useValue: {} }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ServerResponseService, useValue: {} }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } }, + { provide: PLATFORM_ID, useValue: 'browser' }, + KeyValuePipe, + { + provide: Store, + useValue: { + pipe: () => observableOf({}), + dispatch: () => { + }, + select: () => observableOf({}), + }, + }, + { + provide: AuthService, useValue: { + isAuthenticated: () => observableOf(true), + }, + }, + ], + }).overrideComponent(ItemAccessByTokenPageComponent, { + set: { + template: '
', + }, + }).overrideComponent(ItemAccessByTokenViewComponent, { + set: { + template: '
', + }, + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemAccessByTokenPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + }); + + /** + * Tests in this component are concerned only with successful access token processing (or error handling) + * and a resulting item request object. Testing of template elements is out of scope and left for child components. + */ + describe('ngOnInit - basic component testing', () => { + it('should find valid access token and sanitize it', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + KeyValuePipe, + RouterTestingModule.withRoutes([]), + ], + providers: [ + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: AuthService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ServerResponseService, useValue: {} }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } }, + { provide: PLATFORM_ID, useValue: 'browser' }, + KeyValuePipe, + { + provide: Store, + useValue: { + pipe: () => observableOf({}), + dispatch: () => {}, + select: () => observableOf({}), + }, + }, + { provide: AuthService, useValue: { + isAuthenticated: () => observableOf(false ) }, + }, + ], + }).overrideComponent(ItemAccessByTokenViewComponent, { + set: { template: '
' } } ).compileComponents(); + + fixture = TestBed.createComponent(ItemAccessByTokenPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(itemRequestService.getSanitizedRequestByAccessToken).toHaveBeenCalledWith('valid-token'); + + })); + + it('should process valid access token and load item request', fakeAsync(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + KeyValuePipe, + RouterTestingModule.withRoutes([]), + ], + providers: [ + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: AuthService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ServerResponseService, useValue: {} }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } }, + { provide: PLATFORM_ID, useValue: 'browser' }, + KeyValuePipe, + { + provide: Store, + useValue: { + pipe: () => observableOf({}), + dispatch: () => {}, + select: () => observableOf({}), + }, + }, + { provide: AuthService, useValue: { + isAuthenticated: () => observableOf(false ) }, + }, + ], + }).overrideComponent(ItemAccessByTokenViewComponent, { + set: { template: '
' } } ).compileComponents(); + + fixture = TestBed.createComponent(ItemAccessByTokenPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.itemRequest$.subscribe((request) => { + expect(request).toBeTruthy(); + }); + })); + + it('should redirect to forbidden route when access token is missing', fakeAsync(() => { + const routeWithoutToken = { + queryParams: observableOf({}), + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem), + }), + params: observableOf({ itemId: mockItem.uuid }), + children: [], + }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + KeyValuePipe, + ], + providers: [ + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: routeWithoutToken }, + { provide: AuthService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ServerResponseService, useValue: {} }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } }, + { provide: PLATFORM_ID, useValue: 'browser' }, + { + provide: Store, + useValue: { + pipe: () => observableOf({}), + dispatch: () => {}, + select: () => observableOf({}), + }, + }, + { provide: AuthService, useValue: { + isAuthenticated: () => observableOf(false ) }, + }, + ], + }).overrideComponent(ItemAccessByTokenViewComponent, { + set: { + template: '
', + } }) + .compileComponents(); + + fixture = TestBed.createComponent(ItemAccessByTokenPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: false }); + })); + }); +}); + diff --git a/src/app/item-page/access-by-token/item-access-by-token-page.component.ts b/src/app/item-page/access-by-token/item-access-by-token-page.component.ts new file mode 100644 index 0000000000..5250ac37a2 --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-page.component.ts @@ -0,0 +1,178 @@ +import { + AsyncPipe, + KeyValuePipe, + Location, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Inject, + OnDestroy, + OnInit, + PLATFORM_ID, +} from '@angular/core'; +import { + ActivatedRoute, + Router, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { + filter, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { getForbiddenRoute } from '../../app-routing-paths'; +import { AuthService } from '../../core/auth/auth.service'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../core/shared/operators'; +import { fadeInOut } from '../../shared/animations/fade'; +import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component'; +import { hasValue } from '../../shared/empty.util'; +import { ErrorComponent } from '../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; +import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component'; +import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component'; +import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; +import { CollectionsComponent } from '../field-components/collections/collections.component'; +import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component'; +import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component'; +import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component'; +import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component'; +import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component'; +import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component'; +import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component'; +import { ItemPageComponent } from '../simple/item-page.component'; +import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component'; +import { ItemVersionsComponent } from '../versions/item-versions.component'; +import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component'; +import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component'; +import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component'; + +@Component({ + selector: 'ds-access-by-token-item-page', + styleUrls: ['./item-access-by-token-page.component.scss'], + templateUrl: './item-access-by-token-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [fadeInOut], + standalone: true, + imports: [ + ErrorComponent, + ThemedLoadingComponent, + TranslateModule, + ThemedFullFileSectionComponent, + CollectionsComponent, + ItemVersionsComponent, + NgIf, + NgForOf, + AsyncPipe, + KeyValuePipe, + RouterLink, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + ItemVersionsNoticeComponent, + ViewTrackerComponent, + ThemedItemAlertsComponent, + VarDirective, + ItemSecureFileSectionComponent, + GenericItemPageFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageDateFieldComponent, + ItemPageUriFieldComponent, + MetadataFieldWrapperComponent, + MiradorViewerComponent, + ThemedFileSectionComponent, + ThemedMediaViewerComponent, + ThemedMetadataRepresentationListComponent, + ThemedResultsBackButtonComponent, + ThemedThumbnailComponent, + ItemAccessByTokenViewComponent, + ], +}) +export class ItemAccessByTokenPageComponent extends ItemPageComponent implements OnInit, OnDestroy { + + itemRequest$: Observable; + + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected _location: Location, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService, + protected notifyInfoService: NotifyInfoService, + private itemRequestDataService: ItemRequestDataService, + @Inject(PLATFORM_ID) protected platformId: string, + ) { + super(route, router, items, authorizationService, responseService, signpostingDataService, linkHeadService, notifyInfoService, platformId); + } + + protected readonly hasValue = hasValue; + + /** + * Initialise this component + * 1. take the access token from the query params and complete the stream + * 2. test for access token or redirect to forbidden page + * 3. get the sanitized token, make sure it is valid (if not, redirect to forbidden page) + * 4. return observable to itemRequest$ for the view to subscribe to + */ + ngOnInit(): void { + this.itemRequest$ = this.route.queryParams.pipe( + take(1), + map(params => { + if (!hasValue(params?.accessToken)) { + this.router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: false }); + return null; + } + return params.accessToken; + }), + filter(token => hasValue(token)), + switchMap(token => this.itemRequestDataService.getSanitizedRequestByAccessToken(token)), + getFirstCompletedRemoteData(), + redirectOn4xx(this.router, this.authService), + getFirstSucceededRemoteDataPayload(), + tap(request => { + if (!hasValue(request)) { + this.router.navigateByUrl(getForbiddenRoute()); + } + }), + ); + + // Call item page component initialization. + super.ngOnInit(); + } + + /** + * Navigate back in browser history. + */ + back() { + this._location.back(); + } + +} + diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.html b/src/app/item-page/access-by-token/item-access-by-token-view.component.html new file mode 100644 index 0000000000..047966e615 --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.html @@ -0,0 +1,101 @@ + + +
+
+ + +
+
+ +
+ + + +
+
+

{{'bitstream-request-a-copy.access-by-token.warning' | translate}}

+

{{ 'bitstream-request-a-copy.access-by-token.expiry-label' | translate }} {{ getAccessPeriodEndDate() }}

+
+
+
+ + + + + +
+ + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.scss b/src/app/item-page/access-by-token/item-access-by-token-view.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts b/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts new file mode 100644 index 0000000000..78e4544b6c --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.spec.ts @@ -0,0 +1,213 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { + of as observableOf, + of, +} from 'rxjs'; + +import { + APP_CONFIG, + APP_DATA_SERVICES_MAP, +} from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { Item } from '../../core/shared/item.model'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { ITEM_REQUEST } from '../../core/shared/item-request.resource-type'; +import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component'; +import { ErrorComponent } from '../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component'; +import { RouterLinkDirectiveStub } from '../../shared/testing/router-link-directive.stub'; +import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component'; +import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; +import { CollectionsComponent } from '../field-components/collections/collections.component'; +import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component'; +import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component'; +import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component'; +import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component'; +import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component'; +import { ItemVersionsComponent } from '../versions/item-versions.component'; +import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component'; +import { ItemSecureFileDownloadLinkComponent } from './field-components/file-download-link/item-secure-file-download-link.component'; +import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component'; +import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component'; +import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component'; + + +describe('ItemAccessByTokenViewComponent', () => { + let authorizationService: AuthorizationDataService; + let itemRequestDataService: ItemRequestDataService; + let bitstream: Bitstream; + let item: Item; + let itemRequest: ItemRequest; + let component: ItemAccessByTokenViewComponent; + let fixture: ComponentFixture; + let routeStub: any; + + function init() { + itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', { + canDownload: observableOf(true), + }); + bitstream = Object.assign(new Bitstream(), { + uuid: 'bitstreamUuid', + }); + item = Object.assign(new Item(), { + uuid: 'itemUuid', + metadata: { + 'dspace.entity.type': [ + { + value: 'Publication', + }, + ], + }, + _links: { + self: { href: 'obj-selflink' }, + }, + }); + routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(item), + }), + children: [], + }; + + const mockItemRequest: ItemRequest = Object.assign(new ItemRequest(), { + + }); + itemRequest = Object.assign(new ItemRequest(), + { + itemId: item.uuid, + bitstreamId: bitstream.uuid, + allfiles: false, + requestEmail: 'user@name.org', + requestName: 'User Name', + requestMessage: 'I would like to request a copy', + accessPeriod: 3600, + decisionDate: new Date().toISOString(), + token: 'test-token', + type: ITEM_REQUEST, + requestDate: new Date().toISOString(), + accessToken: 'test-token', + expires: null, + acceptRequest: true, + }); + } + + function initTestbed() { + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), ItemSecureFileDownloadLinkComponent, + RouterLinkDirectiveStub, + ], + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: RouterLinkDirectiveStub }, + { provide: ItemRequestDataService, useValue: itemRequestDataService }, + provideMockStore(), + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: APP_CONFIG, useValue: environment }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(ItemAccessByTokenViewComponent, { + remove: { + imports: [ + ErrorComponent, + ThemedLoadingComponent, + ThemedFullFileSectionComponent, + CollectionsComponent, + ItemVersionsComponent, + DsoEditMenuComponent, + ItemVersionsNoticeComponent, + ViewTrackerComponent, + ThemedItemAlertsComponent, + ItemSecureFileSectionComponent, + MiradorViewerComponent, + ThemedFileSectionComponent, + ThemedMediaViewerComponent, + ThemedMetadataRepresentationListComponent, + ThemedResultsBackButtonComponent, + ItemSecureMediaViewerComponent, + ], + }, + }).compileComponents(); + } + + const mockItem = Object.assign(new Item(), { + uuid: 'test-item-uuid', + id: 'test-item-id', + }); + + + + + beforeEach(waitForAsync(() => { + init(); + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(ItemAccessByTokenViewComponent); + component = fixture.componentInstance; + component.object = item; + component.itemRequest$ = of(itemRequest); + component.itemRequestSubject.next(itemRequest); + fixture.detectChanges(); + }); + + describe('Component and inputs initialised properly', () => { + it('should initialize with valid ItemRequest input', () => { + //component.itemRequestSubject.next(itemRequest); + component.itemRequest$.subscribe(request => { + expect(request).toBeDefined(); + expect(request.accessPeriod).toBe(3600); + expect(request.token).toBe('test-token'); + expect(request.requestName).toBe('User Name'); + expect(request.requestEmail).toBe('user@name.org'); + expect(request.requestMessage).toBe('I would like to request a copy'); + expect(request.allfiles).toBe(false); + expect(request.bitstreamId).toBe(bitstream.uuid); + expect(request.acceptRequest).toBe(true); + }); + }); + }); + + describe('getAccessPeriodEndDate', () => { + it('should calculate correct end date based on decision date and access period', () => { + const testDecisionDate = '2024-01-01T00:00:00Z'; + const testAccessPeriod = 3600; + + const testRequest = { + ...itemRequest, + decisionDate: testDecisionDate, + accessPeriod: testAccessPeriod, + }; + component.itemRequest$ = of(testRequest); + component.itemRequestSubject.next(testRequest); + const expectedDate = new Date(testDecisionDate); + expectedDate.setUTCSeconds(expectedDate.getUTCSeconds() + testAccessPeriod); + + expect(component.getAccessPeriodEndDate()).toEqual(expectedDate); + }); + + it('should return undefined when access period is 0', () => { + component.itemRequestSubject.next({ ...itemRequest, accessPeriod: 0 }); + expect(component.getAccessPeriodEndDate()).toBeUndefined(); + }); + }); +}); + diff --git a/src/app/item-page/access-by-token/item-access-by-token-view.component.ts b/src/app/item-page/access-by-token/item-access-by-token-view.component.ts new file mode 100644 index 0000000000..61f7679cfc --- /dev/null +++ b/src/app/item-page/access-by-token/item-access-by-token-view.component.ts @@ -0,0 +1,128 @@ +import { + AsyncPipe, + KeyValuePipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + Observable, +} from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { RouteService } from '../../core/services/route.service'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component'; +import { hasValue } from '../../shared/empty.util'; +import { ErrorComponent } from '../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; +import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component'; +import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component'; +import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; +import { CollectionsComponent } from '../field-components/collections/collections.component'; +import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component'; +import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component'; +import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component'; +import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component'; +import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component'; +import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component'; +import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component'; +import { ItemComponent } from '../simple/item-types/shared/item.component'; +import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component'; +import { ItemVersionsComponent } from '../versions/item-versions.component'; +import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component'; +import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component'; +import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component'; + +@Component({ + selector: 'ds-item-access-by-token-view', + styleUrls: ['./item-access-by-token-view.component.scss'], + templateUrl: './item-access-by-token-view.component.html', + standalone: true, + imports: [ + ErrorComponent, + ThemedLoadingComponent, + TranslateModule, + ThemedFullFileSectionComponent, + CollectionsComponent, + ItemVersionsComponent, + NgIf, + NgForOf, + AsyncPipe, + KeyValuePipe, + RouterLink, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + ItemVersionsNoticeComponent, + ViewTrackerComponent, + ThemedItemAlertsComponent, + VarDirective, + ItemSecureFileSectionComponent, + GenericItemPageFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageDateFieldComponent, + ItemPageUriFieldComponent, + MetadataFieldWrapperComponent, + MiradorViewerComponent, + ThemedFileSectionComponent, + ThemedMediaViewerComponent, + ThemedMetadataRepresentationListComponent, + ThemedResultsBackButtonComponent, + ThemedThumbnailComponent, + ItemSecureMediaViewerComponent, + //ItemPageTitleFieldComponent, + //ThumbnailComponent, + //MetadataRepresentationListComponent, + ], +}) +export class ItemAccessByTokenViewComponent extends ItemComponent implements OnInit { + + @Input() itemRequest$: Observable; + itemRequestSubject = new BehaviorSubject(null); + expiryDate: Date; + + constructor( + protected routeService: RouteService, + protected router: Router, + ) { + super(routeService, router); + } + + protected readonly hasValue = hasValue; + + ngOnInit(): void { + this.itemRequest$.pipe( + filter(request => hasValue(request)), + ).subscribe(request => { + this.itemRequestSubject.next(request); + super.ngOnInit(); + }); + + + } + + getAccessPeriodEndDate(): Date { + const request = this.itemRequestSubject.getValue(); + // Set expiry, if not 0 + if (hasValue(request) && request.accessPeriod > 0) { + const date = new Date(request.decisionDate); + date.setUTCSeconds(date.getUTCSeconds() + request.accessPeriod); + return date; + } + } +} diff --git a/src/app/shared/testing/router-link-directive.stub.ts b/src/app/shared/testing/router-link-directive.stub.ts index b3e2b09fb3..35d668c1bf 100644 --- a/src/app/shared/testing/router-link-directive.stub.ts +++ b/src/app/shared/testing/router-link-directive.stub.ts @@ -11,4 +11,5 @@ import { }) export class RouterLinkDirectiveStub { @Input() routerLink: any; + @Input() queryParams: any; }