Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TRIB-105: Create endpoint for removing connection #146

Merged
merged 10 commits into from
Jan 19, 2024
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.savvato.tribeapp.controllers;

import com.savvato.tribeapp.config.principal.UserPrincipal;
import com.savvato.tribeapp.controllers.annotations.controllers.ConnectAPIController.Connect;
import com.savvato.tribeapp.controllers.annotations.controllers.ConnectAPIController.GetQRCodeString;
import com.savvato.tribeapp.controllers.dto.ConnectRequest;
import com.savvato.tribeapp.controllers.dto.ConnectionRemovalRequest;
import com.savvato.tribeapp.dto.ConnectIncomingMessageDTO;
import com.savvato.tribeapp.services.ConnectService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Optional;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -18,48 +16,57 @@
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Optional;

@RestController
@Tag(name = "connect", description = "Connections between users")
@RequestMapping("/api/connect")
public class ConnectAPIController {
@Autowired ConnectService connectService;
@Autowired
ConnectService connectService;

ConnectAPIController() {}
ConnectAPIController() {
}

@GetQRCodeString
@GetMapping("/{userId}")
public ResponseEntity getQrCodeString(
@Parameter(description = "The user ID of a user", example = "1") @PathVariable Long userId) {

@GetQRCodeString
@GetMapping("/{userId}")
public ResponseEntity getQrCodeString(
@Parameter(description = "The user ID of a user", example = "1") @PathVariable Long userId) {
Optional<String> opt = connectService.storeQRCodeString(userId);

Optional<String> opt = connectService.storeQRCodeString(userId);
if (opt.isPresent()) {
return ResponseEntity.status(HttpStatus.OK).body(opt.get());
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
}

if (opt.isPresent()) {
return ResponseEntity.status(HttpStatus.OK).body(opt.get());
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
@Connect
@PostMapping
public boolean connect(@RequestBody @Valid ConnectRequest connectRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets return a ResponseEntity here as well, rather than just boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a different endpoint, but I'd be glad to refactor this in another issue! The endpoint in this PR is the one with the @DeleteMapping - this line seems to have changed due to reformatting.

if (connectService.validateQRCode(
connectRequest.qrcodePhrase, connectRequest.toBeConnectedWithUserId)) {
boolean isConnectionSaved =
connectService.saveConnectionDetails(
connectRequest.requestingUserId, connectRequest.toBeConnectedWithUserId);
return isConnectionSaved;
} else {
return false;
}
}
}

@Connect
@PostMapping
public boolean connect(@RequestBody @Valid ConnectRequest connectRequest) {
if (connectService.validateQRCode(
connectRequest.qrcodePhrase, connectRequest.toBeConnectedWithUserId)) {
boolean isConnectionSaved =
connectService.saveConnectionDetails(
connectRequest.requestingUserId, connectRequest.toBeConnectedWithUserId);
if (isConnectionSaved) {
return true;
} else {
return false;
}
} else {
return false;
@DeleteMapping
public ResponseEntity<Boolean> removeConnection(@RequestBody @Valid ConnectionRemovalRequest connectionRemovalRequest) {
if (connectService.removeConnection(connectionRemovalRequest)) {
return ResponseEntity.ok().body(true);
}
return ResponseEntity.badRequest().body(false);
}
}

@MessageMapping("/connect/room")
public void connect(@Payload ConnectIncomingMessageDTO incoming, @Header("simpSessionId") String sessionId) {
connectService.connect(incoming);
}
@MessageMapping("/connect/room")
public void connect(@Payload ConnectIncomingMessageDTO incoming, @Header("simpSessionId") String sessionId) {
connectService.connect(incoming);
}

Check warning on line 71 in src/main/java/com/savvato/tribeapp/controllers/ConnectAPIController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/savvato/tribeapp/controllers/ConnectAPIController.java#L70-L71

Added lines #L70 - L71 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.savvato.tribeapp.controllers.annotations.controllers.ConnectAPIController;


import com.savvato.tribeapp.controllers.annotations.requests.DocumentedRequestBody;
import com.savvato.tribeapp.controllers.annotations.responses.BadRequest;
import com.savvato.tribeapp.controllers.annotations.responses.Success;
import com.savvato.tribeapp.controllers.dto.ConnectionRemovalRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ExampleObject;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Operation(
summary = "Delete connection between two users",
description = "Provided a ConnectionRemovalRequest (see schemas), save the connection.")
@DocumentedRequestBody(description = "A request to delete connection", implementation = ConnectionRemovalRequest.class)
@Success(
description = "Status of attempt to delete connection",
examples = {
@ExampleObject(name = "Connection deleted successfully", value = "true"),
@ExampleObject(name = "Connection could not be deleted", value = "false"),
})
@BadRequest(description = "Could not delete the connection", noContent = true)
public @interface RemoveConnection {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.savvato.tribeapp.controllers.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "A request to delete a connection between two users")
public class ConnectionRemovalRequest {
@Schema(example = "1")
public Long requestingUserId;

@Schema(example = "2")
public Long connectedWithUserId;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.savvato.tribeapp.repositories;

import com.savvato.tribeapp.entities.Connection;
import com.savvato.tribeapp.entities.Noun;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ConnectionsRepository extends CrudRepository<Connection, Long> {
@Query(nativeQuery = true, value = "delete from connections where requesting_user_id=?1 AND to_be_connected_with_user_id=?2")
void removeConnection(Long requestingUserId, Long connectedWithUserId);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.savvato.tribeapp.services;

import com.savvato.tribeapp.config.principal.UserPrincipal;
import com.savvato.tribeapp.controllers.dto.ConnectionRemovalRequest;
import com.savvato.tribeapp.dto.ConnectIncomingMessageDTO;
import com.savvato.tribeapp.dto.ConnectOutgoingMessageDTO;
import org.springframework.messaging.handler.annotation.Header;

import java.util.Optional;

Expand All @@ -14,8 +13,12 @@ public interface ConnectService {
Optional<String> storeQRCodeString(long userId);

Boolean validateQRCode(String qrcodePhrase, Long toBeConnectedWithUserId);

void connect(ConnectIncomingMessageDTO incoming);

boolean saveConnectionDetails(Long requestingUserId, Long toBeConnectedWithUserId);

ConnectOutgoingMessageDTO handleConnectionIntent(String connectionIntent, Long requestingUserId, Long toBeRequestedWithUserId);

boolean removeConnection(ConnectionRemovalRequest connectionDeleteRequest);
}
33 changes: 21 additions & 12 deletions src/main/java/com/savvato/tribeapp/services/ConnectServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.savvato.tribeapp.services;

import com.savvato.tribeapp.config.principal.UserPrincipal;
import com.savvato.tribeapp.controllers.dto.ConnectRequest;
import com.savvato.tribeapp.controllers.dto.ConnectionRemovalRequest;
import com.savvato.tribeapp.dto.ConnectIncomingMessageDTO;
import com.savvato.tribeapp.dto.ConnectOutgoingMessageDTO;
import com.savvato.tribeapp.entities.Connection;
Expand All @@ -10,9 +9,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

Expand All @@ -37,23 +34,23 @@ public class ConnectServiceImpl implements ConnectService {

private final int QRCODE_STRING_LENGTH = 12;

public Optional<String> getQRCodeString(long userId){
public Optional<String> getQRCodeString(long userId) {
String userIdToCacheKey = String.valueOf(userId);
String getCode = cache.get("ConnectQRCodeString", userIdToCacheKey);
Optional<String> opt = Optional.of(getCode);
return opt;

}

public Optional<String> storeQRCodeString(long userId){
public Optional<String> storeQRCodeString(long userId) {
String generatedQRCodeString = generateRandomString(QRCODE_STRING_LENGTH);
String userIdToCacheKey = String.valueOf(userId);
cache.put("ConnectQRCodeString", userIdToCacheKey, generatedQRCodeString);
logger.debug("User ID: " + userId + " ConnectQRCodeString: " + generatedQRCodeString);
return Optional.of(generatedQRCodeString);
}

private String generateRandomString(int length){
private String generateRandomString(int length) {
Random random = new Random();
char[] digits = new char[length];
digits[0] = (char) (random.nextInt(9) + '1');
Expand All @@ -80,11 +77,11 @@ public Boolean validateQRCode(String qrcodePhrase, Long toBeConnectedWithUserId)

@MessageMapping("/connect/room")
public void connect(ConnectIncomingMessageDTO incoming) {
if(!validateQRCode(incoming.qrcodePhrase, incoming.toBeConnectedWithUserId)) {
if (!validateQRCode(incoming.qrcodePhrase, incoming.toBeConnectedWithUserId)) {
ConnectOutgoingMessageDTO msg = ConnectOutgoingMessageDTO.builder()
.connectionError(true)
.message("Invalid QR code; failed to connect.")
.build();
.connectionError(true)
.message("Invalid QR code; failed to connect.")
.build();
simpMessagingTemplate.convertAndSendToUser(
String.valueOf(incoming.toBeConnectedWithUserId),
"/connect/user/queue/specific-user",
Expand All @@ -102,7 +99,7 @@ public void connect(ConnectIncomingMessageDTO incoming) {

public ConnectOutgoingMessageDTO handleConnectionIntent(String connectionIntent, Long requestingUserId, Long toBeConnectedWithUserId) {
if (connectionIntent == "") {
List<Long> recipients = new ArrayList<>(Arrays.asList(toBeConnectedWithUserId));
List<Long> recipients = new ArrayList<>(Collections.singletonList(toBeConnectedWithUserId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason you used Collections.singletonList rather than Array.asList?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was experimenting with the Collections.singletonList method and accidentally modified it here. I'll undo this change.

return ConnectOutgoingMessageDTO.builder().message("Please confirm that you wish to connect.").to(recipients).build();
} else if (connectionIntent == "confirmed") {
Boolean connectionStatus = saveConnectionDetails(requestingUserId, toBeConnectedWithUserId);
Expand All @@ -128,4 +125,16 @@ public ConnectOutgoingMessageDTO handleConnectionIntent(String connectionIntent,
}
return null;
}

public boolean removeConnection(ConnectionRemovalRequest connectionRemovalRequest) {
if (Objects.equals(connectionRemovalRequest.requestingUserId, connectionRemovalRequest.connectedWithUserId)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Object.equals() necessary here? Would == work as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the ConnectionRemovalRequest DTO uses the Long wrapper type for the user IDs, they're technically different objects, so I don't think == would work. But if I use the "long" primitive type, then == should also work fine. (Other DTO objects also use Long wrapper types though, so for consistency, using the wrapper type may be better.)

return false;
}
try {
connectionsRepository.removeConnection(connectionRemovalRequest.requestingUserId, connectionRemovalRequest.connectedWithUserId);
return true;
} catch (Exception e) {
return false;
}
}
}
53 changes: 53 additions & 0 deletions src/test/java/com/savvato/tribeapp/controllers/ConnectAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.savvato.tribeapp.config.principal.UserPrincipal;
import com.savvato.tribeapp.constants.Constants;
import com.savvato.tribeapp.controllers.dto.ConnectRequest;
import com.savvato.tribeapp.controllers.dto.ConnectionRemovalRequest;
import com.savvato.tribeapp.entities.User;
import com.savvato.tribeapp.entities.UserRole;
import com.savvato.tribeapp.services.*;
Expand All @@ -24,11 +25,13 @@
import java.util.Optional;
import java.util.Set;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

Expand Down Expand Up @@ -187,5 +190,55 @@ public void connectWhenQrCodeInvalid() throws Exception {
assertEquals(toBeConnectedWithUserIdCaptor.getValue(), connectRequest.toBeConnectedWithUserId);
}

@Test
public void removeConnectionHappyPath() throws Exception {
when(userPrincipalService.getUserPrincipalByEmail(Mockito.anyString()))
.thenReturn(new UserPrincipal(user));
String auth = AuthServiceImpl.generateAccessToken(user);

ConnectionRemovalRequest connectionDeleteRequest = new ConnectionRemovalRequest();
connectionDeleteRequest.requestingUserId = 1L;
connectionDeleteRequest.connectedWithUserId = 2L;
when(connectService.removeConnection(any())).thenReturn(true);
ArgumentCaptor<ConnectionRemovalRequest> connectionDeleteRequestCaptor = ArgumentCaptor.forClass(ConnectionRemovalRequest.class);
this.mockMvc
.perform(
delete("/api/connect")
.content(gson.toJson(connectionDeleteRequest))
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + auth)
.characterEncoding("utf-8"))
.andExpect(status().isOk())
.andExpect(content().string("true"))
.andReturn();
verify(connectService, times(1)).removeConnection(connectionDeleteRequestCaptor.capture());
assertThat(connectionDeleteRequestCaptor.getValue()).usingRecursiveComparison().isEqualTo(connectionDeleteRequest);

}

@Test
public void removeConnectionWhenRemovalUnsuccessful() throws Exception {
when(userPrincipalService.getUserPrincipalByEmail(Mockito.anyString()))
.thenReturn(new UserPrincipal(user));
String auth = AuthServiceImpl.generateAccessToken(user);

ConnectionRemovalRequest connectionDeleteRequest = new ConnectionRemovalRequest();
connectionDeleteRequest.requestingUserId = 1L;
connectionDeleteRequest.connectedWithUserId = 2L;
when(connectService.removeConnection(any())).thenReturn(false);
ArgumentCaptor<ConnectionRemovalRequest> connectionDeleteRequestCaptor = ArgumentCaptor.forClass(ConnectionRemovalRequest.class);
this.mockMvc
.perform(
delete("/api/connect")
.content(gson.toJson(connectionDeleteRequest))
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + auth)
.characterEncoding("utf-8"))
.andExpect(status().isBadRequest())
.andExpect(content().string("false"))
.andReturn();
verify(connectService, times(1)).removeConnection(connectionDeleteRequestCaptor.capture());
assertThat(connectionDeleteRequestCaptor.getValue()).usingRecursiveComparison().isEqualTo(connectionDeleteRequest);

}
}
Loading