diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.html b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.html index f6a1ebd55e0db35ad39e55888ecca1d511452bc1..696169f3474d0aacf9feb45c554a25d291b979fd 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.html +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.html @@ -24,5 +24,5 @@ --> <div class="status-name text-sm" data-test-id="vorgang-status-text"> - {{ status }} + {{ status | enumToLabel: vorgangStatusLabel }} </div> diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts index 15774e62da602caa5cf4f474762e6464a245dfb9..31b6f8e0de9bc3cfa18c45e1ad5860f747589982 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts @@ -21,19 +21,23 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { EnumToLabelPipe } from '@alfa-client/tech-shared'; +import { VorgangResource, VorgangStatusLabel } from '@alfa-client/vorgang-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; import { VorgangStatusTextComponent } from './vorgang-status-text.component'; describe('VorgangStatusTextComponent', () => { let component: VorgangStatusTextComponent; let fixture: ComponentFixture<VorgangStatusTextComponent>; + const vorgang: VorgangResource = createVorgangResource(); const statusTextTestId: string = getDataTestIdOf('vorgang-status-text'); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [VorgangStatusTextComponent], + declarations: [EnumToLabelPipe, VorgangStatusTextComponent], }).compileComponents(); }); @@ -48,11 +52,12 @@ describe('VorgangStatusTextComponent', () => { }); it('should show status text', () => { - component.status = 'Neu'; + component.status = vorgang.status; + const statusText: string = VorgangStatusLabel[vorgang.status]; fixture.detectChanges(); const statusTextElement: HTMLElement = fixture.nativeElement.querySelector(statusTextTestId); - expect(statusTextElement.innerHTML).toContain('Neu'); + expect(statusTextElement.innerHTML).toContain(statusText); }); }); diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts index de4e772d0b94942199e6bdd381077201de758b46..a1c90fd8afdb3114a17086f359b36ee5110a4f42 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { VorgangStatus, VorgangStatusLabel } from '@alfa-client/vorgang-shared'; import { Component, Input } from '@angular/core'; @Component({ @@ -29,5 +30,6 @@ import { Component, Input } from '@angular/core'; styleUrls: ['./vorgang-status-text.component.scss'], }) export class VorgangStatusTextComponent { - @Input() status: string; + @Input() status: VorgangStatus; + readonly vorgangStatusLabel = VorgangStatusLabel; } diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.html b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.html index d7faafb103e54a0b4abc767e586bec558d2a5982..228c9808d45c6f5b81e47b178e21cfe28e4fe01c 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.html +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.html @@ -38,7 +38,7 @@ data-test-class="status-dot" ></alfa-vorgang-status-dot> <alfa-vorgang-status-text - [status]="status" + [status]="vorgang.status" data-test-class="status-text" class="status-text" ></alfa-vorgang-status-text> diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts index 0c9700bd02d85554970bdcad3706bd6f808c8e3b..6015354498a4860c7c4bfabd7feb57ce2f9c7a93 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts @@ -22,9 +22,8 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { - convertForDataTest, ConvertForDataTestPipe, - createEmptyStateResource, + createStateResource, EnumToLabelPipe, HasLinkPipe, ToResourceUriPipe, @@ -32,7 +31,7 @@ import { import { getElementFromFixture, mock } from '@alfa-client/test-utils'; import { PostfachIconComponent } from '@alfa-client/ui'; import { UserProfileInVorgangListItemContainerComponent } from '@alfa-client/user-profile'; -import { UserProfileService } from '@alfa-client/user-profile-shared'; +import { UserProfileResource, UserProfileService } from '@alfa-client/user-profile-shared'; import { VorgangHeaderLinkRel, VorgangStatus } from '@alfa-client/vorgang-shared'; import { AktenzeichenComponent, @@ -52,6 +51,7 @@ import { MatIconTestingModule } from '@angular/material/icon/testing'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterTestingModule } from '@angular/router/testing'; import { getDataTestClassOf, getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; import { MockComponent, MockModule } from 'ng-mocks'; import { of } from 'rxjs'; @@ -72,6 +72,8 @@ describe('VorgangListItemComponent', () => { const postfachStatus: string = getDataTestClassOf('postfach-icon'); const bescheidStatus: string = getDataTestIdOf('vorgang-list-item-bescheid-status'); + const userProfile: UserProfileResource = createUserProfileResource(); + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [RouterTestingModule, MatIconTestingModule], @@ -114,43 +116,47 @@ describe('VorgangListItemComponent', () => { }); describe('ngOnInit', () => { - it('should set status', () => { - component.getStatus = jest.fn().mockReturnValue('Test'); - - component.ngOnInit(); - - expect(component.status).toBe('Test'); - }); - - it('should build aria label', () => { - component.buildAriaLabel = jest.fn(); - - component.ngOnInit(); - - expect(component.buildAriaLabel).toHaveBeenCalled(); - }); - describe('user profile', () => { beforeEach(() => { + component.buildAriaLabel = jest.fn(); component.userProfileService.getAssignedUserProfile = jest .fn() - .mockReturnValue(of(createEmptyStateResource())); + .mockReturnValue(of(createStateResource(userProfile))); }); - it('should not get profile if vorgang has no "assigned to" link', () => { - component.vorgang = createVorgangResource(); + describe('vorgang has no "assigned to" link', () => { + beforeEach(() => { + component.vorgang = createVorgangResource(); + }); + it('should not get profile', () => { + component.ngOnInit(); + + expect(component.userProfileService.getAssignedUserProfile).not.toHaveBeenCalled(); + }); - component.ngOnInit(); + it('should build aria label without user profile resource', () => { + component.ngOnInit(); - expect(component.userProfileService.getAssignedUserProfile).not.toHaveBeenCalled(); + expect(component.buildAriaLabel).toHaveBeenCalledWith(); + }); }); - it('should get profile', () => { - component.vorgang = createVorgangResource([VorgangHeaderLinkRel.ASSIGNED_TO]); + describe('vorgang has "assigned to" link', () => { + beforeEach(() => { + component.vorgang = createVorgangResource([VorgangHeaderLinkRel.ASSIGNED_TO]); + }); + + it('should get profile', () => { + component.ngOnInit(); + + expect(component.userProfileService.getAssignedUserProfile).toHaveBeenCalled(); + }); - component.ngOnInit(); + it('should build aria label with user profile resource', () => { + component.ngOnInit(); - expect(component.userProfileService.getAssignedUserProfile).toHaveBeenCalled(); + expect(component.buildAriaLabel).toHaveBeenCalledWith(userProfile); + }); }); }); }); @@ -178,142 +184,166 @@ describe('VorgangListItemComponent', () => { }); }); - describe('Aria label', () => { - it('should contain Wiedervorlage', () => { + describe('mail icon', () => { + beforeEach(() => { component.vorgang = createVorgangResource([ VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.WIEDERVORLAGEN, + VorgangHeaderLinkRel.POSTFACH_MAILS, ]); - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + }); + + it('should show mail icon if Vorgang has new Postfachnachricht', () => { + component.vorgang.hasPostfachNachricht = true; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const statusElement = getElementFromFixture(fixture, postfachStatus); - expect(ariaLabel).toContain('Wiedervorlage'); + expect(statusElement).toBeInstanceOf(HTMLElement); }); - it('should not contain Wiedervorlage if no nextFrist but LinkRel.WIEDERVORLAGEN', () => { - component.vorgang = { - ...createVorgangResource([ - VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.WIEDERVORLAGEN, - ]), - nextFrist: null, - }; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should not show mail icon if Vorgang has no new Postfachnachricht', () => { + component.vorgang.hasPostfachNachricht = false; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const statusElement = getElementFromFixture(fixture, postfachStatus); - expect(ariaLabel).not.toContain('Wiedervorlage'); + expect(statusElement).not.toBeInstanceOf(HTMLElement); }); + }); - it('should not contain Wiedervorlage if no LinkRel.WIEDERVORLAGEN', () => { - component.vorgang = createVorgangResource([VorgangHeaderLinkRel.VORGANG_WITH_EINGANG]); - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + describe('Bescheid-Status', () => { + it('should show bescheid status if Vorgang has antragBewilligt true', () => { + component.vorgang.antragBewilligt = true; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).not.toContain('Wiedervorlage'); + expect(element).toBeInstanceOf(HTMLElement); }); - it('should contain hasPostfachnachricht text if vorgang has Postfachnachricht', () => { - component.vorgang.hasPostfachNachricht = true; - component.vorgang.hasNewPostfachNachricht = false; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should show bescheid status if Vorgang has antragBewilligt false', () => { + component.vorgang.antragBewilligt = false; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).toContain('enthält Postfachnachrichten'); + expect(element).toBeInstanceOf(HTMLElement); }); - it('should contain hasNewPostfachnachricht text if vorgang has new Postfachnachricht', () => { - component.vorgang.hasPostfachNachricht = true; - component.vorgang.hasNewPostfachNachricht = true; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should not show bescheid status if Vorgang has no antragBewilligt', () => { + component.vorgang.antragBewilligt = null; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).toContain('enthält neue Postfachnachrichten'); + expect(element).not.toBeInstanceOf(HTMLElement); }); }); - describe('mail icon', () => { + describe('buildAriaLabel', () => { beforeEach(() => { + component.getWiedervorlageText = jest.fn(); + component.getPostfachNachricht = jest.fn(); + }); + it('should get status', () => { + component.getStatus = jest.fn(); + + component.buildAriaLabel(); + + expect(component.getStatus).toHaveBeenCalled(); + }); + + it('should get approval text', () => { + component.getApprovalText = jest.fn(); + + component.buildAriaLabel(); + + expect(component.getApprovalText).toHaveBeenCalled(); + }); + + it('should get user text', () => { + component.getUserText = jest.fn(); + + component.buildAriaLabel(userProfile); + + expect(component.getUserText).toHaveBeenCalledWith(userProfile); + }); + + it('should get Wiedervorlage', () => { component.vorgang = createVorgangResource([ VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.POSTFACH_MAILS, + VorgangHeaderLinkRel.WIEDERVORLAGEN, ]); + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).toHaveBeenCalled(); }); - it('should show mail icon if Vorgang has new Postfachnachricht', () => { + it('should not get Wiedervorlage if no nextFrist but LinkRel.WIEDERVORLAGEN', () => { + component.vorgang = { + ...createVorgangResource([ + VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, + VorgangHeaderLinkRel.WIEDERVORLAGEN, + ]), + nextFrist: null, + }; + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).not.toHaveBeenCalled(); + }); + + it('should not get Wiedervorlage if no LinkRel.WIEDERVORLAGEN', () => { + component.vorgang = createVorgangResource([VorgangHeaderLinkRel.VORGANG_WITH_EINGANG]); + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).not.toHaveBeenCalled(); + }); + + it('should get message text if vorgang has message', () => { component.vorgang.hasPostfachNachricht = true; - fixture.detectChanges(); - const statusElement = getElementFromFixture(fixture, postfachStatus); + component.buildAriaLabel(); - expect(statusElement).toBeInstanceOf(HTMLElement); + expect(component.getPostfachNachricht).toHaveBeenCalled(); }); - it('should not show mail icon if Vorgang has no new Postfachnachricht', () => { + it('should not get message text if vorgang has no messages', () => { component.vorgang.hasPostfachNachricht = false; - fixture.detectChanges(); - const statusElement = getElementFromFixture(fixture, postfachStatus); + component.buildAriaLabel(); - expect(statusElement).not.toBeInstanceOf(HTMLElement); + expect(component.getPostfachNachricht).not.toHaveBeenCalled(); }); }); - describe('Bescheid-Status', () => { - it('should show bescheid status if Vorgang has antragBewilligt true', () => { - component.vorgang.antragBewilligt = true; - fixture.detectChanges(); + describe('getWiedervorlageText', () => { + it('should return text for next resubmission', () => { + component.vorgang.nextFrist = new Date('07.02.1977'); - const element = getElementFromFixture(fixture, bescheidStatus); + const result: string = component.getWiedervorlageText(); - expect(element).toBeInstanceOf(HTMLElement); + expect(result).toBe(', Nächste Wiedervorlage am 02.07.1977'); }); + }); - it('should show bescheid status if Vorgang has antragBewilligt false', () => { - component.vorgang.antragBewilligt = false; - fixture.detectChanges(); + describe('getPostfachNachricht', () => { + it('should return "contains new messages"', () => { + component.vorgang.hasNewPostfachNachricht = true; - const element = getElementFromFixture(fixture, bescheidStatus); + const result: string = component.getPostfachNachricht(); - expect(element).toBeInstanceOf(HTMLElement); + expect(result).toBe(', enthält neue Postfachnachrichten'); }); - it('should not show bescheid status if Vorgang has no antragBewilligt', () => { - component.vorgang.antragBewilligt = null; - fixture.detectChanges(); + it('should return "contains messages"', () => { + component.vorgang.hasNewPostfachNachricht = false; - const element = getElementFromFixture(fixture, bescheidStatus); + const result: string = component.getPostfachNachricht(); - expect(element).not.toBeInstanceOf(HTMLElement); + expect(result).toBe(', enthält Postfachnachrichten'); }); }); @@ -352,4 +382,20 @@ describe('VorgangListItemComponent', () => { expect(result).toBe('abgelehnt'); }); }); + + describe('getUserText', () => { + it('should return no assigned user text', () => { + const result: string = component.getUserText(undefined); + + expect(result).toBe('Kein Bearbeiter zugewiesen'); + }); + + it('should return assigned user text', () => { + const result: string = component.getUserText(userProfile); + + expect(result).toBe( + `Aktuell zugewiesener Nutzer: ${userProfile.firstName} ${userProfile.lastName}`, + ); + }); + }); }); diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts index b695b0b3740025faa936742b5e531aa68997b44d..b8cbab9cabd6479cc85550faf03fd35dc9550a9c 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts @@ -22,7 +22,6 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { - createEmptyStateResource, EnumToLabelPipe, formatFullDateWithTimeWithoutSeconds, formatToPrettyDate, @@ -53,10 +52,8 @@ export class VorgangListItemComponent implements OnInit { @Input() vorgang: VorgangResource; readonly vorgangLinkRel = VorgangHeaderLinkRel; - userProfile: StateResource<UserProfileResource> = createEmptyStateResource<UserProfileResource>(); public ariaLabel: string = ''; - public status: string = ''; constructor(public userProfileService: UserProfileService) {} @@ -64,23 +61,27 @@ export class VorgangListItemComponent implements OnInit { if (hasLink(this.vorgang, VorgangHeaderLinkRel.ASSIGNED_TO)) { this.userProfileService .getAssignedUserProfile(this.vorgang, VorgangHeaderLinkRel.ASSIGNED_TO) - .pipe(first(isNotNull)) + .pipe( + first((userProfile: StateResource<UserProfileResource>) => + isNotNull(userProfile.resource), + ), + ) .subscribe((userProfileStateResource: StateResource<UserProfileResource>) => { - this.userProfile = userProfileStateResource; + this.buildAriaLabel(userProfileStateResource.resource); }); - } - this.status = this.getStatus(); - this.buildAriaLabel(); + } else this.buildAriaLabel(); } - buildAriaLabel() { + buildAriaLabel(userProfileResource: UserProfileResource = undefined) { const name: string = this.vorgang.name; const aktenzeichen: string = getAktenzeichenText(this.vorgang); const nummer: string = this.vorgang.nummer; + const status: string = this.getStatus(); const approvalStatus: string = this.getApprovalText(); const createdAt: string = formatFullDateWithTimeWithoutSeconds(this.vorgang.createdAt); + const userText: string = this.getUserText(userProfileResource); - this.ariaLabel = `Vorgang: ${name}, Aktenzeichen: ${aktenzeichen}, Nummer: ${nummer} Status: ${this.status} ${approvalStatus}, Eingang: ${createdAt}, Aktuell zugewiesener Nutzer: ${getUserName(this.userProfile.resource)}`; + this.ariaLabel = `Vorgang: ${name}, Aktenzeichen: ${aktenzeichen}, Nummer: ${nummer} Status: ${status} ${approvalStatus}, Eingang: ${createdAt}, ${userText}`; if ( hasLink(this.vorgang, VorgangHeaderLinkRel.WIEDERVORLAGEN) && @@ -117,4 +118,10 @@ export class VorgangListItemComponent implements OnInit { return this.vorgang.antragBewilligt ? 'bewilligt' : 'abgelehnt'; } + + getUserText(userProfileResource: UserProfileResource): string { + return userProfileResource ? + `Aktuell zugewiesener Nutzer: ${getUserName(userProfileResource)}` + : 'Kein Bearbeiter zugewiesen'; + } } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java new file mode 100644 index 0000000000000000000000000000000000000000..3a5d37676d95578418ebba3385ddb8f5f7287e99 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java @@ -0,0 +1,8 @@ +package de.ozgcloud.alfa.resource; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown=true) +public class OzgcloudResource { + +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java new file mode 100644 index 0000000000000000000000000000000000000000..a0ccfcd416ac66767589bc373197b2238d4e99c5 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java @@ -0,0 +1,47 @@ +package de.ozgcloud.alfa.resource; + +import java.util.Collection; +import java.util.Optional; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.ozgcloud.alfa.common.errorhandling.ResourceNotFoundException; +import de.ozgcloud.common.errorhandling.TechnicalException; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(OzgcloudResourceController.PATH) +@RequiredArgsConstructor +public class OzgcloudResourceController { + + static final String PATH = "/api/resources"; + static final String PARAM_URI = "uri"; + + private final Collection<OzgcloudResourceURIResolver> resolvers; + private final OzgcloudResourceModelAssembler assembler; + + @GetMapping(params = PARAM_URI) + public RepresentationModel<EntityModel<OzgcloudResource>> getOzgcloudResource(@RequestParam @NotBlank String uri) { + var resourceLink = resolveUri(uri); + if (resourceLink.isEmpty()) { + throw new ResourceNotFoundException(OzgcloudResource.class, uri); + } + return assembler.toModel(new OzgcloudResourceURIResolveResult(uri, resourceLink.get())); + } + + private Optional<Link> resolveUri(String uri) { + return resolvers.stream() + .map(resolver -> resolver.resolve(uri)) + .flatMap(Optional::stream) + .reduce((r1, r2) -> { + throw new TechnicalException("Multiple resolvers accepted uri " + uri); + }); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java new file mode 100644 index 0000000000000000000000000000000000000000..e3e83cf3a97ba6615974c72667fb32b976b6a733 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java @@ -0,0 +1,24 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +class OzgcloudResourceModelAssembler implements RepresentationModelAssembler<OzgcloudResourceURIResolveResult, EntityModel<OzgcloudResource>> { + + @Override + public EntityModel<OzgcloudResource> toModel(OzgcloudResourceURIResolveResult uriResolveResult) { + return EntityModel.of(new OzgcloudResource()).add(getSelfLink(uriResolveResult.getResourceURI())).add(uriResolveResult.getResourceLink()); + } + + private Link getSelfLink(String uri) { + return linkTo(methodOn(OzgcloudResourceController.class).getOzgcloudResource(uri)).withSelfRel(); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..ebe46beb467ae69720dafe31da6121fcd4b4a956 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java @@ -0,0 +1,20 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.Root; + +@Component +class OzgcloudResourceRootProcessor implements RepresentationModelProcessor<EntityModel<Root>> { + + static final String REL_RESOURCE = "resource"; + + @Override + public EntityModel<Root> process(EntityModel<Root> model) { + return model.add(linkTo(methodOn(OzgcloudResourceController.class).getOzgcloudResource(null)).withRel(REL_RESOURCE)); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java new file mode 100644 index 0000000000000000000000000000000000000000..edcbd9ef072905e7a30930396e1afa0d68a90aa3 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java @@ -0,0 +1,15 @@ +package de.ozgcloud.alfa.resource; + +import org.springframework.hateoas.Link; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +public class OzgcloudResourceURIResolveResult { + private final String resourceURI; + private final Link resourceLink; +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..de38d882ecb8d41c198915eafc20b830798f4fbc --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java @@ -0,0 +1,10 @@ +package de.ozgcloud.alfa.resource; + +import java.util.Optional; + +import org.springframework.hateoas.Link; + +public interface OzgcloudResourceURIResolver { + + Optional<Link> resolve(String uri); +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java b/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..c3b6a185dce5c3e3189fabe78e6301c23ccce5a0 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java @@ -0,0 +1,29 @@ +package de.ozgcloud.alfa.vorgang; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import java.util.Optional; +import java.util.regex.Pattern; + +import org.springframework.hateoas.Link; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.resource.OzgcloudResourceURIResolver; + +@Component +class VorgangURIResolver implements OzgcloudResourceURIResolver { + + static final String REL_NAME = "vorgang"; + + private final Pattern pattern = Pattern.compile("ozgcloud://[^/]+/vorgangs/([0-9a-fA-F]+)"); + + @Override + public Optional<Link> resolve(String uri) { + var matcher = pattern.matcher(uri); + if (!matcher.matches()) { + return Optional.empty(); + } + var vorgangId = matcher.group(1); + return Optional.of(linkTo(VorgangController.class).slash(vorgangId).withRel(REL_NAME)); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c4bc47bb1aba700a268c32f282fb569ecb3f909e --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java @@ -0,0 +1,52 @@ +package de.ozgcloud.alfa; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.hamcrest.core.StringEndsWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import de.ozgcloud.alfa.common.user.CurrentUserService; +import de.ozgcloud.alfa.common.user.UserProfileTestFactory; + +@AutoConfigureMockMvc +@SpringBootTest +@WithMockUser +class RootControllerITCase { + + @MockBean + private CurrentUserService currentUserService; + + @Autowired + private MockMvc mockMvc; + + @BeforeEach + void init() { + when(currentUserService.getUser()).thenReturn(UserProfileTestFactory.create()); + } + + @Nested + class TestProcess { + + @Test + void shouldAddResourceLink() throws Exception { + var response = doRequest(); + + response.andExpect(jsonPath("$._links.resource.href").value(StringEndsWith.endsWith("/api/resources?uri={uri}"))); + } + } + + private ResultActions doRequest() throws Exception { + return mockMvc.perform(get(RootController.PATH)).andExpect(status().isOk()); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..a545d2ee05325037544e883b529b7c39d4d9c991 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java @@ -0,0 +1,90 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.hamcrest.core.StringEndsWith; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import de.ozgcloud.alfa.common.AlfaTestUtils; +import lombok.SneakyThrows; + +@AutoConfigureMockMvc +@SpringBootTest +@WithMockUser +class OzgcloudResourceControllerITCase { + + @Autowired + private MockMvc mockMvc; + + @Nested + class TestGetOzgcloudResource { + + private static final String VORGANG_ID = AlfaTestUtils.createMongoDbObjectId(); + + @Test + void shouldReturnStatusOk() throws Exception { + var response = doRequest("ozgcloud://test.de/vorgangs/" + VORGANG_ID); + + response.andExpect(status().isOk()); + } + + @Test + void shouldHaveVorgangLink() throws Exception { + var response = doRequest("ozgcloud://test.de/vorgangs/" + VORGANG_ID); + + response.andExpect(jsonPath("$._links.vorgang.href").value(StringEndsWith.endsWith("/api/vorgangs/" + VORGANG_ID))); + } + + @Test + void shouldHaveSelfLink() throws Exception { + var uri = "ozgcloud://test.de/vorgangs/" + VORGANG_ID; + var encodedUri = URLEncoder.encode(uri, StandardCharsets.UTF_8); + + var response = doRequest(uri); + + response.andExpect(jsonPath("$._links.self.href").value(StringEndsWith.endsWith(OzgcloudResourceController.PATH + "?uri=" + encodedUri))); + } + + @Test + void shouldReturnStatusNotFound() throws Exception { + var response = doRequest("dummy://test.de"); + + response.andExpect(status().isNotFound()); + } + + @Test + void shouldReturnBadRequestOnNoRequestParam() throws Exception { + var response = doRequestWithQueryString(""); + + response.andExpect(status().isBadRequest()); + } + + @Test + void shouldReturnBadRequestOnEmptyUri() throws Exception { + var response = doRequestWithQueryString("?uri="); + + response.andExpect(status().isBadRequest()); + } + + @SneakyThrows + private ResultActions doRequest(String uri) { + return doRequestWithQueryString("?uri=" + uri); + } + + @SneakyThrows + private ResultActions doRequestWithQueryString(String queryString) { + return mockMvc.perform(get(OzgcloudResourceController.PATH + queryString)); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9ea5e3d62933188bc4a839a69b7b3cb4d2c63cb1 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java @@ -0,0 +1,130 @@ +package de.ozgcloud.alfa.resource; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.RepresentationModel; + +import de.ozgcloud.alfa.common.errorhandling.ResourceNotFoundException; +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.SneakyThrows; + +class OzgcloudResourceControllerTest { + + @Mock + private OzgcloudResourceURIResolver resourceAResolver; + @Mock + private OzgcloudResourceURIResolver resourceBResolver; + @Spy + private Collection<OzgcloudResourceURIResolver> resourceResolvers = new ArrayList<>(); + @Mock + private OzgcloudResourceModelAssembler assembler; + @InjectMocks + private OzgcloudResourceController controller; + + @BeforeEach + void init() { + resourceResolvers.add(resourceAResolver); + resourceResolvers.add(resourceBResolver); + } + + @Nested + class TestGetOzgcloudResource { + + @Nested + class OnUriCouldBeResolved { + + private final ArgumentMatcher<OzgcloudResourceURIResolveResult> HAS_MATCHING_RESOURCE_URI = mapping -> mapping.getResourceURI().equals( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + private final ArgumentMatcher<OzgcloudResourceURIResolveResult> HAS_MATCHING_RESOURCE_LINK = mapping -> mapping.getResourceLink().equals( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK); + + @BeforeEach + void init() { + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + } + + @Test + void shouldCallResolver() { + callController(); + + verify(resourceBResolver).resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + } + + @Test + void shouldCallAssemblerWithUri() { + callController(); + + verify(assembler).toModel(argThat(HAS_MATCHING_RESOURCE_URI)); + } + + @Test + void shouldCallAssemblerWithResourceLink() { + callController(); + + verify(assembler).toModel(argThat(HAS_MATCHING_RESOURCE_LINK)); + } + + @Test + void shouldReturnModelFromAssembler() { + EntityModel<OzgcloudResource> modelFromAssembler = EntityModel.of(new OzgcloudResource()); + when(assembler.toModel(argThat(mapping -> HAS_MATCHING_RESOURCE_URI.matches(mapping) && HAS_MATCHING_RESOURCE_LINK.matches(mapping)))) + .thenReturn(modelFromAssembler); + + var model = callController(); + + assertThat(model).isEqualTo(modelFromAssembler); + } + } + + @Nested + class OnUriCouldNotBeResolved { + + @BeforeEach + void init() { + when(resourceAResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.empty()); + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.empty()); + } + + @Test + void shouldThrowResourceNotFoundException() { + assertThatThrownBy(TestGetOzgcloudResource.this::callController).isInstanceOf(ResourceNotFoundException.class); + } + } + + @Nested + class OnMultipleResolveResults { + + @BeforeEach + void init() { + when(resourceAResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + } + + @Test + void shouldThrowTechnicalException() { + assertThatThrownBy(TestGetOzgcloudResource.this::callController).isInstanceOf(TechnicalException.class); + } + } + + @SneakyThrows + private RepresentationModel<EntityModel<OzgcloudResource>> callController() { + return controller.getOzgcloudResource(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ed144040a39978e418d2248ca496007c4c4f05ac --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java @@ -0,0 +1,43 @@ +package de.ozgcloud.alfa.resource; + +import static de.ozgcloud.alfa.resource.OzgcloudResourceController.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; + +class OzgcloudResourceModelAssemblerTest { + + @InjectMocks + private OzgcloudResourceModelAssembler assembler; + + @Nested + class TestToModel { + + private final OzgcloudResourceURIResolveResult uriResolveResult = OzgcloudResourceURIResolveResultTestFactory.create(); + + @Test + void shouldHaveSelfLink() { + var model = callAssembler(); + + assertThat(model.getLink(IanaLinkRelations.SELF_VALUE)).isPresent().get().extracting(Link::getHref) + .isEqualTo(OzgcloudResourceController.PATH + "?" + PARAM_URI + "=" + OzgcloudResourceURIResolveResultTestFactory.ENCODED_RESOURCE_URI); + } + + @Test + void shouldHaveResourceLink() { + var model = callAssembler(); + + assertThat(model.getLink(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK.getRel())).isPresent().get().isEqualTo( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK); + } + + private EntityModel<OzgcloudResource> callAssembler() { + return assembler.toModel(uriResolveResult); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..71d60de1e746dd5f801b5d0a4f87b0d93493bcc7 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java @@ -0,0 +1,39 @@ +package de.ozgcloud.alfa.resource; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; + +import de.ozgcloud.alfa.Root; +import de.ozgcloud.alfa.RootTestFactory; + +class OzgcloudResourceRootProcessorTest { + + private final OzgcloudResourceRootProcessor processor = new OzgcloudResourceRootProcessor(); + + @Nested + class TestProcess { + + private final EntityModel<Root> model = EntityModel.of(RootTestFactory.create()); + + @Test + void shouldReturnOriginalModel() { + var result = processor.process(model); + + assertThat(result).isEqualTo(model); + } + + @Test + void shouldAddResourceLink() { + processor.process(model); + + assertThat(model.getLink(OzgcloudResourceRootProcessor.REL_RESOURCE)).isPresent().get().extracting(Link::getHref) + .isEqualTo(OzgcloudResourceController.PATH + "?uri={uri}"); + } + } +} + + diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..dd0ea565f584133ece1265c2c2a9782135a65a74 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java @@ -0,0 +1,23 @@ +package de.ozgcloud.alfa.resource; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.springframework.hateoas.Link; + +import com.thedeanda.lorem.LoremIpsum; + +class OzgcloudResourceURIResolveResultTestFactory { + + public static final String RESOURCE_URI = String.format("%s://%s.%s/%s", (Object[]) LoremIpsum.getInstance().getWords(4).split("\\s")); + public static final String ENCODED_RESOURCE_URI = URLEncoder.encode(RESOURCE_URI, StandardCharsets.UTF_8); + public static final Link RESOURCE_LINK = Link.of(LoremIpsum.getInstance().getUrl()).withRel(LoremIpsum.getInstance().getWords(1)); + + public static OzgcloudResourceURIResolveResult create() { + return createBuilder().build(); + } + + public static OzgcloudResourceURIResolveResult.OzgcloudResourceURIResolveResultBuilder createBuilder() { + return OzgcloudResourceURIResolveResult.builder().resourceURI(RESOURCE_URI).resourceLink(RESOURCE_LINK); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1d691c621cc7e42d207861ccb6503bd04026a74c --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java @@ -0,0 +1,61 @@ +package de.ozgcloud.alfa.vorgang; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkRelation; + +import de.ozgcloud.alfa.common.AlfaTestUtils; + +class VorgangURIResolverTest { + + private VorgangURIResolver resolver = new VorgangURIResolver(); + + @Nested + class TestResolve { + + private final String VORGANG_ID = AlfaTestUtils.createMongoDbObjectId(); + private final String VORGANG_URI = "ozgcloud://dummy.de/vorgangs/" + VORGANG_ID; + + @ParameterizedTest + @ValueSource(strings = { + "http://dummy.de/vorgangs/123", + "ozgcloud://dummy.de/res-a/123", + "ozgcloud://dummy.de/xyz/vorgangs/123", + "ozgcloud://dummy.de/vorgangs/xyz/123" }) + void shouldReturnEmptyIfUriIsNotRecognized(String uri) { + var result = resolver.resolve(uri); + + assertThat(result).isEmpty(); + } + + @Test + void shouldReturnLink() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).isPresent(); + } + + @Nested + class TestVorgangLink { + + @Test + void shouldHaveCorrectRelValue() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).get().extracting(Link::getRel).extracting(LinkRelation::value).isEqualTo(VorgangURIResolver.REL_NAME); + } + + @Test + void shouldHaveHrefOfVorgang() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).get().extracting(Link::getHref).isEqualTo("/api/vorgangs/" + VORGANG_ID); + } + } + } +}