]> juplo.de Git - demos/kafka/chat/commitdiff
feat: If not known, an existing user can be selected or a new one created
authorKai Moritz <kai@juplo.de>
Sun, 12 Oct 2025 20:23:00 +0000 (22:23 +0200)
committerKai Moritz <kai@juplo.de>
Mon, 13 Oct 2025 19:24:09 +0000 (21:24 +0200)
* 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
src/app/chatroom/chatroom.model.ts
src/app/user/user.component.html
src/app/user/user.component.ts
src/app/user/user.service.pact.spec.ts
src/app/user/user.service.ts

diff --git a/pacts b/pacts
index 6d555b93a94e959095008ce51b4dacd986998eec..a66eed70035c1adf76d312ad78530b54893e7946 160000 (submodule)
--- a/pacts
+++ b/pacts
@@ -1 +1 @@
-Subproject commit 6d555b93a94e959095008ce51b4dacd986998eec
+Subproject commit a66eed70035c1adf76d312ad78530b54893e7946
index 9eaa9a8c691e8a9e19843283da5218630cd12de6..a36d111bfe591bb4918813acf064c7b9f2559bf1 100644 (file)
@@ -1,3 +1,8 @@
+export interface Shard
+{
+  shard: number
+}
+
 export interface User
 {
   id: string,
index bec378c0725e31e2a4f1b5d7f6051725e782d93f..721d7023553e8361deb6a32fe520c55a6e1f2e56 100644 (file)
@@ -1,26 +1,36 @@
 <div class="card card-primary">
-  <h1 class="card-header h5">Please pick a Username</h1>
-  <div class="card-body form-inline">
-    <div class="form-group">
-      <div class="input-group input-group-primary">
-        <label for="name" class="input-group-text">Name</label>
-        <input id="name" type="text" class="form-control" placeholder="Enter your username" [formControl]="usernameForm">
-        <button type="submit" class="btn btn-primary" (click)="updateName()" [disabled]="usernameForm.invalid">Pick</button>
+  <h1 class="card-header h5">Please enter your user-ID, or create a new user</h1>
+  <div class="container card-body form-inline">
+    <div class="row form-group">
+      <div class="col input-group input-group-primary">
+        <button type="submit" class="btn btn-primary" (click)="createUserId()">Create a new user-ID</button>
       </div>
+      <div class="col input-group input-group-primary">
+        <label for="id" class="input-group-text">User-ID</label>
+        <input id="id" type="text" class="form-control" placeholder="Enter your user-ID" [formControl]="useridForm">
+        <button type="submit" class="btn btn-primary" (click)="updateUserId()" [disabled]="useridForm.invalid">Send</button>
+      </div>
+    </div>
+  </div>
+  <div class="container card-footer">
+    <div class="row">
+      <div class="col">Value: {{ useridForm.value }}</div>
+      <div class="col">Form Status: {{ useridForm.status }}</div>
     </div>
-    @if (usernameForm.invalid && (usernameForm.dirty || usernameForm.touched)) {
-      <div>
-        @if (usernameForm.errors?.['required']) {
-          <div>Name is required.</div>
+    @if (useridForm.invalid && (useridForm.dirty || useridForm.touched)) {
+    <div class="row">
+      <div class="col">
+        @if (useridForm.errors?.['required']) {
+        <div>User-ID is required.</div>
         }
-        @if (usernameForm.hasError('whitespace')) {
-          <div>The username must not be empty</div>
+        @if (useridForm.hasError('whitespace')) {
+        <div>The user-ID must not be empty</div>
+        }
+        @if (useridForm.hasError('whitespace')) {
+        <div>The user-ID must be given in the UUID-format</div>
         }
       </div>
+    </div>
     }
   </div>
-  <div class="card-footer">
-    <span class="float-start">Value: {{ usernameForm.value }}</span>
-    <span class="float-end">Form Status: {{ usernameForm.status }}</span>
-  </div>
 </div>
index 4d7b3ab0747256c9319cdd83b8073e74f3e70c74..027261c2c1e9dfde2c57fb63b093cb13b8a5d1d6 100644 (file)
@@ -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;
index e4802f72fa622e2e984d244ca48a786ed3de24be..c0acedac17cb01699d1152db123db9c1c5326ef6 100644 (file)
@@ -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);
+      });
+  });
 });
index 24bb23e6d5c8b71277f76cdd330d8eed68d68635..7abe275e489dd2fc223ff32bfcc783535bb674b8 100644 (file)
@@ -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<User>(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<User> {
+    return await firstValueFrom(
+      this
+        .http
+        .get<Shard>(this.backendUri + 'shard/' + id)
+        .pipe(
+          switchMap((result) => {
+            const shard = result.shard;
+            return this.http.get<User>(
+              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;
   }
 }