feat: implement tenant service with bootstrap functionality and error handling
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig, provideAppInitializer, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideClientHydration } from '@angular/platform-browser';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideClientHydration } from '@angular/platform-browser';
|
||||
import { tenantBootstrap } from './core/services/tenant-bootstrap';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(routes), provideClientHydration()
|
||||
provideRouter(routes),
|
||||
provideClientHydration(),
|
||||
provideHttpClient(),
|
||||
provideAppInitializer(tenantBootstrap)
|
||||
]
|
||||
};
|
||||
|
||||
55
src/app/core/services/tenant-bootstrap.spec.ts
Normal file
55
src/app/core/services/tenant-bootstrap.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { tenantBootstrap } from './tenant-bootstrap';
|
||||
import { TenantService } from './tenant.service';
|
||||
|
||||
describe('tenantBootstrap', () => {
|
||||
it('waits for TenantService.bootstrap to resolve', async () => {
|
||||
const bootstrapSpy = vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
codigo: 'test',
|
||||
nombre: 'Test Tennant',
|
||||
dominio: 'localhost',
|
||||
primary_color: '#6376F3',
|
||||
secondary_color: '#A0A0A0',
|
||||
danger_color: '#FF8888',
|
||||
header_footer_bg_color: '#313131',
|
||||
header_logo: 'https://example.com/header.png',
|
||||
footer_logo: 'https://example.com/footer.png'
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: TenantService,
|
||||
useValue: {
|
||||
bootstrap: bootstrapSpy
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await expect(TestBed.runInInjectionContext(() => tenantBootstrap())).resolves.toBeUndefined();
|
||||
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('propagates tenant bootstrap failures', async () => {
|
||||
const bootstrapSpy = vi.fn().mockRejectedValue(new Error('bootstrap failed'));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: TenantService,
|
||||
useValue: {
|
||||
bootstrap: bootstrapSpy
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await expect(TestBed.runInInjectionContext(() => tenantBootstrap())).rejects.toThrow(
|
||||
'bootstrap failed'
|
||||
);
|
||||
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
9
src/app/core/services/tenant-bootstrap.ts
Normal file
9
src/app/core/services/tenant-bootstrap.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { inject } from '@angular/core';
|
||||
|
||||
import { TenantService } from './tenant.service';
|
||||
|
||||
export function tenantBootstrap(): Promise<void> {
|
||||
const tenantService = inject(TenantService);
|
||||
|
||||
return tenantService.bootstrap().then(() => undefined);
|
||||
}
|
||||
16
src/app/core/services/tenant.interface.ts
Normal file
16
src/app/core/services/tenant.interface.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
dominio: string;
|
||||
primary_color: string;
|
||||
secondary_color: string;
|
||||
danger_color: string;
|
||||
header_footer_bg_color: string;
|
||||
header_logo: string;
|
||||
footer_logo: string;
|
||||
}
|
||||
|
||||
export interface TenantBootstrapResponse {
|
||||
data: Tenant;
|
||||
}
|
||||
88
src/app/core/services/tenant.service.spec.ts
Normal file
88
src/app/core/services/tenant.service.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { PLATFORM_ID } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { TenantBootstrapResponse } from './tenant.interface';
|
||||
import { TenantService } from './tenant.service';
|
||||
|
||||
describe('TenantService', () => {
|
||||
beforeEach(() => {
|
||||
window.history.replaceState({}, '', 'http://localhost:4200/store');
|
||||
});
|
||||
|
||||
it('requests the tenant bootstrap endpoint using the current hostname and stores the response', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting(), TenantService]
|
||||
});
|
||||
|
||||
const service = TestBed.inject(TenantService);
|
||||
const httpController = TestBed.inject(HttpTestingController);
|
||||
|
||||
const bootstrapPromise = service.bootstrap();
|
||||
const request = httpController.expectOne(`${environment.url}tennatns/bootstrap/localhost`);
|
||||
|
||||
expect(request.request.method).toBe('GET');
|
||||
|
||||
const response: TenantBootstrapResponse = {
|
||||
data: {
|
||||
id: 1,
|
||||
codigo: 'test',
|
||||
nombre: 'Test Tennant',
|
||||
dominio: 'localhost',
|
||||
primary_color: '#6376F3',
|
||||
secondary_color: '#A0A0A0',
|
||||
danger_color: '#FF8888',
|
||||
header_footer_bg_color: '#313131',
|
||||
header_logo: 'https://example.com/header.png',
|
||||
footer_logo: 'https://example.com/footer.png'
|
||||
}
|
||||
};
|
||||
|
||||
request.flush(response);
|
||||
|
||||
await expect(bootstrapPromise).resolves.toEqual(response.data);
|
||||
expect(service.tenant()).toEqual(response.data);
|
||||
|
||||
httpController.verify();
|
||||
});
|
||||
|
||||
it('rejects when the bootstrap response does not contain tenant data', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideHttpClientTesting(), TenantService]
|
||||
});
|
||||
|
||||
const service = TestBed.inject(TenantService);
|
||||
const httpController = TestBed.inject(HttpTestingController);
|
||||
|
||||
const bootstrapPromise = service.bootstrap();
|
||||
const request = httpController.expectOne(`${environment.url}tennatns/bootstrap/localhost`);
|
||||
|
||||
request.flush({});
|
||||
|
||||
await expect(bootstrapPromise).rejects.toThrow('Tenant bootstrap returned no data');
|
||||
|
||||
httpController.verify();
|
||||
});
|
||||
|
||||
it('skips the HTTP bootstrap on the server platform', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
TenantService,
|
||||
{ provide: PLATFORM_ID, useValue: 'server' }
|
||||
]
|
||||
});
|
||||
|
||||
const service = TestBed.inject(TenantService);
|
||||
const httpController = TestBed.inject(HttpTestingController);
|
||||
|
||||
await expect(service.bootstrap()).resolves.toBeNull();
|
||||
expect(service.tenant()).toBeNull();
|
||||
|
||||
httpController.expectNone(() => true);
|
||||
httpController.verify();
|
||||
});
|
||||
});
|
||||
47
src/app/core/services/tenant.service.ts
Normal file
47
src/app/core/services/tenant.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { Tenant, TenantBootstrapResponse } from './tenant.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TenantService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly tenantState = signal<Tenant | null>(null);
|
||||
|
||||
readonly tenant = this.tenantState.asReadonly();
|
||||
|
||||
async bootstrap(): Promise<Tenant | null> {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domain = this.resolveDomain();
|
||||
const response = await firstValueFrom(
|
||||
this.http.get<TenantBootstrapResponse>(`${environment.url}tennants/bootstrap/${domain}`)
|
||||
);
|
||||
|
||||
if (!response?.data) {
|
||||
throw new Error(`Tenant bootstrap returned no data for domain "${domain}".`);
|
||||
}
|
||||
|
||||
this.tenantState.set(response.data);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private resolveDomain(): string {
|
||||
const domain = window.location.hostname;
|
||||
|
||||
if (!domain) {
|
||||
throw new Error('Tenant bootstrap could not resolve the current domain.');
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
21
src/main.ts
21
src/main.ts
@@ -2,5 +2,24 @@ import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
function renderBootstrapError(error: unknown): void {
|
||||
const message = error instanceof Error ? error.message : 'No se pudo cargar la configuracion del tenant.';
|
||||
const root = document.querySelector('app-root');
|
||||
|
||||
if (root) {
|
||||
root.innerHTML = `
|
||||
<section style="font-family: Arial, sans-serif; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #f5f5f5; color: #1f1f1f;">
|
||||
<div style="max-width: 480px; width: 100%; background: #ffffff; border: 1px solid #d9d9d9; border-radius: 12px; padding: 24px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);">
|
||||
<h1 style="margin: 0 0 12px; font-size: 24px;">Error al cargar el tenant</h1>
|
||||
<p style="margin: 0; line-height: 1.5;">${message}</p>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
renderBootstrapError(err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user