From c416da73192c64fe369a46aa18c1f2d803282c14 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:29:09 -0800 Subject: [PATCH] Frontend coverage for bulk editor changes, sharelink bundle service --- .../bulk-editor/bulk-editor.component.spec.ts | 141 ++++++++++++++++++ .../rest/share-link-bundle.service.spec.ts | 60 ++++++++ 2 files changed, 201 insertions(+) create mode 100644 src-ui/src/app/services/rest/share-link-bundle.service.spec.ts diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index 457e1888d..694899d3d 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -3,6 +3,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing' +import { EventEmitter } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' @@ -25,6 +26,7 @@ import { SelectionData, } from 'src/app/services/rest/document.service' import { GroupService } from 'src/app/services/rest/group.service' +import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { TagService } from 'src/app/services/rest/tag.service' import { UserService } from 'src/app/services/rest/user.service' @@ -38,6 +40,8 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' import { FilterableDropdownComponent } from '../../common/filterable-dropdown/filterable-dropdown.component' +import { ShareLinkBundleDialogComponent } from '../../common/share-link-bundle-dialog/share-link-bundle-dialog.component' +import { ShareLinkBundleManageDialogComponent } from '../../common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component' import { BulkEditorComponent } from './bulk-editor.component' const selectionData: SelectionData = { @@ -72,6 +76,7 @@ describe('BulkEditorComponent', () => { let storagePathService: StoragePathService let customFieldsService: CustomFieldsService let httpTestingController: HttpTestingController + let shareLinkBundleService: ShareLinkBundleService beforeEach(async () => { TestBed.configureTestingModule({ @@ -152,6 +157,15 @@ describe('BulkEditorComponent', () => { }), }, }, + { + provide: ShareLinkBundleService, + useValue: { + createBundle: jest.fn(), + listAllBundles: jest.fn(), + rebuildBundle: jest.fn(), + delete: jest.fn(), + }, + }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], @@ -168,6 +182,7 @@ describe('BulkEditorComponent', () => { storagePathService = TestBed.inject(StoragePathService) customFieldsService = TestBed.inject(CustomFieldsService) httpTestingController = TestBed.inject(HttpTestingController) + shareLinkBundleService = TestBed.inject(ShareLinkBundleService) fixture = TestBed.createComponent(BulkEditorComponent) component = fixture.componentInstance @@ -1454,4 +1469,130 @@ describe('BulkEditorComponent', () => { `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id` ) // listAllFilteredIds }) + + it('should create share link bundle and enable manage callback', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest + .spyOn(documentListViewService, 'documents', 'get') + .mockReturnValue([{ id: 5 }, { id: 7 }] as any) + jest + .spyOn(documentListViewService, 'selected', 'get') + .mockReturnValue(new Set([5, 7])) + + const confirmClicked = new EventEmitter() + const modalRef: Partial = { + close: jest.fn(), + componentInstance: { + documents: [], + confirmClicked, + payload: { + document_ids: [5, 7], + file_version: 'archive', + expiration_days: 7, + }, + loading: false, + buttonsEnabled: true, + copied: false, + }, + } + + const openSpy = jest.spyOn(modalService, 'open') + openSpy.mockReturnValueOnce(modalRef as NgbModalRef) + openSpy.mockReturnValueOnce({} as NgbModalRef) + ;(shareLinkBundleService.createBundle as jest.Mock).mockReturnValueOnce( + of({ id: 42 }) + ) + const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + + component.createShareLinkBundle() + + expect(openSpy).toHaveBeenNthCalledWith( + 1, + ShareLinkBundleDialogComponent, + expect.objectContaining({ backdrop: 'static', size: 'lg' }) + ) + + const dialogInstance = modalRef.componentInstance as any + expect(dialogInstance.documents).toEqual([{ id: 5 }, { id: 7 }]) + + confirmClicked.emit() + + expect(shareLinkBundleService.createBundle).toHaveBeenCalledWith({ + document_ids: [5, 7], + file_version: 'archive', + expiration_days: 7, + }) + expect(dialogInstance.loading).toBe(false) + expect(dialogInstance.buttonsEnabled).toBe(false) + expect(dialogInstance.createdBundle).toEqual({ id: 42 }) + expect(typeof dialogInstance.onOpenManage).toBe('function') + expect(toastInfoSpy).toHaveBeenCalledWith( + $localize`Share link bundle creation requested.` + ) + + dialogInstance.onOpenManage() + expect(modalRef.close).toHaveBeenCalled() + expect(openSpy).toHaveBeenNthCalledWith( + 2, + ShareLinkBundleManageDialogComponent, + expect.objectContaining({ backdrop: 'static', size: 'lg' }) + ) + openSpy.mockRestore() + }) + + it('should handle share link bundle creation errors', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest + .spyOn(documentListViewService, 'documents', 'get') + .mockReturnValue([{ id: 9 }] as any) + jest + .spyOn(documentListViewService, 'selected', 'get') + .mockReturnValue(new Set([9])) + + const confirmClicked = new EventEmitter() + const modalRef: Partial = { + componentInstance: { + documents: [], + confirmClicked, + payload: { + document_ids: [9], + file_version: 'original', + expiration_days: null, + }, + loading: false, + buttonsEnabled: true, + }, + } + + const openSpy = jest + .spyOn(modalService, 'open') + .mockReturnValue(modalRef as NgbModalRef) + ;(shareLinkBundleService.createBundle as jest.Mock).mockReturnValueOnce( + throwError(() => new Error('bundle failure')) + ) + const toastErrorSpy = jest.spyOn(toastService, 'showError') + + component.createShareLinkBundle() + + const dialogInstance = modalRef.componentInstance as any + confirmClicked.emit() + + expect(toastErrorSpy).toHaveBeenCalledWith( + $localize`Share link bundle creation is not available yet.`, + expect.any(Error) + ) + expect(dialogInstance.loading).toBe(false) + expect(dialogInstance.buttonsEnabled).toBe(true) + openSpy.mockRestore() + }) + + it('should open share link bundle management dialog', () => { + const openSpy = jest.spyOn(modalService, 'open') + component.manageShareLinkBundles() + expect(openSpy).toHaveBeenCalledWith( + ShareLinkBundleManageDialogComponent, + expect.objectContaining({ backdrop: 'static', size: 'lg' }) + ) + openSpy.mockRestore() + }) }) diff --git a/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts b/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts new file mode 100644 index 000000000..6b87ddf04 --- /dev/null +++ b/src-ui/src/app/services/rest/share-link-bundle.service.spec.ts @@ -0,0 +1,60 @@ +import { HttpTestingController } from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { Subscription } from 'rxjs' +import { environment } from 'src/environments/environment' +import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec' +import { ShareLinkBundleService } from './share-link-bundle.service' + +const endpoint = 'share_link_bundles' + +commonAbstractPaperlessServiceTests(endpoint, ShareLinkBundleService) + +describe('ShareLinkBundleService', () => { + let httpTestingController: HttpTestingController + let service: ShareLinkBundleService + let subscription: Subscription | undefined + + beforeEach(() => { + httpTestingController = TestBed.inject(HttpTestingController) + service = TestBed.inject(ShareLinkBundleService) + }) + + afterEach(() => { + subscription?.unsubscribe() + httpTestingController.verify() + }) + + it('creates bundled share links', () => { + const payload = { + document_ids: [1, 2], + file_version: 'archive', + expiration_days: 7, + } + subscription = service.createBundle(payload as any).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/` + ) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual(payload) + req.flush({}) + }) + + it('rebuilds bundles', () => { + subscription = service.rebuildBundle(12).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/12/rebuild/` + ) + expect(req.request.method).toBe('POST') + expect(req.request.body).toEqual({}) + req.flush({}) + }) + + it('lists bundles with expected parameters', () => { + subscription = service.listAllBundles().subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=1000&ordering=-created` + ) + expect(req.request.method).toBe('GET') + req.flush({ results: [] }) + }) +})