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
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ module.exports = {
collectCoverage: true,
modulePathIgnorePatterns: ['<rootDir>/dist/'],
coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
moduleNameMapper: {
'^@luigi-project/client-support-angular$': '<rootDir>/projects/lib/_mocks_/luigi-client-support-angular.ts',
},
};
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@angular-builders/jest": "^20.0.0",
"@angular-devkit/build-angular": "^20.0.0",
"@angular-eslint/builder": "^20.2.0",
"@angular/cli": "^20.2.0",
"@angular/build": "^20.2.1",
"@angular/cli": "^20.2.0",
"@angular/compiler-cli": "^20.2.1",
"@angular/localize": "^20.2.1",
"@briebug/jest-schematic": "^6.0.0",
Expand All @@ -30,11 +30,11 @@
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jmespath": "0.16.0",
"mkdirp": "^3.0.1",
"ng-packagr": "^20.2.0",
"ts-jest": "29.3.2",
"typescript": "~5.8.0",
"nodemon": "3.1.10",
"rimraf": "6.0.1",
"mkdirp": "^3.0.1",
"nodemon": "3.1.10"
"ts-jest": "29.3.2",
"typescript": "~5.8.0"
}
}
6 changes: 6 additions & 0 deletions projects/lib/_mocks_/luigi-client-support-angular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Observable, of } from 'rxjs';
export class LuigiContextService {
contextObservable(): Observable<{ context: any }> {
return of({ context: null });
}
}
32 changes: 32 additions & 0 deletions projects/lib/_mocks_/ui5‑mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component } from '@angular/core';
import { ButtonComponent } from '@ui5/webcomponents-ngx';

@Component({ selector: 'ui5-component', template: '', standalone: true })
export class MockComponent {}

jest.mock('@ui5/webcomponents-ngx', () => {
return {
BreadcrumbsComponent: MockComponent,
BreadcrumbsItemComponent: MockComponent,
ButtonComponent: MockComponent,
DialogComponent: MockComponent,
DynamicPageComponent: MockComponent,
DynamicPageHeaderComponent: MockComponent,
DynamicPageTitleComponent: MockComponent,
IconComponent: MockComponent,
IllustratedMessageComponent: MockComponent,
InputComponent: MockComponent,
LabelComponent: MockComponent,
OptionComponent: MockComponent,
SelectComponent: MockComponent,
TableCellComponent: MockComponent,
TableComponent: MockComponent,
TableHeaderCellComponent: MockComponent,
TableHeaderRowComponent: MockComponent,
TableRowComponent: MockComponent,
TextComponent: MockComponent,
TitleComponent: MockComponent,
ToolbarButtonComponent: MockComponent,
ToolbarComponent: MockComponent,
};
});
1 change: 1 addition & 0 deletions projects/lib/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
module.exports = {
displayName: 'lib',
coverageDirectory: path.resolve(__dirname, '../../coverage/lib'),
setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`],
coverageThreshold: {
global: {
branches: 67,
Expand Down
1 change: 1 addition & 0 deletions projects/lib/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jest.requireMock('./_mocks_/ui5‑mock');
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="organization-management">
<div>
{{ texts.explanation }}
</div>

<div>
<ui5-label for="select-switch" show-colon>{{
texts.switchOrganization.label
}}</ui5-label
><br />
<div class="organization-management-input">
<ui5-select
id="select-switch"
[value]="organizationToSwitch"
(input)="setOrganizationToSwitch($event)"
(change)="setOrganizationToSwitch($event)"
>
@for (org of organizations(); track org) {
<ui5-option [value]="org" [selected]="org === organizationToSwitch">{{
org
}}</ui5-option>
}
</ui5-select>
<ui5-button design="Emphasized" (ui5Click)="switchOrganization()">{{
texts.switchOrganization.button
}}</ui5-button>
</div>
</div>

<div>
<ui5-label for="input-onboard" show-colon>{{
texts.onboardOrganization.label
}}</ui5-label
><br />
<div class="organization-management-input">
<ui5-input
id="input-onboard"
placeholder="{{ texts.onboardOrganization.placeholder }}"
[(ngModel)]="newOrganization"
></ui5-input>
<ui5-button design="Emphasized" (ui5Click)="onboardOrganization()">{{
texts.onboardOrganization.button
}}</ui5-button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.organization-management {
margin: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

.organization-management-input {
display: flex;
align-items: center;
gap: 1rem;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import {
CUSTOM_ELEMENTS_SCHEMA,
NO_ERRORS_SCHEMA,
signal
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MutationResult } from '@apollo/client';
import { LuigiContextService } from '@luigi-project/client-support-angular';
import {
ClientEnvironment, EnvConfigService,
I18nService,
LuigiCoreService, LuigiGlobalContext, NodeContext, ResourceService
} from '@openmfp/portal-ui-lib';
import { of, throwError } from 'rxjs';
import { OrganizationManagementComponent } from './organization-management.component';

describe('OrganizationManagementComponent', () => {
let component: OrganizationManagementComponent;
let fixture: ComponentFixture<OrganizationManagementComponent>;
let resourceServiceMock: jest.Mocked<ResourceService>;
let i18nServiceMock: jest.Mocked<I18nService>;
let luigiCoreServiceMock: jest.Mocked<LuigiCoreService>;
let envConfigServiceMock: jest.Mocked<EnvConfigService>;

beforeEach(async () => {
resourceServiceMock = {
readOrganizations: jest.fn(),
create: jest.fn(),
} as any;

i18nServiceMock = {
translationTable: {},
getTranslation: jest.fn(),
} as any;

luigiCoreServiceMock = {
getGlobalContext: jest.fn(),
showAlert: jest.fn(),
} as any;

envConfigServiceMock = {
getEnvConfig: jest.fn(),
} as any;

await TestBed.configureTestingModule({
imports: [OrganizationManagementComponent, FormsModule],
providers: [
{ provide: ResourceService, useValue: resourceServiceMock },
{ provide: I18nService, useValue: i18nServiceMock },
{ provide: LuigiCoreService, useValue: luigiCoreServiceMock },
{ provide: EnvConfigService, useValue: envConfigServiceMock },
LuigiContextService,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
})
.overrideComponent(OrganizationManagementComponent, {
set: { template: '', imports: [] },
})
.compileComponents();

fixture = TestBed.createComponent(OrganizationManagementComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should react to context input change', () => {
const mockContext = {
translationTable: { hello: 'world' },
} as any as NodeContext;

resourceServiceMock.readOrganizations.mockReturnValue(of({} as any));

const contextSignal = signal<NodeContext | null>(mockContext);
component.context = contextSignal as any;

fixture.detectChanges();

expect(component['i18nService'].translationTable).toEqual(
mockContext.translationTable,
);
});

it('should initialize with empty organizations', () => {
expect(component.organizations()).toEqual([]);
});

it('should read organizations on init', () => {
const mockOrganizations = {
Accounts: [
{ metadata: { name: 'org1' } },
{ metadata: { name: 'org2' } },
],
};
const mockGlobalContext: LuigiGlobalContext = {
portalContext: {},
userId: 'user1',
userEmail: '[email protected]',
token: 'token',
organization: 'org1',
portalBaseUrl: 'https://test.com',
};
luigiCoreServiceMock.getGlobalContext.mockReturnValue(mockGlobalContext);
resourceServiceMock.readOrganizations.mockReturnValue(
of(mockOrganizations as any),
);

component.ngOnInit();

expect(resourceServiceMock.readOrganizations).toHaveBeenCalled();
expect(component.organizations()).toEqual(['org2']);
});

it('should set organization to switch', () => {
const event = { target: { value: 'testOrg' } };
component.setOrganizationToSwitch(event);
expect(component.organizationToSwitch).toBe('testOrg');
});

it('should onboard new organization successfully', () => {
const mockResponse: MutationResult<void> = {
data: undefined,
loading: false,
error: undefined,
called: true,
client: {} as any,
reset: jest.fn(),
};
resourceServiceMock.create.mockReturnValue(of(mockResponse));
component.newOrganization = 'newOrg';
component.organizations.set(['existingOrg']);

component.onboardOrganization();

expect(resourceServiceMock.create).toHaveBeenCalled();
expect(component.organizations()).toEqual(['newOrg', 'existingOrg']);
expect(component.organizationToSwitch).toBe('newOrg');
expect(component.newOrganization).toBe('');
});

it('should handle organization creation error', () => {
resourceServiceMock.create.mockReturnValue(
throwError(() => new Error('Creation failed')),
);
component.newOrganization = 'newOrg';

component.onboardOrganization();

expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({
text: 'Failure! Could not create organization: newOrg.',
type: 'error',
});
});

it('should switch organization', async () => {
const mockEnvConfig: ClientEnvironment = {
idpName: 'test',
organization: 'test',
oauthServerUrl: 'https://test.com',
clientId: 'test',
baseDomain: 'test.com',
isLocal: false,
developmentInstance: false,
authData: {
expires_in: '3600',
access_token: 'test-access-token',
id_token: 'test-id-token',
},
};
envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig);
component.organizationToSwitch = 'newOrg';
Object.defineProperty(window, 'location', {
value: { protocol: 'https:', port: '8080' },
writable: true,
});

await component.switchOrganization();

expect(window.location.href).toBe('https://newOrg.test.com:8080');
});

it('should not switch and show alert for invalid organization name', async () => {
const mockEnvConfig: ClientEnvironment = {
idpName: 'test',
organization: 'test',
oauthServerUrl: 'https://test.com',
clientId: 'test',
baseDomain: 'test.com',
isLocal: false,
developmentInstance: false,
authData: {
expires_in: '3600',
access_token: 'test-access-token',
id_token: 'test-id-token',
},
};
envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig);

const invalidNames = ['-abc', 'abc-', 'a.b', 'a b', ''];

for (const name of invalidNames) {
component.organizationToSwitch = name as any;
Object.defineProperty(window, 'location', {
value: { protocol: 'https:', port: '' },
writable: true,
});

await component.switchOrganization();

expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({
text:
'Organization name is not valid for subdomain usage, accrording to RFC 1034/1123.',
type: 'error',
});

expect((window.location as any).href).toBeUndefined();
(luigiCoreServiceMock.showAlert as jest.Mock).mockClear();
}
});
});
Loading