* 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.
-Subproject commit 6d555b93a94e959095008ce51b4dacd986998eec
+Subproject commit a66eed70035c1adf76d312ad78530b54893e7946
+export interface Shard
+{
+ shard: number
+}
+
export interface User
{
id: string,
<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>
} 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',
})
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;
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';
});
+ const EXAMPLE_SHARD: Shard = {
+ shard: 2
+ }
+
const EXAMPLE_USER: User = {
id: "5c73531c-6fc4-426c-adcb-afc5c140a0f7",
shard: 2
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);
+ });
+ });
});
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'
private readonly http = inject(HttpClient);
private readonly backendUri: string;
- private unknown: boolean = true;
private user: User|undefined = undefined;
- private name = '';
constructor() {
}
assertUserisKnown(callback: Function): void {
- if(this.unknown) {
+ if(this.user === undefined) {
this.router.navigate(['user']);
}
else {
.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;
}
}