From 4c2c206ef817dad0e557103ecdc8c1cd4735d345 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 4 Feb 2025 09:24:21 -0600 Subject: [PATCH 1/3] Touched to reset the database --- server/src/main/resources/db/dev/R__Load_testing_data.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index 198035b47..95722469b 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -60,7 +60,7 @@ VALUES INSERT INTO member_profile -- Unreal Ulysses (id, firstName, lastName, title, pdlid, location, workEmail, employeeid, startdate, biotext, supervisorid, birthDate, last_seen) VALUES -('dfe2f986-fac0-11eb-9a03-0242ac130003', PGP_SYM_ENCRYPT('Unreal','${aeskey}'), PGP_SYM_ENCRYPT('Ulysses','${aeskey}'), PGP_SYM_ENCRYPT('Test Engineer 2','${aeskey}'), '1c813446-c65a-4f49-b980-0193f7bfff8c', PGP_SYM_ENCRYPT('St. Louis','${aeskey}'), PGP_SYM_ENCRYPT('testing2@objectcomputing.com','${aeskey}'), '010101012', '2021-05-22', PGP_SYM_ENCRYPT('Test user 2','${aeskey}'), 'e4b2fe52-1915-4544-83c5-21b8f871f6db', '1950-02-01', '2021-05-22'); +('dfe2f986-fac0-11eb-9a03-0242ac130003', PGP_SYM_ENCRYPT('Unreal','${aeskey}'), PGP_SYM_ENCRYPT('Ulysses','${aeskey}'), PGP_SYM_ENCRYPT('Test Engineer 2','${aeskey}'), '1c813446-c65a-4f49-b980-0193f7bfff8c', PGP_SYM_ENCRYPT('St. Louis','${aeskey}'), PGP_SYM_ENCRYPT('testing2@objectcomputing.com','${aeskey}'), '010101012', '2021-05-22', PGP_SYM_ENCRYPT('Test user 2','${aeskey}'), 'e4b2fe52-1915-4544-83c5-21b8f871f6db', '1950-01-01', '2021-05-22'); INSERT INTO member_profile -- Kazuhira Miller (id, firstName, lastName, title, pdlid, location, workEmail, employeeid, startdate, biotext, supervisorid, birthDate, last_seen) From b90adccba8057e2f16078bef67b5a7528a8f73eb Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 4 Feb 2025 09:25:02 -0600 Subject: [PATCH 2/3] Fixed the case of the key for the internal feelings text and removed the debug logging. --- .../pulseresponse/PulseResponseController.java | 15 +-------------- .../SlackPulseResponseConverter.java | 15 +-------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java index 9c42795be..921598c25 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java @@ -25,9 +25,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.net.URI; import java.time.LocalDate; import java.util.Set; @@ -39,8 +36,6 @@ @ExecuteOn(TaskExecutors.BLOCKING) @Tag(name = "pulse-responses") public class PulseResponseController { - private static final Logger LOG = LoggerFactory.getLogger(PulseResponseController.class); - private final PulseResponseService pulseResponseServices; private final MemberProfileServices memberProfileServices; private final SlackSignatureVerifier slackSignatureVerifier; @@ -153,15 +148,9 @@ public HttpResponse externalPulseResponse( @Header("X-Slack-Request-Timestamp") String timestamp, @Body String requestBody, HttpRequest request) { - // DEBUG Only - LOG.info(requestBody); - // Validate the request if (slackSignatureVerifier.verifyRequest(signature, timestamp, requestBody)) { - // DEBUG Only - LOG.info("Request has been verified"); - // Convert the request body to a map of values. FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder(); @@ -174,6 +163,7 @@ public HttpResponse externalPulseResponse( PulseResponseCreateDTO pulseResponseDTO = slackPulseResponseConverter.get(memberProfileServices, (String)body.get(key)); + // If we receive a null DTO, that means that this is not the // actual submission of the form. We can just return 200 so // that Slack knows to continue without error. @@ -181,9 +171,6 @@ public HttpResponse externalPulseResponse( return HttpResponse.ok(); } - // DEBUG Only - LOG.info("Request has been converted"); - // Create the pulse response PulseResponse pulseResponse = pulseResponseServices.unsecureSave( diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java index be9ba90ff..4e00287cb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java @@ -45,8 +45,6 @@ public PulseResponseCreateDTO get( final Map values = (Map)state.get("values"); - dumpMap(values, ""); - // Create the pulse DTO and fill in the values. PulseResponseCreateDTO response = new PulseResponseCreateDTO(); response.setTeamMemberId(lookupUser(memberProfileServices, map)); @@ -59,7 +57,7 @@ public PulseResponseCreateDTO get( internalBlock, "internalScore", "selected_option", true))); // Internal Feelings response.setInternalFeelings(getMappedValue( - values, "internaltext", "internalFeelings", false)); + values, "internalText", "internalFeelings", false)); // External Score Map externalBlock = @@ -126,15 +124,4 @@ private UUID lookupUser(MemberProfileServices memberProfileServices, MemberProfile member = memberProfileServices.findByWorkEmail(email); return member.getId(); } - - // DEBUG Only - private void dumpMap(Map map, String indent) { - for (Map.Entry entry : map.entrySet()) { - LOG.info(indent + entry.getKey() + " : " + entry.getValue()); - - if (entry.getValue() instanceof Map) { - dumpMap((Map) entry.getValue(), indent + " "); - } - } - } } From a73d878058939fe1f916f9eaf97c0578c4a6e151 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 4 Feb 2025 14:26:17 -0600 Subject: [PATCH 3/3] Added a test for posting a pulse response via Slack --- .../services/SlackSearchReplacement.java | 49 +++++++++++++++ .../PulseResponseControllerTest.java | 61 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java new file mode 100644 index 000000000..c239da699 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java @@ -0,0 +1,49 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; + +import jakarta.inject.Singleton; + +import java.util.HashMap; +import java.util.Map; + +@Singleton +@Replaces(SlackSearch.class) +@Requires(property = "replace.slacksearch", value = StringUtils.TRUE) +public class SlackSearchReplacement extends SlackSearch { + public final Map channels = new HashMap<>(); + public final Map users = new HashMap<>(); + + public SlackSearchReplacement(CheckInsConfiguration checkInsConfiguration) { + super(checkInsConfiguration); + } + + @Override + public String findChannelId(String channelName) { + return channels.containsKey(channelName) ? + channels.get(channelName) : null; + } + + @Override + public String findUserEmail(String userId) { + return users.containsKey(userId) ? users.get(userId) : null; + } + + @Override + public String findUserId(String userEmail) { + for (Map.Entry entry : users.entrySet()) { + if (entry.getValue().equals(userEmail)) { + return entry.getKey(); + } + } + return null; + } +} + diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java index 904271d0c..fa7d0c0b5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java @@ -7,7 +7,12 @@ import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.util.Util; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; +import com.objectcomputing.checkins.services.SlackSearchReplacement; + +import io.micronaut.core.util.StringUtils; import io.micronaut.core.type.Argument; +import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; @@ -27,6 +32,11 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.time.Instant; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; @@ -34,12 +44,19 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@Property(name = "replace.slacksearch", value = StringUtils.TRUE) class PulseResponseControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, PulseResponseFixture { @Inject @Client("/services/pulse-responses") protected HttpClient client; + @Inject + private CheckInsConfiguration configuration; + + @Inject + private SlackSearchReplacement slackSearch; + private Map hierarchy; @BeforeEach @@ -516,6 +533,50 @@ void testUpdateInvalidDatePulseResponse() { assertEquals(request.getPath(), href); } + @Test + void testCreateAPulseResponseFromSlack() { + MemberProfile memberProfile = createADefaultMemberProfile(); + slackSearch.users.put("SLACK_ID_HI", memberProfile.getWorkEmail()); + + final String rawBody = "payload=%7B%22type%22%3A+%22view_submission%22%2C+%22user%22%3A+%7B%22id%22%3A+%22SLACK_ID_HI%22%7D%2C+%22view%22%3A+%7B%22id%22%3A+%22VNHU13V36%22%2C+%22type%22%3A+%22modal%22%2C+%22state%22%3A+%7B%22values%22%3A+%7B%22internalNumber%22%3A+%7B%22internalScore%22%3A+%7B%22selected_option%22%3A+%7B%22type%22%3A+%22radio_buttons%22%2C+%22value%22%3A+%224%22%7D%7D%7D%2C+%22internalText%22%3A+%7B%22internalFeelings%22%3A+%7B%22type%22%3A+%22plain_text_input%22%2C+%22value%22%3A+%22I+am+a+robot.%22%7D%7D%2C+%22externalNumber%22%3A+%7B%22externalScore%22%3A+%7B%22selected_option%22%3A+%7B%22type%22%3A+%22radio_buttons%22%2C+%22value%22%3A+%225%22%7D%7D%7D%2C+%22externalText%22%3A+%7B%22externalFeelings%22%3A+%7B%22type%22%3A+%22plain_text_input%22%2C+%22value%22%3A+%22You+are+a+robot.%22%7D%7D%7D%7D%7D%7D"; + + long currentTime = Instant.now().getEpochSecond(); + String timestamp = String.valueOf(currentTime); + + final HttpRequest request = HttpRequest.POST("/external", rawBody) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("X-Slack-Signature", slackSignature(timestamp, rawBody)) + .header("X-Slack-Request-Timestamp", timestamp); + + final HttpResponse response = client.toBlocking().exchange(request); + + assertEquals(HttpStatus.OK, response.getStatus()); + } + + private String slackSignature(String timestamp, String rawBody) { + String baseString = "v0:" + timestamp + ":" + rawBody; + String secret = configuration.getApplication() + .getPulseResponse() + .getSlack().getSigningSecret(); + + try { + // Generate HMAC SHA-256 signature + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hash = mac.doFinal(baseString.getBytes(StandardCharsets.UTF_8)); + + // Convert hash to hex + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + hexString.append(String.format("%02x", b)); + } + return "v0=" + hexString.toString(); + } catch (Exception e) { + return null; + } + } + private static PulseResponseCreateDTO createPulseResponseCreateDTO() { return createPulseResponseCreateDTO(UUID.randomUUID()); }