Skip to content

Commit be20a5f

Browse files
authored
Merge pull request #96 from umd-lib/feature/LIBDRUM-1005
LIBDRUM-1005. "Restricted Access" pages return 401/403 to bots
2 parents b514b7b + 2cd1c85 commit be20a5f

2 files changed

Lines changed: 348 additions & 17 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import {
2+
DatePipe,
3+
Location,
4+
} from '@angular/common';
5+
import {
6+
ComponentFixture,
7+
TestBed,
8+
waitForAsync,
9+
} from '@angular/core/testing';
10+
import {
11+
ActivatedRoute,
12+
Router,
13+
} from '@angular/router';
14+
import { TranslateModule } from '@ngx-translate/core';
15+
import { of as observableOf } from 'rxjs';
16+
17+
import { AuthService } from '../core/auth/auth.service';
18+
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
19+
import { HardRedirectService } from '../core/services/hard-redirect.service';
20+
import { ServerResponseService } from '../core/services/server-response.service';
21+
import { Bitstream } from '../core/shared/bitstream.model';
22+
import { FileService } from '../core/shared/file.service';
23+
import { createSuccessfulRemoteDataObject } from '../shared/remote-data.utils';
24+
import { RestrictedAccessComponent } from './restricted-access.component';
25+
26+
describe('RestrictedAccessComponent', () => {
27+
let component: RestrictedAccessComponent;
28+
let fixture: ComponentFixture<RestrictedAccessComponent>;
29+
30+
let authService: jasmine.SpyObj<AuthService>;
31+
let authorizationService: jasmine.SpyObj<AuthorizationDataService>;
32+
let fileService: jasmine.SpyObj<FileService>;
33+
let hardRedirectService: jasmine.SpyObj<HardRedirectService>;
34+
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
35+
let router: jasmine.SpyObj<Router>;
36+
let location: jasmine.SpyObj<Location>;
37+
let activatedRoute;
38+
39+
let bitstream: Bitstream;
40+
41+
function initBitstream(overrides: Partial<Bitstream> = {}): Bitstream {
42+
return Object.assign(new Bitstream(), {
43+
uuid: 'test-bitstream-uuid',
44+
metadata: {
45+
'dc.title': [{ value: 'test-file.pdf', language: null, authority: null, confidence: -1, place: 0 }],
46+
},
47+
_links: {
48+
content: { href: 'bitstream-content-link' },
49+
self: { href: 'bitstream-self-link' },
50+
},
51+
// Default in tests to "FOREVER" embargo
52+
embargoRestriction: 'FOREVER',
53+
...overrides,
54+
});
55+
}
56+
57+
function init(bitstreamOverrides: Partial<Bitstream> = {}) {
58+
bitstream = initBitstream(bitstreamOverrides);
59+
60+
authService = jasmine.createSpyObj('AuthService', {
61+
isAuthenticated: observableOf(false),
62+
setRedirectUrl: {},
63+
});
64+
65+
authorizationService = jasmine.createSpyObj('AuthorizationDataService', {
66+
isAuthorized: observableOf(false),
67+
});
68+
69+
fileService = jasmine.createSpyObj('FileService', {
70+
retrieveFileDownloadLink: observableOf('content-url-with-headers'),
71+
});
72+
73+
hardRedirectService = jasmine.createSpyObj('HardRedirectService', {
74+
redirect: {},
75+
});
76+
77+
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
78+
setUnauthorized: {},
79+
setForbidden: {},
80+
setNotFound: {},
81+
setStatus: {},
82+
});
83+
84+
router = jasmine.createSpyObj('Router', ['navigateByUrl']);
85+
// Provide a url property for redirectOn4xx
86+
(router as any).url = '/restricted-access/test-bitstream-uuid';
87+
88+
location = jasmine.createSpyObj('Location', ['back']);
89+
90+
activatedRoute = {
91+
data: observableOf({
92+
bitstream: createSuccessfulRemoteDataObject(bitstream),
93+
}),
94+
};
95+
}
96+
97+
function initTestBed() {
98+
void TestBed.configureTestingModule({
99+
imports: [
100+
TranslateModule.forRoot(),
101+
RestrictedAccessComponent,
102+
],
103+
providers: [
104+
{ provide: ActivatedRoute, useValue: activatedRoute },
105+
{ provide: Router, useValue: router },
106+
{ provide: AuthorizationDataService, useValue: authorizationService },
107+
{ provide: AuthService, useValue: authService },
108+
{ provide: FileService, useValue: fileService },
109+
{ provide: HardRedirectService, useValue: hardRedirectService },
110+
{ provide: ServerResponseService, useValue: serverResponseService },
111+
{ provide: Location, useValue: location },
112+
DatePipe,
113+
],
114+
}).compileComponents();
115+
}
116+
117+
// Helper function for setting up anonymous tests with a specific embargo
118+
// restriction
119+
function setupAnonymous(bitstreamOverrides: Partial<Bitstream>) {
120+
beforeEach(waitForAsync(() => {
121+
init(bitstreamOverrides);
122+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
123+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
124+
initTestBed();
125+
}));
126+
127+
beforeEach(() => {
128+
fixture = TestBed.createComponent(RestrictedAccessComponent);
129+
component = fixture.componentInstance;
130+
fixture.detectChanges();
131+
});
132+
}
133+
134+
// Helper function verifying that a HTTP 401 Unauthorized status code is
135+
// set, and that a redirect to the bitstream is not performed.
136+
function verify401StatusCodeAndNoRedirectToDownload() {
137+
it('should set 401 Unauthorized and not redirect to the file', () => {
138+
expect(serverResponseService.setUnauthorized).toHaveBeenCalled();
139+
expect(serverResponseService.setForbidden).not.toHaveBeenCalled();
140+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
141+
});
142+
}
143+
144+
describe('when the user is anonymous (not logged in)', () => {
145+
describe('when embargoRestriction is FOREVER', () => {
146+
setupAnonymous({ embargoRestriction: 'FOREVER' });
147+
148+
verify401StatusCodeAndNoRedirectToDownload();
149+
150+
it('should set the restrictedAccessMessage indicating the file is embargoed forever', () => {
151+
expect(component.restrictedAccessMessage.value).toBe('bitstream.restricted-access.embargo.forever.message');
152+
});
153+
});
154+
155+
describe('when there is an embargo end date', () => {
156+
setupAnonymous({ embargoRestriction: '2199-04-08' });
157+
158+
verify401StatusCodeAndNoRedirectToDownload();
159+
160+
it('should set the restrictedAccessMessage indicating an end date', () => {
161+
expect(component.restrictedAccessMessage.value).toBe(
162+
'bitstream.restricted-access.embargo.restricted-until.message');
163+
});
164+
});
165+
166+
describe('when embargoRestriction is NONE (embargo over, but file is restricted for another reason)', () => {
167+
setupAnonymous({ embargoRestriction: 'NONE' });
168+
169+
verify401StatusCodeAndNoRedirectToDownload();
170+
171+
it('should set the restrictedAccessMessage to a simple "forbidden" message', () => {
172+
expect(component.restrictedAccessMessage.value).toBe('bitstream.restricted-access.anonymous.forbidden.message');
173+
});
174+
});
175+
176+
describe('when file is restricted for non-embargo reasons (such as Campus IP restriction)', () => {
177+
setupAnonymous({ embargoRestriction:null });
178+
179+
verify401StatusCodeAndNoRedirectToDownload();
180+
181+
it('should set the restrictedAccessMessage to a simple "forbidden" message', () => {
182+
expect(component.restrictedAccessMessage.value).toBe('bitstream.restricted-access.anonymous.forbidden.message');
183+
});
184+
});
185+
186+
describe('but the user is authorized (even if there is an embargo)', () => {
187+
beforeEach(waitForAsync(() => {
188+
init();
189+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
190+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true));
191+
initTestBed();
192+
}));
193+
194+
beforeEach(() => {
195+
fixture = TestBed.createComponent(RestrictedAccessComponent);
196+
component = fixture.componentInstance;
197+
fixture.detectChanges();
198+
});
199+
200+
it('should redirect to the content link', () => {
201+
expect(hardRedirectService.redirect).toHaveBeenCalled();
202+
});
203+
204+
it('should NOT call setUnauthorized', () => {
205+
expect(serverResponseService.setUnauthorized).not.toHaveBeenCalled();
206+
});
207+
208+
it('should NOT call setForbidden', () => {
209+
expect(serverResponseService.setForbidden).not.toHaveBeenCalled();
210+
});
211+
});
212+
});
213+
214+
describe('when the user is logged in', () => {
215+
describe('returns 403 Forbidden when the user is not authorized to access the file', () => {
216+
beforeEach(waitForAsync(() => {
217+
init();
218+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
219+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
220+
initTestBed();
221+
}));
222+
223+
beforeEach(() => {
224+
fixture = TestBed.createComponent(RestrictedAccessComponent);
225+
component = fixture.componentInstance;
226+
fixture.detectChanges();
227+
});
228+
229+
it('should call setForbidden on ServerResponseService', () => {
230+
expect(serverResponseService.setForbidden).toHaveBeenCalled();
231+
});
232+
233+
it('should NOT call setUnauthorized on ServerResponseService', () => {
234+
expect(serverResponseService.setUnauthorized).not.toHaveBeenCalled();
235+
});
236+
237+
it('should NOT redirect to a download', () => {
238+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
239+
});
240+
241+
it('should set the restrictedAccessHeader', () => {
242+
expect(component.restrictedAccessHeader.value).toBe('bitstream.restricted-access.user.forbidden.header');
243+
});
244+
245+
it('should set the restrictedAccessMessage', () => {
246+
expect(component.restrictedAccessMessage.value).toBe(
247+
'bitstream.restricted-access.user.forbidden.with_file.message');
248+
});
249+
});
250+
251+
describe('returns 403 Forbidden with a generic message when a filename is not provided', () => {
252+
beforeEach(waitForAsync(() => {
253+
init({ metadata: {} });
254+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
255+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
256+
initTestBed();
257+
}));
258+
259+
beforeEach(() => {
260+
fixture = TestBed.createComponent(RestrictedAccessComponent);
261+
component = fixture.componentInstance;
262+
fixture.detectChanges();
263+
});
264+
265+
it('should set the generic forbidden message', () => {
266+
expect(component.restrictedAccessMessage.value).toBe(
267+
'bitstream.restricted-access.user.forbidden.generic.message');
268+
});
269+
});
270+
271+
describe('allows access to the file when the user is authorized', () => {
272+
beforeEach(waitForAsync(() => {
273+
init();
274+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
275+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true));
276+
initTestBed();
277+
}));
278+
279+
beforeEach(() => {
280+
fixture = TestBed.createComponent(RestrictedAccessComponent);
281+
component = fixture.componentInstance;
282+
fixture.detectChanges();
283+
});
284+
285+
it('should NOT call setUnauthorized', () => {
286+
expect(serverResponseService.setUnauthorized).not.toHaveBeenCalled();
287+
});
288+
289+
it('should NOT call setForbidden', () => {
290+
expect(serverResponseService.setForbidden).not.toHaveBeenCalled();
291+
});
292+
293+
it('should redirect to the file download link', () => {
294+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
295+
});
296+
});
297+
});
298+
299+
describe('back()', () => {
300+
beforeEach(waitForAsync(() => {
301+
init();
302+
initTestBed();
303+
}));
304+
305+
beforeEach(() => {
306+
fixture = TestBed.createComponent(RestrictedAccessComponent);
307+
component = fixture.componentInstance;
308+
fixture.detectChanges();
309+
});
310+
311+
it('should call location.back()', () => {
312+
component.back();
313+
expect(location.back).toHaveBeenCalled();
314+
});
315+
});
316+
});

0 commit comments

Comments
 (0)