Skip to content

Commit da74f6f

Browse files
UoE/Changing the resource policy for a bitstream during the submission process must be allowed only for administrators
UoE/Changing the resource policy for a bitstream during the submission process must be allowed only for administrators
2 parents 0f0555e + c8912c6 commit da74f6f

2 files changed

Lines changed: 145 additions & 21 deletions

File tree

src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,21 @@ import {
2828
import { provideMockStore } from '@ngrx/store/testing';
2929
import { TranslateModule } from '@ngx-translate/core';
3030
import { NgxMaskModule } from 'ngx-mask';
31-
import { of } from 'rxjs';
31+
import {
32+
of,
33+
throwError,
34+
} from 'rxjs';
3235

3336
import {
3437
APP_CONFIG,
3538
APP_DATA_SERVICES_MAP,
3639
} from '../../../../../../config/app-config.interface';
3740
import { environment } from '../../../../../../environments/environment.test';
41+
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
3842
import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
3943
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
4044
import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service';
45+
import { SubmissionScopeType } from '../../../../../core/submission/submission-scope-type';
4146
import { XSRFService } from '../../../../../core/xsrf/xsrf.service';
4247
import { dateToISOFormat } from '../../../../../shared/date.util';
4348
import { DsDynamicTypeBindRelationService } from '../../../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
@@ -79,6 +84,10 @@ const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
7984
remove: jasmine.createSpy('remove'),
8085
});
8186

87+
const authorizationServiceMock: any = jasmine.createSpyObj('authorizationService', {
88+
isAuthorized: of(true),
89+
});
90+
8291
const formMetadataMock = ['dc.title', 'dc.description'];
8392

8493
const initialState: any = {
@@ -108,6 +117,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
108117
let operationsService: any;
109118
let formService: any;
110119
let uploadService: any;
120+
let authorizationService: any;
111121

112122
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
113123
const submissionId = mockSubmissionId;
@@ -145,6 +155,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
145155
{ provide: SubmissionService, useClass: SubmissionServiceStub },
146156
{ provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub },
147157
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
158+
{ provide: AuthorizationDataService, useValue: authorizationServiceMock },
148159
{ provide: SectionUploadService, useValue: getMockSectionUploadService() },
149160
provideMockStore({ initialState }),
150161
FormBuilderService,
@@ -204,6 +215,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
204215
operationsService = TestBed.inject(SubmissionJsonPatchOperationsService);
205216
formService = TestBed.inject(FormService);
206217
uploadService = TestBed.inject(SectionUploadService);
218+
authorizationService = TestBed.inject(AuthorizationDataService);
207219

208220
comp.submissionId = submissionId;
209221
comp.collectionId = collectionId;
@@ -215,6 +227,8 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
215227
comp.configMetadataForm = configMetadataForm;
216228
comp.formMetadata = formMetadataMock;
217229

230+
submissionServiceStub.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem);
231+
authorizationService.isAuthorized.and.returnValue(of(true));
218232
formService.isValid.and.returnValue(of(true));
219233
});
220234

@@ -227,6 +241,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
227241
it('should init form model properly', () => {
228242
comp.fileData = fileData;
229243
comp.formId = 'testFileForm';
244+
comp.canEditAccessConditions = true;
230245
const maxStartDate = { year: 2022, month: 1, day: 12 };
231246
const maxEndDate = { year: 2019, month: 7, day: 12 };
232247

@@ -267,6 +282,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
267282

268283
comp.fileData = fileData;
269284
comp.formId = 'testFileForm';
285+
comp.canEditAccessConditions = true;
270286

271287
comp.formModel = compAsAny.buildFileEditForm();
272288

@@ -300,11 +316,40 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
300316
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
301317
});
302318

319+
it('should show access-condition form controls for admin users', () => {
320+
comp.fileData = fileData;
321+
comp.formId = 'testFileForm';
322+
authorizationService.isAuthorized.and.returnValue(of(true));
323+
// only the form model is under test — don't render the template
324+
spyOn(compAsAny.cdr, 'detectChanges');
325+
326+
comp.ngOnInit();
327+
328+
expect(comp.canEditAccessConditions).toBeTrue();
329+
expect(comp.formModel).toBeDefined();
330+
expect(formbuilderService.findById('accessConditions', comp.formModel)).not.toBeNull();
331+
});
332+
333+
it('should hide access-condition form controls for non-admin users', () => {
334+
comp.fileData = fileData;
335+
comp.formId = 'testFileForm';
336+
authorizationService.isAuthorized.and.returnValue(of(false));
337+
// only the form model is under test — don't render the template
338+
spyOn(compAsAny.cdr, 'detectChanges');
339+
340+
comp.ngOnInit();
341+
342+
expect(comp.canEditAccessConditions).toBeFalse();
343+
expect(comp.formModel).toBeDefined();
344+
expect(formbuilderService.findById('accessConditions', comp.formModel)).toBeNull();
345+
});
346+
303347
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
304348
compAsAny.formRef = { formGroup: null };
305349
compAsAny.fileData = fileData;
306350
compAsAny.pathCombiner = pathCombiner;
307351
compAsAny.isPrimary = null;
352+
comp.canEditAccessConditions = true;
308353
formService.validateAllFormFields.and.callFake(() => null);
309354
formService.isValid.and.returnValue(of(true));
310355
formService.getFormData.and.returnValue(of(mockFileFormData));
@@ -381,6 +426,56 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
381426
expect(uploadService.updateFileData).toHaveBeenCalled();
382427
}));
383428

429+
it('should not patch access conditions for non-admin users', fakeAsync(() => {
430+
compAsAny.formRef = { formGroup: null };
431+
compAsAny.fileData = fileData;
432+
compAsAny.pathCombiner = pathCombiner;
433+
compAsAny.canEditAccessConditions = false;
434+
435+
operationsBuilder.add.calls.reset();
436+
formService.validateAllFormFields.and.callFake(() => null);
437+
formService.isValid.and.returnValue(of(true));
438+
formService.getFormData.and.returnValue(of(mockFileFormData));
439+
440+
const response = [
441+
Object.assign(mockSubmissionObject, {
442+
sections: {
443+
upload: {
444+
files: mockUploadFiles,
445+
},
446+
},
447+
}),
448+
];
449+
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
450+
451+
comp.saveBitstreamData();
452+
tick();
453+
454+
expect(operationsBuilder.add).not.toHaveBeenCalledWith(
455+
pathCombiner.getPath(['files', fileIndex, 'accessConditions']),
456+
jasmine.anything(),
457+
true,
458+
);
459+
}));
460+
461+
it('should clear isSaving and keep the modal open when the patch request fails', fakeAsync(() => {
462+
compAsAny.formRef = { formGroup: null };
463+
compAsAny.fileData = fileData;
464+
compAsAny.pathCombiner = pathCombiner;
465+
spyOn(compAsAny.cdr, 'detectChanges');
466+
const modalCloseSpy = spyOn(compAsAny.activeModal, 'close');
467+
formService.validateAllFormFields.and.callFake(() => null);
468+
formService.isValid.and.returnValue(of(true));
469+
formService.getFormData.and.returnValue(of(mockFileFormData));
470+
operationsService.jsonPatchByResourceID.and.returnValue(throwError(() => new Error('patch failed')));
471+
472+
comp.saveBitstreamData();
473+
tick();
474+
475+
expect(compAsAny.isSaving).toBeFalse();
476+
expect(modalCloseSpy).not.toHaveBeenCalled();
477+
}));
478+
384479
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
385480
compAsAny.formRef = { formGroup: null };
386481
compAsAny.pathCombiner = pathCombiner;

src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ import { DynamicCustomSwitchModel } from 'src/app/shared/form/builder/ds-dynamic
3434

3535
import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model';
3636
import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model';
37+
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
38+
import { FeatureID } from '../../../../../core/data/feature-authorization/feature-id';
3739
import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
3840
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
41+
import { SubmitDataResponseDefinitionObject } from '../../../../../core/shared/submit-data-response-definition.model';
3942
import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model';
4043
import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service';
4144
import { BtnDisabledDirective } from '../../../../../shared/btn-disabled.directive';
@@ -186,6 +189,12 @@ implements OnInit, OnDestroy {
186189

187190
protected subscriptions: Subscription[] = [];
188191

192+
/**
193+
* Whether the current user may edit bitstream access conditions.
194+
* Fail-closed: stays false until the site-administrator check resolves in ngOnInit.
195+
*/
196+
public canEditAccessConditions = false;
197+
189198
/**
190199
* Initialize instance variables
191200
*
@@ -201,6 +210,7 @@ implements OnInit, OnDestroy {
201210
constructor(
202211
protected activeModal: NgbActiveModal,
203212
private cdr: ChangeDetectorRef,
213+
private authorizationService: AuthorizationDataService,
204214
private formBuilderService: FormBuilderService,
205215
private formService: FormService,
206216
private submissionService: SubmissionService,
@@ -294,8 +304,17 @@ implements OnInit, OnDestroy {
294304
*/
295305
ngOnInit() {
296306
if (this.fileData && this.formId) {
297-
this.formModel = this.buildFileEditForm();
298-
this.cdr.detectChanges();
307+
const initSubscription = this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
308+
take(1),
309+
).subscribe({
310+
next: (canEdit: boolean) => {
311+
this.canEditAccessConditions = canEdit;
312+
this.formModel = this.buildFileEditForm();
313+
this.cdr.detectChanges();
314+
},
315+
});
316+
317+
this.subscriptions.push(initSubscription);
299318
}
300319
}
301320

@@ -337,7 +356,7 @@ implements OnInit, OnDestroy {
337356
const accessConditionsArrayConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG);
338357
const accessConditionTypeOptions = [];
339358

340-
if (this.collectionPolicyType === POLICY_DEFAULT_WITH_LIST) {
359+
if (this.collectionPolicyType === POLICY_DEFAULT_WITH_LIST && this.canEditAccessConditions) {
341360
for (const accessCondition of this.availableAccessConditionOptions) {
342361
accessConditionTypeOptions.push(
343362
{
@@ -455,7 +474,7 @@ implements OnInit, OnDestroy {
455474
this.operationsBuilder.remove(this.pathCombiner.getPath([...pathFragment, path]));
456475
});
457476
const accessConditionsToSave = [];
458-
if (formData.hasOwnProperty('accessConditions')) {
477+
if (this.canEditAccessConditions && formData.hasOwnProperty('accessConditions')) {
459478
formData.accessConditions
460479
.filter((accessConditions) => isNotNull(accessConditions))
461480
.map((accessConditions) => accessConditions.accessConditionGroup)
@@ -517,22 +536,32 @@ implements OnInit, OnDestroy {
517536
this.pathCombiner.rootElement,
518537
this.pathCombiner.subRootElement);
519538
}),
520-
).subscribe((result: SubmissionObject[]) => {
521-
const section = result[0].sections[this.sectionId];
522-
if (!section) {
523-
return;
524-
}
525-
const uploadSection = (section as WorkspaceitemSectionUploadObject);
526-
527-
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, uploadSection.primary);
528-
529-
Object.keys(uploadSection.files)
530-
.filter((key) => uploadSection.files[key].uuid === this.fileId)
531-
.forEach((key) => this.uploadService.updateFileData(
532-
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]),
533-
);
534-
this.isSaving = false;
535-
this.activeModal.close();
539+
).subscribe({
540+
next: (result: SubmitDataResponseDefinitionObject) => {
541+
const submissionObject = result[0] as SubmissionObject;
542+
const section = submissionObject?.sections?.[this.sectionId];
543+
if (!section) {
544+
this.isSaving = false;
545+
this.cdr.detectChanges();
546+
return;
547+
}
548+
const uploadSection = (section as WorkspaceitemSectionUploadObject);
549+
550+
this.uploadService.updateFilePrimaryBitstream(this.submissionId, this.sectionId, uploadSection.primary);
551+
552+
Object.keys(uploadSection.files)
553+
.filter((key) => uploadSection.files[key].uuid === this.fileId)
554+
.forEach((key) => this.uploadService.updateFileData(
555+
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key]),
556+
);
557+
this.isSaving = false;
558+
this.activeModal.close();
559+
},
560+
error: () => {
561+
// keep the modal usable when the PATCH request fails
562+
this.isSaving = false;
563+
this.cdr.detectChanges();
564+
},
536565
});
537566
this.subscriptions.push(saveBitstreamDataSubscription);
538567
}

0 commit comments

Comments
 (0)