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
26 changes: 15 additions & 11 deletions src/app/core/_decorators/autotitle.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { AutoTitleService } from "../_services/shared/autotitle.service";
import { AppModule } from "src/app/app.module";
import { AutoTitleService } from '@services/shared/autotitle.service';

export function PageTitle(title: string | string[]): ClassDecorator {

return function (constructor: any){
const titleService = AppModule.injector.get(AutoTitleService);
import { AppModule } from '@src/app/app.module';

const ngOnInit = constructor.prototype.ngOnInit;
export function PageTitle(title: string | string[]): ClassDecorator {
return function (constructor: any) {
const originalNgOnInit = constructor.prototype.ngOnInit;

constructor.prototype.ngOnInit = function () {
ngOnInit && ngOnInit.apply(this);
titleService.set(title)
}
}
if (!this.__titleService) {
// Lazy get the service only once per instance
this.__titleService = AppModule.injector.get(AutoTitleService);
}
if (originalNgOnInit) {
originalNgOnInit.apply(this);
}

this.__titleService.set(title);
};
};
}
336 changes: 336 additions & 0 deletions src/app/home/home.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
import { of } from 'rxjs';

import { provideHttpClientTesting } from '@angular/common/http/testing';
import { Component, DebugElement, Injector, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { By } from '@angular/platform-browser';
import { RouterLink, RouterLinkWithHref, provideRouter } from '@angular/router';

import { Filter, FilterType, RequestParams } from '@models/request-params.model';
import { TaskType } from '@models/task.model';

import { SERV, ServiceConfig } from '@services/main.config';
import { GlobalService } from '@services/main.service';
import { PermissionService } from '@services/permission/permission.service';
import { LocalStorageService } from '@services/storage/local-storage.service';

import { AppModule } from '@src/app/app.module';
import { Perm, PermissionValues } from '@src/app/core/_constants/userpermissions.config';
import { PageTitle } from '@src/app/core/_decorators/autotitle';
import { HomeComponent } from '@src/app/home/home.component';

/**
* Stub component to replace the real app-heatmap-chart component in tests.
* Prevents the need to load its internal logic or dependencies.
*/
@Component({
selector: 'app-heatmap-chart',
template: '<div>Heatmap Chart Stub</div>'
})
class HeatmapChartStubComponent {
/** Input for heatmap data */
@Input() data: unknown;

/** Input for dark mode styling */
@Input() isDarkMode: boolean;
}

/**
* Dummy component used for routing configuration during tests.
* Acts as a placeholder target for router navigation.
*/
@Component({ template: '' })
class DummyComponent {}

/**
* Stub routes used during test setup to initialize the RouterTestingModule.
*/
const routes = [{ path: '', component: DummyComponent }];

/**
* Mock implementation of GlobalService.
* Simulates various backend service responses based on service type and filter params.
*/
const globalServiceMock = jasmine.createSpyObj('GlobalService', ['getAll']);

/**
* Conditional return logic for the mocked `getAll` method of GlobalService.
* Returns different counts depending on the service and filter parameters.
*/
globalServiceMock.getAll.and.callFake((service: ServiceConfig, params?: RequestParams) => {
// Handle TASKS_WRAPPER_COUNT — differentiate between Supertasks and regular Tasks
if (service === SERV.TASKS_WRAPPER_COUNT) {
const isSuperTask = params?.filter?.some(
(filter: Filter) => filter.field === 'taskType' && filter.value === TaskType.SUPERTASK
);

const isCompleted = params?.filter?.some(
(filter: Filter) => filter.field === 'keyspace' && filter.operator === FilterType.GREATER
);

// Return counts based on task type and completion status
if (isSuperTask) {
return of({ meta: { count: isCompleted ? 5 : 10 } }); // 5 completed, 10 total supertasks
} else {
return of({ meta: { count: isCompleted ? 15 : 30 } }); // 15 completed, 30 total tasks
}
}

// Handle AGENTS_COUNT — returns total and active agent counts
if (service === SERV.AGENTS_COUNT) {
return of({ meta: { total_count: 50, count: 20 } }); // 20 active, 50 total agents
}

// Handle HASHES_COUNT — returns number of cracked hashes
if (service === SERV.HASHES_COUNT) {
return of({ meta: { count: 7 } }); // 7 hashes cracked
}

// Handle HASHES — return an empty dataset for now
if (service === SERV.HASHES) {
return of({ data: [], included: [] }); // simulate empty response
}

// Default fallback for any other service
return of({ meta: { count: 0 }, results: [] });
});

/**
* Mock implementation of LocalStorageService for testing purposes.
* Simulates localStorage behavior by stubbing `getItem` and `setItem`.
*/
const mockLocalStorageService = {
/** Simulates retrieving a value from localStorage. Returns `null` by default. */
getItem: jasmine.createSpy('getItem').and.returnValue(null),

/** Simulates saving a value in localStorage. Does not persist anything. */
setItem: jasmine.createSpy('setItem')
};

/**
* Stub for PageTitleService used to mock the `set` method that updates the page title.
*/
const pageTitleStub = jasmine.createSpyObj('PageTitleService', ['set']);

/**
* Mock for the PermissionService.
* All permissions return `true` by default unless explicitly overridden.
*/
const permissionServiceMock = jasmine.createSpyObj('PermissionService', ['hasPermissionSync']);
permissionServiceMock.hasPermissionSync.and.returnValue(true);

describe('HomeComponent (template permissions and view)', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let debugEl: DebugElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HomeComponent],
imports: [MatCardModule, MatIconModule, RouterLink, RouterLinkWithHref, HeatmapChartStubComponent],
providers: [
provideHttpClientTesting(),
provideRouter(routes),
{ provide: GlobalService, useValue: globalServiceMock },
{ provide: LocalStorageService, useValue: mockLocalStorageService },
{ provide: PageTitle, useValue: pageTitleStub },
{ provide: PermissionService, useValue: permissionServiceMock }
]
}).compileComponents();

AppModule.injector = TestBed.inject(Injector);

fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;

component.activeAgents = 10;
component.totalAgents = 20;
component.completedTasks = 15;
component.totalTasks = 30;
component.completedSupertasks = 5;
component.totalSupertasks = 10;
component.totalCracks = 7;
component.heatmapData = [
['2025-07-01', 3],
['2025-07-02', 4]
];
component.lastUpdated = 'July 20, 2025 10:00 AM';
component.isDarkMode = false;

permissionServiceMock.hasPermissionSync.calls.reset();
globalServiceMock.getAll.calls.reset();

fixture.detectChanges();
component.setAutoreload(false);
});

it('should show agent count when permission is granted', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Agent.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const text = fixture.nativeElement.textContent;
const agentCard = debugEl.query(By.css('.co-25:nth-child(1)'));
expect(text).toContain('20 / 50');
expect(agentCard.nativeElement.textContent).not.toContain('No permission');
});

it('should show "No permission" for agents when canReadAgents false', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Agent.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const agentCard = debugEl.query(By.css('.co-25:nth-child(1)'));
expect(agentCard.nativeElement.textContent).toContain('No permission');
expect(agentCard.nativeElement.textContent).not.toContain('10 / 20');
});

it('should show tasks stats and hide "no permission" when canReadTasks true', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Task.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const tasksCard = debugEl.queryAll(By.css('.co-25'))[1];
expect(tasksCard.nativeElement.textContent).toContain('Tasks');
expect(tasksCard.nativeElement.textContent).toContain('15 / 30');
expect(tasksCard.query(By.css('.no-permission'))).toBeNull();
});

it('should show "No permission" for tasks when canReadTasks false', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Task.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const tasksCard = debugEl.queryAll(By.css('.co-25'))[1];
expect(tasksCard.nativeElement.textContent).toContain('No permission');
expect(tasksCard.nativeElement.textContent).not.toContain('15 / 30');
});

it('should show supertask count when permission is granted', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Task.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();

const text = fixture.nativeElement.textContent;
const noPermText = fixture.debugElement.query(By.css('.supertasks-section .no-permission'));
expect(text).toContain('5 / 10');
expect(noPermText).toBeNull();
});

it('should show "No permission" for supertasks when canReadTasks false', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Task.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const supertasksCard = debugEl.queryAll(By.css('.co-25'))[2];
expect(supertasksCard.nativeElement.textContent).toContain('No permission');
expect(supertasksCard.nativeElement.textContent).not.toContain('5 / 10');
});

it('should show cracks count and hide "no permission" when canReadCracks true', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Hash.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const cracksCard = debugEl.queryAll(By.css('.co-25'))[3];
expect(cracksCard.nativeElement.textContent).toContain('Cracks');
expect(cracksCard.nativeElement.textContent).toContain('7');
expect(cracksCard.query(By.css('.no-permission'))).toBeNull();
});

it('should show "No permission" for cracks when canReadCracks is false', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const cracksCard = debugEl.query(By.css('.co-25:nth-child(4)'));
const cracksValueSpan = cracksCard.query(By.css('.value'));
const noPermissionSpan = cracksCard.query(By.css('.no-permission'));
expect(cracksCard).toBeTruthy();
expect(cracksValueSpan).toBeNull();
expect(noPermissionSpan).toBeTruthy();
expect(noPermissionSpan.nativeElement.textContent).toContain('No permission');
});

it('should NOT show heatmap chart when canReadCracks false', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const heatmap = debugEl.query(By.directive(HeatmapChartStubComponent));
const chartContainer = debugEl.query(By.css('.app-echarts'));
const noPermText = chartContainer.query(By.css('.no-permission'));
expect(heatmap).toBeNull();
expect(chartContainer).toBeTruthy();
expect(noPermText).toBeTruthy();
expect(noPermText.nativeElement.textContent).toContain('No permission to view chart data');
});

it('should show "No permission to view chart data" when no permission for cracks', () => {
permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();

const chartContainer = debugEl.query(By.css('.app-echarts'));
const noPermText = chartContainer.query(By.css('.no-permission'));
expect(noPermText.nativeElement.textContent).toContain('No permission to view chart data');
});

it('should show enable auto reload button when refreshPage false', () => {
component.setAutoreload(false);
fixture.detectChanges();

const enableButton = debugEl.query(By.css('button[mattooltip="Enable Auto Reload"]'));
expect(enableButton).toBeTruthy();
});

it('should show pause auto reload button when refreshPage true', () => {
component.setAutoreload(true);
fixture.detectChanges();

const pauseButton = debugEl.query(By.css('button[mattooltip="Pause Auto Reload"]'));
expect(pauseButton).toBeTruthy();
});

it('should call setAutoreload(true) when enable button clicked', () => {
component.setAutoreload(false);
spyOn(component, 'setAutoreload');
fixture.detectChanges();

const enableButton = debugEl.query(By.css('button[mattooltip="Enable Auto Reload"]'));
enableButton.nativeElement.click();
expect(component.setAutoreload).toHaveBeenCalledWith(true);
});

it('should call setAutoreload(false) when pause button clicked', () => {
component.setAutoreload(true);
spyOn(component, 'setAutoreload');
fixture.detectChanges();

const pauseButton = debugEl.query(By.css('button[mattooltip="Pause Auto Reload"]'));
pauseButton.nativeElement.click();
expect(component.setAutoreload).toHaveBeenCalledWith(false);
});
});
4 changes: 2 additions & 2 deletions src/polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ import '@angular/localize/init';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.

import 'zone.js'; // Included with Angular CLI.

/***************************************************************************************************
* APPLICATION IMPORTS
*/
import 'reflect-metadata';