@@ -164,6 +164,38 @@ void followManyToManyRelation() throws Exception {
164164 .findById (TestApplication .APPLICATION , TestApplication .PRODUCT , PRODUCT_ID );
165165 }
166166
167+ @ Test
168+ void followOneToManyRelationItem () throws Exception {
169+ var targetId = EntityId .of (UUID .randomUUID ());
170+
171+ Mockito .doReturn (true ).when (datamodelApi )
172+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PERSON_INVOICES , PERSON_ID , targetId );
173+
174+ mockMvc .perform (get ("/persons/{sourceId}/invoices/{targetId}" , PERSON_ID , targetId ))
175+ .andExpect (status ().isFound ())
176+ .andExpect (
177+ header ().string (HttpHeaders .LOCATION , "http://localhost/invoices/%s" .formatted (targetId )));
178+
179+ Mockito .verify (datamodelApi )
180+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PERSON_INVOICES , PERSON_ID , targetId );
181+ }
182+
183+ @ Test
184+ void followManyToManyRelationItem () throws Exception {
185+ var targetId = EntityId .of (UUID .randomUUID ());
186+
187+ Mockito .doReturn (true ).when (datamodelApi )
188+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PRODUCT_INVOICES , PRODUCT_ID , targetId );
189+
190+ mockMvc .perform (get ("/products/{sourceId}/invoices/{targetId}" , PRODUCT_ID , targetId ))
191+ .andExpect (status ().isFound ())
192+ .andExpect (
193+ header ().string (HttpHeaders .LOCATION , "http://localhost/invoices/%s" .formatted (targetId )));
194+
195+ Mockito .verify (datamodelApi )
196+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PRODUCT_INVOICES , PRODUCT_ID , targetId );
197+ }
198+
167199 @ Test
168200 void setOneToOneRelation () throws Exception {
169201 var targetId = EntityId .of (UUID .randomUUID ());
@@ -410,12 +442,8 @@ static Stream<Arguments> unsupportedMethod() {
410442 Arguments .of (HttpMethod .PATCH , "/invoices/%s/previous-invoice" .formatted (INVOICE_ID )),
411443 // property item endpoint
412444 Arguments .of (HttpMethod .POST , "/persons/%s/invoices/%s" .formatted (PERSON_ID , targetId )),
413- Arguments .of (HttpMethod .POST , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
414445 Arguments .of (HttpMethod .PUT , "/persons/%s/invoices/%s" .formatted (PERSON_ID , targetId )),
415- Arguments .of (HttpMethod .PUT , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
416- Arguments .of (HttpMethod .PATCH , "/persons/%s/invoices/%s" .formatted (PERSON_ID , targetId )),
417- Arguments .of (HttpMethod .PATCH , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
418- Arguments .of (HttpMethod .DELETE , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId ))
446+ Arguments .of (HttpMethod .PATCH , "/persons/%s/invoices/%s" .formatted (PERSON_ID , targetId ))
419447 );
420448 }
421449
@@ -434,6 +462,30 @@ void unsupportedMethod(HttpMethod method, String url) throws Exception {
434462 .andExpect (header ().string (HttpHeaders .ALLOW , not (containsString (method .name ()))));
435463 }
436464
465+ static Stream <Arguments > unsupportedUrl () {
466+ var targetId = EntityId .of (UUID .randomUUID ());
467+ return Stream .of (
468+ // property item endpoint of *-to-one relation
469+ Arguments .of (HttpMethod .GET , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
470+ Arguments .of (HttpMethod .POST , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
471+ Arguments .of (HttpMethod .PUT , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
472+ Arguments .of (HttpMethod .PATCH , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
473+ Arguments .of (HttpMethod .DELETE , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId ))
474+ );
475+ }
476+
477+ @ ParameterizedTest
478+ @ MethodSource
479+ void unsupportedUrl (HttpMethod method , String url ) throws Exception {
480+ var requestBuilder = request (method , url );
481+ if (Set .of (HttpMethod .POST , HttpMethod .PUT , HttpMethod .PATCH ).contains (method )) {
482+ requestBuilder = requestBuilder .contentType ("text/uri-list" )
483+ .content ("http://localhost/invoices/%s%n" .formatted (UUID .randomUUID ()));
484+ }
485+ mockMvc .perform (requestBuilder )
486+ .andExpect (status ().isNotFound ());
487+ }
488+
437489 }
438490
439491 @ Nested
@@ -470,6 +522,18 @@ void followToManyRelationSourceIdNotFound() throws Exception {
470522 .andExpect (status ().isNotFound ());
471523 }
472524
525+ @ Test
526+ void followToManyRelationItemSourceIdOrTargetIdNotFound () throws Exception {
527+ var targetId = EntityId .of (UUID .randomUUID ());
528+
529+ // Returns false if sourceId or targetId does not exist, or if there is no relation between sourceId and targetId
530+ Mockito .doReturn (false ).when (datamodelApi )
531+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PERSON_INVOICES , PERSON_ID , targetId );
532+
533+ mockMvc .perform (get ("/persons/{sourceId}/invoices/{targetId}" , PERSON_ID , targetId ))
534+ .andExpect (status ().isNotFound ());
535+ }
536+
473537 @ Test
474538 void setRelationEntityIdNotFound () throws Exception {
475539 var targetId = EntityId .of (UUID .randomUUID ());
0 commit comments