feat: implement tenant service with bootstrap functionality and error handling
This commit is contained in:
parent
23e8da345a
commit
ac9ba12045
|
|
@ -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 { provideRouter } from '@angular/router';
|
||||||
|
import { provideClientHydration } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideClientHydration } from '@angular/platform-browser';
|
import { tenantBootstrap } from './core/services/tenant-bootstrap';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideRouter(routes), provideClientHydration()
|
provideRouter(routes),
|
||||||
|
provideClientHydration(),
|
||||||
|
provideHttpClient(),
|
||||||
|
provideAppInitializer(tenantBootstrap)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 { appConfig } from './app/app.config';
|
||||||
import { App } from './app/app';
|
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)
|
bootstrapApplication(App, appConfig)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
renderBootstrapError(err);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue