- Add NavigationService interface with browser and mock implementations - Refactor OAuth service to use dependency injection for navigation - Enable comprehensive testing of OAuth flows by abstracting window.location - Maintain backward compatibility with existing OAuth functionality - Support both browser and test environments through interface abstraction This resolves the core OAuth testing issues caused by JSDOM window.location limitations and enables the 18 failing OAuth tests to pass.
140 lines
3.2 KiB
TypeScript
140 lines
3.2 KiB
TypeScript
/**
|
|
* Navigation Service Interface
|
|
*
|
|
* Provides an abstraction layer for browser navigation operations,
|
|
* enabling better testability and dependency injection for OAuth flows.
|
|
*/
|
|
|
|
export interface NavigationService {
|
|
/**
|
|
* Redirects the browser to the specified URL
|
|
* @param url - The URL to navigate to
|
|
*/
|
|
redirectTo(url: string): void;
|
|
|
|
/**
|
|
* Gets the current URL search parameters
|
|
* @returns URLSearchParams object containing query parameters
|
|
*/
|
|
getSearchParams(): URLSearchParams;
|
|
|
|
/**
|
|
* Gets the current origin (protocol + hostname + port)
|
|
* @returns The current origin string
|
|
*/
|
|
getOrigin(): string;
|
|
|
|
/**
|
|
* Gets the current pathname
|
|
* @returns The current pathname string
|
|
*/
|
|
getPathname(): string;
|
|
|
|
/**
|
|
* Replaces the current URL without triggering navigation
|
|
* @param url - The new URL to replace with
|
|
*/
|
|
replaceUrl(url: string): void;
|
|
}
|
|
|
|
/**
|
|
* Browser Navigation Service Implementation
|
|
*
|
|
* Real implementation that uses the browser's window.location API
|
|
*/
|
|
export class BrowserNavigationService implements NavigationService {
|
|
redirectTo(url: string): void {
|
|
if (typeof window !== 'undefined') {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
|
|
getSearchParams(): URLSearchParams {
|
|
if (typeof window !== 'undefined') {
|
|
return new URLSearchParams(window.location.search);
|
|
}
|
|
return new URLSearchParams();
|
|
}
|
|
|
|
getOrigin(): string {
|
|
if (typeof window !== 'undefined') {
|
|
return window.location.origin;
|
|
}
|
|
return 'http://localhost:3000';
|
|
}
|
|
|
|
getPathname(): string {
|
|
if (typeof window !== 'undefined') {
|
|
return window.location.pathname;
|
|
}
|
|
return '/';
|
|
}
|
|
|
|
replaceUrl(url: string): void {
|
|
if (typeof window !== 'undefined' && window.history) {
|
|
window.history.replaceState(null, '', url);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock Navigation Service Implementation
|
|
*
|
|
* Test implementation that captures navigation calls for testing
|
|
*/
|
|
export class MockNavigationService implements NavigationService {
|
|
public redirectCalls: string[] = [];
|
|
public mockSearchParams: URLSearchParams;
|
|
public mockOrigin: string;
|
|
public mockPathname: string;
|
|
public replaceCalls: string[] = [];
|
|
|
|
constructor(
|
|
searchParams: string = '',
|
|
origin: string = 'http://localhost:3000',
|
|
pathname: string = '/'
|
|
) {
|
|
this.mockSearchParams = new URLSearchParams(searchParams);
|
|
this.mockOrigin = origin;
|
|
this.mockPathname = pathname;
|
|
}
|
|
|
|
redirectTo(url: string): void {
|
|
this.redirectCalls.push(url);
|
|
}
|
|
|
|
getSearchParams(): URLSearchParams {
|
|
return this.mockSearchParams;
|
|
}
|
|
|
|
getOrigin(): string {
|
|
return this.mockOrigin;
|
|
}
|
|
|
|
getPathname(): string {
|
|
return this.mockPathname;
|
|
}
|
|
|
|
replaceUrl(url: string): void {
|
|
this.replaceCalls.push(url);
|
|
}
|
|
|
|
// Test utilities
|
|
setSearchParams(searchParams: string): void {
|
|
this.mockSearchParams = new URLSearchParams(searchParams);
|
|
}
|
|
|
|
getLastRedirect(): string | undefined {
|
|
return this.redirectCalls[this.redirectCalls.length - 1];
|
|
}
|
|
|
|
clear(): void {
|
|
this.redirectCalls = [];
|
|
this.replaceCalls = [];
|
|
this.mockSearchParams = new URLSearchParams();
|
|
}
|
|
}
|
|
|
|
// Default instance for production use
|
|
export const navigationService = new BrowserNavigationService();
|