-
-
- {{ username.charAt(0).toUpperCase() }}
-
-
-
{{ username }}
-
{{ email }}
-
Connecté
-
-
+
+
-
-
Vérification API Sécurisée
-
Testez la transmission et la validation du jeton JWT auprès du backend Java 25.
-
-
-
-
- @if (helloMessage()) {
-
-
Réponse du backend :
-
{{ helloMessage() }}
+
+ @if (activeTab() === 'dashboard') {
+
+
+
+ {{ username.charAt(0).toUpperCase() }}
+
+
+
{{ username }}
+
{{ email }}
+
Bailleur Connecté
+
- }
-
-
+ }
+
+
+ @if (activeTab() === 'apartments') {
+
+ @if (!showForm()) {
+
+
+ @if (apartments().length === 0) {
+
+
+
Aucun appartement enregistré
+
Commencez par ajouter votre premier bien immobilier LMNP à votre portefeuille.
+
+ } @else {
+
+ @for (apt of apartments(); track apt.id) {
+
+
+
{{ apt.name }}
+
+
+ {{ apt.address }}
+
+
{{ apt.purchasePrice.toLocaleString('fr-FR') }} €
+ @if (apt.description) {
+
{{ apt.description }}
+ }
+
+
+
+ Modifier
+
+ @if (apt.id) {
+
+ Supprimer
+
+ }
+
+
+ }
+
+ }
+ } @else {
+
+ }
+
+ }
+
+
Se déconnecter
diff --git a/frontend/src/app/app.spec.ts b/frontend/src/app/app.spec.ts
index 7e632f6..e780d67 100644
--- a/frontend/src/app/app.spec.ts
+++ b/frontend/src/app/app.spec.ts
@@ -2,12 +2,14 @@ import { TestBed } from '@angular/core/testing';
import { App } from './app';
import { OAuthService } from 'angular-oauth2-oidc';
import { HttpClient } from '@angular/common/http';
+import { ApartmentService } from './apartment.service';
import { of, throwError } from 'rxjs';
import { vi } from 'vitest';
describe('App', () => {
let mockOAuthService: any;
let mockHttpClient: any;
+ let mockApartmentService: any;
beforeEach(async () => {
mockOAuthService = {
@@ -22,11 +24,22 @@ describe('App', () => {
get: () => of('Hello from mocked API!')
};
+ mockApartmentService = {
+ getApartments: () => of([
+ { id: '1', name: 'Apt 1', address: 'Addr 1', purchasePrice: 100000, description: 'Desc 1' }
+ ]),
+ getApartment: (id: string) => of({ id, name: 'Apt 1', address: 'Addr 1', purchasePrice: 100000, description: 'Desc 1' }),
+ createApartment: (apt: any) => of({ id: '2', ...apt }),
+ updateApartment: (id: string, apt: any) => of({ id, ...apt }),
+ deleteApartment: () => of(undefined)
+ };
+
await TestBed.configureTestingModule({
imports: [App],
providers: [
{ provide: OAuthService, useValue: mockOAuthService },
- { provide: HttpClient, useValue: mockHttpClient }
+ { provide: HttpClient, useValue: mockHttpClient },
+ { provide: ApartmentService, useValue: mockApartmentService }
]
}).compileComponents();
});
@@ -149,4 +162,178 @@ describe('App', () => {
fixture.detectChanges();
expect(compiled.querySelector('.api-response-code')?.textContent).toContain('Hello from mocked API!');
});
+
+ // Apartment CRUD tests
+ it('should load apartments on init if authenticated', async () => {
+ mockOAuthService.hasValidAccessToken = () => true;
+ const loadSpy = vi.spyOn(mockApartmentService, 'getApartments');
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.ngOnInit();
+ expect(loadSpy).toHaveBeenCalled();
+ expect(app.apartments().length).toBe(1);
+ expect(app.apartments()[0].name).toBe('Apt 1');
+ });
+
+ it('should navigate between tabs', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(app.activeTab()).toBe('dashboard');
+ app.activeTab.set('apartments');
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(app.activeTab()).toBe('apartments');
+ });
+
+ it('should open add form and reset fields', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.openAddForm();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(app.showForm()).toBe(true);
+ expect(app.editingApartment()).toBeNull();
+ expect(app.formName).toBe('');
+ expect(app.formPurchasePrice).toBe(0);
+ });
+
+ it('should open edit form and populate fields', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+ const apt = { id: '123', name: 'Apartment 123', address: 'Addr', purchasePrice: 90000, description: 'Test' };
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.openEditForm(apt);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(app.showForm()).toBe(true);
+ expect(app.editingApartment()).toEqual(apt);
+ expect(app.formName).toBe('Apartment 123');
+ expect(app.formPurchasePrice).toBe(90000);
+ });
+
+ it('should display error if fields are invalid on save', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.openAddForm();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.saveApartment();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(app.formError).toContain('Veuillez remplir tous les champs obligatoires');
+ });
+
+ it('should save a new apartment successfully', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+ const createSpy = vi.spyOn(mockApartmentService, 'createApartment');
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.openAddForm();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.formName = 'New Apt';
+ app.formAddress = 'New Addr';
+ app.formPurchasePrice = 200000;
+ app.formDescription = 'New Desc';
+
+ app.saveApartment();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(createSpy).toHaveBeenCalledWith({
+ name: 'New Apt',
+ address: 'New Addr',
+ purchasePrice: 200000,
+ description: 'New Desc'
+ });
+ expect(app.showForm()).toBe(false);
+ });
+
+ it('should update an existing apartment successfully', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+ const updateSpy = vi.spyOn(mockApartmentService, 'updateApartment');
+ const apt = { id: '1', name: 'Old Apt', address: 'Old Addr', purchasePrice: 100000, description: 'Old Desc' };
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.openEditForm(apt);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.formName = 'Updated Apt';
+ app.formAddress = 'Updated Addr';
+ app.formPurchasePrice = 110000;
+ app.formDescription = 'Updated Desc';
+
+ app.saveApartment();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(updateSpy).toHaveBeenCalledWith('1', {
+ name: 'Updated Apt',
+ address: 'Updated Addr',
+ purchasePrice: 110000,
+ description: 'Updated Desc'
+ });
+ expect(app.showForm()).toBe(false);
+ });
+
+ it('should delete an apartment when confirmed', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+ const deleteSpy = vi.spyOn(mockApartmentService, 'deleteApartment');
+ vi.spyOn(window, 'confirm').mockImplementation(() => true);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.deleteApartment('1');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(deleteSpy).toHaveBeenCalledWith('1');
+ });
+
+ it('should not delete an apartment when not confirmed', async () => {
+ const fixture = TestBed.createComponent(App);
+ const app = fixture.componentInstance;
+ const deleteSpy = vi.spyOn(mockApartmentService, 'deleteApartment');
+ vi.spyOn(window, 'confirm').mockImplementation(() => false);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ app.deleteApartment('1');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(deleteSpy).not.toHaveBeenCalled();
+ });
});
diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts
index cb06fd1..efb6ed5 100644
--- a/frontend/src/app/app.ts
+++ b/frontend/src/app/app.ts
@@ -1,19 +1,49 @@
-import { Component, signal, inject } from '@angular/core';
+import { Component, signal, inject, OnInit, effect } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { ApartmentService, Apartment } from './apartment.service';
+import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
- imports: [],
+ imports: [FormsModule],
templateUrl: './app.html',
styleUrl: './app.css'
})
-export class App {
+export class App implements OnInit {
protected readonly title = signal('LMNP Organizer');
readonly helloMessage = signal
('');
+ // Tab & CRUD States
+ readonly activeTab = signal<'dashboard' | 'apartments'>('dashboard');
+ readonly apartments = signal([]);
+ readonly showForm = signal(false);
+ readonly editingApartment = signal(null);
+
+ // Form Fields
+ formName = '';
+ formAddress = '';
+ formPurchasePrice = 0;
+ formDescription = '';
+ formError = '';
+
private readonly oauthService = inject(OAuthService);
private readonly http = inject(HttpClient);
+ private readonly apartmentService = inject(ApartmentService);
+
+ constructor() {
+ effect(() => {
+ if (this.isLoggedIn) {
+ this.loadApartments();
+ }
+ });
+ }
+
+ ngOnInit() {
+ if (this.isLoggedIn) {
+ this.loadApartments();
+ }
+ }
get isLoggedIn(): boolean {
return this.oauthService.hasValidAccessToken();
@@ -51,4 +81,85 @@ export class App {
}
});
}
+
+ // Apartment CRUD operations
+ loadApartments() {
+ this.apartmentService.getApartments().subscribe({
+ next: (data) => this.apartments.set(data),
+ error: (err) => console.error('Error loading apartments', err)
+ });
+ }
+
+ openAddForm() {
+ this.editingApartment.set(null);
+ this.formName = '';
+ this.formAddress = '';
+ this.formPurchasePrice = 0;
+ this.formDescription = '';
+ this.formError = '';
+ this.showForm.set(true);
+ }
+
+ openEditForm(apartment: Apartment) {
+ this.editingApartment.set(apartment);
+ this.formName = apartment.name;
+ this.formAddress = apartment.address;
+ this.formPurchasePrice = apartment.purchasePrice;
+ this.formDescription = apartment.description;
+ this.formError = '';
+ this.showForm.set(true);
+ }
+
+ closeForm() {
+ this.showForm.set(false);
+ this.editingApartment.set(null);
+ }
+
+ saveApartment() {
+ if (!this.formName.trim() || !this.formAddress.trim() || this.formPurchasePrice <= 0) {
+ this.formError = 'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).';
+ return;
+ }
+
+ const apartmentData: Apartment = {
+ name: this.formName.trim(),
+ address: this.formAddress.trim(),
+ purchasePrice: this.formPurchasePrice,
+ description: this.formDescription.trim()
+ };
+
+ const editMode = this.editingApartment();
+ if (editMode && editMode.id) {
+ this.apartmentService.updateApartment(editMode.id, apartmentData).subscribe({
+ next: () => {
+ this.loadApartments();
+ this.closeForm();
+ },
+ error: (err) => {
+ console.error(err);
+ this.formError = 'Erreur lors de la modification.';
+ }
+ });
+ } else {
+ this.apartmentService.createApartment(apartmentData).subscribe({
+ next: () => {
+ this.loadApartments();
+ this.closeForm();
+ },
+ error: (err) => {
+ console.error(err);
+ this.formError = 'Erreur lors de la création.';
+ }
+ });
+ }
+ }
+
+ deleteApartment(id: string) {
+ if (confirm('Êtes-vous sûr de vouloir supprimer cet appartement ?')) {
+ this.apartmentService.deleteApartment(id).subscribe({
+ next: () => this.loadApartments(),
+ error: (err) => console.error('Error deleting apartment', err)
+ });
+ }
+ }
}
diff --git a/frontend/src/app/config.service.spec.ts b/frontend/src/app/config.service.spec.ts
new file mode 100644
index 0000000..a3c7bfd
--- /dev/null
+++ b/frontend/src/app/config.service.spec.ts
@@ -0,0 +1,48 @@
+import { TestBed } from '@angular/core/testing';
+import { HttpClient } from '@angular/common/http';
+import { ConfigService } from './config.service';
+import { of, throwError } from 'rxjs';
+import { vi } from 'vitest';
+
+describe('ConfigService', () => {
+ let service: ConfigService;
+ let mockHttpClient: any;
+
+ beforeEach(() => {
+ mockHttpClient = {
+ get: () => of({ oidcIssuer: 'http://custom-issuer', clientId: 'custom-client' })
+ };
+
+ TestBed.configureTestingModule({
+ providers: [
+ ConfigService,
+ { provide: HttpClient, useValue: mockHttpClient }
+ ]
+ });
+
+ service = TestBed.inject(ConfigService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should load config successfully', async () => {
+ await service.loadConfig();
+ expect(service.oidcIssuer).toBe('http://custom-issuer');
+ expect(service.clientId).toBe('custom-client');
+ });
+
+ it('should use default values on load error', async () => {
+ mockHttpClient.get = () => throwError(() => new Error('Load failed'));
+
+ // Silence warning log
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+ await service.loadConfig();
+ expect(service.oidcIssuer).toBe('http://localhost:8090/realms/voidAuth');
+ expect(service.clientId).toBe('lmnp-client');
+
+ warnSpy.mockRestore();
+ });
+});