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
11 changes: 0 additions & 11 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,6 @@ module.exports = {
'<rootDir>/src/environments/',
'<rootDir>/src/@types/',
],
watchPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/dist/',
'<rootDir>/coverage/',
'<rootDir>/src/assets/',
'<rootDir>/src/environments/',
'<rootDir>/src/@types/',
],
testPathIgnorePatterns: [
'<rootDir>/src/app/app.config.ts',
'<rootDir>/src/app/app.routes.ts',
Expand All @@ -86,11 +78,8 @@ module.exports = {
'<rootDir>/src/app/features/project/project.component.ts',
'<rootDir>/src/app/features/registries/',
'<rootDir>/src/app/features/settings/addons/',
'<rootDir>/src/app/features/settings/settings-container.component.ts',
'<rootDir>/src/app/features/settings/tokens/components/',
'<rootDir>/src/app/features/settings/tokens/mappers/',
'<rootDir>/src/app/features/settings/tokens/store/',
'<rootDir>/src/app/features/settings/tokens/pages/tokens-list/',
'<rootDir>/src/app/shared/components/file-menu/',
'<rootDir>/src/app/shared/components/files-tree/',
'<rootDir>/src/app/shared/components/line-chart/',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,57 @@
import { Store } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';
import { MockPipe, MockProvider } from 'ng-mocks';
import { TranslateService } from '@ngx-translate/core';
import { MockProvider } from 'ng-mocks';

import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';

import { of } from 'rxjs';

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';

import { TranslateServiceMock } from '@shared/mocks';
import { TokenCreatedDialogComponent } from '@osf/features/settings/tokens/components';
import { InputLimits } from '@osf/shared/constants';
import { MOCK_SCOPES, MOCK_STORE, MOCK_TOKEN, TranslateServiceMock } from '@shared/mocks';
import { ToastService } from '@shared/services';

import { TokenFormControls, TokenModel } from '../../models';
import { CreateToken, TokensSelectors } from '../../store';

import { TokenAddEditFormComponent } from './token-add-edit-form.component';

import { OSFTestingStoreModule } from '@testing/osf.testing.module';

describe('TokenAddEditFormComponent', () => {
let component: TokenAddEditFormComponent;
let fixture: ComponentFixture<TokenAddEditFormComponent>;
let store: Partial<Store>;
let dialogService: Partial<DialogService>;
let dialogRef: Partial<DynamicDialogRef>;
let activatedRoute: Partial<ActivatedRoute>;
let router: Partial<Router>;
let toastService: jest.Mocked<ToastService>;
let translateService: jest.Mocked<TranslateService>;

const mockToken = {
id: '1',
name: 'Test Token',
tokenId: 'token1',
scopes: ['read', 'write'],
ownerId: 'user1',
};
const mockTokens: TokenModel[] = [MOCK_TOKEN];

const mockScopes = [
{ id: 'read', attributes: { description: 'Read access' } },
{ id: 'write', attributes: { description: 'Write access' } },
];
const fillForm = (tokenName: string = MOCK_TOKEN.name, scopes: string[] = MOCK_TOKEN.scopes): void => {
component.tokenForm.patchValue({
[TokenFormControls.TokenName]: tokenName,
[TokenFormControls.Scopes]: scopes,
});
};

beforeEach(async () => {
store = {
dispatch: jest.fn().mockReturnValue(of(undefined)),
selectSignal: jest.fn().mockReturnValue(() => mockScopes),
selectSnapshot: jest.fn().mockReturnValue([mockToken]),
};
(MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => {
if (selector === TokensSelectors.getScopes) return () => MOCK_SCOPES;
if (selector === TokensSelectors.isTokensLoading) return () => false;
if (selector === TokensSelectors.getTokens) return () => mockTokens;
if (selector === TokensSelectors.getTokenById) {
return () => (id: string) => mockTokens.find((token) => token.id === id);
}
return () => null;
});

dialogService = {
open: jest.fn(),
Expand All @@ -52,27 +62,217 @@ describe('TokenAddEditFormComponent', () => {
};

activatedRoute = {
params: of({ id: mockToken.id }),
params: of({ id: MOCK_TOKEN.id }),
};

router = {
navigate: jest.fn(),
};

await TestBed.configureTestingModule({
imports: [TokenAddEditFormComponent, MockPipe(TranslatePipe)],
imports: [TokenAddEditFormComponent, ReactiveFormsModule, OSFTestingStoreModule],
providers: [
TranslateServiceMock,
MockProvider(Store, store),
MockProvider(Store, MOCK_STORE),
MockProvider(DialogService, dialogService),
MockProvider(DynamicDialogRef, dialogRef),
MockProvider(ActivatedRoute, activatedRoute),
MockProvider(ToastService),
MockProvider(Router, router),
MockProvider(ToastService, {
showSuccess: jest.fn(),
showWarn: jest.fn(),
showError: jest.fn(),
}),
],
}).compileComponents();

fixture = TestBed.createComponent(TokenAddEditFormComponent);
component = fixture.componentInstance;

toastService = TestBed.inject(ToastService) as jest.Mocked<ToastService>;
translateService = TestBed.inject(TranslateService) as jest.Mocked<TranslateService>;

fixture.detectChanges();
});

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

it('should patch form with initial values on init', () => {
fixture.componentRef.setInput('initialValues', MOCK_TOKEN);
const patchSpy = jest.spyOn(component.tokenForm, 'patchValue');

component.ngOnInit();

expect(patchSpy).toHaveBeenCalledWith(
expect.objectContaining({
[TokenFormControls.TokenName]: MOCK_TOKEN.name,
[TokenFormControls.Scopes]: MOCK_TOKEN.scopes,
})
);
expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe(MOCK_TOKEN.name);
expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(MOCK_TOKEN.scopes);
});

it('should not patch form when initialValues are not provided', () => {
fixture.componentRef.setInput('initialValues', null);

fillForm('Existing Name', ['read']);

component.ngOnInit();

expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe('Existing Name');
expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(['read']);
});

it('should not submit when form is invalid', () => {
fillForm('', []);

const markAllAsTouchedSpy = jest.spyOn(component.tokenForm, 'markAllAsTouched');
const markAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.TokenName)!, 'markAsDirty');
const markScopesAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.Scopes)!, 'markAsDirty');

component.handleSubmitForm();

expect(markAllAsTouchedSpy).toHaveBeenCalled();
expect(markAsDirtySpy).toHaveBeenCalled();
expect(markScopesAsDirtySpy).toHaveBeenCalled();
expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
});

it('should return early when tokenName is missing', () => {
fillForm('', ['read']);

component.handleSubmitForm();

expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
});

it('should return early when scopes is missing', () => {
fillForm('Test Token', []);

component.handleSubmitForm();

expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
});

it('should create token when not in edit mode', () => {
fixture.componentRef.setInput('isEditMode', false);
fillForm('Test Token', ['read', 'write']);

MOCK_STORE.dispatch.mockReturnValue(of(undefined));

component.handleSubmitForm();

expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateToken('Test Token', ['read', 'write']));
});

it('should show success toast and close dialog after creating token', () => {
fixture.componentRef.setInput('isEditMode', false);
fillForm('Test Token', ['read', 'write']);

MOCK_STORE.dispatch.mockReturnValue(of(undefined));

component.handleSubmitForm();

expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successCreate');
expect(dialogRef.close).toHaveBeenCalled();
});

it('should open created dialog with new token name and value after create', () => {
fixture.componentRef.setInput('isEditMode', false);
fillForm('Test Token', ['read', 'write']);

const showDialogSpy = jest.spyOn(component, 'showTokenCreatedDialog');

MOCK_STORE.dispatch.mockReturnValue(of(undefined));

component.handleSubmitForm();

expect(showDialogSpy).toHaveBeenCalledWith(MOCK_TOKEN.name, MOCK_TOKEN.id);
});

it('should show success toast and navigate after updating token', () => {
fixture.componentRef.setInput('isEditMode', true);
fillForm('Updated Token', ['read', 'write']);

MOCK_STORE.dispatch.mockReturnValue(of(undefined));

component.handleSubmitForm();

expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successEdit');
expect(router.navigate).toHaveBeenCalledWith(['settings/tokens']);
});

it('should open dialog with correct configuration', () => {
const tokenName = 'Test Token';
const tokenValue = 'test-token-value';

component.showTokenCreatedDialog(tokenName, tokenValue);

expect(dialogService.open).toHaveBeenCalledWith(
TokenCreatedDialogComponent,
expect.objectContaining({
width: '500px',
header: 'settings.tokens.createdDialog.title',
closeOnEscape: true,
modal: true,
closable: true,
data: {
tokenName,
tokenValue,
},
})
);
});

it('should use TranslateService.instant for dialog header', () => {
component.showTokenCreatedDialog('Name', 'Value');
expect(translateService.instant).toHaveBeenCalledWith('settings.tokens.createdDialog.title');
});

it('should read tokens via selectSignal after create', () => {
fixture.componentRef.setInput('isEditMode', false);
fillForm('Test Token', ['read']);

const selectSpy = jest.spyOn(MOCK_STORE, 'selectSignal');
MOCK_STORE.dispatch.mockReturnValue(of(undefined));

component.handleSubmitForm();

expect(selectSpy).toHaveBeenCalledWith(TokensSelectors.getTokens);
});

it('should expose the same inputLimits as InputLimits.fullName', () => {
expect(component.inputLimits).toBe(InputLimits.fullName);
});

it('should require token name', () => {
const tokenNameControl = component.tokenForm.get(TokenFormControls.TokenName);
expect(tokenNameControl?.hasError('required')).toBe(true);
});

it('should require scopes', () => {
const scopesControl = component.tokenForm.get(TokenFormControls.Scopes);
expect(scopesControl?.hasError('required')).toBe(true);
});

it('should be valid when both fields are filled', () => {
fillForm('Test Token', ['read']);

expect(component.tokenForm.valid).toBe(true);
});

it('should have correct input limits for token name', () => {
expect(component.inputLimits).toBeDefined();
});

it('should expose tokenId from route params', () => {
expect(component.tokenId()).toBe(MOCK_TOKEN.id);
});

it('should expose scopes from store via tokenScopes signal', () => {
expect(component.tokenScopes()).toEqual(MOCK_SCOPES);
});
});
Loading
Loading