@@ -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 ());
@@ -315,8 +347,9 @@ static Stream<String> invalidContentType() {
315347
316348 @ ParameterizedTest
317349 @ CsvSource ({
318- "/invoices/01234567-89ab-cdef-0123-456789abcdef/previous" , // non-existent relation
319- "/invoice/01234567-89ab-cdef-0123-456789abcdef/previous-invoice" , // non-existent entity
350+ "/invoices/01234567-89ab-cdef-0123-456789abcdef/non-existing" , // non-existing relation
351+ "/non-existing/01234567-89ab-cdef-0123-456789abcdef/previous-invoice" , // non-existing entity
352+ "/invoices/01234567-89ab-cdef-0123-456789abcdef/non-existing/01234567-89ab-cdef-0123-456789abcdef" , // non-existing relation
320353 })
321354 void followRelationInvalidUrl (String url ) throws Exception {
322355 mockMvc .perform (get (url ))
@@ -410,12 +443,8 @@ static Stream<Arguments> unsupportedMethod() {
410443 Arguments .of (HttpMethod .PATCH , "/invoices/%s/previous-invoice" .formatted (INVOICE_ID )),
411444 // property item endpoint
412445 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 )),
414446 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 ))
447+ Arguments .of (HttpMethod .PATCH , "/persons/%s/invoices/%s" .formatted (PERSON_ID , targetId ))
419448 );
420449 }
421450
@@ -434,6 +463,30 @@ void unsupportedMethod(HttpMethod method, String url) throws Exception {
434463 .andExpect (header ().string (HttpHeaders .ALLOW , not (containsString (method .name ()))));
435464 }
436465
466+ static Stream <Arguments > unsupportedUrl () {
467+ var targetId = EntityId .of (UUID .randomUUID ());
468+ return Stream .of (
469+ // property item endpoint of *-to-one relation
470+ Arguments .of (HttpMethod .GET , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
471+ Arguments .of (HttpMethod .POST , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
472+ Arguments .of (HttpMethod .PUT , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
473+ Arguments .of (HttpMethod .PATCH , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId )),
474+ Arguments .of (HttpMethod .DELETE , "/invoices/%s/previous-invoice/%s" .formatted (INVOICE_ID , targetId ))
475+ );
476+ }
477+
478+ @ ParameterizedTest
479+ @ MethodSource
480+ void unsupportedUrl (HttpMethod method , String url ) throws Exception {
481+ var requestBuilder = request (method , url );
482+ if (Set .of (HttpMethod .POST , HttpMethod .PUT , HttpMethod .PATCH ).contains (method )) {
483+ requestBuilder = requestBuilder .contentType ("text/uri-list" )
484+ .content ("http://localhost/invoices/%s%n" .formatted (UUID .randomUUID ()));
485+ }
486+ mockMvc .perform (requestBuilder )
487+ .andExpect (status ().isNotFound ());
488+ }
489+
437490 }
438491
439492 @ Nested
@@ -470,6 +523,18 @@ void followToManyRelationSourceIdNotFound() throws Exception {
470523 .andExpect (status ().isNotFound ());
471524 }
472525
526+ @ Test
527+ void followToManyRelationItemSourceIdOrTargetIdNotFound () throws Exception {
528+ var targetId = EntityId .of (UUID .randomUUID ());
529+
530+ // Returns false if sourceId or targetId does not exist, or if there is no relation between sourceId and targetId
531+ Mockito .doReturn (false ).when (datamodelApi )
532+ .hasRelationTarget (TestApplication .APPLICATION , TestApplication .PERSON_INVOICES , PERSON_ID , targetId );
533+
534+ mockMvc .perform (get ("/persons/{sourceId}/invoices/{targetId}" , PERSON_ID , targetId ))
535+ .andExpect (status ().isNotFound ());
536+ }
537+
473538 @ Test
474539 void setRelationEntityIdNotFound () throws Exception {
475540 var targetId = EntityId .of (UUID .randomUUID ());
0 commit comments