Skip to main content

Testing entry point

import {
withFakeAuth,
FakeAuthProvider,
type FakeAuthOptions,
type RecordedCall,
} from '@ismailza/ngx-auth-client/testing';

No keycloak-js needed — this entry point has no identity-provider dependency.

withFakeAuth()

function withFakeAuth(options?: FakeAuthOptions): AuthFeature;

Configures the in-memory FakeAuthProvider as the identity provider. Pass the result to provideAuth() exactly as you would withKeycloak() — that symmetry is the point. Nothing under test knows which adapter it got. kind is 'fake'.

TestBed.configureTestingModule({
providers: [
provideAuth(
withFakeAuth({
authenticated: true,
roles: ['admin'],
claims: { sub: 'user-1', name: 'Test User' },
}),
),
provideRouter(routes),
],
});

The same instance backs both FakeAuthProvider and AUTH_PROVIDER, so a test can inject FakeAuthProvider to drive state while the code under test sees only the port.

FakeAuthOptions

interface FakeAuthOptions {
authenticated?: boolean;
claims?: Claims | null;
roles?: readonly string[];
profile?: UserProfile | null;
token?: string;
failInit?: Error;
failToken?: Error;
}
OptionTypeDefaultEffect
authenticatedbooleanfalseInitial session state
claimsClaims | nullnullClaims exposed while authenticated
rolesreadonly string[][]Roles exposed while authenticated
profileUserProfile | nullnullWhat loadProfile() resolves to
tokenstring'fake-token'What getToken() resolves to
failInitErrorMakes init() reject
failTokenErrorMakes getToken() reject
profile isn't published until loaded

Setting profile decides what loadProfile() resolves to; the profile signal stays null until something calls it. The real adapters load it lazily, and a fake that pre-populated it would hide a missing loadProfile() call in the code under test.

failInit exercises the "provider unreachable" path — whenReady() still resolves, ready() becomes true, and initError() is set. failToken exercises expired-session handling.

FakeAuthProvider

A real implementation of AuthProvider plus all four capability interfaces — not a stub. Guards, interceptors and components exercise the same code paths they do in production.

It doubles as a check on the abstraction: a port that's honest is easy to implement twice; one that leaks provider assumptions is not.

State signals

SignalType
authenticatedSignal<boolean>
claimsSignal<Claims | null>
rolesSignal<readonly string[]>
profileSignal<UserProfile | null>
initializedSignal<boolean>

initialized reports whether init() has been called — useful for asserting that provideAuth() started the adapter at all.

Driving state

setAuthenticated(authenticated: boolean): void;
setRoles(roles: readonly string[]): void;
setClaims(claims: Claims | null): void;
const fake = TestBed.inject(FakeAuthProvider);

fake.setRoles(['viewer']);
fake.setClaims({ sub: 'user-2' });
fake.setAuthenticated(false);

setAuthenticated(false) also clears claims, roles and profile, so a test can't assert against a signed-out user who still holds roles — a state the real adapter never reaches.

Recorded calls

PropertyType
loginCallsRecordedCall<LoginOptions>[]
logoutCallsRecordedCall<LogoutOptions>[]
registerCallsRecordedCall<RegisterOptions>[]
updatePasswordCallsRecordedCall<UpdatePasswordOptions>[]
accountManagementCallsnumber
interface RecordedCall<T> {
readonly options: T | undefined;
}

Each array holds the calls in order, with the options they were given:

expect(fake.loginCalls.length).toBe(1);
expect(fake.loginCalls[0].options?.redirectUri).toBe('https://app.example.com/orders');

login() records and resolves without navigating anywhere — which is what makes guard tests possible without a real redirect. logout() records and then sets authenticated to false.

configure()

configure(options: FakeAuthOptions): void;

Applies the options withFakeAuth() was configured with. Called by the feature's factory; you don't normally call it yourself. Reach for it only to reset a provider mid-test, which is usually a sign the test should be two tests.

Remember to await readiness

init() settles a microtask after the injector is created:

const fixture = TestBed.createComponent(Header);

await TestBed.inject(AuthService).whenReady(); // ← don't skip

fixture.detectChanges();

Skipping this is the most common cause of an auth test asserting against the loading state.

Next

  • Testing guide — guards, interceptors, failure paths, harness pattern
  • Ports — the interface this fake implements