I am making a site to maintain some devices. There is a reactive form to create new devices, and because i will need it on multiple pages, i made a dedicated component with the device FormGroup. This way i can reuse it in other forms. The device also has an access-control-list (acl) with rules that determine who can access the device and who not. This acl is also used for locations, and users. So i made an acl-form component, to be able to reuse this form. This acl-form populates a formArray.
The acl-form has 2 extra functions, the prepareACL and the sync. The first function is used to create the FormControls for the access control entries, and to populate them with data. the second function is used to do some data manipulation because the data in the form can't be send to the server as-is.
This all works fine, but now i wanted to write some tests for the forms. Testing the acl-form itself was no problem, because all the dependencies could be easily mocked. The device-form on the other hand is a lot harder. When i was writing the tests, i could only get it to work if i mocked all the dependencies of the device-form (what is normal) and the dependencies of the acl-form. But as the acl-form is already tested by itself, i don't want to bother about it.
My questions are, how can i mock the acl-form so i don't need to provide all its dependencies, and how can i spy on the prepareACL-function and sync-function, to make sure they are called?
here is my code:
acl-form.component.ts:
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormArray, Validators } from '@angular/forms';
import { UserService } from '../../../services/user.service';
@Component({
selector: 'acl-form',
templateUrl: './acl-form.component.html'
})
export class ACLFormComponent implements OnInit {
@Input()
public group: FormArray;
public users: Object[];
public formErrors = {};
build() {
let group = this.fb.array([]);
return group;
}
constructor(private fb: FormBuilder, private userService: UserService) { }
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
buildACE() {
let group = this.fb.group({
guid: ['', []],
user: ['', [Validators.required]],
permission: ['', []],
read: [{ value: '', disabled: true }, []],
write: [{ value: '', disabled: true }, []],
execute: [{ value: '', disabled: true }, []]
});
group.controls['user'].valueChanges.subscribe(guid => {
if (guid && guid !== '') {
group.controls['read'].enable();
group.controls['write'].enable();
group.controls['execute'].enable();
} else {
group.controls['read'].disable();
group.controls['write'].disable();
group.controls['execute'].disable();
}
});
return group;
}
createACEs(count: Number) {
const control = this.group;
while (control.length) {
control.removeAt(0);
}
for (let i = 0; i < count; i++) {
control.push(this.buildACE());
}
}
addACE() {
this.group.push(this.buildACE());
}
removeACE(i: number) {
this.group.removeAt(i);
}
encodePermission(ace) {
let read = ace.read;
let write = ace.write;
let execute = ace.execute;
let permission = 0;
permission += read ? 4 : 0;
permission += write ? 2 : 0;
permission += execute ? 1 : 0;
ace.permission = permission;
}
decodePermission(ace) {
let permission = ace.permission;
ace.read = (permission & 4) > 0;
ace.write = (permission & 2) > 0;
ace.execute = (permission & 1) > 0;
}
encodePermissions() {
let acl = this.group.value;
for (let i = 0; i < acl.length; i++) {
this.encodePermission(acl[i]);
// remove secondary fields to`enter code here` prevent api necking about it
delete acl[i].read;
delete acl[i].write;
delete acl[i].execute;
// remove guid to prevent api necking about it
if (acl[i].guid === '') {
delete acl[i].guid;
}
}
}
decodePermissions(acl) {
for (let i = 0; i < acl.length; i++) {
this.decodePermission(acl[i]);
}
}
prepareACL(acl) {
this.createACEs(acl.length);
this.decodePermissions(acl);
}
// assign removedACE to the entity
handleRemovedACE(entity) {
let acl = this.group.value;
if (entity.acl) {
// remove acl
acl = acl.filter(x => x.permission > 0);
entity.removedACE = [...entity.acl].filter(x => acl.find(y => y.guid === x['guid']) === undefined)
.map(x => x['guid']);
} else {
console.error('no acl entry found');
}
entity.acl = acl;
}
sync(entity) {
this.encodePermissions();
this.handleRemovedACE(entity);
}
}
device.component.ts:
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { EnvironmentService } from '../../../services/environment.service';
import { ResidenceService } from '../../../services/residence.service';
import { DeviceService } from '../../../services/device.service';
import { FormUtility } from '../../utility/form-utility';
import { macValidator } from '../../validators/global.validators';
import { ACLFormComponent } from '../acl-form/acl-form.component';
import { NetworkConfigFormComponent } from '../../components/network-config-form/network-config-form.component';
@Component({
selector: 'device-form',
templateUrl: './device-form.component.html'
})
export class DeviceFormComponent implements OnInit {
@ViewChild(ACLFormComponent)
public aclForm: ACLFormComponent;
@ViewChild(NetworkConfigFormComponent)
public networkConfigForm: NetworkConfigFormComponent;
@Input()
public group: FormGroup;
public states: String[];
public types: Object[];
public environments: Object[];
public residences: Object[];
private residence: Object;
public rooms: Object[];
private onValueChanged: Function;
public formErrors = {};
private validationMessages = {
mac: {
required: 'mac address is required',
mac: 'Ivalid mac address'
}
};
constructor(
private fb: FormBuilder,
private deviceService: DeviceService,
private environmentService: EnvironmentService,
private residenceService: ResidenceService
) { }
ngOnInit() {
this.deviceService.getDeviceStates().subscribe(states => {
this.states = states;
});
this.deviceService.getDeviceTypes().subscribe(types => {
this.types = types;
});
this.environmentService.getEnvironmentList().subscribe(envs => {
this.environments = envs;
});
this.onValueChanged = FormUtility.valueChangeGenerator(this.group, this.formErrors, this.validationMessages);
this.group.valueChanges.subscribe(data => {
this.onValueChanged(data);
});
this.group.controls['environment'].valueChanges.subscribe(data => {
this.residenceService.getResidencesInEnvironmentList(data).subscribe(res => {
this.group.controls['residence'].enable();
this.residences = res;
// add empty residence to make it possible only select environment
let emptyRes = {name: '-', guid: ''};
this.residences.push(emptyRes);
});
});
this.group.controls['residence'].valueChanges.subscribe(data => {
this.residenceService.getResidence(data).subscribe(res => {
this.group.controls['room'].enable();
this.residence = res;
this.rooms = res.room;
if (this.rooms) {
// add empty room to make it possible only select residence and environment
this.rooms.push({comment: '-', guid: ''});
}
});
});
}
build() {
return this.fb.group({
mac: [
'', [
Validators.required,
macValidator
]
],
status: [
'', []
],
type: [
'', []
],
network: this.fb.group({}),
environment: [
'', [
Validators.required
],
],
residence: [
{
value: '',
disabled: true
}, [
]
],
room: [
{
value: '',
disabled: true
}, [
]
],
acl: this.fb.group({})
});
}
sync(device) {
// encode befor getting group.value because encode is working on the formdata itself
// encode permissions rwx => 1-7 and remove entries with permission === 0
this.aclForm.sync(device);
// handle parent_path
let formdata = this.group.value;
device.parent_path = ',' + formdata.environment + ',' + formdata.residence + ',' + formdata.room + ',';
}
patchValue(entity) {
this.aclForm.prepareACL(entity.acl);
// get parent_path objects
if (entity.parent_path && entity.parent_path !== '') {
let chunks = entity.parent_path.split(',');
if (chunks.length === 5) {
entity.environment = chunks[1];
entity.residence = chunks[2];
entity.room = chunks[3];
}
}
this.group.patchValue(entity);
}
}
StringandObjecttypes, note the initial caps, is an extremely dangerous practice and does not do what you likely think it does. Usestringand{}. The types of the capitalized variants refer to the boxed intrinsics produced by calling them as constructor functions and have problematic behavior. UsingFunctionas a type is also highly suspect.(...args: {}[]) => {}or(...args: any[]) => any