From e9b3a9bc6a22ff7759f0157c700d291beb1651f7 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Sun, 12 Oct 2025 22:23:00 +0200 Subject: [PATCH] feat: If not known, an existing user can be selected or a new one created * Introduced the method `setUser(id: string)` in `UserService`. * The method fetches the user from the backend and remembers it. * Also updated the pact between the `UserService` and the `ChatBackendController`. * Remodeld the `user.component` accordingly. --- pacts | 2 +- src/app/chatroom/chatroom.model.ts | 5 +++ src/app/user/user.component.html | 44 ++++++++++++-------- src/app/user/user.component.ts | 22 ++++++++-- src/app/user/user.service.pact.spec.ts | 57 ++++++++++++++++++++++++-- src/app/user/user.service.ts | 42 +++++++++++-------- 6 files changed, 130 insertions(+), 42 deletions(-) diff --git a/pacts b/pacts index 6d555b93..a66eed70 160000 --- a/pacts +++ b/pacts @@ -1 +1 @@ -Subproject commit 6d555b93a94e959095008ce51b4dacd986998eec +Subproject commit a66eed70035c1adf76d312ad78530b54893e7946 diff --git a/src/app/chatroom/chatroom.model.ts b/src/app/chatroom/chatroom.model.ts index 9eaa9a8c..a36d111b 100644 --- a/src/app/chatroom/chatroom.model.ts +++ b/src/app/chatroom/chatroom.model.ts @@ -1,3 +1,8 @@ +export interface Shard +{ + shard: number +} + export interface User { id: string, diff --git a/src/app/user/user.component.html b/src/app/user/user.component.html index bec378c0..721d7023 100644 --- a/src/app/user/user.component.html +++ b/src/app/user/user.component.html @@ -1,26 +1,36 @@
-

Please pick a Username

-
-
-
- - - +

Please enter your user-ID, or create a new user

+
+
+
+
+
+ + + +
+
+
+ diff --git a/src/app/user/user.component.ts b/src/app/user/user.component.ts index 4d7b3ab0..027261c2 100644 --- a/src/app/user/user.component.ts +++ b/src/app/user/user.component.ts @@ -7,8 +7,11 @@ import { } from '@angular/forms'; import { Router } from "@angular/router"; import { UserService } from "./index"; +import { UUID_V4_FORMAT } from '@pact-foundation/pact/src/dsl/matchers'; +const UUID_FORMAT = "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"; + @Component({ selector: 'app-user', templateUrl: './user.component.html', @@ -17,16 +20,27 @@ import { UserService } from "./index"; }) export class UserComponent { - usernameForm = new FormControl('', [ Validators.required, this.noWhitespaceValidator ]); - updateName(): void { - let input = this.usernameForm.getRawValue(); + useridForm = new FormControl( + '', + [ + Validators.required, + this.noWhitespaceValidator, + Validators.pattern(UUID_FORMAT) + ]); + + updateUserId(): void { + let input = this.useridForm.getRawValue(); if (input !== null) { - this.userService.setUserName(input.trim()); + this.userService.setUser(input.trim()); this.router.navigate(['chatrooms']) } } + createUserId(): void { + this.userService.createUser(); + } + noWhitespaceValidator(control: FormControl) : ValidationErrors { const isWhitespace = (control.value || '').trim().length === 0; const isValid = !isWhitespace; diff --git a/src/app/user/user.service.pact.spec.ts b/src/app/user/user.service.pact.spec.ts index e4802f72..c0acedac 100644 --- a/src/app/user/user.service.pact.spec.ts +++ b/src/app/user/user.service.pact.spec.ts @@ -1,11 +1,8 @@ import { Matchers, Pact, SpecificationVersion } from '@pact-foundation/pact'; import { TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; -import { lastValueFrom } from 'rxjs'; -import { ChatroomService } from './chatroom.service'; -import { Chatroom } from './chatroom.model'; import { APP_PROPS } from '../app.tokens'; -import { User } from '../chatroom'; +import { Shard, User } from '../chatroom'; import { UserService } from './user.service'; @@ -20,6 +17,10 @@ describe('Pact between the UserService and the backend', () => { }); + const EXAMPLE_SHARD: Shard = { + shard: 2 + } + const EXAMPLE_USER: User = { id: "5c73531c-6fc4-426c-adcb-afc5c140a0f7", shard: 2 @@ -58,4 +59,52 @@ describe('Pact between the UserService and the backend', () => { expect(service.getUser()).toEqual(EXAMPLE_USER); }); }); + + it('GET /shard/{ID} -- successfull, GET /user/{ID} -- existing user, shard responsible', async () => { + + await provider + .addInteraction() + .given('there are 10 shards') + .uponReceiving('a request to calculate the shard for a given id') + .withRequest('GET', '/shard/5c73531c-6fc4-426c-adcb-afc5c140a0f7', (builder) => { + builder.headers({ + Accept: Matchers.like('application/json'), + }); + }) + .willRespondWith(200, (builder) => { + builder.headers({ 'Content-Type': 'application/json' }); + builder.jsonBody(EXAMPLE_SHARD); + }); + + await provider + .addInteraction() + .given('there are 10 shards') + .given('the server is responsible for shard 2') + .given('user 5c73531c-6fc4-426c-adcb-afc5c140a0f7 exists in shard 2') + .uponReceiving('a request for /user/5c73531c-6fc4-426c-adcb-afc5c140a0f7') + .withRequest('GET', '/user/5c73531c-6fc4-426c-adcb-afc5c140a0f7', (builder) => { + builder.headers({ + Accept: Matchers.like('application/json'), + }); + }) + .willRespondWith(200, (builder) => { + builder.headers({ 'Content-Type': 'application/json' }); + builder.jsonBody(EXAMPLE_USER); + }) + .executeTest(async (mockserver) => { + await TestBed.configureTestingModule({ + providers: [ + UserService, + {provide: APP_PROPS, useValue: {backendUri: mockserver.url + '/'}}, + provideHttpClient(), + ] + }); + + const service = TestBed.inject(UserService); + const user = await service.setUser('5c73531c-6fc4-426c-adcb-afc5c140a0f7'); + + expect(user).toEqual(EXAMPLE_USER); + expect(service.getUser()).toEqual(EXAMPLE_USER); + }); + }); }); diff --git a/src/app/user/user.service.ts b/src/app/user/user.service.ts index 24bb23e6..7abe275e 100644 --- a/src/app/user/user.service.ts +++ b/src/app/user/user.service.ts @@ -2,8 +2,8 @@ import { inject, Injectable } from '@angular/core'; import { Router } from "@angular/router"; import { HttpClient } from '@angular/common/http'; import { APP_PROPS } from '../app.tokens'; -import { User } from '../chatroom'; -import { firstValueFrom, Observable, tap } from 'rxjs'; +import { Shard, User } from '../chatroom'; +import { catchError, firstValueFrom, Observable, switchMap, tap, throwError } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -15,9 +15,7 @@ export class UserService { private readonly http = inject(HttpClient); private readonly backendUri: string; - private unknown: boolean = true; private user: User|undefined = undefined; - private name = ''; constructor() { @@ -26,7 +24,7 @@ export class UserService { } assertUserisKnown(callback: Function): void { - if(this.unknown) { + if(this.user === undefined) { this.router.navigate(['user']); } else { @@ -40,22 +38,34 @@ export class UserService { .http .post(this.backendUri + 'user/create', undefined) .pipe(tap((user: User) => { - console.log('created a new user: ' + user); + console.log('created a new user: ' + JSON.stringify(user)); this.user = user; }))); } - getUser(): User|undefined { - return this.user; - } - - setUserName(name: string): void { - console.log("New user: " + name); - this.name = name; - this.unknown = false; + async setUser(id: string): Promise { + return await firstValueFrom( + this + .http + .get(this.backendUri + 'shard/' + id) + .pipe( + switchMap((result) => { + const shard = result.shard; + return this.http.get( + this.backendUri + 'user/' + id, + { headers: { 'X-Shard': String(shard) } }); + }), + tap((user: User) => { + this.user = user; + }), + catchError((error) => { + console.error('Error while fetching user ' + id + ':', error); + return throwError(() => error); + }) + )); } - getUserName(): string { - return this.name; + getUser(): User|undefined { + return this.user; } } -- 2.39.5