Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions modules/ui/src/app/mocks/certificate.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@ export const certificate = {
} as Certificate;

export const certificate_uploading = {
name: 'test',
name: 'name.cert',
uploading: true,
} as Certificate;

export const certificate2 = {
name: 'sensor.bms.google.com',
name: 'sensor.bms.google.com.cert',
organisation: 'Google, Inc.',
expires: '2024-09-01T09:00:12Z',
} as Certificate;

export const INVALID_FILE = {
name: 'some very long strange name with symbols!?.jpg',
size: 7000,
} as File;

export const FILE = {
name: 'name.cert',
size: 3000,
} as File;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
*ngIf="certificate.uploading"
mode="indeterminate"></mat-progress-bar>
</div>

<button
[disabled]="certificate.uploading"
class="certificate-item-delete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@
--mdc-linear-progress-active-indicator-color: #1967d2;
}
}

:host:first-child .certificate-item-container {
border-top: 1px solid $lighter-grey;
}

.certificate-item-container {
display: grid;
grid-template-columns: 24px minmax(200px, 1fr) 24px;
gap: 16px;
box-sizing: border-box;
height: 88px;
padding: 12px 0;
border-bottom: 1px solid $lighter-grey;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('CertificateItemComponent', () => {
const deleteSpy = spyOn(component.deleteButtonClicked, 'emit');
deleteButton.click();

expect(deleteSpy).toHaveBeenCalledWith('iot.bms.google.com');
expect(deleteSpy).toHaveBeenCalledWith(certificate.name);
});
});
});
Expand All @@ -83,7 +83,7 @@ describe('CertificateItemComponent', () => {
it('should have loader', () => {
const loader = compiled.querySelector('mat-progress-bar');

expect(loader).toBeDefined();
expect(loader).not.toBeNull();
});

it('should have disabled delete button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { provideAnimations } from '@angular/platform-browser/animations';
import { MatError } from '@angular/material/form-field';

@Component({
selector: 'app-certificate-item',
standalone: true,
imports: [MatIcon, MatButtonModule, MatProgressBarModule, CommonModule],
imports: [
MatIcon,
MatButtonModule,
MatProgressBarModule,
CommonModule,
MatError,
],
providers: [provideAnimations()],
templateUrl: './certificate-item.component.html',
styleUrl: './certificate-item.component.scss',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
#file_input
id="default-file-input"
type="file"
accept=".cert,.crt,.pem,.cer"
(change)="fileChange($event)" />
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { MatButtonModule } from '@angular/material/button';
export class CertificateUploadButtonComponent {
@Output() fileChanged = new EventEmitter<File>();
fileChange(event: Event) {
const fileList = (event.target as HTMLInputElement).files;

const input = event.target as HTMLInputElement;
const fileList = input.files;
if (fileList && fileList.length < 1) {
return;
}
Expand All @@ -21,5 +21,7 @@ export class CertificateUploadButtonComponent {
const file: File = fileList[0];

this.fileChanged.emit(file);
input.value = '';
input.dispatchEvent(new Event('change'));
}
}
36 changes: 36 additions & 0 deletions modules/ui/src/app/pages/certificates/certificate.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const FILE_SIZE = 4;
export const FILE_NAME_LENGTH = 24;
const FILE_NAME_REGEXP = new RegExp('^[\\w.-]{1,24}$', 'u');
const FILE_EXT_REGEXP = new RegExp('(\\.cert|\\.crt|\\.pem|\\.cer)$', 'i');

export const getValidationErrors = (file: File): string[] => {
const errors = [];
errors.push(validateFileName(file.name));
errors.push(validateExtension(file.name));
errors.push(validateFileNameLength(file.name));
errors.push(validateSize(file.size));
// @ts-expect-error null values are filtered
return errors.filter(error => error !== null);
};
const validateFileName = (name: string): string | null => {
const result = FILE_NAME_REGEXP.test(name);
return !result
? 'The file name should be alphanumeric, symbols -_. are allowed.'
: null;
};

const validateExtension = (name: string): string | null => {
const result = FILE_EXT_REGEXP.test(name);
return !result ? 'File extension must be .cert, .crt, .pem, .cer.' : null;
};

const validateFileNameLength = (name: string): string | null => {
return name.length > FILE_NAME_LENGTH
? `Max name length is ${FILE_NAME_LENGTH} characters.`
: null;
};

const validateSize = (size: number): string | null => {
const result = size > FILE_SIZE * 1000;
return result ? `File size should be a max of ${FILE_SIZE}KB` : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { MatDialogRef } from '@angular/material/dialog';
import { DeleteFormComponent } from '../../components/delete-form/delete-form.component';
import { TestRunService } from '../../services/test-run.service';
import { NotificationService } from '../../services/notification.service';
import { FILE, INVALID_FILE } from '../../mocks/certificate.mock';

describe('CertificatesComponent', () => {
let component: CertificatesComponent;
Expand All @@ -43,11 +44,12 @@ describe('CertificatesComponent', () => {
jasmine.createSpyObj(['notify']);

beforeEach(async () => {
mockService = jasmine.createSpyObj([
mockService = jasmine.createSpyObj('mockService', [
'fetchCertificates',
'deleteCertificate',
'uploadCertificate',
]);
mockService.uploadCertificate.and.returnValue(of(true));
mockService.deleteCertificate.and.returnValue(of(true));
mockService.fetchCertificates.and.returnValue(
of([certificate, certificate])
Expand All @@ -65,6 +67,7 @@ describe('CertificatesComponent', () => {
fixture = TestBed.createComponent(CertificatesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
mockService.uploadCertificate.calls.reset();
});

it('should create', () => {
Expand Down Expand Up @@ -175,5 +178,19 @@ describe('CertificatesComponent', () => {
flush();
}));
});

describe('#uploadFile', () => {
it('should not call uploadCertificate if file has errors', () => {
component.uploadFile(INVALID_FILE);

expect(mockService.uploadCertificate).not.toHaveBeenCalled();
});

it('should call uploadCertificate if there is no errors', () => {
component.uploadFile(FILE);

expect(mockService.uploadCertificate).toHaveBeenCalled();
});
});
});
});
15 changes: 11 additions & 4 deletions modules/ui/src/app/pages/certificates/certificates.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CertificatesStore } from './certificates.store';
import { DeleteFormComponent } from '../../components/delete-form/delete-form.component';
import { Subject, takeUntil } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { FILE_NAME_LENGTH } from './certificate.validator';

@Component({
selector: 'app-certificates',
Expand Down Expand Up @@ -67,14 +68,14 @@ export class CertificatesComponent implements OnDestroy {
this.store.uploadCertificate(file);
}

deleteCertificate(name: string) {
this.store.selectCertificate(name);
deleteCertificate(certificate: string) {
this.store.selectCertificate(certificate);

const dialogRef = this.dialog.open(DeleteFormComponent, {
ariaLabel: 'Delete certificate',
data: {
title: 'Delete certificate',
content: `You are about to delete a certificate ${name}. Are you sure?`,
content: `You are about to delete a certificate ${this.getShortCertificateName(certificate)}. Are you sure?`,
},
autoFocus: true,
hasBackdrop: true,
Expand All @@ -87,7 +88,7 @@ export class CertificatesComponent implements OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe(deleteCertificate => {
if (deleteCertificate) {
this.store.deleteCertificate(name);
this.store.deleteCertificate(certificate);
this.focusNextButton();
}
});
Expand All @@ -108,4 +109,10 @@ export class CertificatesComponent implements OnDestroy {
menuButton?.focus();
}
}

private getShortCertificateName(name: string) {
return name.length <= FILE_NAME_LENGTH
? name
: `${name.substring(0, FILE_NAME_LENGTH)}...`;
}
}
56 changes: 38 additions & 18 deletions modules/ui/src/app/pages/certificates/certificates.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
certificate,
certificate2,
certificate_uploading,
FILE,
INVALID_FILE,
} from '../../mocks/certificate.mock';
import { CertificatesStore } from './certificates.store';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
Expand Down Expand Up @@ -111,33 +113,51 @@ describe('CertificatesStore', () => {
});

describe('uploadCertificate', () => {
const uploadingCertificate = certificate_uploading;

beforeEach(() => {
mockService.uploadCertificate.and.returnValue(of(true));
mockService.fetchCertificates.and.returnValue(of([certificate]));
});

it('should update certificates', done => {
certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
expect(store.certificates).toContain(uploadingCertificate);
});
describe('with valid certificate file', () => {
it('should update certificates', done => {
const uploadingCertificate = certificate_uploading;

certificateStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => {
expect(store.certificates).toEqual([certificate]);
done();
certificateStore.viewModel$
.pipe(skip(1), take(1))
.subscribe(store => {
expect(store.certificates).toContain(uploadingCertificate);
});

certificateStore.viewModel$
.pipe(skip(2), take(1))
.subscribe(store => {
expect(store.certificates).toEqual([certificate]);
done();
});

certificateStore.uploadCertificate(FILE);
});

certificateStore.uploadCertificate(new File([], 'test'));
it('should notify', () => {
certificateStore.uploadCertificate(FILE);
expect(notificationServiceMock.notify).toHaveBeenCalledWith(
'Certificate successfully added.\niot.bms.google.com by Google, Inc. valid until 01 Sep 2024',
0,
'certificate-notification'
);
});
});

it('should notify', () => {
certificateStore.uploadCertificate(new File([], 'test'));
expect(notificationServiceMock.notify).toHaveBeenCalledWith(
'Certificate successfully added.\niot.bms.google.com by Google, Inc. valid until 01 Sep 2024',
0,
'certificate-notification'
);
describe('with invalid certificate file', () => {
it('should notify about errors', () => {
certificateStore.uploadCertificate(INVALID_FILE);

expect(notificationServiceMock.notify).toHaveBeenCalledWith(
'The file name should be alphanumeric, symbols -_. are allowed.\nFile extension must be .cert, .crt, .pem, .cer.\nMax name length is 24 characters.\nFile size should be a max of 4KB',
0,
'certificate-notification'
);
});
});
});

Expand All @@ -152,7 +172,7 @@ describe('CertificatesStore', () => {
done();
});

certificateStore.deleteCertificate('iot.bms.google.com');
certificateStore.deleteCertificate(certificate.name);
});
});
});
Expand Down
Loading