Skip to content

Commit

Permalink
[Issue #40] Enable user account editing. (#162)
Browse files Browse the repository at this point in the history
* [Issue #40] Ensure a non-admin cannot change a user's roles.

* [Issue #40] Ensure admin users can't change their own roles.

* [Issue #40] Show UserDetailsEditor as an tab for the user.
  • Loading branch information
mcpierce committed Jun 21, 2020
1 parent 6853667 commit 286e868
Show file tree
Hide file tree
Showing 21 changed files with 550 additions and 149 deletions.
27 changes: 16 additions & 11 deletions comixed-frontend/src/app/user/adaptors/user-admin.adaptor.spec.ts
Expand Up @@ -16,27 +16,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { UserAdminAdaptor } from './user-admin.adaptor';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { async, TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import {
reducer,
USER_ADMIN_FEATURE_KEY
} from 'app/user/reducers/user-admin.reducer';
import { UserAdminEffects } from 'app/user/effects/user-admin.effects';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MessageService } from 'primeng/api';
import { Store, StoreModule } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { AppState, User, USER_ADMIN, USER_READER } from 'app/user';
import {
UserAdminAllReceived,
UserAdminCreateNew, UserAdminDeleteUser,
UserAdminCreateNew,
UserAdminDeleteUser,
UserAdminGetAll,
UserAdminSave,
UserAdminSaved
} from 'app/user/actions/user-admin.actions';
import { UserAdminEffects } from 'app/user/effects/user-admin.effects';
import { SaveUserDetails } from 'app/user/models/save-user-details';
import {
reducer,
USER_ADMIN_FEATURE_KEY
} from 'app/user/reducers/user-admin.reducer';
import { LoggerTestingModule } from 'ngx-logger/testing';
import { MessageService } from 'primeng/api';
import { UserAdminAdaptor } from './user-admin.adaptor';

describe('UserAdminAdaptor', () => {
const USERS = [USER_ADMIN, USER_READER];
Expand All @@ -50,6 +52,7 @@ describe('UserAdminAdaptor', () => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
LoggerTestingModule,
TranslateModule.forRoot(),
StoreModule.forRoot({}),
StoreModule.forFeature(USER_ADMIN_FEATURE_KEY, reducer),
Expand Down Expand Up @@ -185,7 +188,9 @@ describe('UserAdminAdaptor', () => {
});

it('fires an action', () => {
expect(store.dispatch).toHaveBeenCalledWith(new UserAdminDeleteUser({user: USER}));
expect(store.dispatch).toHaveBeenCalledWith(
new UserAdminDeleteUser({ user: USER })
);
});
});
});
20 changes: 14 additions & 6 deletions comixed-frontend/src/app/user/adaptors/user-admin.adaptor.ts
Expand Up @@ -17,12 +17,8 @@
*/

import { Injectable } from '@angular/core';
import { AppState, User } from 'app/user';
import { BehaviorSubject, Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import * as _ from 'lodash';
import { USER_ADMIN_FEATURE_KEY } from 'app/user/reducers/user-admin.reducer';
import { filter } from 'rxjs/operators';
import { AppState, User } from 'app/user';
import {
UserAdminClearCurrent,
UserAdminCreateNew,
Expand All @@ -32,6 +28,11 @@ import {
UserAdminSetCurrent
} from 'app/user/actions/user-admin.actions';
import { SaveUserDetails } from 'app/user/models/save-user-details';
import { USER_ADMIN_FEATURE_KEY } from 'app/user/reducers/user-admin.reducer';
import * as _ from 'lodash';
import { NGXLogger } from 'ngx-logger';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

@Injectable()
export class UserAdminAdaptor {
Expand All @@ -42,11 +43,12 @@ export class UserAdminAdaptor {
private _saved$ = new BehaviorSubject<boolean>(false);
private _deleting$ = new BehaviorSubject<boolean>(false);

constructor(private store: Store<AppState>) {
constructor(private logger: NGXLogger, private store: Store<AppState>) {
this.store
.select(USER_ADMIN_FEATURE_KEY)
.pipe(filter(state => !!state))
.subscribe(state => {
this.logger.debug('user admin state updated:', state);
if (!_.isEqual(state.users, this._user$.getValue())) {
this._user$.next(state.users);
}
Expand All @@ -69,6 +71,7 @@ export class UserAdminAdaptor {
}

getAllUsers() {
this.logger.debug('action: get all users');
this.store.dispatch(new UserAdminGetAll());
}

Expand All @@ -81,10 +84,12 @@ export class UserAdminAdaptor {
}

createNewUser(): void {
this.logger.debug('action: create new user');
this.store.dispatch(new UserAdminCreateNew());
}

saveUser(details: SaveUserDetails): void {
this.logger.debug('action: saving user:', details);
this.store.dispatch(new UserAdminSave({ details: details }));
}

Expand All @@ -101,14 +106,17 @@ export class UserAdminAdaptor {
}

setCurrent(user: User): void {
this.logger.debug('action: set current user:', user);
this.store.dispatch(new UserAdminSetCurrent({ user: user }));
}

clearCurrent(): void {
this.logger.debug('action: clear current user');
this.store.dispatch(new UserAdminClearCurrent());
}

deleteUser(user: User): void {
this.logger.debug('action: delete user:', user);
this.store.dispatch(new UserAdminDeleteUser({ user: user }));
}

Expand Down
@@ -1,13 +1,12 @@
<div id='user-edit-form-container'>
<div id='user-edit-form'>
<h2>{{user?.email || "admin.user-editor.label.new-user"|translate}}</h2>
<form [formGroup]='userForm'>
<div>
<label for='email'
class='cx-input-label'>{{"user-details-editor.label.email-address"|translate}}</label>
<input id='email'
pInputText
class='form-control'
class='form-control cx-input-field'
type='text'
formControlName='email'/>
<span *ngIf='userForm.controls.email.errors?.required'
Expand All @@ -20,46 +19,46 @@ <h2>{{user?.email || "admin.user-editor.label.new-user"|translate}}</h2>
class='cx-input-label'>{{"user-details-editor.label.password"|translate}}</label>
<input id='password'
pInputText
class='form-control'
class='form-control cx-input-field'
type='password'
formControlName='password'/>
<span *ngIf='userForm.controls.password.errors?.required'
class='cx-form-validation-error'>{{'user-details-editor.user-form.password-required'|translate}}</span>
<span *ngIf='userForm.controls.password.errors?.minlength'
class='cx-form-validation-error'>{{'user-details-editor.user-form.password-required-length'|translate:{length: userForm.controls.password.errors.minlength.requiredLength} }}</span>
class='cx-form-validation-error'>{{'user-details-editor.user-form.password-min-length'|translate:{length: userForm.controls.password.errors.minlength.requiredLength} }}</span>
<span *ngIf='userForm.controls.password.errors?.maxlength'
class='cx-form-validation-error'>{{'user-details-editor.user-form.password-max-length'|translate:{length: userForm.controls.password.errors.maxlength.requiredLength} }}</span>
</div>
<div>
<label for='passwordVerify'
class='cx-input-label'>{{"user-details-editor.label.password-verify"|translate}}</label>
<input id='passwordVerify'
pInputText
type='password'
class='form-control'
class='form-control cx-input-field'
formControlName='passwordVerify'/>
<span *ngIf='userForm.controls.passwordVerify.errors?.mustMatch'
class='cx-form-validation-error'>{{'user-details-editor.user-form.passwords-must-match'|translate}}</span>
</div>
<div>
<p-toggleButton onLabel='{{"user-details-editor.label.user-is-admin"|translate}}'
offLabel='{{"user-details-editor.label.user-is-reader"|translate}}'
onIcon='fa fa-fw fa-user-secret'
offIcon='fa fa-fw fa-user'
formControlName='isAdmin'></p-toggleButton>
<div *ngIf='isAdmin'>
<label for='is-admin'
class='cx-input-label'>{{(adminIsSet() ? "user-details-editor.label.user-is-admin" : "user-details-editor.label.user-is-reader")|translate}}</label>
<p-inputSwitch id='is-admin'
formControlName='isAdmin'></p-inputSwitch>
</div>
</form>
<div class='cx-button-box-vertical'>
<button pButton
class='cx-selection-button ui-button-primary'
[disabled]='!userForm.valid'
icon='fa fa-fw fa-save'
label='{{"button.save"|translate}}'
(click)='saveUser()'></button>
<button pButton
class='cx-selection-button ui-button-danger'
icon='fa fa-fw fa-reset'
label='{{"button.reset"|translate}}'
(click)='resetUser()'></button>
</div>
</div>

<div class='cx-button-box-vertical'>
<button pButton
class='cx-selection-button ui-button-primary'
[disabled]='!userForm.valid'
icon='fa fa-fw fa-save'
label='{{"button.save"|translate}}'
(click)='saveUser()'></button>
<button pButton
class='cx-selection-button ui-button-danger'
icon='fa fa-fw fa-reset'
label='{{"button.reset"|translate}}'
(click)='resetUser()'></button>
</div>
</div>
</div>
Expand Up @@ -4,6 +4,5 @@
}

#user-edit-form {
border: 1px #000 solid;
padding: 25px;
}
Expand Up @@ -18,14 +18,19 @@

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { USER_ADMIN } from 'app/user';
import { SaveUserDetails } from 'app/user/models/save-user-details';
import { ButtonModule } from 'primeng/button';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { InputSwitchModule } from 'primeng/inputswitch';
import { UserDetailsEditorComponent } from './user-details-editor.component';
import { EffectsModule } from '@ngrx/effects';

describe('UserDetailsEditorComponent', () => {
const USER = USER_ADMIN;
const PASSWORD = 'this!is!a!password';

let component: UserDetailsEditorComponent;
let fixture: ComponentFixture<UserDetailsEditorComponent>;

Expand All @@ -38,7 +43,7 @@ describe('UserDetailsEditorComponent', () => {
EffectsModule.forRoot([]),
TranslateModule.forRoot(),
ButtonModule,
ToggleButtonModule
InputSwitchModule
],
declarations: [UserDetailsEditorComponent]
}).compileComponents();
Expand All @@ -51,4 +56,119 @@ describe('UserDetailsEditorComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

describe('setting the user', () => {
beforeEach(() => {
component.userForm.controls['email'].setValue('');
component.user = USER;
});

it('loads the email field', () => {
expect(component.userForm.controls['email'].value).toEqual(USER.email);
});

it('returns the same user', () => {
expect(component.user).toEqual(USER);
});
});

describe('requiring passwords to be valid', () => {
beforeEach(() => {
component.userForm.controls['email'].setValue(USER.email);
component.userForm.controls['password'].setValue(PASSWORD);
component.userForm.controls['passwordVerify'].setValue(PASSWORD);
component.requirePassword = true;
});

it('makes the password required', () => {
component.userForm.controls['password'].setValue('');
expect(component.userForm.valid).toBeFalsy();
});

it('requires the password verification', () => {
component.userForm.controls['passwordVerify'].setValue('');
expect(component.userForm.valid).toBeFalsy();
});
});

describe('not requiring passwords to be valid', () => {
beforeEach(() => {
component.userForm.controls['email'].setValue(USER.email);
component.userForm.controls['password'].setValue(PASSWORD);
component.userForm.controls['passwordVerify'].setValue(PASSWORD);
component.requirePassword = false;
});

it('allows passwords to be blank', () => {
component.userForm.controls['password'].setValue('');
component.userForm.controls['passwordVerify'].setValue('');
expect(component.userForm.valid).toBeTruthy();
});

it('still requires the password match', () => {
component.userForm.controls['passwordVerify'].setValue('');
expect(component.userForm.valid).toBeFalsy();
});
});

describe('saving changes to the user', () => {
beforeEach(() => {
component.user = USER;
component.userForm.controls['password'].setValue(PASSWORD);
component.userForm.controls['passwordVerify'].setValue(PASSWORD);
});

it('sends a notification', () => {
component.save.subscribe(result => {
expect(result).toEqual({
id: USER.id,
email: USER.email,
password: PASSWORD,
isAdmin: true
} as SaveUserDetails);
});

component.saveUser();
});
});

describe('resetting the form', () => {
beforeEach(() => {
component.user = USER;
component.userForm.controls['email'].setValue(USER.email.substr(1));
component.userForm.controls['email'].markAsDirty();
});

it('can restore original values', () => {
component.resetUser();
expect(component.userForm.controls['email'].value).toEqual(USER.email);
});

it('marks the form as pristine', () => {
component.resetUser();
expect(component.userForm.dirty).toBeFalsy();
});
});

describe('setting the admin flag', () => {
beforeEach(() => {
component.isAdmin = true;
});

it('updates the state', () => {
expect(component.isAdmin).toBeTruthy();
});
});

describe('setting the admin toggle switch', () => {
it('can be turned on', () => {
component.userForm.controls['isAdmin'].setValue(true);
expect(component.adminIsSet()).toBeTruthy();
});

it('can be turned off', () => {
component.userForm.controls['isAdmin'].setValue(false);
expect(component.adminIsSet()).toBeFalsy();
});
});
});

0 comments on commit 286e868

Please sign in to comment.