Re-implement frontend sso groups

This commit is contained in:
shamoon 2023-09-28 23:15:21 -07:00
parent fa4e78d9c2
commit d23b0e72b3
8 changed files with 197 additions and 539 deletions

View File

@ -93,7 +93,54 @@
<div *ngIf="groups.length === 0">No groups defined</div>
</ng-container>
<div *ngIf="!users || !groups">
<ng-container *ngIf="ssoGroups">
<h4 class="mt-4 d-flex">
<ng-container i18n>SSO Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editSsoGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SsoGroup }">
<svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add SSO Group</ng-container>
</button>
</h4>
<ul *ngIf="ssoGroups.length > 0" class="list-group">
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
<div class="col">Group</div>
<div class="col"></div>
<div class="col" i18n>Actions</div>
</div>
</li>
<li *ngFor="let ssoGroup of ssoGroups" class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editSsoGroup(ssoGroup)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.SsoGroup)">{{ssoGroup.name}}</button></div>
<div class="col">{{ getGroupName(ssoGroup.group) }}</div>
<div class="col"></div>
<div class="col">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editSsoGroup(ssoGroup)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SsoGroup }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteSsoGroup(ssoGroup)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SsoGroup }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>
</li>
</ul>
<div *ngIf="ssoGroups.length === 0">No groups defined</div>
</ng-container>
<div *ngIf="!users || !groups || !ssoGroups">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>

View File

@ -25,11 +25,13 @@ import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SsoGroupService } from 'src/app/services/rest/sso-group.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { SsoGroupEditDialogComponent } from '../../common/edit-dialog/sso-group-edit-dialog/sso-group-edit-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
import { NumberComponent } from '../../common/input/number/number.component'
import { PasswordComponent } from '../../common/input/password/password.component'
@ -43,6 +45,7 @@ import { SettingsComponent } from '../settings/settings.component'
import { UsersAndGroupsComponent } from './users-groups.component'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessSSOGroup } from 'src/app/data/paperless-sso-group'
const users = [
{ id: 1, username: 'user1', is_superuser: false },
@ -52,6 +55,10 @@ const groups = [
{ id: 1, name: 'group1' },
{ id: 2, name: 'group2' },
]
const ssoGroups = [
{ id: 1, name: 'sso group1', group: 1 },
{ id: 2, name: 'sso group2', group: 2 },
]
describe('UsersAndGroupsComponent', () => {
let component: UsersAndGroupsComponent
@ -62,6 +69,7 @@ describe('UsersAndGroupsComponent', () => {
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
let ssoGroupService: SsoGroupService
beforeEach(() => {
TestBed.configureTestingModule({
@ -109,6 +117,7 @@ describe('UsersAndGroupsComponent', () => {
.spyOn(permissionsService, 'currentUserOwnsObject')
.mockReturnValue(true)
groupService = TestBed.inject(GroupService)
ssoGroupService = TestBed.inject(SsoGroupService)
component = fixture.componentInstance
fixture.detectChanges()
})
@ -132,6 +141,15 @@ describe('UsersAndGroupsComponent', () => {
})
)
}
if (excludeService !== ssoGroupService) {
jest.spyOn(ssoGroupService, 'listAll').mockReturnValue(
of({
all: ssoGroups.map((r) => r.id),
count: ssoGroups.length,
results: (ssoGroups as PaperlessSSOGroup[]).concat([]),
})
)
}
fixture = TestBed.createComponent(UsersAndGroupsComponent)
component = fixture.componentInstance
@ -264,4 +282,54 @@ describe('UsersAndGroupsComponent', () => {
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
})
it('should show errors on load if load sso groups failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(ssoGroupService, 'listAll')
.mockImplementation(() =>
throwError(() => new Error('failed to load SSO groups'))
)
completeSetup(ssoGroupService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
})
it('should support edit / create sso group, show error if needed', () => {
completeSetup()
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editSsoGroup(ssoGroups[0])
const editDialog = modal.componentInstance as SsoGroupEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(ssoGroups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved SSO group "${ssoGroups[0].name}".`
)
component.editSsoGroup()
})
it('should support delete sso group, show error if needed', () => {
completeSetup()
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteSsoGroup(ssoGroups[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(ssoGroupService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(ssoGroupService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting SSO group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted SSO group')
})
})

View File

@ -3,14 +3,17 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first, takeUntil } from 'rxjs'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { PaperlessSSOGroup } from 'src/app/data/paperless-sso-group'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SsoGroupService } from 'src/app/services/rest/sso-group.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { SsoGroupEditDialogComponent } from '../../common/edit-dialog/sso-group-edit-dialog/sso-group-edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SettingsService } from 'src/app/services/settings.service'
@ -25,12 +28,14 @@ export class UsersAndGroupsComponent
{
users: PaperlessUser[]
groups: PaperlessGroup[]
ssoGroups: PaperlessSSOGroup[]
unsubscribeNotifier: Subject<any> = new Subject()
constructor(
private usersService: UserService,
private groupsService: GroupService,
private ssoGroupService: SsoGroupService,
private toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
@ -63,6 +68,18 @@ export class UsersAndGroupsComponent
this.toastService.showError($localize`Error retrieving groups`, e)
},
})
this.ssoGroupService
.listAll(null, null, { full_perms: true })
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (r) => {
this.ssoGroups = r.results
},
error: (e) => {
this.toastService.showError($localize`Error retrieving SSO groups`, e)
},
})
}
ngOnDestroy() {
@ -183,6 +200,57 @@ export class UsersAndGroupsComponent
})
}
editSsoGroup(ssoGroup: PaperlessSSOGroup = null) {
var modal = this.modalService.open(SsoGroupEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = ssoGroup
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.object = ssoGroup
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newGroup) => {
this.toastService.showInfo(
$localize`Saved SSO group "${newGroup.name}".`
)
this.ssoGroupService.listAll().subscribe((r) => {
this.ssoGroups = r.results
})
})
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving SSO group.`, e)
})
}
deleteSsoGroup(ssoGroup: PaperlessSSOGroup) {
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Confirm delete SSO group`
modal.componentInstance.messageBold = $localize`This operation will permanently delete this SSO group.`
modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.ssoGroupService.delete(ssoGroup).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted SSO group`)
this.ssoGroupService.listAll().subscribe((r) => {
this.ssoGroups = r.results
})
},
error: (e) => {
this.toastService.showError($localize`Error deleting SSO group.`, e)
},
})
})
}
getGroupName(id: number): string {
return this.groups?.find((g) => g.id === id)?.name ?? ''
}

View File

@ -7,8 +7,8 @@
<div class="modal-body">
<div class="row">
<div class="col">
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<app-input-select i18n-title title="Group" [items]="groups" formControlName="group"></app-input-select>
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Group" [items]="groups" formControlName="group"></pngx-input-select>
</div>
</div>
</div>

View File

@ -11,10 +11,12 @@ import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { SsoGroupEditDialogComponent } from './sso-group-edit-dialog.component'
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
import { SettingsService } from 'src/app/services/settings.service'
describe('GroupEditDialogComponent', () => {
describe('SSoGroupEditDialogComponent', () => {
let component: SsoGroupEditDialogComponent
let fixture: ComponentFixture<SsoGroupEditDialogComponent>
let settingsService: SettingsService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -38,6 +40,8 @@ describe('GroupEditDialogComponent', () => {
}).compileComponents()
fixture = TestBed.createComponent(SsoGroupEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
component = fixture.componentInstance
fixture.detectChanges()

View File

@ -1,533 +0,0 @@
import { ViewportScroller, DatePipe } from '@angular/common'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import {
NgbModal,
NgbModule,
NgbNavLink,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService, Toast } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
import { ColorComponent } from '../../common/input/color/color.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from './settings.component'
import { SsoGroupService } from '../../../services/rest/sso-group.service'
import { SsoGroupEditDialogComponent } from '../../common/edit-dialog/sso-group-edit-dialog/sso-group-edit-dialog.component'
const savedViews = [
{ id: 1, name: 'view1' },
{ id: 2, name: 'view2' },
]
const users = [
{ id: 1, username: 'user1', is_superuser: false },
{ id: 2, username: 'user2', is_superuser: false },
]
const groups = [
{ id: 1, name: 'group1' },
{ id: 2, name: 'group2' },
]
const sso_groups = [
{ id: 1, name: 'sso_group_1', group: 1 },
{ id: 1, name: 'sso_group_2', group: 2 },
]
const mailAccounts = [
{ id: 1, name: 'account1' },
{ id: 2, name: 'account2' },
]
const mailRules = [
{ id: 1, name: 'rule1', owner: 1 },
{ id: 2, name: 'rule2', owner: 2 },
]
describe('SettingsComponent', () => {
let component: SettingsComponent
let fixture: ComponentFixture<SettingsComponent>
let modalService: NgbModal
let router: Router
let settingsService: SettingsService
let savedViewService: SavedViewService
let activatedRoute: ActivatedRoute
let viewportScroller: ViewportScroller
let toastService: ToastService
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
let ssoGroupService: SsoGroupService
let mailAccountService: MailAccountService
let mailRuleService: MailRuleService
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
SettingsComponent,
PageHeaderComponent,
IfPermissionsDirective,
CustomDatePipe,
ConfirmDialogComponent,
CheckComponent,
ColorComponent,
],
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
imports: [
NgbModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes(routes),
FormsModule,
ReactiveFormsModule,
],
}).compileComponents()
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
activatedRoute = TestBed.inject(ActivatedRoute)
viewportScroller = TestBed.inject(ViewportScroller)
toastService = TestBed.inject(ToastService)
settingsService = TestBed.inject(SettingsService)
userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
.mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserOwnsObject')
.mockReturnValue(true)
jest.spyOn(userService, 'listAll').mockReturnValue(
of({
all: users.map((u) => u.id),
count: users.length,
results: users.concat([]),
})
)
groupService = TestBed.inject(GroupService)
jest.spyOn(groupService, 'listAll').mockReturnValue(
of({
all: groups.map((g) => g.id),
count: groups.length,
results: groups.concat([]),
})
)
ssoGroupService = TestBed.inject(SsoGroupService)
jest.spyOn(ssoGroupService, 'listAll').mockReturnValue(
of({
all: sso_groups.map((g) => g.id),
count: sso_groups.length,
results: sso_groups.concat([]),
})
)
savedViewService = TestBed.inject(SavedViewService)
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
of({
all: savedViews.map((v) => v.id),
count: savedViews.length,
results: (savedViews as PaperlessSavedView[]).concat([]),
})
)
mailAccountService = TestBed.inject(MailAccountService)
jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
of({
all: mailAccounts.map((a) => a.id),
count: mailAccounts.length,
results: (mailAccounts as PaperlessMailAccount[]).concat([]),
})
)
mailRuleService = TestBed.inject(MailRuleService)
jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
of({
all: mailRules.map((r) => r.id),
count: mailRules.length,
results: (mailRules as PaperlessMailRule[]).concat([]),
})
)
fixture = TestBed.createComponent(SettingsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
const navigateSpy = jest.spyOn(router, 'navigate')
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'mail'])
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'usersgroups'])
const initSpy = jest.spyOn(component, 'initialize')
component.isDirty = true // mock dirty
navigateSpy.mockResolvedValueOnce(false) // nav rejected cause dirty
tabButtons[0].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'general'])
expect(initSpy).not.toHaveBeenCalled()
navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
expect(initSpy).toHaveBeenCalled()
})
it('should support direct link to tab by URL, scroll if needed', () => {
jest
.spyOn(activatedRoute, 'paramMap', 'get')
.mockReturnValue(of(convertToParamMap({ section: 'mail' })))
activatedRoute.snapshot.fragment = '#mail'
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
component.ngOnInit()
expect(component.activeNavID).toEqual(4) // Mail
component.ngAfterViewInit()
expect(scrollSpy).toHaveBeenCalledWith('#mail')
})
it('should lazy load tab data', () => {
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
expect(component.savedViews).toBeUndefined()
tabButtons[2].nativeElement.dispatchEvent(
new MouseEvent('mouseover', { bubbles: true })
)
expect(component.savedViews).not.toBeUndefined()
expect(component.mailAccounts).toBeUndefined()
tabButtons[3].nativeElement.dispatchEvent(
new MouseEvent('mouseover', { bubbles: true })
)
expect(component.mailAccounts).not.toBeUndefined()
expect(component.users).toBeUndefined()
tabButtons[4].nativeElement.dispatchEvent(
new MouseEvent('mouseover', { bubbles: true })
)
expect(component.users).not.toBeUndefined()
})
it('should support save saved views, show error', () => {
component.maybeInitializeTab(3) // SavedViews
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
// saved views error first
savedViewPatchSpy.mockReturnValueOnce(
throwError(() => new Error('unable to save saved views'))
)
component.saveSettings()
expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
toastSpy.mockClear()
toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear()
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(
of(savedViews as PaperlessSavedView[])
)
component.saveSettings()
expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
})
it('should support save local settings updating appearance settings and calling API, show error', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
const appearanceSettingsSpy = jest.spyOn(
settingsService,
'updateAppearanceSettings'
)
const setSpy = jest.spyOn(settingsService, 'set')
// error first
storeSpy.mockReturnValueOnce(
throwError(() => new Error('unable to save settings'))
)
component.saveSettings()
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(19)
// succeed
storeSpy.mockReturnValueOnce(of(true))
component.saveSettings()
expect(toastSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).toHaveBeenCalled()
})
it('should offer reload if settings changes require', () => {
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
component.initialize(true) // reset
component.store.getValue()['displayLanguage'] = 'en-US'
component.store.getValue()['updateCheckingEnabled'] = false
component.settingsForm.value.displayLanguage = 'en-GB'
component.settingsForm.value.updateCheckingEnabled = true
jest.spyOn(settingsService, 'storeSettings').mockReturnValueOnce(of(true))
component.saveSettings()
expect(toast.actionName).toEqual('Reload now')
})
it('should allow setting theme color, visually apply change immediately but not save', () => {
const appearanceSpy = jest.spyOn(
settingsService,
'updateAppearanceSettings'
)
const colorInput = fixture.debugElement.query(By.directive(ColorComponent))
colorInput.query(By.css('input')).nativeElement.value = '#ff0000'
colorInput
.query(By.css('input'))
.nativeElement.dispatchEvent(new Event('change'))
fixture.detectChanges()
expect(appearanceSpy).toHaveBeenCalled()
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('')
component.clearThemeColor()
})
it('should support delete saved view', () => {
component.maybeInitializeTab(3) // SavedViews
const toastSpy = jest.spyOn(toastService, 'showInfo')
const deleteSpy = jest.spyOn(savedViewService, 'delete')
deleteSpy.mockReturnValue(of(true))
component.deleteSavedView(savedViews[0] as PaperlessSavedView)
expect(deleteSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
`Saved view "${savedViews[0].name}" deleted.`
)
})
it('should support edit / create user, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0])
const editDialog = modal.componentInstance as UserEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved user "${users[0].username}".`
)
})
it('should support delete user, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteUser(users[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(userService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(userService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting user'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user')
})
it('should logout current user if password changed, after delay', fakeAsync(() => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0])
const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
tick(2600)
expect(window.location.href).toContain('logout')
}))
it('should support edit / create group, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editGroup(groups[0])
const editDialog = modal.componentInstance as GroupEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved group "${groups[0].name}".`
)
})
it('should support delete group, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteGroup(users[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(groupService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(groupService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group')
})
it('should get group name', () => {
component.maybeInitializeTab(5) // UsersGroups
expect(component.getGroupName(1)).toEqual(groups[0].name)
expect(component.getGroupName(11)).toEqual('')
})
it('should support edit / create mail account, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
const editDialog = modal.componentInstance as MailAccountEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailAccounts[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved account "${mailAccounts[0].name}".`
)
})
it('should support delete mail account, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(mailAccountService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(mailAccountService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting mail account'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
})
it('should support edit / create mail rule, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editMailRule(mailRules[0] as PaperlessMailRule)
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(mailRules[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved rule "${mailRules[0].name}".`
)
})
it('should support delete mail rule, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteMailRule(mailRules[0] as PaperlessMailRule)
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(mailRuleService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting mail rule'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
})
it('should support edit / create sso group, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editSSOGroup(sso_groups[0])
const editDialog = modal.componentInstance as SsoGroupEditDialogComponent
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(sso_groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved SSO group "${sso_groups[0].name}".`
)
})
it('should support delete SSO group, show error if needed', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.deleteSSOGroup(sso_groups[0])
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
const deleteSpy = jest.spyOn(ssoGroupService, 'delete')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const listAllSpy = jest.spyOn(ssoGroupService, 'listAll')
deleteSpy.mockReturnValueOnce(
throwError(() => new Error('error deleting sso group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted SSO group')
})
})

View File

@ -1,6 +1,6 @@
import { ObjectWithId } from './object-with-id'
export interface PaperlessSSOGroup extends ObjectWithId {
name?: string
group?: number
name: string
group: number
}

View File

@ -0,0 +1,4 @@
import { SsoGroupService } from './sso-group.service'
import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
commonAbstractNameFilterPaperlessServiceTests('sso_groups', SsoGroupService)