From 219c3304b7f8954ba56b01f4c91a2e12bea9a92a Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 12:13:02 -0600 Subject: [PATCH 01/64] Send an approved public kudos to the #kudos slack channel. --- server/build.gradle | 1 + .../social_media/SlackPoster.java | 33 +++++ .../social_media/SlackSearch.java | 73 ++++++++++ .../services/kudos/KudosConverter.java | 130 ++++++++++++++++++ .../services/kudos/KudosServicesImpl.java | 22 ++- 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java diff --git a/server/build.gradle b/server/build.gradle index 54a3b0f28b..8d680f9dd3 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -116,6 +116,7 @@ dependencies { implementation("io.micrometer:context-propagation") implementation 'ch.digitalfondue.mjml4j:mjml4j:1.0.3' + implementation("com.slack.api:slack-api-client:1.44.1") testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion" testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion" diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java new file mode 100644 index 0000000000..0d561b2f0f --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java @@ -0,0 +1,33 @@ +package com.objectcomputing.checkins.notifications.social_media; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import java.util.List; + +@Singleton +public class SlackPoster { + @Inject + private HttpClient slackClient; + + public HttpResponse post(String slackBlock) { + // See if we can have a webhook URL. + String slackWebHook = System.getenv("SLACK_WEBHOOK_URL"); + if (slackWebHook != null) { + // POST it to Slack. + BlockingHttpClient client = slackClient.toBlocking(); + HttpRequest request = HttpRequest.POST(slackWebHook, + slackBlock); + return client.exchange(request); + } + return HttpResponse.status(HttpStatus.GONE, + "Slack Webhook URL is not configured"); + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java new file mode 100644 index 0000000000..58ae30c75a --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -0,0 +1,73 @@ +package com.objectcomputing.checkins.notifications.social_media; + +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.model.Conversation; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.conversations.ConversationsListRequest; +import com.slack.api.methods.response.conversations.ConversationsListResponse; +import com.slack.api.methods.request.users.UsersLookupByEmailRequest; +import com.slack.api.methods.response.users.UsersLookupByEmailResponse; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import java.util.List; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class SlackSearch { + private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class); + private static final String env = "SLACK_BOT_TOKEN"; + + public String findChannelId(String channelName) { + String token = System.getenv(env); + if (token != null) { + try { + MethodsClient client = Slack.getInstance().methods(token); + ConversationsListResponse response = client.conversationsList( + ConversationsListRequest.builder().build() + ); + + if (response.isOk()) { + for (Conversation conversation: response.getChannels()) { + if (conversation.getName().equals(channelName)) { + return conversation.getId(); + } + } + } + } catch(IOException e) { + LOG.error("SlackSearch.findChannelId: " + e.toString()); + } catch(SlackApiException e) { + LOG.error("SlackSearch.findChannelId: " + e.toString()); + } + } + return null; + } + + public String findUserId(String userEmail) { + String token = System.getenv(env); + if (token != null) { + try { + MethodsClient client = Slack.getInstance().methods(token); + UsersLookupByEmailResponse response = client.usersLookupByEmail( + UsersLookupByEmailRequest.builder().email(userEmail).build() + ); + + if (response.isOk()) { + return response.getUser().getId(); + } + } catch(IOException e) { + LOG.error("SlackSearch.findUserId: " + e.toString()); + } catch(SlackApiException e) { + LOG.error("SlackSearch.findUserId: " + e.toString()); + } + } + return null; + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java new file mode 100644 index 0000000000..460dddabdb --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -0,0 +1,130 @@ +package com.objectcomputing.checkins.services.kudos; + +import com.objectcomputing.checkins.notifications.social_media.SlackPoster; +import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServices; +import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; + +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.RichTextBlock; +import com.slack.api.model.block.element.RichTextElement; +import com.slack.api.model.block.element.RichTextSectionElement; +import com.slack.api.util.json.GsonFactory; +import com.google.gson.Gson; + +import java.util.UUID; +import java.util.List; +import java.util.ArrayList; + +public class KudosConverter { + private record InternalBlock( + List blocks + ) {} + + private final MemberProfileServices memberProfileServices; + private final KudosRecipientServices kudosRecipientServices; + + public KudosConverter(MemberProfileServices memberProfileServices, + KudosRecipientServices kudosRecipientServices) { + this.memberProfileServices = memberProfileServices; + this.kudosRecipientServices = kudosRecipientServices; + } + + public String toSlackBlock(Kudos kudos) { + // Build some the message text out of the Kudos data. + List content = new ArrayList<>(); + + // Look up the channel id from Slack + String channelName = "kudos"; + SlackSearch search = new SlackSearch(); + String channelId = search.findChannelId(channelName); + if (channelId == null) { + content.add( + RichTextSectionElement.Text.builder() + .text("#" + channelName) + .style(boldItalic()) + .build() + ); + } else { + content.add( + RichTextSectionElement.Channel.builder() + .channelId(channelId) + .style(limitedBoldItalic()) + .build() + ); + } + content.add( + RichTextSectionElement.Text.builder() + .text(" from ") + .style(boldItalic()) + .build() + ); + content.add(memberAsRichText(kudos.getSenderId())); + content.addAll(recipients(kudos)); + + content.add( + RichTextSectionElement.Text.builder() + .text("\n" + kudos.getMessage() + "\n") + .style(boldItalic()) + .build() + ); + + // Bring it all together. + RichTextSectionElement element = RichTextSectionElement.builder() + .elements(content).build(); + RichTextBlock richTextBlock = RichTextBlock.builder() + .elements(List.of(element)).build(); + InternalBlock block = new InternalBlock(List.of(richTextBlock)); + Gson mapper = GsonFactory.createSnakeCase(); + return mapper.toJson(block); + } + + private RichTextSectionElement.TextStyle boldItalic() { + return RichTextSectionElement.TextStyle.builder() + .bold(true).italic(true).build(); + } + + private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() { + return RichTextSectionElement.LimitedTextStyle.builder() + .bold(true).italic(true).build(); + } + + private RichTextElement memberAsRichText(UUID memberId) { + // Look up the user name to get the user id from Slack + SlackSearch search = new SlackSearch(); + MemberProfile profile = memberProfileServices.getById(memberId); + String userId = search.findUserId(profile.getWorkEmail()); + if (userId == null) { + String name = MemberProfileUtils.getFullName(profile); + return RichTextSectionElement.Text.builder() + .text("@" + name) + .style(boldItalic()) + .build(); + } else { + return RichTextSectionElement.User.builder() + .userId(userId) + .style(limitedBoldItalic()) + .build(); + } + } + + private List recipients(Kudos kudos) { + List list = new ArrayList<>(); + List recipients = + kudosRecipientServices.getAllByKudosId(kudos.getId()); + String separator = " to "; + for (KudosRecipient recipient : recipients) { + list.add(RichTextSectionElement.Text.builder() + .text(separator) + .style(boldItalic()) + .build()); + list.add(memberAsRichText(recipient.getMemberId())); + separator = ", "; + } + return list; + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index c672dbd439..8d429b8aa1 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -3,6 +3,7 @@ import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.notifications.email.EmailSender; import com.objectcomputing.checkins.notifications.email.MailJetFactory; +import com.objectcomputing.checkins.notifications.social_media.SlackPoster; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.exceptions.NotFoundException; import com.objectcomputing.checkins.exceptions.PermissionException; @@ -21,6 +22,9 @@ import com.objectcomputing.checkins.util.Util; import io.micronaut.core.annotation.Nullable; import io.micronaut.transaction.annotation.Transactional; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; + import jakarta.inject.Named; import jakarta.inject.Singleton; import org.slf4j.Logger; @@ -49,6 +53,7 @@ class KudosServicesImpl implements KudosServices { private final CheckInsConfiguration checkInsConfiguration; private final RoleServices roleServices; private final MemberProfileServices memberProfileServices; + private final SlackPoster slackPoster; private enum NotificationType { creation, approval @@ -63,7 +68,8 @@ private enum NotificationType { RoleServices roleServices, MemberProfileServices memberProfileServices, @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, - CheckInsConfiguration checkInsConfiguration) { + CheckInsConfiguration checkInsConfiguration, + SlackPoster slackPoster) { this.kudosRepository = kudosRepository; this.kudosRecipientServices = kudosRecipientServices; this.kudosRecipientRepository = kudosRecipientRepository; @@ -74,6 +80,7 @@ private enum NotificationType { this.currentUserServices = currentUserServices; this.emailSender = emailSender; this.checkInsConfiguration = checkInsConfiguration; + this.slackPoster = slackPoster; } @Override @@ -341,6 +348,7 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { recipientAddresses.add(member.getWorkEmail()); } } + slackApprovedKudos(kudos); break; case NotificationType.creation: content = getAdminEmailContent(checkInsConfiguration); @@ -366,4 +374,16 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { LOG.error("An unexpected error occurred while sending notifications: {}", ex.getLocalizedMessage(), ex); } } + + private void slackApprovedKudos(Kudos kudos) { + KudosConverter converter = new KudosConverter(memberProfileServices, + kudosRecipientServices); + + String slackBlock = converter.toSlackBlock(kudos); + HttpResponse httpResponse = + slackPoster.post(converter.toSlackBlock(kudos)); + if (httpResponse.status() != HttpStatus.OK) { + LOG.error("Unable to POST to Slack: " + httpResponse.reason()); + } + } } From bce7706035e5fb6160edf28129a67c3cc771fe74 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 13:15:16 -0600 Subject: [PATCH 02/64] Disable this test in native due to reflexive nature of Gson and records. --- .../checkins/services/kudos/KudosControllerTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 92975598a4..6dbe8400c0 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.time.LocalDate; import java.util.Collections; @@ -46,6 +47,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +// Disabled in nativetest due to a ReflectiveOperationException from Gson +// when attempting to post public Kudos to Slack. +@DisabledInNativeImage @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) class KudosControllerTest extends TestContainersSuite implements KudosFixture, TeamFixture, RoleFixture { @Inject From 59ec1159e4428a913362bb92e5851ebf799f28fc Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 13:17:29 -0600 Subject: [PATCH 03/64] Handle situation where daysBetween is zero. --- .../checkins/services/fixture/FeedbackRequestFixture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java index 201a67023e..f3aec2d02b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java @@ -62,7 +62,7 @@ default LocalDate getRandomLocalDateTime(LocalDateTime start, LocalDateTime end) LocalDate startDate = start.toLocalDate(); long daysBetween = ChronoUnit.DAYS.between(startDate, end.toLocalDate()); Random random = new Random(); - long randomDays = random.nextLong(daysBetween); + long randomDays = daysBetween > 0 ? random.nextLong(daysBetween) : 0; return startDate.plusDays(randomDays); } From 917e22f6dd8a571865599c097769f9bb63fb4867 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 18 Nov 2024 08:49:27 -0600 Subject: [PATCH 04/64] Inspect the posted slack block during kudos approval. --- .../services/SlackPosterReplacement.java | 31 ++++++++++ .../services/kudos/KudosControllerTest.java | 62 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java new file mode 100644 index 0000000000..c4283cc651 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java @@ -0,0 +1,31 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.notifications.social_media.SlackPoster; + +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.List; +import java.util.ArrayList; + +@Singleton +@Replaces(SlackPoster.class) +@Requires(property = "replace.slackposter", value = StringUtils.TRUE) +public class SlackPosterReplacement extends SlackPoster { + public final List posted = new ArrayList<>(); + + public void reset() { + posted.clear(); + } + + public HttpResponse post(String slackBlock) { + posted.add(slackBlock); + return HttpResponse.status(HttpStatus.OK); + } +} + diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 6dbe8400c0..94f9756fca 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -3,12 +3,14 @@ import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; +import com.objectcomputing.checkins.services.SlackPosterReplacement; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.fixture.KudosFixture; import com.objectcomputing.checkins.services.fixture.TeamFixture; import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServicesImpl; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.team.Team; import io.micronaut.core.type.Argument; @@ -30,9 +32,16 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.api.condition.DisabledInNativeImage; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.core.JsonProcessingException; + import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -51,11 +60,15 @@ // when attempting to post public Kudos to Slack. @DisabledInNativeImage @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) +@Property(name = "replace.slackposter", value = StringUtils.TRUE) class KudosControllerTest extends TestContainersSuite implements KudosFixture, TeamFixture, RoleFixture { @Inject @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; + @Inject + private SlackPosterReplacement slackPoster; + @Inject @Client("/services/kudos") HttpClient httpClient; @@ -93,6 +106,7 @@ void setUp() { message = "Kudos!"; emailSender.reset(); + slackPoster.reset(); } @ParameterizedTest @@ -210,7 +224,7 @@ void testCreateKudosWithEmptyRecipientMembers() { } @Test - void testApproveKudos() { + void testApproveKudos() throws JsonProcessingException { Kudos kudos = createPublicKudos(senderId); assertNull(kudos.getDateApproved()); KudosRecipient recipient = createKudosRecipient(kudos.getId(), recipientMembers.getFirst().getId()); @@ -231,6 +245,52 @@ void testApproveKudos() { ), emailSender.events.getFirst() ); + + // Check the posted slack block + assertEquals(1, slackPoster.posted.size()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode posted = mapper.readTree(slackPoster.posted.get(0)); + + assertEquals(JsonNodeType.OBJECT, posted.getNodeType()); + JsonNode blocks = posted.get("blocks"); + assertEquals(JsonNodeType.ARRAY, blocks.getNodeType()); + + var iter = blocks.elements(); + assertTrue(iter.hasNext()); + JsonNode block = iter.next(); + + assertEquals(JsonNodeType.OBJECT, block.getNodeType()); + JsonNode elements = block.get("elements"); + assertEquals(JsonNodeType.ARRAY, elements.getNodeType()); + + iter = elements.elements(); + assertTrue(iter.hasNext()); + JsonNode element = iter.next(); + + assertEquals(JsonNodeType.OBJECT, element.getNodeType()); + JsonNode innerElements = element.get("elements"); + assertEquals(JsonNodeType.ARRAY, innerElements.getNodeType()); + + iter = innerElements.elements(); + assertTrue(iter.hasNext()); + + // The real SlackPoster will look up user ids in Slack and use those in + // the posted message. Failing the lookup, it will use @. + String from = "@" + MemberProfileUtils.getFullName(sender); + String to = "@" + MemberProfileUtils.getFullName(recipientMembers.get(0)); + boolean foundFrom = false; + boolean foundTo = false; + while(iter.hasNext()) { + element = iter.next(); + assertEquals(JsonNodeType.OBJECT, element.getNodeType()); + String value = element.get("text").asText(); + if (value.equals(from)) { + foundFrom = true; + } else if (value.equals(to)) { + foundTo = true; + } + } + assertTrue(foundFrom && foundTo); } @Test From c9805297bd83f7a49b185ec3fba5bd93bf4d51be Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 18 Nov 2024 10:09:34 -0600 Subject: [PATCH 05/64] Non-functional cleanup. --- .../checkins/services/kudos/KudosConverter.java | 4 ++-- .../checkins/services/kudos/KudosControllerTest.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index 460dddabdb..ee3ff465f5 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -34,7 +34,7 @@ public KudosConverter(MemberProfileServices memberProfileServices, } public String toSlackBlock(Kudos kudos) { - // Build some the message text out of the Kudos data. + // Build the message text out of the Kudos data. List content = new ArrayList<>(); // Look up the channel id from Slack @@ -93,7 +93,7 @@ private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() { } private RichTextElement memberAsRichText(UUID memberId) { - // Look up the user name to get the user id from Slack + // Look up the user id by email address on Slack SlackSearch search = new SlackSearch(); MemberProfile profile = memberProfileServices.getById(memberId); String userId = search.findUserId(profile.getWorkEmail()); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 94f9756fca..e166954846 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -41,7 +41,6 @@ import java.time.LocalDate; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; From 16402cd6eee41dec5e97225aea30e0d765edf544 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 19 Nov 2024 16:16:40 -0600 Subject: [PATCH 06/64] Switch to server-side markdown. This has not been fully tested. --- server/build.gradle | 2 + .../services/reports/MarkdownGeneration.java | 397 ++++++++++++++++++ .../reports/ReportDataController.java | 52 +-- web-ui/src/api/generic.js | 14 +- web-ui/src/pages/MeritReportPage.jsx | 249 +---------- 5 files changed, 439 insertions(+), 275 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java diff --git a/server/build.gradle b/server/build.gradle index 8c70f18a6f..693b5cf376 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -12,6 +12,7 @@ group "com.objectcomputing.checkins" repositories { mavenCentral() + maven { url 'https://jitpack.io' } } micronaut { @@ -69,6 +70,7 @@ dependencies { runtimeOnly("org.postgresql:postgresql") compileOnly ("org.projectlombok:lombok") + implementation("com.github.Steppschuh:Java-Markdown-Generator:master-SNAPSHOT") annotationProcessor ("org.projectlombok:lombok") annotationProcessor ("io.micronaut:micronaut-inject-java") diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java new file mode 100644 index 0000000000..d26afd3fed --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java @@ -0,0 +1,397 @@ +package com.objectcomputing.checkins.services.reports; + +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import com.objectcomputing.checkins.services.kudos.KudosRepository; +import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientRepository; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; +import com.objectcomputing.checkins.services.reviews.ReviewPeriodServices; +import com.objectcomputing.checkins.services.feedback_template.FeedbackTemplateServices; +import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServices; +import com.objectcomputing.checkins.services.feedback_answer.FeedbackAnswerServices; +import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestionServices; +import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursServices; +import com.objectcomputing.checkins.services.file.FileServices; + +import net.steppschuh.markdowngenerator.*; +import net.steppschuh.markdowngenerator.text.heading.Heading; +import net.steppschuh.markdowngenerator.text.emphasis.ItalicText; +import net.steppschuh.markdowngenerator.list.UnorderedList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Comparator; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.function.Predicate; +import java.io.IOException; + +class MarkdownGeneration { + class AnswerComparator implements java.util.Comparator { + @Override + public int compare(Feedback.Answer a, Feedback.Answer b) { + return a.getNumber() - b.getNumber(); + } + } + + class PositionComparator implements java.util.Comparator { + @Override + public int compare(PositionHistory.Position a, + PositionHistory.Position b) { + LocalDate left = a.date(); + LocalDate right = b.date(); + if (left.isBefore(right)) { + return -1; + } else if (left.isEqual(right)) { + return 0; + } else { + return 1; + } + } + } + + class CompensationComparator implements java.util.Comparator { + @Override + public int compare(CompensationHistory.Compensation a, + CompensationHistory.Compensation b) { + LocalDate left = a.startDate(); + LocalDate right = b.startDate(); + if (left.isBefore(right)) { + return -1; + } else if (left.isEqual(right)) { + return 0; + } else { + return 1; + } + } + } + + private static final Logger LOG = LoggerFactory.getLogger(MarkdownGeneration.class); + private static final String noneAvailable = "None available during the period covered by this review."; + private static final String directory = "merit-reports"; + + private final ReportDataServices reportDataServices; + private final KudosRepository kudosRepository; + private final KudosRecipientRepository kudosRecipientRepository; + private final MemberProfileServices memberProfileServices; + private final ReviewPeriodServices reviewPeriodServices; + private final FeedbackTemplateServices feedbackTemplateServices; + private final FeedbackRequestServices feedbackRequestServices; + private final FeedbackAnswerServices feedbackAnswerServices; + private final TemplateQuestionServices templateQuestionServices; + private final EmployeeHoursServices employeeHoursServices; + private final FileServices fileServices; + + public MarkdownGeneration(ReportDataServices reportDataServices, + KudosRepository kudosRepository, + KudosRecipientRepository kudosRecipientRepository, + MemberProfileServices memberProfileServices, + ReviewPeriodServices reviewPeriodServices, + FeedbackTemplateServices feedbackTemplateServices, + FeedbackRequestServices feedbackRequestServices, + FeedbackAnswerServices feedbackAnswerServices, + TemplateQuestionServices templateQuestionServices, + EmployeeHoursServices employeeHoursServices, + FileServices fileServices) { + this.reportDataServices = reportDataServices; + this.kudosRepository = kudosRepository; + this.kudosRecipientRepository = kudosRecipientRepository; + this.memberProfileServices = memberProfileServices; + this.reviewPeriodServices = reviewPeriodServices; + this.feedbackTemplateServices = feedbackTemplateServices; + this.feedbackRequestServices = feedbackRequestServices; + this.feedbackAnswerServices = feedbackAnswerServices; + this.templateQuestionServices = templateQuestionServices; + this.employeeHoursServices = employeeHoursServices; + this.fileServices = fileServices; + } + + void upload(List memberIds, UUID reviewPeriodId) { + for (UUID memberId : memberIds) { + ReportDataCollation data = new ReportDataCollation( + memberId, reviewPeriodId, + kudosRepository, + kudosRecipientRepository, + memberProfileServices, + reviewPeriodServices, + reportDataServices, + feedbackTemplateServices, + feedbackRequestServices, + feedbackAnswerServices, + templateQuestionServices, + employeeHoursServices); + generateAndStore(data); + } + } + + void generateAndStore(ReportDataCollation data) { + final String markdown = generate(data); + store(data, markdown); + } + + String generate(ReportDataCollation data) { + StringBuilder sb = new StringBuilder(); + title(data, sb); + currentInfo(data, sb); + kudos(data, sb); + reviewsImpl("Self-Review", data.getSelfReviews(), false, sb); + reviewsImpl("Reviews", data.getReviews(), true, sb); + feedback(data, sb); + titleHistory(data, sb); + employeeHours(data, sb); + compensation(data, sb); + compensationHistory(data, sb); + sb.append(new Heading("Reviewer Notes", 4)).append("\n"); + return sb.toString(); + } + + void store(ReportDataCollation data, String markdown) { + // Send this text over to be uploaded to the google drive. + fileServices.uploadDocument(directory, + data.getMemberProfile().getWorkEmail(), + markdown); + } + + private String formatDate(LocalDate date) { + return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); + } + + private void title(ReportDataCollation data, StringBuilder sb) { + MemberProfile profile = data.getMemberProfile(); + sb.append(new Heading(MemberProfileUtils.getFullName(profile), 1)) + .append("\n") + .append(profile.getTitle()).append("\n\n") + .append("Review Period: ") + .append(formatDate(data.getStartDate())) + .append(" - ") + .append(formatDate(data.getEndDate())).append("\n"); + } + + private void currentInfo(ReportDataCollation data, StringBuilder sb) { + MemberProfile profile = data.getMemberProfile(); + CurrentInformation.Information current = data.getCurrentInformation(); + String bio = current.biography(); + ZonedDateTime zdt = ZonedDateTime.of( + profile.getStartDate().atTime(0, 0), + ZoneId.systemDefault()); + long ms = System.currentTimeMillis() - zdt.toInstant().toEpochMilli(); + double years = TimeUnit.DAYS.convert(ms, TimeUnit.MILLISECONDS) / 365.25; + sb.append(new Heading("Current Information", 1)).append("\n") + .append(String.format("%.1f", years)).append(" years\n\n") + .append(new Heading("Biographical Notes", 2)).append("\n") + .append(bio.isEmpty() ? noneAvailable : bio).append("\n\n"); + } + + private void kudos(ReportDataCollation data, StringBuilder sb) { + List received = data.getKudos(); + sb.append(new Heading("Kudos", 1)).append("\n\n"); + if (received.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (ReportKudos kudo : received) { + sb.append(kudo.message()).append("\n\n") + .append("     ") + .append(new ItalicText("Submitted on " + + formatDate(kudo.dateCreated()) + + ", by " + kudo.sender())) + .append("\n\n\n"); + } + } + } + + private Map getUniqueMembers(List answers) { + Map members = new HashMap<>(); + List sorted = new ArrayList<>(answers); + Collections.sort(sorted, new AnswerComparator()); + for (Feedback.Answer answer : sorted) { + if (!members.containsKey(answer.getMemberName())) { + members.put(answer.getMemberName(), answer.getSubmitted()); + } + } + return members; + } + + private Map>> getUniqueQuestions(List answers) { + Map>> questions = new HashMap<>(); + for (Feedback.Answer answer : answers) { + String key = answer.getQuestion(); + if (!questions.containsKey(key)) { + questions.put(key, new ArrayList>()); + } + List list = new ArrayList(); + list.add(answer.getMemberName()); + list.add(answer.getAnswer()); + questions.get(key).add(list); + } + return questions; + } + + private void reviewsImpl(String title, List feedbackList, boolean listMembers, StringBuilder sb) { + sb.append(new Heading(title, 1)).append("\n"); + if (feedbackList.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (Feedback feedback : feedbackList) { + Map members = + getUniqueMembers(feedback.getAnswers()); + for(Map.Entry entry : members.entrySet()) { + if (listMembers) { + sb.append(entry.getKey()).append(": "); + } + sb.append("Submitted - ") + .append(formatDate(entry.getValue())).append("\n\n"); + } + sb.append("\n"); + + Map>> questions = + getUniqueQuestions(feedback.getAnswers()); + for (Map.Entry>> question : questions.entrySet()) { + sb.append(new Heading(question.getKey(), 4)).append("\n"); + for (List answer : question.getValue()) { + if (listMembers) { + sb.append(answer.get(0)).append(": "); + } + sb.append(answer.get(1)).append("\n\n"); + } + sb.append("\n"); + } + } + sb.append("\n"); + } + } + + private void feedback(ReportDataCollation data, StringBuilder sb) { + sb.append(new Heading("Feedback", 1)).append("\n"); + List feedbackList = data.getFeedback(); + if (feedbackList.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (Feedback feedback : feedbackList) { + sb.append(new Heading("Template: " + feedback.getName(), 2)) + .append("\n"); + + Map members = + getUniqueMembers(feedback.getAnswers()); + for (Map.Entry entry : members.entrySet()) { + sb.append(entry.getKey()).append(": "); + sb.append(formatDate(entry.getValue())).append("\n\n"); + } + sb.append("\n"); + + Map>> questions = + getUniqueQuestions(feedback.getAnswers()); + for (Map.Entry>> question : questions.entrySet()) { + sb.append(new Heading(question.getKey(), 4)).append("\n"); + for (List answer : question.getValue()) { + sb.append(answer.get(0)).append(": "); + sb.append(answer.get(1)).append("\n\n"); + } + sb.append("\n"); + } + } + sb.append("\n"); + } + } + + private void titleHistory(ReportDataCollation data, StringBuilder sb) { + List posHistory = + new ArrayList<>(data.getPositionHistory()); + Collections.sort(posHistory, new PositionComparator()); + sb.append(new Heading("Title History", 2)).append("\n"); + List positions = new ArrayList<>(); + for (PositionHistory.Position position : posHistory) { + positions.add(String.valueOf(position.date().getYear()) + " - " + + position.title()); + } + sb.append(new UnorderedList<>(positions)).append("\n\n"); + } + + private void compensation(ReportDataCollation data, StringBuilder sb) { + CurrentInformation.Information current = data.getCurrentInformation(); + sb.append(new Heading("Compensation and Commitments", 2)).append("\n") + .append("$").append(String.format("%.2f", current.salary())) + .append(" Base Salary\n\n") + .append("OCI Range for role: ").append(current.range()) + .append("\n\n"); + String commitments = current.commitments(); + if (commitments == null || commitments.isEmpty()) { + sb.append("No current bonus commitments\n"); + } else { + sb.append("Commitments: ").append(current.commitments()) + .append("\n"); + } + sb.append("\n"); + } + + private List prepareCompHistory(ReportDataCollation data, Predicate fn) { + List comp = + data.getCompensationHistory().stream() + .filter(fn).collect(Collectors.toList()); + Collections.sort(comp, new CompensationComparator()); + return comp.subList(0, Math.min(3, comp.size())); + } + + private void compensationHistory(ReportDataCollation data, StringBuilder sb) { + List compBase = + prepareCompHistory(data, comp -> comp.amount() != null && + !comp.amount().isEmpty()); + List compTotal = + prepareCompHistory(data, comp -> comp.totalComp() != null && + !comp.totalComp().isEmpty()); + sb.append(new Heading("Compensation History", 2)).append("\n") + .append(new Heading("Base Compensation (annual or hourly)", 3)) + .append("\n"); + List list = new ArrayList<>(); + for (CompensationHistory.Compensation comp : compBase) { + String value = comp.amount(); + try { + double val = Double.parseDouble(value); + value = String.format("%.2f", val); + } catch (Exception e) { + } + list.add(formatDate(comp.startDate()) + " - $" + value); + } + sb.append(new UnorderedList<>(list)).append("\n\n") + .append(new Heading("Total Compensation", 3)) + .append("\n"); + list.clear(); + for (CompensationHistory.Compensation comp : compTotal) { + LocalDate startDate = comp.startDate(); + String date = startDate.getMonthValue() == 0 && + startDate.getDayOfMonth() == 1 ? + String.valueOf(startDate.getYear()) : + formatDate(startDate); + list.add(date + " - " + comp.totalComp()); + } + sb.append(new UnorderedList<>(list)).append("\n\n"); + } + + private void employeeHours(ReportDataCollation data, StringBuilder sb) { + sb.append(new Heading("Employee Hours", 2)).append("\n"); + List list = new ArrayList<>(); + ReportHours hours = data.getReportHours(); + list.add("Contribution Hours: " + + String.format("%f", hours.contributionHours())); + list.add("PTO Hours: " + String.format("%f", hours.ptoHours())); + list.add("Overtime Hours: " + + String.format("%f", hours.overtimeHours())); + list.add("Billable Utilization: " + + String.format("%f", hours.billableUtilization())); + sb.append(new UnorderedList<>(list)).append("\n\n"); + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index 15647cede2..406701b092 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -12,6 +12,8 @@ import com.objectcomputing.checkins.services.feedback_answer.FeedbackAnswerServices; import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestionServices; import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursServices; +import com.objectcomputing.checkins.services.file.FileServices; + import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; @@ -51,6 +53,7 @@ public class ReportDataController { private final FeedbackAnswerServices feedbackAnswerServices; private final TemplateQuestionServices templateQuestionServices; private final EmployeeHoursServices employeeHoursServices; + private final FileServices fileServices; public ReportDataController(ReportDataServices reportDataServices, KudosRepository kudosRepository, @@ -61,7 +64,8 @@ public ReportDataController(ReportDataServices reportDataServices, FeedbackRequestServices feedbackRequestServices, FeedbackAnswerServices feedbackAnswerServices, TemplateQuestionServices templateQuestionServices, - EmployeeHoursServices employeeHoursServices) { + EmployeeHoursServices employeeHoursServices, + FileServices fileServices) { this.reportDataServices = reportDataServices; this.kudosRepository = kudosRepository; this.kudosRecipientRepository = kudosRecipientRepository; @@ -72,6 +76,7 @@ public ReportDataController(ReportDataServices reportDataServices, this.feedbackAnswerServices = feedbackAnswerServices; this.templateQuestionServices = templateQuestionServices; this.employeeHoursServices = employeeHoursServices; + this.fileServices = fileServices; } @Post(uri="/upload", consumes = MediaType.MULTIPART_FORM_DATA) @@ -115,35 +120,22 @@ private String uploadHelper(ReportDataServices.DataType dataType, } } - @Get + @Get(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public List get(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { - List list = new ArrayList(); - for (UUID memberId : memberIds) { - ReportDataCollation data = new ReportDataCollation( - memberId, reviewPeriodId, - kudosRepository, - kudosRecipientRepository, - memberProfileServices, - reviewPeriodServices, - reportDataServices, - feedbackTemplateServices, - feedbackRequestServices, - feedbackAnswerServices, - templateQuestionServices, - employeeHoursServices); - list.add(new ReportDataDTO(memberId, reviewPeriodId, - data.getStartDate(), data.getEndDate(), - data.getMemberProfile(), data.getKudos(), - data.getCompensationHistory(), - data.getCurrentInformation(), - data.getPositionHistory(), - data.getSelfReviews(), - data.getReviews(), - data.getFeedback(), - data.getReportHours())); - } - return list; + public void generate(@NotNull List memberIds, + @NotNull UUID reviewPeriodId) { + MarkdownGeneration markdown = + new MarkdownGeneration(reportDataServices, + kudosRepository, + kudosRecipientRepository, + memberProfileServices, + reviewPeriodServices, + feedbackTemplateServices, + feedbackRequestServices, + feedbackAnswerServices, + templateQuestionServices, + employeeHoursServices, + fileServices); + markdown.upload(memberIds, reviewPeriodId); } } diff --git a/web-ui/src/api/generic.js b/web-ui/src/api/generic.js index 85bc63bc2d..712f072e04 100644 --- a/web-ui/src/api/generic.js +++ b/web-ui/src/api/generic.js @@ -20,6 +20,18 @@ export const downloadData = (url, cookie, params) => { 'X-CSRF-Header': cookie, Accept: 'application/json', }, - url: url //fullURL + url: url + }); +}; + +export const initiate = (url, cookie, params) => { + return resolve({ + method: 'GET', + params: params, + headers: { + 'X-CSRF-Header': cookie, + Accept: 'application/json', + }, + url: url }); }; diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index a590607277..008e78458c 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -2,7 +2,7 @@ import React, { useContext, useRef, useState, useEffect } from 'react'; import { Autocomplete, Button, TextField } from '@mui/material'; -import { uploadData, downloadData } from '../api/generic'; +import { uploadData, initiate } from '../api/generic'; import { getReviewPeriods } from '../api/reviewperiods'; import { UPDATE_TOAST } from '../context/actions'; import { AppContext } from '../context/AppContext'; @@ -148,7 +148,7 @@ const MeritReportPage = () => { } }; - const download = async () => { + const createReportMarkdownDocuments = async () => { let data; let error; @@ -166,9 +166,9 @@ const MeritReportPage = () => { } if (!error) { - const res = await downloadData("/services/report/data", - csrf, {memberIds: selected, - reviewPeriodId: reviewPeriodId.id}); + const res = await initiate("/services/report/data/generate", + csrf, {memberIds: selected, + reviewPeriodId: reviewPeriodId.id}); error = res?.error?.message; data = res?.payload?.data; } @@ -236,245 +236,6 @@ const MeritReportPage = () => { } }; - const markdownTitle = (data) => { - const memberProfile = data.memberProfile; - const startDate = dateFromArray(data.startDate); - const endDate = dateFromArray(data.endDate); - let text = markdown.headers.h1(memberProfile.firstName + " " + - memberProfile.lastName); - text += memberProfile.title + "\n\n"; - text += "Review Period: " + - formatDate(startDate) + " - " + formatDate(endDate) + "\n\n"; - return text; - }; - - const markdownCurrentInformation = (data) => { - const memberProfile = data.memberProfile; - const currentInfo = data.currentInformation; - const startDate = dateFromArray(memberProfile.startDate); - const years = (Date.now() - startDate) / (1000 * 60 * 60 * 24 * 365.25); - let text = markdown.headers.h1("Current Information"); - text += years.toFixed(1) + " years\n\n"; - text += markdown.headers.h2("Biographical Notes"); - text += currentInfo.biography + "\n\n"; - return text; - }; - - const markdownKudos = (data) => { - const kudosList = data.kudos; - let text = markdown.headers.h1("Kudos"); - for (let kudos of kudosList) { - const date = dateFromArray(kudos.dateCreated); - text += kudos.message + "\n\n"; - text += "     " + - markdown.emphasis.i("Submitted on " + formatDate(date) + - ", by " + kudos.sender) + - "\n\n\n"; - } - return text; - }; - - const markdownReviewsImpl = (title, feedbackList, listMembers) => { - let text = markdown.headers.h1(title); - for(let feedback of feedbackList) { - const members = getUniqueMembers(feedback.answers); - for(let member of Object.keys(members)) { - if (listMembers) { - text += member + ": "; - } - text += "Submitted - " + formatDate(members[member]) + "\n\n"; - } - text += "\n"; - - const questions = getUniqueQuestions(feedback.answers); - for(let question of Object.keys(questions)) { - text += markdown.headers.h4(question) + "\n"; - for(let answer of questions[question]) { - if (listMembers) { - text += answer[0] + ": "; - } - text += answer[1] + "\n\n"; - } - text += "\n"; - } - } - text += "\n"; - return text; - } - - const markdownSelfReviews = (data) => { - return markdownReviewsImpl("Self-Review", data.selfReviews, false); - } - - const markdownReviews = (data) => { - return markdownReviewsImpl("Reviews", data.reviews, true); - }; - - const getUniqueMembers = (answers) => { - let members = {}; - for(let answer of answers) { - const key = answer.memberName; - if (!(key in members)) { - // Put in member name and date - members[key] = dateFromArray(answer.submitted); - } - } - return members; - }; - - const getAnswerText = (answer) => { - return answer.answer; - }; - - const getUniqueQuestions = (answers) => { - let questions = {}; - answers = answers.sort((a, b) => { - return a.number - b.number; - }); - - for(let answer of answers) { - const key = answer.question; - if (!(key in questions)) { - // Put in member name and answer - questions[key] = []; - } - const text = getAnswerText(answer); - questions[key].push([answer.memberName, text]); - } - return questions; - }; - - const markdownFeedback = (data) => { - let text = markdown.headers.h1("Feedback"); - const feedbackList = data.feedback; - for(let feedback of feedbackList) { - text += markdown.headers.h2("Template: " + feedback.name); - const members = getUniqueMembers(feedback.answers); - for(let member of Object.keys(members)) { - text += member + ": " + formatDate(members[member]) + "\n\n"; - } - text += "\n"; - - const questions = getUniqueQuestions(feedback.answers); - for(let question of Object.keys(questions)) { - text += markdown.headers.h4(question) + "\n"; - for(let answer of questions[question]) { - text += answer[0] + ": " + answer[1] + "\n\n"; - } - text += "\n"; - } - } - text += "\n"; - return text; - }; - - const markdownTitleHistory = (data) => { - // Get the position history sorted latest to earliest - const posHistory = data.positionHistory.sort((a, b) => { - for(let i = 0; i < a.length; i++) { - if (a.date[i] != b.date[i]) { - return b.date[i] - a.date[i]; - } - } - return 0; - }); - - let text = markdown.headers.h2("Title History"); - text += markdown.lists.ul(posHistory, - (position) => position.date[0] + " - " + - position.title); - return text; - }; - - const markdownCompensation = (data) => { - const currentInfo = data.currentInformation; - let text = markdown.headers.h2("Compensation and Commitments"); - text += "$" + currentInfo.salary.toFixed(2) + " Base Salary\n\n"; - text += "OCI Range for role: " + currentInfo.range + "\n\n"; - text += "National Range for role: " + currentInfo.nationalRange + "\n\n"; - if (currentInfo.commitments) { - text += "Commitments: " + currentInfo.commitments + "\n"; - } else { - text += "No current bonus commitments\n"; - } - text += "\n"; - return text; - }; - - const prepareCompensationHistory = (data, fn) => { - return data.compensationHistory.filter(fn).sort((a, b) => { - for(let i = 0; i < a.startDate.length; i++) { - if (a.startDate[i] != b.startDate[i]) { - return b.startDate[i] - a.startDate[i]; - } - } - return 0; - }).slice(0, 3); - }; - - const markdownCompensationHistory = (data) => { - // Sort them latest to oldest and truncate to the first 3. - const compBase = prepareCompensationHistory(data, (comp) => !!comp.amount); - const compTotal = prepareCompensationHistory(data, (comp) => !!comp.totalComp); - - let text = markdown.headers.h2("Compensation History"); - text += markdown.headers.h3("Base Compensation (annual or hourly)"); - text += markdown.lists.ul(compBase, - (comp) => formatDate(dateFromArray(comp.startDate)) + " - " + - "$" + parseFloat(comp.amount).toFixed(2)); - text += markdown.headers.h3("Total Compensation") - text += markdown.lists.ul(compTotal, - (comp) => { - var date = dateFromArray(comp.startDate); - date = date.getMonth() === 0 && date.getDate() === 1 ? date.getFullYear() : formatDate(date); - return date + " - " + comp.totalComp; - }); - return text; - }; - - const markdownEmployeeHours = (data) => { - let text = markdown.headers.h2("Employee Hours"); - let hours = { - 'Contribution Hours': data.hours.contributionHours, - 'PTO Hours': data.hours.ptoHours, - 'Overtime Hours': data.hours.overtimeHours, - 'Billable Utilization': data.hours.billableUtilization, - }; - text += markdown.lists.ul(Object.keys(hours), - (key) => key + ": " + hours[key]); - return text; - }; - - const markdownReviewerNotes = (data) => { - let text = markdown.headers.h4("Reviewer Notes"); - return text; - }; - - const createReportMarkdownDocuments = async () => { - const dataSet = await download(); - if (dataSet) { - for (let data of dataSet) { - // Generate markdown - let text = markdownTitle(data); - text += markdownCurrentInformation(data); - text += markdownKudos(data); - text += markdownSelfReviews(data); - text += markdownReviews(data); - text += markdownFeedback(data); - text += markdownTitleHistory(data); - text += markdownEmployeeHours(data); - text += markdownCompensation(data); - text += markdownCompensationHistory(data); - text += markdownReviewerNotes(data); - - // Store the markdown on the google drive. - const directory = "merit-reports"; - const fileName = data.memberProfile.workEmail; - uploadDocument(directory, fileName, text); - } - } - }; - const onReviewPeriodChange = (event, newValue) => { setReviewPeriodId(newValue); }; From b401698e56b128a159c6b045b18a3537644180a2 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 09:13:41 -0600 Subject: [PATCH 07/64] #2790 - Added a bar chart per day showing the number of responses. --- web-ui/src/pages/PulseReportPage.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 108051804e..b329cc47d4 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -7,7 +7,7 @@ import { CartesianGrid, Legend, Line, - LineChart, + ComposedChart, ResponsiveContainer, Tooltip, XAxis, @@ -48,7 +48,7 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -//const ociLightBlue = '#76c8d4'; // not currently used +const ociLightBlue = '#76c8d4'; // const ociOrange = '#f8b576'; // too light const orange = '#b26801'; @@ -209,7 +209,8 @@ const PulseReportPage = () => { { date: day.date, internal: day.datapoints.reduce((acc, current) => acc + current.internalScore, 0)/day.datapoints.length, - external: day.datapoints.reduce((acc, current) => acc + current.externalScore, 0)/day.datapoints.length + external: day.datapoints.reduce((acc, current) => acc + current.externalScore, 0)/day.datapoints.length, + responses: day.datapoints.length, } ))); setBarChartData(frequencies); @@ -436,7 +437,7 @@ const PulseReportPage = () => { /> - + { { stroke={orange} type="monotone" /> - + + From ad2b6d54be3c284e7b4f51dea2a63fe249d99e75 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:10:19 -0600 Subject: [PATCH 08/64] Use version 1.3.1.1 of the markdowngenerator from mavenCentral. --- server/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 693b5cf376..857628bfa3 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -12,7 +12,6 @@ group "com.objectcomputing.checkins" repositories { mavenCentral() - maven { url 'https://jitpack.io' } } micronaut { @@ -70,7 +69,6 @@ dependencies { runtimeOnly("org.postgresql:postgresql") compileOnly ("org.projectlombok:lombok") - implementation("com.github.Steppschuh:Java-Markdown-Generator:master-SNAPSHOT") annotationProcessor ("org.projectlombok:lombok") annotationProcessor ("io.micronaut:micronaut-inject-java") @@ -81,6 +79,7 @@ dependencies { yarnBuildElements(project(":web-ui")) + implementation("net.steppschuh.markdowngenerator:markdowngenerator:1.3.1.1") implementation("io.micronaut:micronaut-jackson-databind") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut:micronaut-management") From aa60fef35b5b9283ff98056ad54442e8c2866e05 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:11:06 -0600 Subject: [PATCH 09/64] Provide an indicator for when document generation is complete. --- web-ui/src/pages/MeritReportPage.jsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index 008e78458c..d98675fb72 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -149,14 +149,13 @@ const MeritReportPage = () => { }; const createReportMarkdownDocuments = async () => { - let data; let error; // Get the list of selected member ids. - let selected = selectedMembers.reduce((result, item) => { - result.push(item.id); - return result; - }, []); + const selected = selectedMembers.reduce((result, item) => { + result.push(item.id); + return result; + }, []); // Check for required parameters before calling the server. if (selected.length == 0) { @@ -170,7 +169,6 @@ const MeritReportPage = () => { csrf, {memberIds: selected, reviewPeriodId: reviewPeriodId.id}); error = res?.error?.message; - data = res?.payload?.data; } // Display the error, if there was one. @@ -182,9 +180,16 @@ const MeritReportPage = () => { toast: error } }); + } else { + dispatch({ + type: UPDATE_TOAST, + payload: { + severity: 'success', + toast: selected.length == 1 ? 'The report has been generated' + : 'The reports have been generated' + } + }); } - - return data; }; const uploadDocument = async (directory, name, text) => { From f145ced89972d8d12d8172fe1253f09976a22187 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:25:44 -0600 Subject: [PATCH 10/64] Added the review period name to the review period pull down. --- web-ui/src/pages/MeritReportPage.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index d98675fb72..9ccb98acfb 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -79,7 +79,8 @@ const MeritReportPage = () => { if (data) { let periods = data.reduce((result, item) => { if (item.closeDate) { - result.push({label: formatReviewDate(item.closeDate), + result.push({label: item.name + " - " + + formatReviewDate(item.closeDate), id: item.id}); } return result; @@ -309,7 +310,7 @@ const MeritReportPage = () => { )} From 819a7464b58a815bc10938f589fa79ecc9fd1da4 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 09:58:09 -0600 Subject: [PATCH 11/64] Corrected hour format of the generated markdown and updated the ReportDataController test. --- .../services/reports/MarkdownGeneration.java | 12 +- .../reports/ReportDataController.java | 6 +- .../reports/FileServicesImplReplacement.java | 59 +++++++++ .../reports/ReportDataControllerTest.java | 119 +++++++----------- 4 files changed, 117 insertions(+), 79 deletions(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java index d26afd3fed..a1eea89b78 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java @@ -356,11 +356,12 @@ private void compensationHistory(ReportDataCollation data, StringBuilder sb) { .append(new Heading("Base Compensation (annual or hourly)", 3)) .append("\n"); List list = new ArrayList<>(); + final String compFormat = "%.2f"; for (CompensationHistory.Compensation comp : compBase) { String value = comp.amount(); try { double val = Double.parseDouble(value); - value = String.format("%.2f", val); + value = String.format(compFormat, val); } catch (Exception e) { } list.add(formatDate(comp.startDate()) + " - $" + value); @@ -384,13 +385,14 @@ private void employeeHours(ReportDataCollation data, StringBuilder sb) { sb.append(new Heading("Employee Hours", 2)).append("\n"); List list = new ArrayList<>(); ReportHours hours = data.getReportHours(); + final String hourFormat = "%.2f"; list.add("Contribution Hours: " + - String.format("%f", hours.contributionHours())); - list.add("PTO Hours: " + String.format("%f", hours.ptoHours())); + String.format(hourFormat, hours.contributionHours())); + list.add("PTO Hours: " + String.format(hourFormat, hours.ptoHours())); list.add("Overtime Hours: " + - String.format("%f", hours.overtimeHours())); + String.format(hourFormat, hours.overtimeHours())); list.add("Billable Utilization: " + - String.format("%f", hours.billableUtilization())); + String.format(hourFormat, hours.billableUtilization())); sb.append(new UnorderedList<>(list)).append("\n\n"); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index 406701b092..ddf448a30a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -15,6 +15,7 @@ import com.objectcomputing.checkins.services.file.FileServices; import io.micronaut.http.MediaType; +import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Get; @@ -122,8 +123,8 @@ private String uploadHelper(ReportDataServices.DataType dataType, @Get(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public void generate(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { + public HttpStatus generate(@NotNull List memberIds, + @NotNull UUID reviewPeriodId) { MarkdownGeneration markdown = new MarkdownGeneration(reportDataServices, kudosRepository, @@ -137,5 +138,6 @@ public void generate(@NotNull List memberIds, employeeHoursServices, fileServices); markdown.upload(memberIds, reviewPeriodId); + return HttpStatus.OK; } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java new file mode 100644 index 0000000000..68b6ec4ea1 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java @@ -0,0 +1,59 @@ +package com.objectcomputing.checkins.services.reports; + +/************************************************************************* + * + * This class is here due to the fact that the ReportDataController now + * references the FileServices. The real FileServicesImpl requires the + * GoogleApiAccess class that does not exist during testing. + * + * This replacement class does not require that and can help us test the + * output of the MarkdownGeneration class. + * + ************************************************************************/ + +import com.objectcomputing.checkins.services.file.FileInfoDTO; +import com.objectcomputing.checkins.services.file.FileServices; +import com.objectcomputing.checkins.services.file.FileServicesImpl; + +import io.micronaut.http.multipart.CompletedFileUpload; + +import java.io.File; +import java.util.Set; +import java.util.HashSet; +import java.util.UUID; + +import jakarta.inject.Singleton; +import io.micronaut.context.env.Environment; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; + +@Singleton +@Replaces(FileServicesImpl.class) +@Requires(env = Environment.TEST) +public class FileServicesImplReplacement implements FileServices { + public String documentName = ""; + public String documentText = ""; + + public Set findFiles(UUID checkInId) { + return new HashSet(); + } + + public File downloadFiles(String uploadDocId) { + return null; + } + + public FileInfoDTO uploadFile(UUID checkInID, CompletedFileUpload file) { + return new FileInfoDTO(); + } + + public FileInfoDTO uploadDocument(String directory, + String name, String text) { + documentName = name; + documentText = text; + return new FileInfoDTO(); + } + + public boolean deleteFile(String uploadDocId) { + return true; + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java index d88012c0b9..baed8cbe3e 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java @@ -1,9 +1,5 @@ package com.objectcomputing.checkins.services.reports; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.core.JsonProcessingException; import com.objectcomputing.checkins.services.kudos.Kudos; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.fixture.KudosFixture; @@ -22,6 +18,8 @@ import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestion; import com.objectcomputing.checkins.services.employee_hours.EmployeeHours; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; @@ -34,6 +32,7 @@ import java.io.File; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; @@ -44,12 +43,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@Property(name = "replace.fileservicesimpl", value = StringUtils.TRUE) class ReportDataControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, KudosFixture, ReviewPeriodFixture, FeedbackTemplateFixture, FeedbackRequestFixture, TemplateQuestionFixture, FeedbackAnswerFixture, EmployeeHoursFixture { @Inject @Client("/services/report/data") HttpClient client; + @Inject + private FileServicesImplReplacement fileServices; + private EmployeeHours employeeHours; private FeedbackTemplate feedbackTemplate; private ReviewPeriod reviewPeriod; @@ -58,6 +61,7 @@ class ReportDataControllerTest extends TestContainersSuite implements MemberProf private TemplateQuestion questionTwo; private MemberProfile regular; private MemberProfile admin; + private Kudos kudos; private final String basePath = "src/test/java/com/objectcomputing/checkins/services/reports/"; @BeforeEach @@ -79,7 +83,7 @@ void createRolesAndPermissions() { saveSampleFeedbackAnswer(questionOne.getId(), feedbackRequest.getId()); saveSampleFeedbackAnswer(questionTwo.getId(), feedbackRequest.getId()); - Kudos kudos = createApprovedKudos(admin.getId()); + kudos = createApprovedKudos(admin.getId()); createKudosRecipient(kudos.getId(), regular.getId()); employeeHours = new EmployeeHours(regular.getEmployeeId(), @@ -106,45 +110,27 @@ void uploadReportDataWithoutPermission() { } @Test - void getReportData() throws JsonProcessingException { + void processReportData() { MemberProfile target = regular; HttpRequest request = postData(admin, ADMIN_ROLE); final String response = client.toBlocking().retrieve(request); assertNotNull(response); request = HttpRequest.GET( - String.format("/?memberIds=%s&reviewPeriodId=%s", + String.format("/generate?memberIds=%s&reviewPeriodId=%s", target.getId(), reviewPeriod.getId().toString())) .basicAuth(admin.getWorkEmail(), ADMIN_ROLE); - final String data = client.toBlocking().retrieve(request); - ObjectMapper objectMapper = new ObjectMapper(); - assertNotNull(data); - - // Perform minimal validation of returned data - JsonNode root = objectMapper.readTree(data); - assertTrue(root.isArray()); - assertFalse(root.isEmpty()); - - ArrayNode arrayNode = (ArrayNode)root; - JsonNode first = arrayNode.get(0); - assertNotNull(first.get("memberProfile")); - assertNotNull(first.get("kudos")); - assertNotNull(first.get("compensationHistory")); - assertNotNull(first.get("currentInformation")); - assertNotNull(first.get("positionHistory")); - assertNotNull(first.get("selfReviews")); - assertNotNull(first.get("reviews")); - assertNotNull(first.get("feedback")); - assertNotNull(first.get("hours")); - - validateReportData(first, target); + client.toBlocking().exchange(request); + + validateReportData(fileServices.documentName, + fileServices.documentText, target); } @Test - void getReportDataWithoutPermission() { + void processReportDataWithoutPermission() { final HttpRequest request = HttpRequest.GET( - String.format("/?memberIds=%s&reviewPeriodId=%s", + String.format("/generate?memberIds=%s&reviewPeriodId=%s", regular.getId(), reviewPeriod.getId())) .basicAuth(regular.getWorkEmail(), MEMBER_ROLE); @@ -168,54 +154,43 @@ HttpRequest postData(MemberProfile user, String role) { .contentType(MULTIPART_FORM_DATA); } - void validateReportData(JsonNode node, MemberProfile user) { - final String memberId = user.getId().toString(); + private String formatDate(LocalDate date) { + return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); + } + + void validateReportData(String filename, String text, MemberProfile user) { + assertEquals(user.getWorkEmail(), filename); + + // Review Period + assertTrue(text.contains( + formatDate(reviewPeriod.getPeriodStartDate().toLocalDate()))); + assertTrue(text.contains( + formatDate(reviewPeriod.getPeriodEndDate().toLocalDate()))); // Member Info - assertEquals(memberId, node.get("memberId").asText()); - JsonNode profile = node.get("memberProfile"); - assertEquals(user.getFirstName(), profile.get("firstName").asText()); - assertEquals(user.getLastName(), profile.get("lastName").asText()); - assertEquals(user.getTitle(), profile.get("title").asText()); + assertTrue(text.contains(user.getFirstName())); + assertTrue(text.contains(user.getLastName())); + assertTrue(text.contains(user.getTitle())); // Kudos - ArrayNode kudos = (ArrayNode)node.get("kudos"); - assertEquals(1, kudos.size()); - assertEquals("Default Kudos", kudos.get(0).get("message").asText()); - - // Compensation History - ArrayNode comp = (ArrayNode)node.get("compensationHistory"); - assertEquals(10, comp.size()); - assertEquals(memberId, comp.get(0).get("memberId").asText()); - assertTrue(comp.get(0).get("amount").asDouble() > 0); - assertFalse(comp.get(9).get("totalComp").asText().isEmpty()); - - // Current Information - JsonNode curr = node.get("currentInformation"); - assertEquals(memberId, curr.get("memberId").asText()); - assertTrue(curr.get("salary").asDouble() > 0); - assertEquals("$90000 - $150000", curr.get("range").asText()); - assertEquals("$89000 - $155000", curr.get("nationalRange").asText()); - - // Position History - ArrayNode pos = (ArrayNode)node.get("positionHistory"); - assertEquals(3, pos.size()); - assertEquals(memberId, pos.get(2).get("memberId").asText()); - assertEquals("Software Engineer", pos.get(2).get("title").asText()); + assertTrue(text.contains(kudos.getMessage())); // Feedback - ArrayNode feedback = (ArrayNode)node.get("feedback"); - assertEquals(1, feedback.size()); - ArrayNode answers = (ArrayNode)feedback.get(0).get("answers"); - assertEquals(2, answers.size()); - assertEquals("TEXT", answers.get(0).get("type").asText()); - assertEquals(1, answers.get(0).get("number").asInt()); + assertTrue(text.contains(feedbackTemplate.getTitle())); + assertTrue(text.contains(questionOne.getQuestion())); + assertTrue(text.contains(questionTwo.getQuestion())); + assertTrue(text.contains( + formatDate(feedbackRequest.getSubmitDate()))); // Hours - JsonNode hours = node.get("hours"); - assertEquals(employeeHours.getContributionHours(), hours.get("contributionHours").asDouble(), 0.0); - assertEquals(employeeHours.getPtoHours(), hours.get("ptoHours").asDouble(), 0.0); - assertEquals(employeeHours.getOvertimeWorked(), hours.get("overtimeHours").asDouble(), 0.0); - assertEquals(employeeHours.getBillableUtilization(), hours.get("billableUtilization").asDouble(), 0.0); + final String format = "%.2f"; + assertTrue(text.contains( + String.format(format, employeeHours.getContributionHours()))); + assertTrue(text.contains( + String.format(format, employeeHours.getPtoHours()))); + assertTrue(text.contains( + String.format(format, employeeHours.getOvertimeWorked()))); + assertTrue(text.contains( + String.format(format, employeeHours.getBillableUtilization()))); } } From 5a76dd4ed16f8ea30df5dbc02a5741a0c0558765 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 10:24:25 -0600 Subject: [PATCH 12/64] Updated to reflect the label change. --- web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap index 470a70b25a..b85accd776 100644 --- a/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap @@ -173,7 +173,7 @@ exports[`renders correctly 1`] = ` for="reviewPeriodSelect" id="reviewPeriodSelect-label" > - ReviewPeriod + Review Period
- ReviewPeriod + Review Period From b5e49776be1fa99a42a8828a404777a285b6637e Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 11:54:59 -0600 Subject: [PATCH 13/64] Changed report generation to a POST so that member ids could be part of the body instead of a URL parameter. --- .../reports/ReportDataController.java | 9 +++-- .../services/reports/ReportDataDTO.java | 40 +------------------ .../reports/ReportDataControllerTest.java | 22 ++++++---- web-ui/src/api/generic.js | 7 ++-- 4 files changed, 24 insertions(+), 54 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index ddf448a30a..1a5a15626b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -17,6 +17,7 @@ import io.micronaut.http.MediaType; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Get; import io.micronaut.http.multipart.CompletedFileUpload; @@ -30,6 +31,7 @@ import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; import jakarta.validation.constraints.NotNull; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,10 +123,9 @@ private String uploadHelper(ReportDataServices.DataType dataType, } } - @Get(uri="/generate") + @Post(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public HttpStatus generate(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { + public HttpStatus generate(@Body @Valid ReportDataDTO dto) { MarkdownGeneration markdown = new MarkdownGeneration(reportDataServices, kudosRepository, @@ -137,7 +138,7 @@ public HttpStatus generate(@NotNull List memberIds, templateQuestionServices, employeeHoursServices, fileServices); - markdown.upload(memberIds, reviewPeriodId); + markdown.upload(dto.getMemberIds(), dto.getReviewPeriodId()); return HttpStatus.OK; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java index 2d290145eb..d9fa1566b2 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java @@ -1,58 +1,20 @@ package com.objectcomputing.checkins.services.reports; -import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import io.micronaut.core.annotation.Introspected; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; -import java.time.LocalDate; import java.util.List; import java.util.UUID; @Getter @Setter -@AllArgsConstructor @Introspected public class ReportDataDTO { - @NotNull - private UUID memberId; + private List memberIds; @NotNull private UUID reviewPeriodId; - - @NotNull - private LocalDate startDate; - - @NotNull - private LocalDate endDate; - - @NotNull - private MemberProfile memberProfile; - - @NotNull - private List kudos; - - @NotNull - private List compensationHistory; - - @NotNull - private CurrentInformation.Information currentInformation; - - @NotNull - private List positionHistory; - - @NotNull - private List selfReviews; - - @NotNull - private List reviews; - - @NotNull - private List feedback; - - @NotNull - private ReportHours hours; } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java index baed8cbe3e..06c9cb7fa5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java @@ -31,6 +31,8 @@ import org.junit.jupiter.api.Test; import java.io.File; +import java.util.UUID; +import java.util.ArrayList; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -116,10 +118,12 @@ void processReportData() { final String response = client.toBlocking().retrieve(request); assertNotNull(response); - request = HttpRequest.GET( - String.format("/generate?memberIds=%s&reviewPeriodId=%s", - target.getId(), - reviewPeriod.getId().toString())) + ReportDataDTO dto = new ReportDataDTO(); + ArrayList memberIds = new ArrayList<>(); + memberIds.add(target.getId()); + dto.setReviewPeriodId(reviewPeriod.getId()); + dto.setMemberIds(memberIds); + request = HttpRequest.POST("/generate", dto) .basicAuth(admin.getWorkEmail(), ADMIN_ROLE); client.toBlocking().exchange(request); @@ -129,10 +133,12 @@ void processReportData() { @Test void processReportDataWithoutPermission() { - final HttpRequest request = HttpRequest.GET( - String.format("/generate?memberIds=%s&reviewPeriodId=%s", - regular.getId(), - reviewPeriod.getId())) + ReportDataDTO dto = new ReportDataDTO(); + ArrayList memberIds = new ArrayList<>(); + memberIds.add(regular.getId()); + dto.setReviewPeriodId(reviewPeriod.getId()); + dto.setMemberIds(memberIds); + final HttpRequest request = HttpRequest.POST("/generate", dto) .basicAuth(regular.getWorkEmail(), MEMBER_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, diff --git a/web-ui/src/api/generic.js b/web-ui/src/api/generic.js index 712f072e04..23998c6403 100644 --- a/web-ui/src/api/generic.js +++ b/web-ui/src/api/generic.js @@ -26,12 +26,13 @@ export const downloadData = (url, cookie, params) => { export const initiate = (url, cookie, params) => { return resolve({ - method: 'GET', - params: params, + method: 'POST', headers: { 'X-CSRF-Header': cookie, Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' }, - url: url + url: url, + data: params, }); }; From ad41c960fedad5312694919e97625aabadf6e592 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 18 Dec 2024 10:09:44 -0600 Subject: [PATCH 14/64] Use a stacked bar chart to display counts for the day. --- web-ui/src/pages/PulseReportPage.jsx | 130 ++++++++++++++++++++------- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index b329cc47d4..bb6457fdf1 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -6,14 +6,19 @@ import { BarChart, CartesianGrid, Legend, - Line, - ComposedChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import { Comment } from '@mui/icons-material'; +import { + Comment, + SentimentVeryDissatisfied, + SentimentDissatisfied, + SentimentNeutral, + SentimentSatisfied, + SentimentVerySatisfied, +} from '@mui/icons-material'; import { Avatar, Card, @@ -48,7 +53,7 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -const ociLightBlue = '#76c8d4'; +//const ociLightBlue = '#76c8d4'; // not currently used // const ociOrange = '#f8b576'; // too light const orange = '#b26801'; @@ -205,14 +210,31 @@ const PulseReportPage = () => { } } - setLineChartData(lineChartDataPoints.map(day => ( - { + setLineChartData(lineChartDataPoints.map(day => { + const iScores = {}; + const eScores = {}; + + day.datapoints.forEach(datapoint => { + iScores[datapoint.internalScore] = + (iScores[datapoint.internalScore] || 0) + 1; + eScores[datapoint.externalScore] = + (eScores[datapoint.externalScore] || 0) + 1; + }); + + return { date: day.date, - internal: day.datapoints.reduce((acc, current) => acc + current.internalScore, 0)/day.datapoints.length, - external: day.datapoints.reduce((acc, current) => acc + current.externalScore, 0)/day.datapoints.length, - responses: day.datapoints.length, - } - ))); + internalVeryDissatisfied: iScores[1], + internalDissatisfied: iScores[2], + internalNeutral: iScores[3], + internalSatisfied: iScores[4], + internalVerySatisfied: iScores[5], + externalVeryDissatisfied: eScores[1], + externalDissatisfied: eScores[2], + externalNeutral: eScores[3], + externalSatisfied: eScores[4], + externalVerySatisfied: eScores[5], + }; + })); setBarChartData(frequencies); for (const memberId of Object.keys(averageData)) { @@ -407,6 +429,53 @@ const PulseReportPage = () => { setTeamMembers(members); }; + const dataInfo = [ + {key: "internalVeryDissatisfied", stackId: "internal", color: "#273e58", }, + {key: "internalDissatisfied", stackId: "internal", color: "#1a3c6d", }, + {key: "internalNeutral", stackId: "internal", color: "#2c519e", }, + {key: "internalSatisfied", stackId: "internal", color: "#4b7ac7", }, + {key: "internalVerySatisfied", stackId: "internal", color: "#6fa3e6", }, + {key: "externalVeryDissatisfied", stackId: "external", color: "#704401", }, + {key: "externalDissatisfied", stackId: "external", color: "#8a5200", }, + {key: "externalNeutral", stackId: "external", color: "#b26801", }, + {key: "externalSatisfied", stackId: "external", color: "#d48a2c", }, + {key: "externalVerySatisfied", stackId: "external", color: "#e0a456", }, + ]; + + const labelToSentiment = (label) => { + const suffix = label.includes("internal") ? "At Work" : "Outside Work"; + switch(label.replace("internal", "").replace("external", "")) { + case "VeryDissatisfied": + return <> {suffix}; + case "Dissatisfied": + return <> {suffix}; + case "Neutral": + return <> {suffix}; + case "Satisfied": + return <> {suffix}; + case "VerySatisfied": + return <> {suffix}; + } + return "ERROR"; + }; + + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map(p => { + return
+ {p.value} {p.name.props.children} +
; + })} +
+ ); + } + + return null; + }; + const scoreCard = highest => { const label = scope === 'Manager' ? 'Team' : 'Individual'; const property = propertyMap[scoreType]; @@ -432,12 +501,13 @@ const PulseReportPage = () => { const lineChart = () => ( - + + } /> { padding={{ left: 30, right: 30 }} tickMargin={45} /> - + - - - - + {dataInfo.map((obj) => { + return ; + }) + } + From 3291416cc85173f643e0292f2c2ccd2a604ce8aa Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 18 Dec 2024 10:29:22 -0600 Subject: [PATCH 15/64] Renamed the line chart variable. --- web-ui/src/pages/PulseReportPage.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index bb6457fdf1..385db37ddf 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -105,7 +105,7 @@ const PulseReportPage = () => { const [averageData, setAverageData] = useState({}); const [barChartData, setBarChartData] = useState([]); const [expanded, setExpanded] = useState(false); - const [lineChartData, setLineChartData] = useState([]); + const [scoreChartData, setScoreChartData] = useState([]); const [pulses, setPulses] = useState([]); const [scope, setScope] = useState('Individual'); const [scoreType, setScoreType] = useState(ScoreOption.COMBINED); @@ -131,7 +131,7 @@ const PulseReportPage = () => { }); date.setDate(date.getDate() + 1); } - setLineChartData(data); + setScoreChartData(data); const frequencies = []; for (let i = 1; i <= 5; i++) { @@ -150,7 +150,7 @@ const PulseReportPage = () => { // This creates data in the format that recharts needs from pulse data. useEffect(() => { const averageData = {}; // key is member id - const lineChartDataPoints = []; + const scoreChartDataPoints = []; const frequencies = []; for (let i = 1; i <= 5; i++) { frequencies.push({ score: i, internal: 0, external: 0 }); @@ -168,11 +168,11 @@ const PulseReportPage = () => { const monthPadded = month.toString().padStart(2, '0'); const dayPadded = day.toString().padStart(2, '0'); const date = `${year}-${monthPadded}-${dayPadded}`; - const found = lineChartDataPoints.find(points => points.date === date) + const found = scoreChartDataPoints.find(points => points.date === date) if(found) { found?.datapoints?.push(pulse); } else { - lineChartDataPoints.push({ + scoreChartDataPoints.push({ date, datapoints: [pulse] }); @@ -210,7 +210,7 @@ const PulseReportPage = () => { } } - setLineChartData(lineChartDataPoints.map(day => { + setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; const eScores = {}; @@ -506,7 +506,7 @@ const PulseReportPage = () => { /> - + } /> Date: Thu, 19 Dec 2024 08:58:13 -0600 Subject: [PATCH 16/64] Added a toggle for at-work/outside-work/both and a pie chart showing breakdown of responses (with emojis). --- web-ui/src/pages/PulseReportPage.jsx | 230 +++++++++++++++++++++++---- 1 file changed, 197 insertions(+), 33 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 385db37ddf..4e1a5badd4 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -6,6 +6,8 @@ import { BarChart, CartesianGrid, Legend, + Pie, + PieChart, ResponsiveContainer, Tooltip, XAxis, @@ -32,7 +34,8 @@ import { TextField, Typography } from '@mui/material'; - +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -53,7 +56,7 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -//const ociLightBlue = '#76c8d4'; // not currently used +const ociLightBlue = '#76c8d4'; // const ociOrange = '#f8b576'; // too light const orange = '#b26801'; @@ -69,6 +72,12 @@ const propertyMap = { [ScoreOption.COMBINED]: 'combinedAverage' }; +const ScoreOptionLabel = { + 'Internal': 'At Work', + 'External': 'Outside Work', + 'Combined': 'Both', +}; + /* // Returns a random, integer score between 1 and 5. // We may want to uncomment this later for testing. @@ -112,6 +121,7 @@ const PulseReportPage = () => { const [selectedPulse, setSelectedPulse] = useState(null); const [showComments, setShowComments] = useState(false); const [teamMembers, setTeamMembers] = useState([]); + const [pieChartData, setPieChartData] = useState([]); /* // This generates random data to use in the line chart. @@ -210,6 +220,21 @@ const PulseReportPage = () => { } } + let pieCounts = [ + {name: "veryDissatisfied", value: 0}, + {name: "dissatisfied", value: 0}, + {name: "neutral", value: 0}, + {name: "satisfied", value: 0}, + {name: "verySatisfied", value: 0}, + ]; + for(let day of scoreChartDataPoints) { + day.datapoints.forEach(datapoint => { + pieCounts[datapoint.internalScore - 1].value++; + pieCounts[datapoint.externalScore - 1].value++; + }); + } + setPieChartData(pieCounts); + setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; const eScores = {}; @@ -319,27 +344,6 @@ const PulseReportPage = () => {
Average Scores - - setScoreType(e.target.value)} - sx={{ width: '8rem' }} - value={scoreType} - variant="outlined" - > - - {ScoreOption.INTERNAL} - - - {ScoreOption.EXTERNAL} - - - {ScoreOption.COMBINED} - - - { ); - const barChart = () => ( + const scoreDistributionChart = () => ( { - - + {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.INTERNAL) && + + } + {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.EXTERNAL) && + + } { ]; const labelToSentiment = (label) => { - const suffix = label.includes("internal") ? "At Work" : "Outside Work"; + const suffix = label.includes("internal") + ? ScoreOptionLabel[ScoreOption.INTERNAL] + : ScoreOptionLabel[ScoreOption.EXTERNAL]; switch(label.replace("internal", "").replace("external", "")) { case "VeryDissatisfied": return <> {suffix}; @@ -465,7 +483,7 @@ const PulseReportPage = () => {

{label}

{payload.map(p => { - return
+ return
{p.value} {p.name.props.children}
; })} @@ -498,10 +516,95 @@ const PulseReportPage = () => { ); }; - const lineChart = () => ( + const pulseScoresTitle = () => { + let title = "Pulse scores for"; + if (scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.INTERNAL) { + title += ` "${ScoreOptionLabel[ScoreOption.INTERNAL]}"`; + } + if (scoreType == ScoreOption.COMBINED) { + title += " and"; + } + if (scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.EXTERNAL) { + title += ` "${ScoreOptionLabel[ScoreOption.EXTERNAL]}"`; + } + return title; + }; + + const pieLabelToSentiment = (label) => { + switch(label.toLowerCase()) { + case "verydissatisfied": + //return ; + return "😦"; + case "dissatisfied": + //return ; + return "🙁"; + case "neutral": + //return ; + return "😐"; + case "satisfied": + //return ; + return "🙂"; + case "verysatisfied": + //return ; + return "😀"; + } + return "ERROR"; + }; + + const RADIAN = Math.PI / 180; + const renderPieLabel = function({ cx, cy, midAngle, innerRadius, outerRadius, + percent, index, name, value }) { + const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return ( + <> + cx ? 'start' : 'end'} + dominantBaseline="central"> + {pieLabelToSentiment(name)} {value} + + + ); + }; + + const titleWords = (text) => { + if (text.match(/^[a-z]+$/)) { + // Uppercase the first letter + text = text[0].toUpperCase() + text.substring(1); + } else { + // Split words and uppercase the first word + let words = text.split(/(?<=[a-z])(?=[A-Z\d])/); + words[0] = words[0][0].toUpperCase() + words[0].substring(1); + text= ""; + let separator = ""; + for(let word of words) { + text += `${separator}${word}`; + separator = " "; + } + } + return text; + }; + + const CustomPieTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{titleWords(payload[0].name)} : {payload[0].value}

+
+ ); + } + + return null; + }; + + const pulseScoresChart = () => ( + <> @@ -519,7 +622,12 @@ const PulseReportPage = () => { - {dataInfo.map((obj) => { + {dataInfo.filter(o => scoreType == ScoreOption.COMBINED || + (scoreType == ScoreOption.INTERNAL && + o.key.includes("internal")) || + (scoreType == ScoreOption.EXTERNAL && + o.key.includes("external"))) + .map((obj) => { return { + + + + + + } + /> + + + + + + ); const responseSummary = () => { @@ -584,6 +717,25 @@ const PulseReportPage = () => { ); }; + const toggleLabels = { + left: { + title: ScoreOptionLabel[ScoreOption.INTERNAL], + value: ScoreOption.INTERNAL, + }, + center: { + title: ScoreOptionLabel[ScoreOption.Combined], + value: ScoreOption.COMBINED, + }, + right: { + title: ScoreOptionLabel[ScoreOption.EXTERNAL], + value: ScoreOption.EXTERNAL, + }, + }; + + const toggleChange = (event, value) => { + setScoreType(value); + }; + return selectHasViewPulseReportPermission(state) ? (
@@ -601,6 +753,18 @@ const PulseReportPage = () => { value={dayjs(dateTo)} /> + + + {ScoreOptionLabel[ScoreOption.INTERNAL]} + + {ScoreOptionLabel[ScoreOption.COMBINED]} + + {ScoreOptionLabel[ScoreOption.EXTERNAL]} +
{pulses.length === 0 ? ( @@ -614,9 +778,9 @@ const PulseReportPage = () => { onChange={handleTeamMembersChange} selected={teamMembers} /> - {lineChart()} + {pulseScoresChart()} {averageScores()} - {barChart()} + {scoreDistributionChart()} setShowComments(false)}> Date: Thu, 19 Dec 2024 09:12:24 -0600 Subject: [PATCH 17/64] Removed commented sentiment icons. --- web-ui/src/pages/PulseReportPage.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 4e1a5badd4..3ad40a8ebe 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -535,19 +535,14 @@ const PulseReportPage = () => { const pieLabelToSentiment = (label) => { switch(label.toLowerCase()) { case "verydissatisfied": - //return ; return "😦"; case "dissatisfied": - //return ; return "🙁"; case "neutral": - //return ; return "😐"; case "satisfied": - //return ; return "🙂"; case "verysatisfied": - //return ; return "😀"; } return "ERROR"; From a0111da4ce91c336696a88559f311e7d74130aa0 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 19 Dec 2024 09:24:23 -0600 Subject: [PATCH 18/64] Updated to reflect the removal of the drop-down. --- .../PulseReportPage.test.jsx.snap | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap index 0006bfbdf6..9c45b22856 100644 --- a/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/PulseReportPage.test.jsx.snap @@ -142,6 +142,47 @@ exports[`renders correctly 1`] = `
+
+ + + +

Date: Thu, 19 Dec 2024 13:25:41 -0600 Subject: [PATCH 19/64] Filter out zero count pie pieces. --- web-ui/src/pages/PulseReportPage.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 3ad40a8ebe..066ff4eea3 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -233,7 +233,9 @@ const PulseReportPage = () => { pieCounts[datapoint.externalScore - 1].value++; }); } - setPieChartData(pieCounts); + // Filter out data with a zero value so that the pie chart does not attempt + // to display them. + setPieChartData(pieCounts.filter((p) => p.value != 0)); setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; From d29e6362a9c74cb1ac341489dc0e6dcda39044c5 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 08:07:00 -0600 Subject: [PATCH 20/64] Allow for anonymous pulse submission and viewing in the pulse report. --- .../services/pulseresponse/PulseResponse.java | 6 +- .../pulseresponse/PulseResponseCreateDTO.java | 2 +- .../PulseResponseServicesImpl.java | 11 ++- .../V119__alter_pulse_response_team_id.sql | 1 + web-ui/package.json | 1 + web-ui/src/pages/PulsePage.css | 6 ++ web-ui/src/pages/PulsePage.jsx | 74 +++++++++++-------- web-ui/src/pages/PulseReportPage.jsx | 27 ++++--- web-ui/yarn.lock | 5 ++ 9 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java index 9c22589300..1b59be5c40 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java @@ -52,7 +52,7 @@ public class PulseResponse { @Column(name="teammemberid") @TypeDef(type=DataType.STRING) - @NotNull + @Nullable @Schema(description = "id of the teamMember this entry is associated with") private UUID teamMemberId; @@ -77,7 +77,7 @@ public class PulseResponse { protected PulseResponse() { } - public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) { + public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, String internalFeelings, String externalFeelings) { this.id = id; this.internalScore = internalScore; this.externalScore = externalScore; @@ -88,7 +88,7 @@ public PulseResponse(UUID id, Integer internalScore, Integer externalScore, Loca } public PulseResponse(Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) { - this(null,internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings); + this(null, internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings); } public UUID getId() { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java index aecb35e607..a5a289afa8 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java @@ -27,7 +27,7 @@ public class PulseResponseCreateDTO { @Schema(description = "date for submissionDate") private LocalDate submissionDate; - @NotNull + @Nullable @Schema(description = "id of the associated member") private UUID teamMemberId; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java index 50004da289..0cd2ee2177 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java @@ -55,11 +55,14 @@ public PulseResponse save(PulseResponse pulseResponse) { LocalDate pulseSubDate = pulseResponse.getSubmissionDate(); if (pulseResponse.getId() != null) { throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId())); - } else if (memberRepo.findById(memberId).isEmpty()) { + } else if (memberId != null && + memberRepo.findById(memberId).isEmpty()) { throw new BadArgException(String.format("Member %s doesn't exists", memberId)); } else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) { throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId)); - } else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) { + } else if (memberId != null && + !currentUserId.equals(memberId) && + !isSubordinateTo(memberId, currentUserId)) { throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId)); } pulseResponseRet = pulseResponseRepo.save(pulseResponse); @@ -94,7 +97,7 @@ public PulseResponse update(PulseResponse pulseResponse) { } else if (memberRepo.findById(memberId).isEmpty()) { throw new BadArgException(String.format("Member %s doesn't exist", memberId)); } else if (memberId == null) { - throw new BadArgException(String.format("Invalid pulseresponse %s", pulseResponse)); + throw new BadArgException("Cannot update anonymous pulse response"); } else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) { throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId)); } else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) { @@ -191,4 +194,4 @@ public void sendPulseLowScoreEmail(PulseResponse pulseResponse) { emailSender.sendEmail(null, null, subject, bodyBuilder.toString(), recipients.toArray(new String[0])); } } -} \ No newline at end of file +} diff --git a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql new file mode 100644 index 0000000000..746e8657d4 --- /dev/null +++ b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql @@ -0,0 +1 @@ +ALTER TABLE pulse_response DROP CONSTRAINT pulse_response_teamMemberId_fkey; diff --git a/web-ui/package.json b/web-ui/package.json index 32ee428d5e..49d35345a6 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -28,6 +28,7 @@ "fuse.js": "^6.4.6", "html-react-parser": "^5.1.12", "isomorphic-fetch": "^3.0.0", + "js-cookie": "^3.0.5", "js-file-download": "^0.4.12", "lodash": "^4.17.21", "markdown-builder": "^0.9.0", diff --git a/web-ui/src/pages/PulsePage.css b/web-ui/src/pages/PulsePage.css index c2f22a657c..923924b421 100644 --- a/web-ui/src/pages/PulsePage.css +++ b/web-ui/src/pages/PulsePage.css @@ -12,3 +12,9 @@ text-align: center; } } + +.submit-row { + align-items: center; + display: flex; + margin-top: 2rem; /* This is the default top margin for Buttons */ +} diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 81f821d249..863741c012 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -1,8 +1,9 @@ +import Cookies from 'js-cookie'; import { format } from 'date-fns'; import React, { useContext, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { Button, Typography } from '@mui/material'; -import { resolve } from '../api/api.js'; +import { Button, Checkbox, Typography } from '@mui/material'; +import { downloadData, initiate } from '../api/generic.js'; import Pulse from '../components/pulse/Pulse.jsx'; import { AppContext } from '../context/AppContext'; import { selectCsrfToken, selectCurrentUser } from '../context/selectors'; @@ -23,9 +24,19 @@ const PulsePage = () => { const [internalScore, setInternalScore] = useState(center); const [pulse, setPulse] = useState(null); const [submittedToday, setSubmittedToday] = useState(false); + const [submitAnonymously, setSubmitAnonymously] = useState(false); + const today = format(new Date(), 'yyyy-MM-dd'); + const cookieName = "pulse_submitted_anonymously"; + const pulseURL = '/services/pulse-responses'; useEffect(() => { + const submitted = Cookies.get(cookieName); + if (submitted) { + setSubmittedToday(true); + return; + } + if (!pulse) return; const now = new Date(); @@ -52,19 +63,8 @@ const PulsePage = () => { dateTo: today, teamMemberId: currentUser.id }; - const queryString = Object.entries(query) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - - const res = await resolve({ - method: 'GET', - url: `/services/pulse-responses?${queryString}`, - headers: { - 'X-CSRF-Header': csrf, - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8' - } - }); + + const res = await downloadData(pulseURL, csrf, query); if (res.error) return; // Sort pulse responses by date, latest to earliest @@ -97,22 +97,18 @@ const PulsePage = () => { internalScore: internalScore + 1, // converts to 1-based submissionDate: today, updatedDate: today, - teamMemberId: myId + teamMemberId: submitAnonymously ? null : myId, }; - const res = await resolve({ - method: 'POST', - url: '/services/pulse-responses', - headers: { - 'X-CSRF-Header': csrf, - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8' - }, - data - }); + const res = await initiate(pulseURL, csrf, data); if (res.error) return; - // Refresh browser to show that pulses where already submitted today. - history.go(0); + if (submitAnonymously) { + setSubmittedToday(true); + Cookies.set(cookieName, 'true', { expires: 1 }); + } else { + // Refresh browser to show that pulses where already submitted today. + history.go(0); + } }; return ( @@ -141,9 +137,25 @@ const PulsePage = () => { setScore={setExternalScore} title="How are you feeling about life outside of work?" /> - +
+ +
+ +
)}
diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 385db37ddf..4fc4415ca5 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -161,7 +161,7 @@ const PulseReportPage = () => { for (const pulse of pulses) { const memberId = pulse.teamMemberId; - if (!teamMemberIds.includes(memberId)) continue; + if (memberId && !teamMemberIds.includes(memberId)) continue; const { externalScore, internalScore, submissionDate } = pulse; const [year, month, day] = submissionDate; @@ -181,18 +181,21 @@ const PulseReportPage = () => { frequencies[internalScore - 1].internal++; frequencies[externalScore - 1].external++; - const member = memberMap[memberId]; - const { supervisorid } = member; - const memberIdToUse = managerMode ? supervisorid : memberId; - - /* For debugging ... - if (supervisorid) { - const supervisor = memberMap[supervisorid]; - console.log(`The supervisor of ${member.name} is ${supervisor.name}`); - } else { - console.log(`${member.name} has no supervisor`); + let memberIdToUse; + if (memberId) { + const member = memberMap[memberId]; + const { supervisorid } = member; + memberIdToUse = managerMode ? supervisorid : memberId; + + /* For debugging ... + if (supervisorid) { + const supervisor = memberMap[supervisorid]; + console.log(`The supervisor of ${member.name} is ${supervisor.name}`); + } else { + console.log(`${member.name} has no supervisor`); + } + */ } - */ // When in manager mode, if the member // doesn't have a supervisor then skip this data. diff --git a/web-ui/yarn.lock b/web-ui/yarn.lock index 51d7dff66c..caac92af81 100644 --- a/web-ui/yarn.lock +++ b/web-ui/yarn.lock @@ -4627,6 +4627,11 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + js-file-download@^0.4.12: version "0.4.12" resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821" From fb62f8375a6d35bda510d56f754b15972042aa46 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 08:32:58 -0600 Subject: [PATCH 21/64] Fixed tests. --- .../pulseresponse/PulseResponseControllerTest.java | 10 +++++++--- .../pulseresponse/PulseResponseCreateDTOTest.java | 2 +- web-ui/src/pages/PulsePage.jsx | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) 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 777f61bac6..912038b5f5 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 @@ -76,8 +76,12 @@ void testCreateAnInvalidPulseResponse() { JsonNode body = responseException.getResponse().getBody(JsonNode.class).orElse(null); JsonNode errors = Objects.requireNonNull(body).get("_embedded").get("errors"); JsonNode href = Objects.requireNonNull(body).get("_links").get("self").get("href"); - List errorList = Stream.of(errors.get(0).get("message").asText(), errors.get(1).get("message").asText(), errors.get(2).get("message").asText()).sorted().collect(Collectors.toList()); - assertEquals(3, errorList.size()); + List errorList = Stream.of( + errors.get(0).get("message").asText(), + errors.get(1).get("message").asText() + ).sorted().collect(Collectors.toList()); + + assertEquals(2, errorList.size()); assertEquals(request.getPath(), href.asText()); assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); } @@ -523,4 +527,4 @@ private MemberProfile profile(String key) { private UUID id(String key) { return profile(key).getId(); } -} \ No newline at end of file +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java index 7b964d776c..761a41a495 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java @@ -35,7 +35,7 @@ void testConstraintViolation() { PulseResponseCreateDTO dto = new PulseResponseCreateDTO(); Set> violations = validator.validate(dto); - assertEquals(3, violations.size()); + assertEquals(2, violations.size()); for (ConstraintViolation violation : violations) { assertEquals("must not be null", violation.getMessage()); } diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 863741c012..7149136570 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -145,7 +145,7 @@ const PulsePage = () => { Submit
-

- +
- + +
`; From 2eb13fb54465bad93091cc132cbd73030caf2d11 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 09:23:43 -0600 Subject: [PATCH 23/64] Correct if check so that we do not auto-fill previous new member. --- web-ui/src/components/member-directory/MemberModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/components/member-directory/MemberModal.jsx b/web-ui/src/components/member-directory/MemberModal.jsx index ef737139da..4ece8bac36 100644 --- a/web-ui/src/components/member-directory/MemberModal.jsx +++ b/web-ui/src/components/member-directory/MemberModal.jsx @@ -144,7 +144,7 @@ const MemberModal = ({ member, open, onSave, onClose }) => { }); } else if (required && inputsFeasible) { onSave(editedMember).then(() => { - if (isNewMember.current) { + if (isNewMember) { setMember({ emptyMember }); setIsNewMember(true); } From b01f8525699f42f6d3a7603e0842a1d852d25e60 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 13:42:37 -0600 Subject: [PATCH 24/64] Rewrote the PulseResponseTest to remove mockito. --- .../CurrentUserServicesReplacement.java | 43 +++ .../pulseresponse/PulseResponseTest.java | 267 +++++------------- 2 files changed, 110 insertions(+), 200 deletions(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/CurrentUserServicesReplacement.java diff --git a/server/src/test/java/com/objectcomputing/checkins/services/CurrentUserServicesReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/CurrentUserServicesReplacement.java new file mode 100644 index 0000000000..60723b8d5e --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/CurrentUserServicesReplacement.java @@ -0,0 +1,43 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; +import com.objectcomputing.checkins.services.role.RoleType; + +import java.util.List; + +import jakarta.inject.Singleton; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.env.Environment; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; + +@Singleton +@Replaces(CurrentUserServices.class) +@Requires(property = "replace.currentuserservices", value = StringUtils.TRUE) +public class CurrentUserServicesReplacement implements CurrentUserServices { + public MemberProfile currentUser; + public List roles; + + @Override + public MemberProfile findOrSaveUser(String firstName, + String lastName, + String workEmail) { + return null; + } + + @Override + public boolean hasRole(RoleType role) { + return roles == null ? false : roles.contains(role); + } + + @Override + public boolean isAdmin() { + return hasRole(RoleType.ADMIN); + } + + @Override + public MemberProfile getCurrentUser() { + return currentUser; + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java index e56aa79a57..206c833764 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java @@ -1,8 +1,11 @@ package com.objectcomputing.checkins.services.pulseresponse; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; +import com.objectcomputing.checkins.services.CurrentUserServicesReplacement; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; @@ -20,7 +23,6 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mockito; import java.time.LocalDate; import java.util.Collections; @@ -33,16 +35,10 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage -class PulseResponseTest extends TestContainersSuite { +@Property(name = "replace.currentuserservices", value = StringUtils.TRUE) +class PulseResponseTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture { @Inject protected Validator validator; @@ -51,41 +47,34 @@ class PulseResponseTest extends TestContainersSuite { @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; - private MemberProfileServices memberProfileServices; - private MemberProfileRepository memberRepo; - private CurrentUserServices currentUserServices; - private RolePermissionServices rolePermissionServices; + @Inject + CurrentUserServicesReplacement currentUserServices; + + @Inject private PulseResponseServicesImpl pulseResponseService; - private PulseResponseRepository pulseResponseRepo; + + private MemberProfile regular; + private MemberProfile another; + private MemberProfile pdlProfile; + private MemberProfile supervisorProfile; @BeforeEach - @Tag("mocked") void setUp() { - pulseResponseRepo = Mockito.mock(PulseResponseRepository.class); - memberProfileServices = Mockito.mock(MemberProfileServices.class); - memberRepo = Mockito.mock(MemberProfileRepository.class); - currentUserServices = Mockito.mock(CurrentUserServices.class); - rolePermissionServices = Mockito.mock(RolePermissionServices.class); + createAndAssignRoles(); + + pdlProfile = createADefaultMemberProfile(); + regular = createADefaultMemberProfileForPdl(pdlProfile); + supervisorProfile = createADefaultSupervisor(); + another = createAProfileWithSupervisorAndPDL(supervisorProfile, + pdlProfile); + currentUserServices.currentUser = regular; emailSender.reset(); - - pulseResponseService = Mockito.spy(new PulseResponseServicesImpl(pulseResponseRepo, - memberProfileServices, - memberRepo, - currentUserServices, - rolePermissionServices, - emailSender)); - } - - @AfterEach - @Tag("mocked") - void tearDown() { - Mockito.reset(memberRepo, memberProfileServices); } @Test void testPulseResponseInstantiation() { LocalDate submissionDate = LocalDate.of(2019, 1, 1); - final UUID teamMemberId = UUID.randomUUID(); + final UUID teamMemberId = regular.getId(); final String internalFeelings = "exampleId"; final String externalFeelings = "exampleId2"; PulseResponse pulseResponse = new PulseResponse(1, 2, submissionDate, teamMemberId, internalFeelings, externalFeelings); @@ -97,7 +86,7 @@ void testPulseResponseInstantiation() { @Test void testConstraintViolation() { LocalDate submissionDate = LocalDate.of(2019, 1, 1); - final UUID teamMemberId = UUID.randomUUID(); + final UUID teamMemberId = regular.getId(); final String internalFeelings = "exampleId"; final String externalFeelings = "exampleId2"; final Integer internalScore = 1; @@ -120,7 +109,7 @@ void testEquals() { final Integer internalScore = 1; final Integer externalScore = 2; LocalDate submissionDate = LocalDate.of(2019, 1, 1); - final UUID teamMemberId = UUID.randomUUID(); + final UUID teamMemberId = regular.getId(); final String internalFeelings = "exampleId"; final String externalFeelings = "exampleId2"; @@ -153,7 +142,7 @@ void testEquals() { void testToString() { final UUID id = UUID.randomUUID(); LocalDate submissionDate = LocalDate.of(2019, 1, 1); - final UUID teamMemberId = UUID.randomUUID(); + final UUID teamMemberId = regular.getId(); final Integer internalScore = 1; final Integer externalScore = 2; final String internalFeelings = "exampleId"; @@ -168,56 +157,31 @@ void testToString() { } @Test - @Tag("mocked") void testSaveWithValidPulseResponse() { - UUID currentUserId = UUID.randomUUID(); - UUID memberId = UUID.randomUUID(); + UUID memberId = regular.getId(); LocalDate pulseSubDate = LocalDate.now(); PulseResponse pulseResponse = new PulseResponse(); pulseResponse.setTeamMemberId(memberId); pulseResponse.setSubmissionDate(pulseSubDate); - MemberProfile memberProfile = new MemberProfile(currentUserId, "John", null, "Doe", - null, null, null, null, "john@oci.com", - null, null, null, null, - null, null, null, null, null); - - MemberProfile memberProfile2 = new MemberProfile(memberId, "Jane", null, "Doe", - null, null, null, null, "jane@oci.com", - null, null, null, currentUserId, - null, null, null, null, null); - - when(currentUserServices.getCurrentUser()).thenReturn(memberProfile); - when(memberRepo.findById(memberId)).thenReturn(Optional.of(memberProfile2)); - when(memberProfileServices.getSubordinatesForId(currentUserId)).thenReturn(Collections.singletonList(memberProfile2)); - - PulseResponse savedResponse = new PulseResponse(); // Assuming this is a valid saved response - when(pulseResponseRepo.save(pulseResponse)).thenReturn(savedResponse); - + PulseResponse savedResponse = new PulseResponse(); PulseResponse result = pulseResponseService.save(pulseResponse); - assertEquals(savedResponse, result); - verify(pulseResponseRepo, times(1)).save(pulseResponse); - verify(pulseResponseService, times(1)).sendPulseLowScoreEmail(savedResponse); + assertTrue(result.getId() != null); + assertEquals(memberId, result.getTeamMemberId()); + assertEquals(LocalDate.now(), result.getSubmissionDate()); } @Test - @Tag("mocked") void testSaveWithNonNullId() { - UUID currentUserId = UUID.randomUUID(); - UUID memberId = UUID.randomUUID(); + UUID memberId = regular.getId(); LocalDate pulseSubDate = LocalDate.now(); PulseResponse pulseResponse = new PulseResponse(); pulseResponse.setId(UUID.randomUUID()); // Non-null ID pulseResponse.setTeamMemberId(memberId); pulseResponse.setSubmissionDate(pulseSubDate); - MemberProfile memberProfile = new MemberProfile(currentUserId, "John", null, "Doe", - null, null, null, null, "john@oci.com", - null, null, null, null, - null, null, null, null, null); - when(currentUserServices.getCurrentUser()).thenReturn(memberProfile); BadArgException exception = assertThrows(BadArgException.class, () -> { pulseResponseService.save(pulseResponse); @@ -228,9 +192,7 @@ void testSaveWithNonNullId() { } @Test - @Tag("mocked") void testSaveWithNonExistentMember() { - UUID currentUserId = UUID.randomUUID(); UUID memberId = UUID.randomUUID(); LocalDate pulseSubDate = LocalDate.now(); @@ -238,13 +200,6 @@ void testSaveWithNonExistentMember() { pulseResponse.setTeamMemberId(memberId); pulseResponse.setSubmissionDate(pulseSubDate); - MemberProfile memberProfile = new MemberProfile(currentUserId, "John", null, "Doe", - null, null, null, null, "john@oci.com", - null, null, null, null, - null, null, null, null, null); - when(currentUserServices.getCurrentUser()).thenReturn(memberProfile); - when(memberRepo.findById(memberId)).thenReturn(Optional.empty()); // Member doesn't exist - BadArgException exception = assertThrows(BadArgException.class, () -> { pulseResponseService.save(pulseResponse); }); @@ -254,29 +209,14 @@ void testSaveWithNonExistentMember() { } @Test - @Tag("mocked") void testSaveWithInvalidDate() { - UUID currentUserId = UUID.randomUUID(); - UUID memberId = UUID.randomUUID(); + UUID memberId = regular.getId(); LocalDate pulseSubDate = LocalDate.of(0000,1,1); PulseResponse pulseResponse = new PulseResponse(); pulseResponse.setTeamMemberId(memberId); pulseResponse.setSubmissionDate(pulseSubDate); - MemberProfile memberProfile = new MemberProfile(currentUserId, "John", null, "Doe", - null, null, null, null, "john@oci.com", - null, null, null, null, - null, null, null, null, null); - - MemberProfile memberProfile2 = new MemberProfile(memberId, "Jane", null, "Doe", - null, null, null, null, "jane@oci.com", - null, null, null, currentUserId, - null, null, null, null, null); - when(currentUserServices.getCurrentUser()).thenReturn(memberProfile); - when(memberRepo.findById(memberId)).thenReturn(Optional.of(memberProfile2)); - when(memberProfileServices.getSubordinatesForId(currentUserId)).thenReturn(Collections.singletonList(memberProfile2)); - BadArgException exception = assertThrows(BadArgException.class, () -> { pulseResponseService.save(pulseResponse); }); @@ -286,29 +226,15 @@ void testSaveWithInvalidDate() { } @Test - @Tag("mocked") void testSaveWithoutPermission() { - UUID currentUserId = UUID.randomUUID(); - UUID memberId = UUID.randomUUID(); + UUID currentUserId = regular.getId(); + UUID memberId = another.getId(); LocalDate pulseSubDate = LocalDate.now(); PulseResponse pulseResponse = new PulseResponse(); pulseResponse.setTeamMemberId(memberId); pulseResponse.setSubmissionDate(pulseSubDate); - MemberProfile memberProfile = new MemberProfile(currentUserId, "John", null, "Doe", - null, null, null, null, "john@oci.com", - null, null, null, null, - null, null, null, null, null); - - MemberProfile memberProfile2 = new MemberProfile(memberId, "Jane", null, "Doe", - null, null, null, null, "jane@oci.com", - null, null, null, currentUserId, - null, null, null, null, null); - when(currentUserServices.getCurrentUser()).thenReturn(memberProfile); - when(memberRepo.findById(memberId)).thenReturn(Optional.of(memberProfile2)); - when(memberProfileServices.getSubordinatesForId(currentUserId)).thenReturn(Collections.emptyList()); // No subordinates - BadArgException exception = assertThrows(BadArgException.class, () -> { pulseResponseService.save(pulseResponse); }); @@ -318,111 +244,76 @@ void testSaveWithoutPermission() { } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_NullPulseResponse() { pulseResponseService.sendPulseLowScoreEmail(null); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_NoLowScores() { - PulseResponse pulseResponse = new PulseResponse(3, 4, LocalDate.now(), UUID.randomUUID(), "Good", "Great"); + PulseResponse pulseResponse = new PulseResponse(3, 4, LocalDate.now(), regular.getId(), "Good", "Great"); pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_LowInternalScore() { - UUID teamMemberId = UUID.randomUUID(); - UUID pdlId = UUID.randomUUID(); + UUID teamMemberId = regular.getId(); + UUID pdlId = pdlProfile.getId(); PulseResponse pulseResponse = new PulseResponse(1, 3, LocalDate.now(), teamMemberId, "Sad", "Neutral"); - MemberProfile surveyTakerProfile = mock(MemberProfile.class); - MemberProfile pdlProfile = mock(MemberProfile.class); - - when(memberRepo.existsById(teamMemberId)).thenReturn(true); - when(memberProfileServices.getById(teamMemberId)).thenReturn(surveyTakerProfile); - when(surveyTakerProfile.getFirstName()).thenReturn("John"); - when(surveyTakerProfile.getLastName()).thenReturn("Doe"); - - when(surveyTakerProfile.getPdlId()).thenReturn(pdlId); - when(memberRepo.existsById(pdlId)).thenReturn(true); - when(memberProfileServices.getById(pdlId)).thenReturn(pdlProfile); - when(pdlProfile.getWorkEmail()).thenReturn("pdl@example.com"); pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Internal pulse scores are low for team member John Doe", "Team member John Doe has left low internal pulse scores. Please consider reaching out to this employee at null
Internal Feelings: Sad
", pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("Internal pulse scores are low for team member %s %s", regular.getFirstName(), regular.getLastName()), + String.format("Team member %s %s has left low internal pulse scores. Please consider reaching out to this employee at %s
Internal Feelings: Sad
", regular.getFirstName(), regular.getLastName(), regular.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_LowExternalScore() { - UUID teamMemberId = UUID.randomUUID(); - UUID pdlId = UUID.randomUUID(); + UUID teamMemberId = regular.getId(); + UUID pdlId = pdlProfile.getId(); PulseResponse pulseResponse = new PulseResponse(3, 1, LocalDate.now(), teamMemberId, "Neutral", "Sad"); - MemberProfile surveyTakerProfile = mock(MemberProfile.class); - MemberProfile pdlProfile = mock(MemberProfile.class); - - when(memberRepo.existsById(teamMemberId)).thenReturn(true); - when(memberProfileServices.getById(teamMemberId)).thenReturn(surveyTakerProfile); - when(surveyTakerProfile.getFirstName()).thenReturn("John"); - when(surveyTakerProfile.getLastName()).thenReturn("Doe"); - - when(surveyTakerProfile.getPdlId()).thenReturn(pdlId); - when(memberRepo.existsById(pdlId)).thenReturn(true); - when(memberProfileServices.getById(pdlId)).thenReturn(pdlProfile); - when(pdlProfile.getWorkEmail()).thenReturn("pdl@example.com"); - pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "External pulse scores are low for team member John Doe", "Team member John Doe has left low external pulse scores. Please consider reaching out to this employee at null
External Feelings: Sad
", pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("External pulse scores are low for team member %s %s", regular.getFirstName(), regular.getLastName()), + String.format("Team member %s %s has left low external pulse scores. Please consider reaching out to this employee at %s
External Feelings: Sad
", regular.getFirstName(), regular.getLastName(), regular.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_LowInternalAndExternalScore() { - UUID teamMemberId = UUID.randomUUID(); - UUID pdlId = UUID.randomUUID(); + UUID teamMemberId = regular.getId(); + UUID pdlId = pdlProfile.getId(); PulseResponse pulseResponse = new PulseResponse(1, 1, LocalDate.now(), teamMemberId, "Very Sad", "Very Sad"); - MemberProfile surveyTakerProfile = mock(MemberProfile.class); - MemberProfile pdlProfile = mock(MemberProfile.class); - - when(memberRepo.existsById(teamMemberId)).thenReturn(true); - when(memberProfileServices.getById(teamMemberId)).thenReturn(surveyTakerProfile); - when(surveyTakerProfile.getFirstName()).thenReturn("John"); - when(surveyTakerProfile.getLastName()).thenReturn("Doe"); - - when(surveyTakerProfile.getPdlId()).thenReturn(pdlId); - when(memberRepo.existsById(pdlId)).thenReturn(true); - when(memberProfileServices.getById(pdlId)).thenReturn(pdlProfile); - when(pdlProfile.getWorkEmail()).thenReturn("pdl@example.com"); - pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Internal and external pulse scores are low for team member John Doe", "Team member John Doe has left low internal and external pulse scores. Please consider reaching out to this employee at null
Internal Feelings: Very Sad
External Feelings: Very Sad
", pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("Internal and external pulse scores are low for team member %s %s", regular.getFirstName(), regular.getLastName()), + String.format("Team member %s %s has left low internal and external pulse scores. Please consider reaching out to this employee at %s
Internal Feelings: Very Sad
External Feelings: Very Sad
", regular.getFirstName(), regular.getLastName(), regular.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_NoTeamMemberId() { PulseResponse pulseResponse = new PulseResponse(1, 1, LocalDate.now(), null, "Very Sad", "Very Sad"); @@ -432,73 +323,49 @@ void testSendPulseLowScoreEmail_NoTeamMemberId() { } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_InvalidTeamMemberId() { UUID teamMemberId = UUID.randomUUID(); PulseResponse pulseResponse = new PulseResponse(1, 1, LocalDate.now(), teamMemberId, "Very Sad", "Very Sad"); - when(memberRepo.existsById(teamMemberId)).thenReturn(false); - pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_WithPdlId() { - UUID teamMemberId = UUID.randomUUID(); - UUID pdlId = UUID.randomUUID(); + UUID teamMemberId = regular.getId(); + UUID pdlId = pdlProfile.getId(); PulseResponse pulseResponse = new PulseResponse(1, 1, LocalDate.now(), teamMemberId, "Very Sad", "Very Sad"); - MemberProfile surveyTakerProfile = mock(MemberProfile.class); - MemberProfile pdlProfile = mock(MemberProfile.class); - - when(memberRepo.existsById(teamMemberId)).thenReturn(true); - when(memberProfileServices.getById(teamMemberId)).thenReturn(surveyTakerProfile); - when(surveyTakerProfile.getFirstName()).thenReturn("John"); - when(surveyTakerProfile.getLastName()).thenReturn("Doe"); - - when(surveyTakerProfile.getPdlId()).thenReturn(pdlId); - when(memberRepo.existsById(pdlId)).thenReturn(true); - when(memberProfileServices.getById(pdlId)).thenReturn(pdlProfile); - when(pdlProfile.getWorkEmail()).thenReturn("pdl@example.com"); - pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Internal and external pulse scores are low for team member John Doe", "Team member John Doe has left low internal and external pulse scores. Please consider reaching out to this employee at null
Internal Feelings: Very Sad
External Feelings: Very Sad
", pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("Internal and external pulse scores are low for team member %s %s", regular.getFirstName(), regular.getLastName()), + String.format("Team member %s %s has left low internal and external pulse scores. Please consider reaching out to this employee at %s
Internal Feelings: Very Sad
External Feelings: Very Sad
", regular.getFirstName(), regular.getLastName(), regular.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testSendPulseLowScoreEmail_WithSupervisorId() { - UUID teamMemberId = UUID.randomUUID(); - UUID supervisorId = UUID.randomUUID(); + UUID teamMemberId = another.getId(); + UUID supervisorId = supervisorProfile.getId(); PulseResponse pulseResponse = new PulseResponse(1, 1, LocalDate.now(), teamMemberId, "Very Sad", "Very Sad"); - MemberProfile surveyTakerProfile = mock(MemberProfile.class); - MemberProfile supervisorProfile = mock(MemberProfile.class); - - when(memberRepo.existsById(teamMemberId)).thenReturn(true); - when(memberProfileServices.getById(teamMemberId)).thenReturn(surveyTakerProfile); - when(surveyTakerProfile.getFirstName()).thenReturn("John"); - when(surveyTakerProfile.getLastName()).thenReturn("Doe"); - - when(surveyTakerProfile.getSupervisorid()).thenReturn(supervisorId); - when(memberRepo.existsById(supervisorId)).thenReturn(true); - when(memberProfileServices.getById(supervisorId)).thenReturn(supervisorProfile); - when(supervisorProfile.getWorkEmail()).thenReturn("supervisor@example.com"); - pulseResponseService.sendPulseLowScoreEmail(pulseResponse); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Internal and external pulse scores are low for team member John Doe", "Team member John Doe has left low internal and external pulse scores. Please consider reaching out to this employee at null
Internal Feelings: Very Sad
External Feelings: Very Sad
", supervisorProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("Internal and external pulse scores are low for team member %s %s", another.getFirstName(), another.getLastName()), + String.format("Team member %s %s has left low internal and external pulse scores. Please consider reaching out to this employee at %s
Internal Feelings: Very Sad
External Feelings: Very Sad
", another.getFirstName(), another.getLastName(), another.getWorkEmail()), + supervisorProfile.getWorkEmail() + + "," + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } From 3dd68a303c3c1fb8f7fbf58bc2aa5d45ca5ad2a1 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 13:53:26 -0600 Subject: [PATCH 25/64] Removed unnecessary db file and unnecessary refresh. --- .../db/common/V119__alter_pulse_response_team_id.sql | 1 - web-ui/src/pages/PulsePage.jsx | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql diff --git a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql deleted file mode 100644 index 746e8657d4..0000000000 --- a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE pulse_response DROP CONSTRAINT pulse_response_teamMemberId_fkey; diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 7149136570..ef6877f019 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -16,7 +16,6 @@ const PulsePage = () => { const { state } = useContext(AppContext); const currentUser = selectCurrentUser(state); const csrf = selectCsrfToken(state); - const history = useHistory(); const [externalComment, setExternalComment] = useState(''); const [externalScore, setExternalScore] = useState(center); @@ -102,12 +101,9 @@ const PulsePage = () => { const res = await initiate(pulseURL, csrf, data); if (res.error) return; + setSubmittedToday(true); if (submitAnonymously) { - setSubmittedToday(true); Cookies.set(cookieName, 'true', { expires: 1 }); - } else { - // Refresh browser to show that pulses where already submitted today. - history.go(0); } }; From a50dc16f12531283b1a33106fde0400d9feee547 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 08:37:28 -0600 Subject: [PATCH 26/64] Split the pie chart into two controlled by the toggle and colored like the other charts. --- web-ui/src/helpers/colors.js | 31 +++++ web-ui/src/helpers/index.js | 1 + web-ui/src/pages/PulseReportPage.jsx | 162 ++++++++++++++++++--------- 3 files changed, 143 insertions(+), 51 deletions(-) create mode 100644 web-ui/src/helpers/colors.js diff --git a/web-ui/src/helpers/colors.js b/web-ui/src/helpers/colors.js new file mode 100644 index 0000000000..e2bf178af3 --- /dev/null +++ b/web-ui/src/helpers/colors.js @@ -0,0 +1,31 @@ +// pSBC - Shade Blend Convert - Version 4.1 - 01/7/2021 +// https://github.com/PimpTrizkit/PJs/blob/master/pSBC.js + +export const pSBC=(p,c0,c1,l)=>{ + let r,g,b,P,f,t,h,m=Math.round,a=typeof(c1)=="string"; + if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null; + h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=pSBC.pSBCr(c0),P=p<0,t=c1&&c1!="c"?pSBC.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p; + if(!f||!t)return null; + if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b); + else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5); + a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0; + if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")"; + else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2) +} + +pSBC.pSBCr=(d)=>{ + const i=parseInt; + let n=d.length,x={}; + if(n>9){ + const [r, g, b, a] = (d = d.split(',')); + n = d.length; + if(n<3||n>4)return null; + x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1 + }else{ + if(n==8||n==6||n<4)return null; + if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:""); + d=i(d.slice(1),16); + if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=Math.round((d&255)/0.255)/1000; + else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1 + }return x +}; diff --git a/web-ui/src/helpers/index.js b/web-ui/src/helpers/index.js index 0cebfe6af0..bf72c325e5 100644 --- a/web-ui/src/helpers/index.js +++ b/web-ui/src/helpers/index.js @@ -4,3 +4,4 @@ export * from './datetime'; export * from './query-parameters'; export * from './sanitizehtml'; export * from './strings'; +export * from './colors'; diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 066ff4eea3..cef70fc00e 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -7,6 +7,7 @@ import { CartesianGrid, Legend, Pie, + Cell, PieChart, ResponsiveContainer, Tooltip, @@ -40,6 +41,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { pSBC } from '../helpers/colors.js'; import { getAvatarURL, resolve } from '../api/api.js'; import MemberSelector from '../components/member_selector/MemberSelector'; import { AppContext } from '../context/AppContext.jsx'; @@ -56,9 +58,8 @@ import './PulseReportPage.css'; // Recharts doesn't support using CSS variables, so we can't // easily use color variables defined in variables.css. const ociDarkBlue = '#2c519e'; -const ociLightBlue = '#76c8d4'; -// const ociOrange = '#f8b576'; // too light -const orange = '#b26801'; +//const ociLightBlue = '#76c8d4'; // not currently used +const ociOrange = '#f8b576'; const ScoreOption = { INTERNAL: 'Internal', @@ -121,7 +122,8 @@ const PulseReportPage = () => { const [selectedPulse, setSelectedPulse] = useState(null); const [showComments, setShowComments] = useState(false); const [teamMembers, setTeamMembers] = useState([]); - const [pieChartData, setPieChartData] = useState([]); + const [internalPieChartData, setInternalPieChartData] = useState([]); + const [externalPieChartData, setExternalPieChartData] = useState([]); /* // This generates random data to use in the line chart. @@ -220,22 +222,37 @@ const PulseReportPage = () => { } } - let pieCounts = [ - {name: "veryDissatisfied", value: 0}, - {name: "dissatisfied", value: 0}, - {name: "neutral", value: 0}, - {name: "satisfied", value: 0}, - {name: "verySatisfied", value: 0}, + let internalPieCounts = [ + {name: "internalVeryDissatisfied", value: 0}, + {name: "internalDissatisfied", value: 0}, + {name: "internalNeutral", value: 0}, + {name: "internalSatisfied", value: 0}, + {name: "internalVerySatisfied", value: 0}, ]; for(let day of scoreChartDataPoints) { day.datapoints.forEach(datapoint => { - pieCounts[datapoint.internalScore - 1].value++; - pieCounts[datapoint.externalScore - 1].value++; + internalPieCounts[datapoint.internalScore - 1].value++; }); } // Filter out data with a zero value so that the pie chart does not attempt // to display them. - setPieChartData(pieCounts.filter((p) => p.value != 0)); + setInternalPieChartData(internalPieCounts.filter((p) => p.value != 0)); + + let externalPieCounts = [ + {name: "externalVeryDissatisfied", value: 0}, + {name: "externalDissatisfied", value: 0}, + {name: "externalNeutral", value: 0}, + {name: "externalSatisfied", value: 0}, + {name: "externalVerySatisfied", value: 0}, + ]; + for(let day of scoreChartDataPoints) { + day.datapoints.forEach(datapoint => { + externalPieCounts[datapoint.externalScore - 1].value++; + }); + } + // Filter out data with a zero value so that the pie chart does not attempt + // to display them. + setExternalPieChartData(externalPieCounts.filter((p) => p.value != 0)); setScoreChartData(scoreChartDataPoints.map(day => { const iScores = {}; @@ -402,7 +419,7 @@ const PulseReportPage = () => { {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.EXTERNAL) && } @@ -448,16 +465,16 @@ const PulseReportPage = () => { }; const dataInfo = [ - {key: "internalVeryDissatisfied", stackId: "internal", color: "#273e58", }, - {key: "internalDissatisfied", stackId: "internal", color: "#1a3c6d", }, - {key: "internalNeutral", stackId: "internal", color: "#2c519e", }, - {key: "internalSatisfied", stackId: "internal", color: "#4b7ac7", }, - {key: "internalVerySatisfied", stackId: "internal", color: "#6fa3e6", }, - {key: "externalVeryDissatisfied", stackId: "external", color: "#704401", }, - {key: "externalDissatisfied", stackId: "external", color: "#8a5200", }, - {key: "externalNeutral", stackId: "external", color: "#b26801", }, - {key: "externalSatisfied", stackId: "external", color: "#d48a2c", }, - {key: "externalVerySatisfied", stackId: "external", color: "#e0a456", }, + {key: "internalVeryDissatisfied", stackId: "internal", color: ociDarkBlue, }, + {key: "internalDissatisfied", stackId: "internal", color: pSBC(.05, ociDarkBlue), }, + {key: "internalNeutral", stackId: "internal", color: pSBC(.10, ociDarkBlue), }, + {key: "internalSatisfied", stackId: "internal", color: pSBC(.15, ociDarkBlue), }, + {key: "internalVerySatisfied", stackId: "internal", color: pSBC(.2, ociDarkBlue), }, + {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.8, ociOrange), }, + {key: "externalDissatisfied", stackId: "external", color: pSBC(-.6, ociOrange), }, + {key: "externalNeutral", stackId: "external", color: pSBC(-.4, ociOrange), }, + {key: "externalSatisfied", stackId: "external", color: pSBC(-.2, ociOrange), }, + {key: "externalVerySatisfied", stackId: "external", color: ociOrange, }, ]; const labelToSentiment = (label) => { @@ -518,8 +535,8 @@ const PulseReportPage = () => { ); }; - const pulseScoresTitle = () => { - let title = "Pulse scores for"; + const sectionTitle = (prefix) => { + let title = `${prefix} for`; if (scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.INTERNAL) { title += ` "${ScoreOptionLabel[ScoreOption.INTERNAL]}"`; @@ -535,16 +552,16 @@ const PulseReportPage = () => { }; const pieLabelToSentiment = (label) => { - switch(label.toLowerCase()) { - case "verydissatisfied": + switch(label.replace("internal", "").replace("external", "")) { + case "VeryDissatisfied": return "😦"; - case "dissatisfied": + case "Dissatisfied": return "🙁"; - case "neutral": + case "Neutral": return "😐"; - case "satisfied": + case "Satisfied": return "🙂"; - case "verysatisfied": + case "VerySatisfied": return "😀"; } return "ERROR"; @@ -589,7 +606,10 @@ const PulseReportPage = () => { if (active && payload && payload.length) { return (
-

{titleWords(payload[0].name)} : {payload[0].value}

+

+ {titleWords(payload[0].name + .replace("internal", "") + .replace("external", ""))} : {payload[0].value}

); } @@ -597,11 +617,17 @@ const PulseReportPage = () => { return null; }; + const pieSliceColor = (entry, index) => { + return value.key == entry.name).color} />; + }; + const pulseScoresChart = () => ( <> @@ -642,26 +668,60 @@ const PulseReportPage = () => { - - - } - /> - - - +
+ {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.INTERNAL) && +
+ + + } + /> + + {internalPieChartData.map(pieSliceColor)} + + + +
+ } + {(scoreType == ScoreOption.COMBINED || + scoreType == ScoreOption.EXTERNAL) && +
+ + + } + /> + + {externalPieChartData.map(pieSliceColor)} + + + +
+ } +
From 054509449e2d784bae2a81b742a99fc3f0a0913d Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 09:00:00 -0600 Subject: [PATCH 27/64] Reversed the tooltip order for the sentiment chart. --- web-ui/src/pages/PulseReportPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 4fc4415ca5..24926e5a4d 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -467,7 +467,7 @@ const PulseReportPage = () => { return (

{label}

- {payload.map(p => { + {payload.slice().reverse().map(p => { return
{p.value} {p.name.props.children}
; From abee33691fa4dafd7eccea43122073659d6672f4 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 10:29:21 -0600 Subject: [PATCH 28/64] Removed mockito usage. --- .../CheckServicesImplTest.java | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java index b0ac32188e..cc34a0daee 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java @@ -2,67 +2,69 @@ import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest; -import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestRepository; -import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServicesImpl; +import com.objectcomputing.checkins.services.feedback_template.FeedbackTemplate; +import com.objectcomputing.checkins.services.fixture.FeedbackRequestFixture; +import com.objectcomputing.checkins.services.fixture.FeedbackTemplateFixture; import com.objectcomputing.checkins.services.pulse.PulseServices; import com.objectcomputing.checkins.services.reviews.ReviewPeriodServices; +import com.objectcomputing.checkins.notifications.email.MailJetFactory; +import com.objectcomputing.checkins.services.MailJetFactoryReplacement; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; + +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; +import jakarta.inject.Inject; +import jakarta.inject.Named; import java.util.Collections; import java.util.List; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class CheckServicesImplTest extends TestContainersSuite { - - @Mock - private FeedbackRequestServicesImpl feedbackRequestServices; +import static org.junit.jupiter.api.Assertions.assertEquals; - @Mock - private FeedbackRequestRepository feedbackRequestRepository; +@Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) +class CheckServicesImplTest extends TestContainersSuite + implements FeedbackTemplateFixture, FeedbackRequestFixture, MemberProfileFixture { - @Mock - private PulseServices pulseServices; + @Inject + @Named(MailJetFactory.MJML_FORMAT) + private MailJetFactoryReplacement.MockEmailSender emailSender; - @Mock - private ReviewPeriodServices reviewPeriodServices; - - @InjectMocks + @Inject private CheckServicesImpl checkServices; - AutoCloseable openMocks; - @BeforeEach - void initMocks() { - openMocks = MockitoAnnotations.openMocks(this); - } - - @AfterEach - void resetMocks() throws Exception { - openMocks.close(); + void resetTest() { + emailSender.reset(); } @Test void sendScheduledEmails() { - FeedbackRequest retrievedRequest = new FeedbackRequest(); - retrievedRequest.setStatus("pending"); - List list = Collections.singletonList(retrievedRequest); - when(feedbackRequestRepository.findBySendDateNotAfterAndStatusEqual(any(),eq("pending"))).thenReturn(list); + // Create a pending feedback request. + MemberProfile pdlMemberProfile = createADefaultMemberProfile(); + MemberProfile employeeMemberProfile = createADefaultMemberProfileForPdl(pdlMemberProfile); + MemberProfile recipient = createADefaultRecipient(); + + FeedbackTemplate template = createFeedbackTemplate(pdlMemberProfile.getId()); + getFeedbackTemplateRepository().save(template); + FeedbackRequest retrievedRequest = + saveSampleFeedbackRequest(pdlMemberProfile, + employeeMemberProfile, + recipient, template.getId()); + + // Send emails for today checkServices.sendScheduledEmails(); - verify(feedbackRequestServices).sendNewRequestEmail(retrievedRequest); - retrievedRequest.setStatus("sent"); - verify(feedbackRequestRepository).update(retrievedRequest); + + // One for the feedback request and one for the pulse email. + assertEquals(2, emailSender.events.size()); + assertEquals(pdlMemberProfile.getWorkEmail(), + emailSender.events.get(0).get(2)); + assertEquals("Feedback request", + emailSender.events.get(0).get(3)); + System.out.println(emailSender.events.get(0)); } } From de8885d1c2edda53cca92b84670b49809be41fff Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 12:19:18 -0600 Subject: [PATCH 29/64] Removed mockito. --- .../SkillsReportServicesImplTest.java | 191 +++++------------- 1 file changed, 52 insertions(+), 139 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java index 17e64ee385..e14ea418e0 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java @@ -2,6 +2,11 @@ import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.SkillFixture; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.MemberSkillFixture; +import com.objectcomputing.checkins.services.skills.Skill; +import com.objectcomputing.checkins.services.member_skill.skillsreport.SkillLevel; import com.objectcomputing.checkins.services.member_skill.MemberSkill; import com.objectcomputing.checkins.services.member_skill.MemberSkillRepository; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; @@ -13,9 +18,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import jakarta.inject.Inject; import java.time.LocalDate; import java.util.ArrayList; @@ -29,50 +32,12 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class SkillsReportServicesImplTest extends TestContainersSuite { - - @Mock - private MemberSkillRepository memberSkillRepository; - - @Mock - private MemberProfileRepository memberProfileRepository; - - @Mock - private MemberProfileServices memberProfileServices; - - @Mock - private SkillRepository skillRepository; - - @InjectMocks +class SkillsReportServicesImplTest extends TestContainersSuite + implements MemberProfileFixture, MemberSkillFixture, SkillFixture { + @Inject private SkillsReportServicesImpl skillsReportServices; - private AutoCloseable mockFinalizer; - - @BeforeAll - void initMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @BeforeEach - void resetMocks() { - reset(memberSkillRepository, memberProfileRepository, skillRepository); - } - - @AfterAll - void finalizeMocks() throws Exception { - mockFinalizer.close(); - } - @Test void testReportSkillNotExist() { final SkillLevelDTO dto = new SkillLevelDTO(); @@ -89,13 +54,10 @@ void testReportSkillNotExist() { @Test void testReportMemberProfileNotExist() { - final SkillLevelDTO dto = new SkillLevelDTO(); - final UUID skillId = UUID.randomUUID(); - dto.setId(skillId); - when(skillRepository.existsById(skillId)).thenReturn(true); + final Skill skill = createADefaultSkill(); final List skills = new ArrayList<>(); - skills.add(dto); + skills.add(new SkillLevelDTO(skill.getId(), SkillLevel.NOVICE)); final SkillsReportRequestDTO request = new SkillsReportRequestDTO(); request.setSkills(skills); @@ -111,72 +73,39 @@ void testReportEmptyRequestedSkillsList() { request.setSkills(new ArrayList<>()); final SkillsReportResponseDTO response = skillsReportServices.report(request); assertNotNull(response); - - verify(memberSkillRepository, never()).findBySkillid(any(UUID.class)); - verify(memberProfileServices, never()).getById(any(UUID.class)); + assertEquals(0, response.getTeamMembers().size()); } @Test void testReport() { - final UUID skillId1 = UUID.randomUUID(); - final UUID skillId2 = UUID.randomUUID(); - final UUID skillId3 = UUID.randomUUID(); - final UUID skillId4 = UUID.randomUUID(); - final UUID memberId1 = UUID.randomUUID(); - final UUID memberId2 = UUID.randomUUID(); - final UUID memberId3 = UUID.randomUUID(); - final UUID memberId4 = UUID.randomUUID(); - - final MemberSkill ms1 = new MemberSkill(memberId1, skillId1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); - final MemberSkill ms2 = new MemberSkill(memberId1, skillId2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); - final MemberSkill ms3 = new MemberSkill(memberId2, skillId3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); - final MemberSkill ms4 = new MemberSkill(memberId2, skillId4, SkillLevel.EXPERT_LEVEL, LocalDate.now()); - final MemberSkill ms5 = new MemberSkill(memberId3, skillId2, SkillLevel.INTERESTED_LEVEL, LocalDate.now()); - final MemberSkill ms6 = new MemberSkill(memberId3, skillId3, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); - final MemberSkill ms7 = new MemberSkill(memberId4, skillId1, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); - final MemberSkill ms8 = new MemberSkill(memberId4, skillId2, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); - final MemberSkill ms9 = new MemberSkill(memberId4, skillId4, SkillLevel.EXPERT_LEVEL, LocalDate.now()); - - final List skillList1 = new ArrayList<>(); - skillList1.add(ms1); - skillList1.add(ms7); - final List skillList2 = new ArrayList<>(); - skillList2.add(ms2); - skillList2.add(ms5); - skillList2.add(ms8); - final List skillList3 = new ArrayList<>(); - skillList3.add(ms3); - skillList3.add(ms6); - final List skillList4 = new ArrayList<>(); - skillList4.add(ms4); - skillList4.add(ms9); - - when(memberSkillRepository.findBySkillid(skillId1)).thenReturn(skillList1); - when(memberSkillRepository.findBySkillid(skillId2)).thenReturn(skillList2); - when(memberSkillRepository.findBySkillid(skillId3)).thenReturn(skillList3); - when(memberSkillRepository.findBySkillid(skillId4)).thenReturn(skillList4); - MemberProfile joey = new MemberProfile("Joey", null, "Tribbiani", null, - null, null, null, null, null, null, null, - null, null, null, null, null, null); - MemberProfile chandler = new MemberProfile("Chandler", null, "Bing", null, - null, null, null, null, null, null, null, - null, null,null, null, null, null); - MemberProfile ross = new MemberProfile("Ross", null, "Geller", null, - null, null, null, null, null, null, null, - null, null,null, null, null, null); - when(memberProfileServices.getById(memberId1)).thenReturn(joey); - when(memberProfileServices.getById(memberId2)).thenReturn(chandler); - when(memberProfileServices.getById(memberId3)).thenReturn(null); - when(memberProfileServices.getById(memberId4)).thenReturn(ross); - - when(skillRepository.existsById(skillId1)).thenReturn(true); - when(skillRepository.existsById(skillId2)).thenReturn(true); - when(skillRepository.existsById(skillId3)).thenReturn(true); - when(skillRepository.existsById(skillId4)).thenReturn(true); - when(memberProfileRepository.existsById(memberId1)).thenReturn(true); - when(memberProfileRepository.existsById(memberId2)).thenReturn(true); - when(memberProfileRepository.existsById(memberId3)).thenReturn(true); - when(memberProfileRepository.existsById(memberId4)).thenReturn(true); + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + Skill skill4 = createSkill("Skill4", false, "Fourth", false); + + MemberProfile member1 = createADefaultMemberProfile(); + MemberProfile member2 = createASecondDefaultMemberProfile(); + MemberProfile member3 = createAThirdDefaultMemberProfile(); + MemberProfile member4 = createADefaultMemberProfileForPdl(member1); + + final UUID skillId1 = skill1.getId(); + final UUID skillId2 = skill2.getId(); + final UUID skillId3 = skill3.getId(); + final UUID skillId4 = skill4.getId(); + final UUID memberId1 = member1.getId(); + final UUID memberId2 = member2.getId(); + final UUID memberId3 = member3.getId(); + final UUID memberId4 = member4.getId(); + + final MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + final MemberSkill ms2 = createMemberSkill(member1, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + final MemberSkill ms3 = createMemberSkill(member2, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); + final MemberSkill ms4 = createMemberSkill(member2, skill4, SkillLevel.EXPERT_LEVEL, LocalDate.now()); + final MemberSkill ms5 = createMemberSkill(member3, skill2, SkillLevel.INTERESTED_LEVEL, LocalDate.now()); + final MemberSkill ms6 = createMemberSkill(member3, skill3, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + final MemberSkill ms7 = createMemberSkill(member4, skill1, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + final MemberSkill ms8 = createMemberSkill(member4, skill2, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + final MemberSkill ms9 = createMemberSkill(member4, skill4, SkillLevel.EXPERT_LEVEL, LocalDate.now()); // List of skills required in first request final SkillLevelDTO dto1 = new SkillLevelDTO(); @@ -203,17 +132,13 @@ void testReport() { assertTrue(elem.getId().equals(memberId1) || elem.getId().equals(memberId3) || elem.getId().equals(memberId4)); if (elem.getId().equals(memberId1)) { - assertReturnedMember1(elem, skillId1, skillId2); + assertReturnedMember1(elem, skillId1, skillId2, member1.getFirstName() + " " + member1.getLastName()); } else if (elem.getId().equals(memberId3)) { - assertReturnedMember3(elem, skillId2, skillId3); + assertReturnedMember3(elem, skillId2, skillId3, member3.getFirstName() + " " + member3.getLastName()); } else { - assertReturnedMember4(elem, skillId1, skillId2); + assertReturnedMember4(elem, skillId1, skillId2, member4.getFirstName() + " " + member4.getLastName()); } } - verify(memberSkillRepository, times(3)).findBySkillid(any(UUID.class)); - verify(memberProfileServices, times(3)).getById(any(UUID.class)); - verify(skillRepository, times(3)).existsById(any(UUID.class)); - verify(memberProfileRepository, never()).existsById(any(UUID.class)); // Specify a list of members final Set members = new HashSet<>(); @@ -227,24 +152,16 @@ void testReport() { for (TeamMemberSkillDTO elem : response2.getTeamMembers()) { assertTrue(elem.getId().equals(memberId3) || elem.getId().equals(memberId4)); if (elem.getId().equals(memberId3)) { - assertReturnedMember3(elem, skillId2, skillId3); + assertReturnedMember3(elem, skillId2, skillId3, member3.getFirstName() + " " + member3.getLastName()); } else { - assertReturnedMember4(elem, skillId1, skillId2); + assertReturnedMember4(elem, skillId1, skillId2, member4.getFirstName() + " " + member4.getLastName()); } } - verify(memberSkillRepository, times(6)).findBySkillid(any(UUID.class)); - verify(memberProfileServices, times(6)).getById(any(UUID.class)); - verify(skillRepository, times(6)).existsById(any(UUID.class)); - verify(memberProfileRepository, times(3)).existsById(any(UUID.class)); // Each returned member must satisfy all requested skills request1.setInclusive(true); final SkillsReportResponseDTO response3 = skillsReportServices.report(request1); assertTrue(response3.getTeamMembers().isEmpty()); - verify(memberSkillRepository, times(9)).findBySkillid(any(UUID.class)); - verify(memberProfileServices, times(9)).getById(any(UUID.class)); - verify(skillRepository, times(9)).existsById(any(UUID.class)); - verify(memberProfileRepository, times(6)).existsById(any(UUID.class)); // Another request final SkillLevelDTO dto4 = new SkillLevelDTO(); @@ -265,7 +182,7 @@ void testReport() { assertEquals(1, response4.getTeamMembers().size()); assertEquals(memberId4, response4.getTeamMembers().get(0).getId()); - assertEquals("Ross Geller", response4.getTeamMembers().get(0).getName()); + assertEquals(member4.getFirstName() + " " + member4.getLastName(), response4.getTeamMembers().get(0).getName()); assertEquals(2, response4.getTeamMembers().get(0).getSkills().size()); for (SkillLevelDTO skill : response4.getTeamMembers().get(0).getSkills()) { assertTrue(skill.getId().equals(skillId2) || skill.getId().equals(skillId4)); @@ -275,14 +192,10 @@ void testReport() { assertEquals(SkillLevel.convertFromString(SkillLevel.EXPERT_LEVEL), skill.getLevel()); } } - verify(memberSkillRepository, times(11)).findBySkillid(any(UUID.class)); - verify(memberProfileServices, times(12)).getById(any(UUID.class)); - verify(skillRepository, times(11)).existsById(any(UUID.class)); - verify(memberProfileRepository, times(6)).existsById(any(UUID.class)); } - private void assertReturnedMember1(TeamMemberSkillDTO elem, UUID skillId1, UUID skillId2) { - assertEquals("Joey Tribbiani", elem.getName()); + private void assertReturnedMember1(TeamMemberSkillDTO elem, UUID skillId1, UUID skillId2, String fullName) { + assertEquals(fullName, elem.getName()); assertEquals(2, elem.getSkills().size()); for (SkillLevelDTO skill : elem.getSkills()) { assertTrue(skill.getId().equals(skillId1) || skill.getId().equals(skillId2)); @@ -294,8 +207,8 @@ private void assertReturnedMember1(TeamMemberSkillDTO elem, UUID skillId1, UUID } } - private void assertReturnedMember3(TeamMemberSkillDTO elem, UUID skillId2, UUID skillId3) { - assertNull(elem.getName()); + private void assertReturnedMember3(TeamMemberSkillDTO elem, UUID skillId2, UUID skillId3, String fullName) { + assertEquals(fullName, elem.getName()); assertEquals(2, elem.getSkills().size()); for (SkillLevelDTO skill : elem.getSkills()) { assertTrue(skill.getId().equals(skillId2) || skill.getId().equals(skillId3)); @@ -307,8 +220,8 @@ private void assertReturnedMember3(TeamMemberSkillDTO elem, UUID skillId2, UUID } } - private void assertReturnedMember4(TeamMemberSkillDTO elem, UUID skillId1, UUID skillId2) { - assertEquals("Ross Geller", elem.getName()); + private void assertReturnedMember4(TeamMemberSkillDTO elem, UUID skillId1, UUID skillId2, String fullName) { + assertEquals(fullName, elem.getName()); assertEquals(2, elem.getSkills().size()); for (SkillLevelDTO skill : elem.getSkills()) { assertTrue(skill.getId().equals(skillId1) || skill.getId().equals(skillId2)); From 74f2cc5105828dd60fce10a7a71bb980b9e91e65 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Mon, 23 Dec 2024 12:33:05 -0600 Subject: [PATCH 30/64] Adjust colors on pulse report --- web-ui/src/pages/PulseReportPage.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 7929f823ac..4cf902f10a 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -468,15 +468,15 @@ const PulseReportPage = () => { }; const dataInfo = [ - {key: "internalVeryDissatisfied", stackId: "internal", color: ociDarkBlue, }, - {key: "internalDissatisfied", stackId: "internal", color: pSBC(.05, ociDarkBlue), }, - {key: "internalNeutral", stackId: "internal", color: pSBC(.10, ociDarkBlue), }, - {key: "internalSatisfied", stackId: "internal", color: pSBC(.15, ociDarkBlue), }, - {key: "internalVerySatisfied", stackId: "internal", color: pSBC(.2, ociDarkBlue), }, - {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.8, ociOrange), }, - {key: "externalDissatisfied", stackId: "external", color: pSBC(-.6, ociOrange), }, - {key: "externalNeutral", stackId: "external", color: pSBC(-.4, ociOrange), }, - {key: "externalSatisfied", stackId: "external", color: pSBC(-.2, ociOrange), }, + {key: "internalVeryDissatisfied", stackId: "internal", color: pSBC(-.9, ociDarkBlue), }, + {key: "internalDissatisfied", stackId: "internal", color: pSBC(-.75, ociDarkBlue), }, + {key: "internalNeutral", stackId: "internal", color: pSBC(-.5, ociDarkBlue), }, + {key: "internalSatisfied", stackId: "internal", color: pSBC(-.25, ociDarkBlue), }, + {key: "internalVerySatisfied", stackId: "internal", color: ociDarkBlue, }, + {key: "externalVeryDissatisfied", stackId: "external", color: pSBC(-.9, ociOrange), }, + {key: "externalDissatisfied", stackId: "external", color: pSBC(-.75, ociOrange), }, + {key: "externalNeutral", stackId: "external", color: pSBC(-.5, ociOrange), }, + {key: "externalSatisfied", stackId: "external", color: pSBC(-.25, ociOrange), }, {key: "externalVerySatisfied", stackId: "external", color: ociOrange, }, ]; From 8c5d52286e1d70531d55d94d38f5e32b30738439 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 13:16:38 -0600 Subject: [PATCH 31/64] Removed mockito. --- .../MemberSkillServiceImplTest.java | 255 ++++++------------ 1 file changed, 78 insertions(+), 177 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/MemberSkillServiceImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/MemberSkillServiceImplTest.java index ea5ef37802..153914a566 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/MemberSkillServiceImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/MemberSkillServiceImplTest.java @@ -3,102 +3,54 @@ import com.objectcomputing.checkins.exceptions.AlreadyExistsException; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.SkillFixture; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.MemberSkillFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; import com.objectcomputing.checkins.services.skills.Skill; +import com.objectcomputing.checkins.services.member_skill.skillsreport.SkillLevel; import com.objectcomputing.checkins.services.skills.SkillRepository; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolationException; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class MemberSkillServiceImplTest extends TestContainersSuite { - - @Mock - private MemberSkillRepository memberSkillRepository; - - @Mock - private MemberProfileRepository memberProfileRepository; - - @Mock - private SkillRepository skillRepository; - - @InjectMocks +class MemberSkillServiceImplTest extends TestContainersSuite + implements MemberProfileFixture, MemberSkillFixture, SkillFixture { + @Inject private MemberSkillServiceImpl memberSkillsServices; - private AutoCloseable mockFinalizer; - - @BeforeAll - void initMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @BeforeEach - void resetMocks() { - reset(memberSkillRepository, skillRepository, memberProfileRepository); - } - - @AfterAll - void closeMocks() throws Exception { - mockFinalizer.close(); - } - @Test void testRead() { - MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()); - - when(memberSkillRepository.findById(memberSkill.getId())).thenReturn(Optional.of(memberSkill)); - + Skill skill = createSkill("Skill1", false, "First", false); + MemberProfile member = createADefaultMemberProfile(); + MemberSkill memberSkill = createMemberSkill(member, skill, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); assertEquals(memberSkill, memberSkillsServices.read(memberSkill.getId())); - - verify(memberSkillRepository, times(1)).findById(any(UUID.class)); } @Test void testReadNullId() { - assertNull(memberSkillsServices.read(null)); - - verify(memberSkillRepository, never()).findById(any(UUID.class)); + assertThrows(ConstraintViolationException.class, () -> memberSkillsServices.read(null)); } @Test void testSave() { - MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), UUID.randomUUID()); - Skill skill = new Skill(); - - when(skillRepository.findById(memberSkill.getSkillid())).thenReturn(Optional.of(skill)); - when(memberProfileRepository.findById(memberSkill.getMemberid())).thenReturn(Optional.of(new MemberProfile())); - when(memberSkillRepository.save(memberSkill)).thenReturn(memberSkill); - + Skill skill = createSkill("Skill1", false, "First", false); + MemberProfile member = createADefaultMemberProfile(); + MemberSkill memberSkill = new MemberSkill(member.getId(), skill.getId()); assertEquals(memberSkill, memberSkillsServices.save(memberSkill)); - - verify(skillRepository, times(1)).findById(any(UUID.class)); - verify(memberProfileRepository, times(1)).findById(any(UUID.class)); - verify(memberSkillRepository, times(1)).save(any(MemberSkill.class)); } @Test @@ -107,10 +59,6 @@ void testSaveWithId() { BadArgException exception = assertThrows(BadArgException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Found unexpected id %s for member skill", memberSkill.getId()), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, never()).findById(any(UUID.class)); - verify(memberProfileRepository, never()).findById(any(UUID.class)); } @Test @@ -119,10 +67,6 @@ void testSaveActionItemNullMemberId() { BadArgException exception = assertThrows(BadArgException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Invalid member skill %s", memberSkill), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, never()).findById(any(UUID.class)); - verify(memberProfileRepository, never()).findById(any(UUID.class)); } @Test @@ -131,178 +75,135 @@ void testSaveActionItemNullSkillId() { BadArgException exception = assertThrows(BadArgException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Invalid member skill %s", memberSkill), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, never()).findById(any(UUID.class)); - verify(memberProfileRepository, never()).findById(any(UUID.class)); } @Test void testSaveNullMemberSkill() { assertNull(memberSkillsServices.save(null)); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, never()).findById(any(UUID.class)); - verify(memberProfileRepository, never()).findById(any(UUID.class)); } @Test void testSaveMemberSkillAlreadyExistingSkill() { - MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), UUID.randomUUID()); - - when(skillRepository.findById(memberSkill.getSkillid())).thenReturn(Optional.of(new Skill())); - when(memberProfileRepository.findById(memberSkill.getMemberid())).thenReturn(Optional.of(new MemberProfile())); - when(memberSkillRepository.findByMemberidAndSkillid(memberSkill.getMemberid(), memberSkill.getSkillid())) - .thenReturn(Optional.of(memberSkill)); - + Skill skill = createSkill("Skill1", false, "First", false); + MemberProfile member = createADefaultMemberProfile(); + MemberSkill savedSkill = createMemberSkill(member, skill, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill memberSkill = new MemberSkill(member.getId(), skill.getId(), SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); AlreadyExistsException exception = assertThrows(AlreadyExistsException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Member %s already has this skill %s", memberSkill.getMemberid(), memberSkill.getSkillid()), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, times(1)).findById(any(UUID.class)); - verify(memberProfileRepository, times(1)).findById(any(UUID.class)); - verify(memberSkillRepository, times(1)).findByMemberidAndSkillid(any(UUID.class), any(UUID.class)); } @Test void testSaveMemberSkillNonExistingSkill() { - MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), UUID.randomUUID()); - - when(skillRepository.findById(memberSkill.getSkillid())).thenReturn(Optional.empty()); - when(memberProfileRepository.findById(memberSkill.getMemberid())).thenReturn(Optional.of(new MemberProfile())); + MemberProfile member = createADefaultMemberProfile(); + MemberSkill memberSkill = new MemberSkill(member.getId(), UUID.randomUUID()); BadArgException exception = assertThrows(BadArgException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Skill %s doesn't exist", memberSkill.getSkillid()), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, times(1)).findById(any(UUID.class)); - verify(memberProfileRepository, times(1)).findById(any(UUID.class)); } @Test void testSaveMemberSkillNonExistingMember() { - MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), UUID.randomUUID()); - - when(skillRepository.findById(memberSkill.getSkillid())).thenReturn(Optional.of(new Skill())); - when(memberProfileRepository.findById(memberSkill.getMemberid())).thenReturn(Optional.empty()); + Skill skill = createSkill("Skill1", false, "First", false); + MemberSkill memberSkill = new MemberSkill(UUID.randomUUID(), skill.getId()); BadArgException exception = assertThrows(BadArgException.class, () -> memberSkillsServices.save(memberSkill)); assertEquals(String.format("Member Profile %s doesn't exist", memberSkill.getMemberid()), exception.getMessage()); - - verify(memberSkillRepository, never()).save(any(MemberSkill.class)); - verify(skillRepository, never()).findById(any(UUID.class)); - verify(memberProfileRepository, times(1)).findById(any(UUID.class)); } @Test void testFindByFieldsNullParams() { - Set memberSkillSet = Set.of( - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()) - ); - - when(memberSkillRepository.findAll()).thenReturn(memberSkillSet.stream().toList()); - + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + MemberProfile member1 = createADefaultMemberProfile(); + MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill ms2 = createMemberSkill(member1, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + MemberSkill ms3 = createMemberSkill(member1, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); + + Set memberSkillSet = Set.of(ms1, ms2, ms3); assertEquals(memberSkillSet, memberSkillsServices.findByFields(null, null)); - - verify(memberSkillRepository, times(1)).findAll(); - verify(memberSkillRepository, never()).findByMemberid(any(UUID.class)); - verify(memberSkillRepository, never()).findBySkillid(any(UUID.class)); } @Test void testFindByFieldsMemberId() { - List memberSkillSet = List.of( - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()) - ); - + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + MemberProfile member1 = createADefaultMemberProfile(); + MemberProfile member2 = createASecondDefaultMemberProfile(); + MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill ms2 = createMemberSkill(member2, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + MemberSkill ms3 = createMemberSkill(member1, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); + + List memberSkillSet = List.of(ms1, ms2, ms3); List memberSkillsToFind = List.of(memberSkillSet.get(1)); MemberSkill memberSkill = memberSkillsToFind.get(0); - when(memberSkillRepository.findAll()).thenReturn(memberSkillSet); - when(memberSkillRepository.findByMemberid(memberSkill.getMemberid())).thenReturn(memberSkillsToFind); - assertEquals(new HashSet<>(memberSkillsToFind), memberSkillsServices.findByFields(memberSkill.getMemberid(), null)); - - verify(memberSkillRepository, times(1)).findAll(); - verify(memberSkillRepository, times(1)).findByMemberid(any(UUID.class)); - verify(memberSkillRepository, never()).findBySkillid(any(UUID.class)); } @Test void testFindByFieldsSkillId() { - List memberSkillSet = List.of( - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()) - ); + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + MemberProfile member1 = createADefaultMemberProfile(); + MemberProfile member2 = createASecondDefaultMemberProfile(); + MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill ms2 = createMemberSkill(member2, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + MemberSkill ms3 = createMemberSkill(member1, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); + + List memberSkillSet = List.of(ms1, ms2, ms3); List memberSkillsToFind = List.of(memberSkillSet.get(1)); MemberSkill memberSkill = memberSkillsToFind.get(0); - when(memberSkillRepository.findAll()).thenReturn(memberSkillSet); - when(memberSkillRepository.findBySkillid(memberSkill.getSkillid())).thenReturn(memberSkillsToFind); - assertEquals(new HashSet<>(memberSkillsToFind), memberSkillsServices.findByFields(null, memberSkill.getSkillid())); - - verify(memberSkillRepository, times(1)).findAll(); - verify(memberSkillRepository, times(1)).findBySkillid(any(UUID.class)); - verify(memberSkillRepository, never()).findByMemberid(any(UUID.class)); } @Test void testFindByFieldsAll() { - List memberSkillSet = List.of( - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()) - ); + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + MemberProfile member1 = createADefaultMemberProfile(); + MemberProfile member2 = createASecondDefaultMemberProfile(); + MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill ms2 = createMemberSkill(member2, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + MemberSkill ms3 = createMemberSkill(member1, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); + + List memberSkillSet = List.of(ms1, ms2, ms3); List memberSkillsToFind = List.of(memberSkillSet.get(1)); MemberSkill memberSkill = memberSkillsToFind.get(0); - when(memberSkillRepository.findAll()).thenReturn(memberSkillSet); - when(memberSkillRepository.findBySkillid(memberSkill.getSkillid())).thenReturn(memberSkillsToFind); - when(memberSkillRepository.findByMemberid(memberSkill.getMemberid())).thenReturn(memberSkillsToFind); assertEquals(new HashSet<>(memberSkillsToFind), memberSkillsServices .findByFields(memberSkill.getMemberid(), memberSkill.getSkillid())); - - verify(memberSkillRepository, times(1)).findAll(); - verify(memberSkillRepository, times(1)).findByMemberid(any(UUID.class)); - verify(memberSkillRepository, times(1)).findBySkillid(any(UUID.class)); } @Test void testReadAll() { - Set memberSkillSet = Set.of( - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()), - new MemberSkill(UUID.randomUUID(), UUID.randomUUID()) - ); + Skill skill1 = createSkill("Skill1", false, "First", false); + Skill skill2 = createSkill("Skill2", false, "Second", false); + Skill skill3 = createSkill("Skill3", false, "Third", false); + MemberProfile member1 = createADefaultMemberProfile(); + MemberProfile member2 = createASecondDefaultMemberProfile(); + MemberSkill ms1 = createMemberSkill(member1, skill1, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); + MemberSkill ms2 = createMemberSkill(member2, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + MemberSkill ms3 = createMemberSkill(member1, skill3, SkillLevel.NOVICE_LEVEL, LocalDate.now()); - when(memberSkillRepository.findAll()).thenReturn(memberSkillSet.stream().toList()); + Set memberSkillSet = Set.of(ms1, ms2, ms3); assertEquals(memberSkillSet, memberSkillsServices.findByFields(null,null)); - - verify(memberSkillRepository, times(1)).findAll(); } @Test void testDelete() { - UUID uuid = UUID.randomUUID(); - - doAnswer(an -> { - assertEquals(uuid, an.getArgument(0)); - return null; - }).when(memberSkillRepository).deleteById(any(UUID.class)); - - memberSkillsServices.delete(uuid); - - verify(memberSkillRepository, times(1)).deleteById(any(UUID.class)); + Skill skill = createSkill("Skill1", false, "First", false); + memberSkillsServices.delete(skill.getId()); + assertFalse(getMemberSkillRepository().findById(skill.getId()) + .isPresent()); } } From ab7461e7d846616778e514dc24c3683d7b607493 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 14:28:16 -0600 Subject: [PATCH 32/64] Removed mockito from MemberProfileTest. --- .../memberprofile/MemberProfileTest.java | 200 +++++++----------- 1 file changed, 73 insertions(+), 127 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java index aa70d850eb..cc43ab26a8 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.services.memberprofile; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; import com.objectcomputing.checkins.exceptions.AlreadyExistsException; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; @@ -18,8 +19,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mockito; import java.time.LocalDate; import java.util.List; @@ -31,13 +30,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage -class MemberProfileTest extends TestContainersSuite { +class MemberProfileTest extends TestContainersSuite + implements MemberProfileFixture { @Inject protected Validator validator; @@ -46,29 +42,12 @@ class MemberProfileTest extends TestContainersSuite { @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; - private MemberProfileRepository memberRepo; - + @Inject private MemberProfileServicesImpl memberProfileServices; @BeforeEach - @Tag("mocked") void setUp() { - memberRepo = Mockito.mock(MemberProfileRepository.class); - CurrentUserServices currentUserServices = Mockito.mock(CurrentUserServices.class); - RoleServices roleServices = Mockito.mock(RoleServices.class); - CheckInServices checkinServices = Mockito.mock(CheckInServices.class); - MemberSkillServices memberSkillServices = Mockito.mock(MemberSkillServices.class); - TeamMemberServices teamMemberServices = Mockito.mock(TeamMemberServices.class); emailSender.reset(); - - memberProfileServices = new MemberProfileServicesImpl( - memberRepo, - currentUserServices, - roleServices, - checkinServices, - memberSkillServices, - teamMemberServices, - emailSender); } @Test @@ -195,38 +174,26 @@ void testToString() { } @Test - @Tag("mocked") void testSaveProfileWithExistingEmail() { - String workEmail = "existing@example.com"; - MemberProfile existingProfile = new MemberProfile(UUID.randomUUID(), "Jane", null, "Doe", null, null, null, null, workEmail, null, null, null, null, null, null, null, null, null); + MemberProfile existingProfile = createADefaultMemberProfile(); + String workEmail = existingProfile.getWorkEmail(); MemberProfile newProfile = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, workEmail, null, null, null, null, null, null, null, null, null); - when(memberRepo.findByWorkEmail(workEmail)).thenReturn(Optional.of(existingProfile)); - assertThrows(AlreadyExistsException.class, () -> memberProfileServices.saveProfile(newProfile)); } @Test - @Tag("mocked") void testSaveProfileWithNewEmail() { - UUID pdlId = UUID.randomUUID(); - UUID supervisorId = UUID.randomUUID(); - - MemberProfile pdlProfile = new MemberProfile(pdlId,"Jane", null, "Smith", null, null, null, null, "jane.smith@example.com", null, null, null, null, null, null, null, null, null); - MemberProfile supervisorProfile = new MemberProfile(supervisorId, "Janine", null, "Smith", null, null, null, null, "janine.smith@example.com", null, null, null, null, null, null, null, null, null); - MemberProfile newProfile = new MemberProfile("John", null, "Smith", null, null, pdlId, null, "john.smith@example.com", null, null, null, supervisorId, null, null, null, null, null); - - - // Mocking findByWorkEmail to return an empty Optional indicating no existing profile with the email - when(memberRepo.findByWorkEmail(newProfile.getWorkEmail())).thenReturn(Optional.empty()); - - // Mocking save to return the profile that is being saved - when(memberRepo.save(newProfile)).thenReturn(newProfile); - - // Mocking findById to return the profile after saving - when(memberRepo.findById(newProfile.getId())).thenReturn(Optional.of(newProfile)); - when(memberRepo.findById(pdlId)).thenReturn(Optional.of(pdlProfile)); - when(memberRepo.findById(supervisorId)).thenReturn(Optional.of(supervisorProfile)); + MemberProfile pdlProfile = createADefaultMemberProfile(); + MemberProfile supervisorProfile = createADefaultSupervisor(); + MemberProfile newProfile = new MemberProfile("Charizard", null, "Char", + null, "Local fire hazard", pdlProfile.getId(), + "New York, New York", + "charizard@objectcomputing.com", "local-kaiju", + LocalDate.now().minusDays(3).minusYears(5), + "Needs supervision due to building being ultra flammable", + supervisorProfile.getId(), null, null, null, null, + LocalDate.now()); MemberProfile savedProfile = memberProfileServices.saveProfile(newProfile); @@ -236,26 +203,26 @@ void testSaveProfileWithNewEmail() { assertEquals(2, emailSender.events.size()); assertEquals(List.of( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the PDL of John Smith", "John Smith will now report to you as their PDL. Please engage with them: john.smith@example.com", pdlProfile.getWorkEmail()), - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the supervisor of John Smith", "John Smith will now report to you as their supervisor. Please engage with them: john.smith@example.com", supervisorProfile.getWorkEmail()) + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the PDL of %s %s", newProfile.getFirstName(), newProfile.getLastName()), + String.format("%s %s will now report to you as their PDL. Please engage with them: %s", newProfile.getFirstName(), newProfile.getLastName(), newProfile.getWorkEmail()), + pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the supervisor of %s %s", newProfile.getFirstName(), newProfile.getLastName()), + String.format("%s %s will now report to you as their supervisor. Please engage with them: %s", newProfile.getFirstName(), newProfile.getLastName(), newProfile.getWorkEmail()), + supervisorProfile.getWorkEmail()) ), emailSender.events ); } @Test - @Tag("mocked") void testUpdateProfileWithChangedPDL() { - UUID id = UUID.randomUUID(); - UUID pdlId = UUID.randomUUID(); - MemberProfile existingProfile = new MemberProfile(id, "John", null, "Smith", null, null, null, null, "john.smith@example.com", null, null, null, null, null, null, null, null, null); - MemberProfile updatedProfile = new MemberProfile(id, "John", null, "Smith", null, null, pdlId, null, "john.smith@example.com", null, null, null, null, null, null, null, null, null); - MemberProfile pdlProfile = new MemberProfile(pdlId, "Jane", null, "Doe", null, null, null, null, "jane.doe@example.com", null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(id)).thenReturn(Optional.of(existingProfile)); - when(memberRepo.findByWorkEmail(updatedProfile.getWorkEmail())).thenReturn(Optional.of(updatedProfile)); - when(memberRepo.findById(pdlId)).thenReturn(Optional.of(pdlProfile)); - when(memberRepo.update(updatedProfile)).thenReturn(updatedProfile); + MemberProfile existingProfile = createADefaultMemberProfile(); + MemberProfile pdlProfile = createASecondDefaultMemberProfile(); + UUID id = existingProfile.getId(); + UUID pdlId = pdlProfile.getId(); + MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null); MemberProfile result = memberProfileServices.saveProfile(updatedProfile); @@ -263,44 +230,38 @@ void testUpdateProfileWithChangedPDL() { assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the PDL of John Smith", "John Smith will now report to you as their PDL. Please engage with them: " + existingProfile.getWorkEmail(), pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the PDL of %s %s", existingProfile.getFirstName(), existingProfile.getLastName()), + String.format("%s %s will now report to you as their PDL. Please engage with them: %s", existingProfile.getFirstName(), existingProfile.getLastName(), existingProfile.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testUpdateProfileWithChangedSupervisor() { - UUID id = UUID.randomUUID(); - UUID supervisorId = UUID.randomUUID(); - MemberProfile existingProfile = new MemberProfile(id, "John", null, "Smith", null, null, null, null, "john.smith@example.com", null, null, null, null, null, null, null, null, null); - MemberProfile updatedProfile = new MemberProfile(id, "John", null, "Smith", null, null, null, null, "john.smith@example.com", null, null, null, supervisorId, null, null, null, null, null); - MemberProfile supervisorProfile = new MemberProfile(supervisorId, "Jane", null, "Doe", null, null, null, null, "jane.doe@example.com", null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(id)).thenReturn(Optional.of(existingProfile)); - when(memberRepo.findByWorkEmail(updatedProfile.getWorkEmail())).thenReturn(Optional.of(updatedProfile)); - when(memberRepo.findById(supervisorId)).thenReturn(Optional.of(supervisorProfile)); - when(memberRepo.update(updatedProfile)).thenReturn(updatedProfile); + MemberProfile existingProfile = createADefaultMemberProfile(); + MemberProfile supervisorProfile = createASecondDefaultMemberProfile(); + UUID id = existingProfile.getId(); + UUID supervisorId = supervisorProfile.getId(); + MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null); MemberProfile result = memberProfileServices.saveProfile(updatedProfile); assertEquals(updatedProfile, result); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the supervisor of John Smith", "John Smith will now report to you as their supervisor. Please engage with them: " + existingProfile.getWorkEmail(), supervisorProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the supervisor of %s %s", existingProfile.getFirstName(), existingProfile.getLastName()), + String.format("%s %s will now report to you as their supervisor. Please engage with them: %s", existingProfile.getFirstName(), existingProfile.getLastName(), existingProfile.getWorkEmail()), + supervisorProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testUpdateProfileWithNoChange() { - UUID id = UUID.randomUUID(); - MemberProfile existingProfile = new MemberProfile(id, "John", null, "Smith", null, null, null, null, "john.smith@example.com", null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(id)).thenReturn(Optional.of(existingProfile)); - when(memberRepo.findByWorkEmail(existingProfile.getWorkEmail())).thenReturn(Optional.of(existingProfile)); - when(memberRepo.update(existingProfile)).thenReturn(existingProfile); + MemberProfile existingProfile = createADefaultMemberProfile(); MemberProfile result = memberProfileServices.saveProfile(existingProfile); @@ -309,81 +270,71 @@ void testUpdateProfileWithNoChange() { } @Test - @Tag("mocked") void testEmailAssignmentWithValidPDL() { - UUID pdlId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, pdlId, null, "john.smith@example.com", - null, null, null, null, null, null, null, null, null); - MemberProfile pdlProfile = new MemberProfile(UUID.randomUUID(), "Jane", null, "Doe", null, null, null, null, "jane.doe@example.com", - null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(pdlId)).thenReturn(Optional.of(pdlProfile)); + MemberProfile pdlProfile = createADefaultMemberProfile(); + MemberProfile member = createADefaultMemberProfileForPdl(pdlProfile); memberProfileServices.emailAssignment(member, true); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the PDL of John Smith", "John Smith will now report to you as their PDL. Please engage with them: john.smith@example.com", pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the PDL of %s %s", member.getFirstName(), member.getLastName()), + String.format("%s %s will now report to you as their PDL. Please engage with them: %s", member.getFirstName(), member.getLastName(), member.getWorkEmail()), + pdlProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testEmailAssignmentWithValidSupervisor() { - UUID supervisorId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, "john.smith@example.com", - null, null, null, supervisorId, null, null, null, null, null); - MemberProfile supervisorProfile = new MemberProfile(UUID.randomUUID(), "Jane", null, "Doe", null, null, null, null, "jane.doe@example.com", - null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(supervisorId)).thenReturn(Optional.of(supervisorProfile)); + MemberProfile pdlProfile = createADefaultMemberProfile(); + MemberProfile supervisorProfile = createADefaultSupervisor(); + MemberProfile member = createAProfileWithSupervisorAndPDL( + supervisorProfile, pdlProfile); memberProfileServices.emailAssignment(member, false); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the supervisor of John Smith", "John Smith will now report to you as their supervisor. Please engage with them: john.smith@example.com", supervisorProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the supervisor of %s %s", member.getFirstName(), member.getLastName()), + String.format("%s %s will now report to you as their supervisor. Please engage with them: %s", member.getFirstName(), member.getLastName(), member.getWorkEmail()), + supervisorProfile.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testEmailAssignmentWithValidPdlAndSupervisor() { - UUID pdlId = UUID.randomUUID(); - UUID supervisorId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, pdlId, null, "john.smith@example.com", - null, null, null, supervisorId, null, null, null, null, null); - MemberProfile pdlProfile = new MemberProfile(UUID.randomUUID(), "Jane", null, "Doe", null, null, null, null, "jane.doe@example.com", - null, null, null, null, null, null, null, null, null); - - MemberProfile supervisorProfile = new MemberProfile(UUID.randomUUID(), "Janine", null, "Doe", null, null, null, null, "janine.doe@example.com", - null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(pdlId)).thenReturn(Optional.of(pdlProfile)); - when(memberRepo.findById(supervisorId)).thenReturn(Optional.of(supervisorProfile)); + MemberProfile pdlProfile = createADefaultMemberProfile(); + MemberProfile supervisorProfile = createADefaultSupervisor(); + MemberProfile member = createAProfileWithSupervisorAndPDL( + supervisorProfile, pdlProfile); memberProfileServices.emailAssignment(member, true); // for PDL memberProfileServices.emailAssignment(member, false); // for supervisor assertEquals(2, emailSender.events.size()); assertEquals(List.of( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the PDL of John Smith", "John Smith will now report to you as their PDL. Please engage with them: john.smith@example.com", pdlProfile.getWorkEmail()), - List.of("SEND_EMAIL", "null", "null", "You have been assigned as the supervisor of John Smith", "John Smith will now report to you as their supervisor. Please engage with them: john.smith@example.com", supervisorProfile.getWorkEmail()) + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the PDL of %s %s", member.getFirstName(), member.getLastName()), + String.format("%s %s will now report to you as their PDL. Please engage with them: %s", member.getFirstName(), member.getLastName(), member.getWorkEmail()), + pdlProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as the supervisor of %s %s", member.getFirstName(), member.getLastName()), + String.format("%s %s will now report to you as their supervisor. Please engage with them: %s", member.getFirstName(), member.getLastName(), member.getWorkEmail()), + supervisorProfile.getWorkEmail()) ), emailSender.events ); } @Test - @Tag("mocked") void testEmailAssignmentWithInvalidPDL() { + MemberProfile existingProfile = createADefaultMemberProfile(); UUID pdlId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, pdlId, null, "john.smith@example.com", - null, null, null, null, null, null, null, null, null); - - when(memberRepo.findById(pdlId)).thenReturn(Optional.empty()); + MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null); memberProfileServices.emailAssignment(member, true); @@ -391,13 +342,10 @@ void testEmailAssignmentWithInvalidPDL() { } @Test - @Tag("mocked") void testEmailAssignmentWithInvalidSupervisor() { + MemberProfile existingProfile = createADefaultMemberProfile(); UUID supervisorId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, "john.smith@example.com", - null, null, null, supervisorId, null, null, null, null, null); - - when(memberRepo.findById(supervisorId)).thenReturn(Optional.empty()); + MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null); memberProfileServices.emailAssignment(member, true); @@ -405,7 +353,6 @@ void testEmailAssignmentWithInvalidSupervisor() { } @Test - @Tag("mocked") void testEmailAssignmentWithInvalidMember() { MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @@ -416,7 +363,6 @@ void testEmailAssignmentWithInvalidMember() { } @Test - @Tag("mocked") void testEmailAssignmentWithNullRoleId() { MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, "john.smith@example.com", null, null, null, null, null, null, null, null, null); From 9b593143a2ba1d59c73cbed16a36a07aab66eecd Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 23 Dec 2024 15:31:22 -0600 Subject: [PATCH 33/64] Removed Mockito from MemberPhotoServiceImplTest. --- .../memberphoto/GooglePhotoAccessorImpl.java | 3 +- .../GooglePhotoAccessorImplReplacement.java | 38 ++++++++ .../MemberPhotoServiceImplTest.java | 90 +++---------------- 3 files changed, 52 insertions(+), 79 deletions(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/GooglePhotoAccessorImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/GooglePhotoAccessorImpl.java index d9f06b5c40..964b6b8fcb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/GooglePhotoAccessorImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/GooglePhotoAccessorImpl.java @@ -18,7 +18,8 @@ import java.util.Optional; @Singleton -class GooglePhotoAccessorImpl implements GooglePhotoAccessor { +// Public so that this class can be replaced during testing. +public class GooglePhotoAccessorImpl implements GooglePhotoAccessor { private static final Logger LOG = LoggerFactory.getLogger(GooglePhotoAccessorImpl.class); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java new file mode 100644 index 0000000000..1c4c434cf6 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java @@ -0,0 +1,38 @@ +package com.objectcomputing.checkins.services; + +import com.google.api.services.directory.model.UserPhoto; +import com.objectcomputing.checkins.services.memberprofile.memberphoto.GooglePhotoAccessorImpl; +import com.objectcomputing.checkins.services.memberprofile.memberphoto.GooglePhotoAccessor; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Base64; + +import jakarta.inject.Singleton; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; + +@Singleton +@Replaces(GooglePhotoAccessorImpl.class) +@Requires(property = "replace.googlephotoaccessorimpl", value = StringUtils.TRUE) +public class GooglePhotoAccessorImplReplacement implements GooglePhotoAccessor { + Map photos = new HashMap<>(); + + public void reset() { + photos.clear(); + } + + public void setUserPhoto(String email, UserPhoto photo) { + photos.put(email, photo); + } + + @Override + public byte[] getPhotoData(String workEmail) { + UserPhoto photo = photos.get(workEmail); + return photo == null + ? new byte[0] + : Base64.getUrlDecoder().decode(photo.getPhotoData().getBytes()); + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoServiceImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoServiceImplTest.java index 2108e86bc0..680d30428c 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoServiceImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoServiceImplTest.java @@ -1,20 +1,15 @@ package com.objectcomputing.checkins.services.memberprofile.memberphoto; import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.services.directory.Directory; import com.google.api.services.directory.model.UserPhoto; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.GooglePhotoAccessorImplReplacement; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; -import com.objectcomputing.checkins.util.googleapiaccess.GoogleApiAccess; -import io.micronaut.context.env.Environment; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Property; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import jakarta.inject.Inject; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -22,64 +17,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage +@Property(name = "replace.googlephotoaccessorimpl", value = StringUtils.TRUE) class MemberPhotoServiceImplTest extends TestContainersSuite { - @Mock - private MemberProfileServices mockMemberProfileServices; - - @Mock - private GoogleApiAccess mockGoogleApiAccess; - - @Mock - private Directory mockDirectory; - - @Mock - private Directory.Users mockUsers; - - @Mock - private Directory.Users.Photos mockPhotos; - - @Mock - private Directory.Users.Photos.Get mockGet; - - @Mock - private Environment mockEnvironment; - - @InjectMocks - private GooglePhotoAccessorImpl accessor; + @Inject + private GooglePhotoAccessorImplReplacement googlePhotoAccessorImpl; + @Inject private MemberPhotoServiceImpl service; - private AutoCloseable mockFinalizer; - - @BeforeAll - void initMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @AfterAll - void close() throws Exception { - mockFinalizer.close(); - } - @BeforeEach - void resetMocks() { - reset(mockMemberProfileServices); - reset(mockGoogleApiAccess); - reset(mockDirectory); - reset(mockUsers); - reset(mockPhotos); - reset(mockGet); - reset(mockEnvironment); - service = new MemberPhotoServiceImpl(accessor); + void reset() { + googlePhotoAccessorImpl.reset(); } // happy path @@ -96,36 +46,20 @@ void testGetImageByEmailAddress() throws IOException { testUserPhoto.setKind("test.kind"); testUserPhoto.setMimeType("test.mime.type"); testUserPhoto.setPhotoData(new String(testData)); - - when(mockGoogleApiAccess.getDirectory()).thenReturn(mockDirectory); - when(mockDirectory.users()).thenReturn(mockUsers); - when(mockUsers.photos()).thenReturn(mockPhotos); - when(mockPhotos.get(testEmail)).thenReturn(mockGet); - when(mockGet.execute()).thenReturn(testUserPhoto); - + googlePhotoAccessorImpl.setUserPhoto(testEmail, testUserPhoto); final byte[] result = service.getImageByEmailAddress(testEmail); assertNotNull(result); assertEquals(testPhotoData, new String(result, StandardCharsets.UTF_8)); - verify(mockGoogleApiAccess, times(1)).getDirectory(); - verify(mockGet, times(1)).execute(); } @Test void testDirectoryServiceThrowsGoogleJsonResponseException() throws IOException { - String testEmail = "test@test.com"; - - when(mockGoogleApiAccess.getDirectory()).thenReturn(mockDirectory); - when(mockDirectory.users()).thenReturn(mockUsers); - when(mockUsers.photos()).thenReturn(mockPhotos); - when(mockPhotos.get(testEmail)).thenReturn(mockGet); - when(mockGet.execute()).thenThrow(GoogleJsonResponseException.class); + String testEmail = "notcached@test.com"; final byte[] result = service.getImageByEmailAddress(testEmail); assertNotNull(result); assertEquals("", new String(result, StandardCharsets.UTF_8)); - verify(mockGoogleApiAccess, times(1)).getDirectory(); - verify(mockGet, times(1)).execute(); } } From daced47da326da68c349068ebcaecd5744b08579 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 07:59:01 -0600 Subject: [PATCH 34/64] Removed mockito from the MemberPhotoControllerTest. --- .../GooglePhotoAccessorImplReplacement.java | 6 ++++ .../MemberPhotoControllerTest.java | 31 ++++++------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java index 1c4c434cf6..122bd9d202 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/GooglePhotoAccessorImplReplacement.java @@ -28,6 +28,12 @@ public void setUserPhoto(String email, UserPhoto photo) { photos.put(email, photo); } + public void setPhotoData(String email, byte[] photoData) { + UserPhoto photo = new UserPhoto(); + photo.setPhotoData(new String(photoData)); + photos.put(email, photo); + } + @Override public byte[] getPhotoData(String workEmail) { UserPhoto photo = photos.get(workEmail); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoControllerTest.java index a16bddb9e5..43384f4635 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/memberphoto/MemberPhotoControllerTest.java @@ -1,15 +1,16 @@ package com.objectcomputing.checkins.services.memberprofile.memberphoto; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.GooglePhotoAccessorImplReplacement; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; -import io.micronaut.test.annotation.MockBean; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.util.Base64; @@ -17,14 +18,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -// Disabled in nativeTest, as we get an exception from Mockito -// => Message: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage +@Property(name = "replace.googlephotoaccessorimpl", value = StringUtils.TRUE) class MemberPhotoControllerTest extends TestContainersSuite { @Inject @@ -32,15 +27,15 @@ class MemberPhotoControllerTest extends TestContainersSuite { private HttpClient client; @Inject - GooglePhotoAccessor googlePhotoAccessor; + GooglePhotoAccessorImplReplacement googlePhotoAccessor; @Test void testGetForValidInput() { String testEmail = "test@test.com"; - byte[] testData = Base64.getUrlEncoder().encode("test.photo.data".getBytes()); - - when(googlePhotoAccessor.getPhotoData(testEmail)).thenReturn(testData); + String testPhotoData = "test.photo.data"; + byte[] testData = Base64.getUrlEncoder().encode(testPhotoData.getBytes()); + googlePhotoAccessor.setPhotoData(testEmail, testData); final HttpRequest request = HttpRequest.GET(String.format("/%s", testEmail)).basicAuth(MEMBER_ROLE, MEMBER_ROLE); final HttpResponse response = client.toBlocking().exchange(request, byte[].class); @@ -51,14 +46,6 @@ void testGetForValidInput() { assertEquals(HttpStatus.OK, response.getStatus()); assertTrue(response.getBody().isPresent()); byte[] result = response.getBody().get(); - assertEquals(new String(testData), new String(result)); - - // Only called once due to the cache - verify(googlePhotoAccessor, times(1)).getPhotoData(testEmail); - } - - @MockBean(GooglePhotoAccessorImpl.class) - public GooglePhotoAccessor googlePhotoAccessor() { - return mock(GooglePhotoAccessor.class); + assertEquals(new String(testPhotoData), new String(result)); } } From 2683afc3a96f83b092402702834db2fc14ef1583 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 08:25:07 -0600 Subject: [PATCH 35/64] Removed mockito from CurrentUserServicesImplTest. --- .../CurrentUserServicesImplTest.java | 62 +++++-------------- 1 file changed, 14 insertions(+), 48 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImplTest.java index 47abbcbe6e..6ee5bc2541 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImplTest.java @@ -1,64 +1,37 @@ package com.objectcomputing.checkins.services.memberprofile.currentuser; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; import com.objectcomputing.checkins.services.role.Role; import com.objectcomputing.checkins.services.role.RoleServices; import com.objectcomputing.checkins.services.role.RoleType; import com.objectcomputing.checkins.services.role.member_roles.MemberRoleServices; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; + +import jakarta.inject.Inject; import java.util.UUID; -import static com.objectcomputing.checkins.services.memberprofile.MemberProfileTestUtil.mkMemberProfile; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class CurrentUserServicesImplTest extends TestContainersSuite { - - @Mock - MemberProfileRepository memberProfileRepo; - @Mock - RoleServices roleServices; +class CurrentUserServicesImplTest extends TestContainersSuite + implements MemberProfileFixture, RoleFixture { - @Mock - MemberRoleServices memberRoleServices; - - @InjectMocks + @Inject CurrentUserServicesImpl testObject; - private AutoCloseable mockFinalizer; - - @BeforeAll - public void before() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @AfterAll - public void after() throws Exception { - mockFinalizer.close(); + @BeforeEach + void createRolesAndPermissions() { + createAndAssignRoles(); } @Test void testFindOrSaveUserForNewUser() { - MemberProfile expected = mkMemberProfile(); - expected.setWorkEmail("test.email"); - - when(memberProfileRepo.findByWorkEmail(expected.getWorkEmail())).thenReturn(java.util.Optional.of(expected)); + MemberProfile expected = createADefaultMemberProfile(); MemberProfile actual = testObject.findOrSaveUser(expected.getFirstName(), expected.getLastName(), expected.getWorkEmail()); @@ -67,18 +40,11 @@ void testFindOrSaveUserForNewUser() { @Test void testFindOrSaveUserForExistingUser() { - MemberProfile expected = mkMemberProfile(); - expected.setId(UUID.randomUUID()); - expected.setWorkEmail("test.email"); - Role mockRole = new Role(RoleType.MEMBER.name(), "role description"); - - when(memberProfileRepo.findByWorkEmail(expected.getWorkEmail())).thenReturn(java.util.Optional.empty()); - when(memberProfileRepo.save(any())).thenReturn(expected); - when(roleServices.save(mockRole)).thenReturn(mockRole); + MemberProfile expected = createADefaultMemberProfile(); + assignMemberRole(expected); MemberProfile actual = testObject.findOrSaveUser(expected.getFirstName(), expected.getLastName(), expected.getWorkEmail()); assertEquals(expected, actual); - verify(roleServices, times(1)).save(any(Role.class)); } } From 35f67db4e74734b58855511fcb49eb66cb022392 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 08:47:16 -0600 Subject: [PATCH 36/64] Removed mockito from CurrentUserControllerTest. --- .../CurrentUserControllerTest.java | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserControllerTest.java index 0f8107236b..69737baef6 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserControllerTest.java @@ -14,48 +14,22 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import java.util.HashMap; import java.util.List; import java.util.Map; -import static com.objectcomputing.checkins.services.memberprofile.MemberProfileTestUtil.mkMemberProfile; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.when; -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage class CurrentUserControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture { private static final Map userAttributes = new HashMap<>(); - private static final String firstName = "some.first.name"; - private static final String lastName = "some.last.name"; - private static final String userEmail = "some.email.address"; private static final String imageUrl = "some.picture.url"; - @Mock - CurrentUserServices currentUserServices; - @Inject CurrentUserController currentUserController; - private AutoCloseable mockFinalizer; - - @BeforeAll - void setupMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @AfterAll - void close() throws Exception { - mockFinalizer.close(); - } - @Test void testCurrentUserReturnsUnauthorizedWhenAuthenticationFails() { HttpResponse response = currentUserController.currentUser(null); @@ -64,12 +38,14 @@ void testCurrentUserReturnsUnauthorizedWhenAuthenticationFails() { @Test void testCurrentUserReturnsValidDTO() { + final MemberProfile expected = createADefaultMemberProfile(); Authentication auth = new Authentication() { @NonNull @Override public Map getAttributes() { - userAttributes.put("name", firstName + ' ' + lastName); - userAttributes.put("email", userEmail); + userAttributes.put("name", expected.getFirstName() + ' ' + + expected.getLastName()); + userAttributes.put("email", expected.getWorkEmail()); userAttributes.put("picture", imageUrl); return userAttributes; } @@ -80,21 +56,14 @@ public String getName() { } }; - MemberProfile expected = mkMemberProfile(); - expected.setWorkEmail(userEmail); - expected.setFirstName(firstName); - expected.setLastName(lastName); - - when(currentUserServices.findOrSaveUser(firstName, lastName, userEmail)).thenReturn(expected); - HttpResponse actual = currentUserController.currentUser(auth); assertEquals(HttpStatus.OK, actual.getStatus()); CurrentUserDTO currentUserDTO = actual.body(); assertNotNull(currentUserDTO); - assertEquals(userEmail, currentUserDTO.getMemberProfile().getWorkEmail()); - assertEquals(firstName, currentUserDTO.getFirstName()); - assertEquals(lastName, currentUserDTO.getLastName()); + assertEquals(expected.getWorkEmail(), currentUserDTO.getMemberProfile().getWorkEmail()); + assertEquals(expected.getFirstName(), currentUserDTO.getFirstName()); + assertEquals(expected.getLastName(), currentUserDTO.getLastName()); assertEquals(imageUrl, currentUserDTO.getImageUrl()); assertNotNull(actual.getHeaders().get("location")); } From e1524da6cf26ce65f548293938f41a770db5a807 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 08:55:00 -0600 Subject: [PATCH 37/64] Account for the fact that the pulse email may or may not be sent. --- .../request_notifications/CheckServicesImplTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java index cc34a0daee..a95ec57bc0 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) @@ -59,12 +60,11 @@ void sendScheduledEmails() { // Send emails for today checkServices.sendScheduledEmails(); - // One for the feedback request and one for the pulse email. - assertEquals(2, emailSender.events.size()); + // One for the feedback request and, possibly, one for the pulse email. + assertTrue(emailSender.events.size() > 0); assertEquals(pdlMemberProfile.getWorkEmail(), emailSender.events.get(0).get(2)); assertEquals("Feedback request", emailSender.events.get(0).get(3)); - System.out.println(emailSender.events.get(0)); } } From a6fcadb3155184ed7847ef9e5bbddf2ecd2dbd45 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 10:20:02 -0600 Subject: [PATCH 38/64] Removed mockito from MemberProfileReportServicesImplTest. --- .../MemberProfileReportServicesImplTest.java | 145 ++++++------------ 1 file changed, 47 insertions(+), 98 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/csvreport/MemberProfileReportServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/csvreport/MemberProfileReportServicesImplTest.java index 244d78cf4e..f8310b8fcb 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/csvreport/MemberProfileReportServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/csvreport/MemberProfileReportServicesImplTest.java @@ -1,6 +1,9 @@ package com.objectcomputing.checkins.services.memberprofile.csvreport; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; @@ -8,11 +11,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; + +import jakarta.inject.Inject; import java.io.File; import java.io.FileReader; @@ -25,46 +25,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class MemberProfileReportServicesImplTest extends TestContainersSuite { - @Mock - private MemberProfileReportRepository memberProfileReportRepository; +class MemberProfileReportServicesImplTest extends TestContainersSuite + implements MemberProfileFixture { + @Inject + private MemberProfileServices memberProfileServices; - @Mock - private MemberProfileFileProvider memberProfileFileProvider; - - @InjectMocks + @Inject private MemberProfileReportServicesImpl memberProfileReportServices; - private AutoCloseable mockFinalizer; - - @BeforeAll - void initMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } - - @BeforeEach - void resetMocks() { - Mockito.reset(memberProfileReportRepository); - } - - @AfterAll - void close() throws Exception { - mockFinalizer.close(); - } - @Test void testGenerateFileWithAllMemberProfiles() throws IOException { List expectedRecords = createSampleRecords(); - when(memberProfileReportRepository.findAll()).thenReturn(expectedRecords); File tmpFile = File.createTempFile("member",".csv"); tmpFile.deleteOnExit(); - when(memberProfileFileProvider.provideFile()).thenReturn(tmpFile); // Generate a file with all members File file = memberProfileReportServices.generateFile(null); @@ -85,12 +59,9 @@ void testGenerateFileWithAllMemberProfiles() throws IOException { void testGenerateFileWithSelectedMemberProfiles() throws IOException { List allRecords = createSampleRecords(); MemberProfileRecord expectedRecord = allRecords.get(1); - when(memberProfileReportRepository - .findAllByMemberIds(List.of(expectedRecord.getId().toString()))) - .thenReturn(List.of(expectedRecord)); File tmpFile = File.createTempFile("member",".csv"); tmpFile.deleteOnExit(); - when(memberProfileFileProvider.provideFile()).thenReturn(tmpFile); + // Generate a file with selected members MemberProfileReportQueryDTO dto = new MemberProfileReportQueryDTO(); dto.setMemberIds(List.of(expectedRecord.getId())); @@ -104,26 +75,6 @@ void testGenerateFileWithSelectedMemberProfiles() throws IOException { assertRecordEquals(expectedRecord, csvRecord1); } - @Test - void testGenerateFileNotGenerated() throws IOException { - List allRecords = createSampleRecords(); - MemberProfileRecord expectedRecord = allRecords.get(1); - when(memberProfileReportRepository - .findAllByMemberIds(List.of(expectedRecord.getId().toString()))) - .thenReturn(List.of(expectedRecord)); - - when(memberProfileFileProvider.provideFile()).thenThrow(new RuntimeException()); - // Generate a file with selected members - MemberProfileReportQueryDTO dto = new MemberProfileReportQueryDTO(); - dto.setMemberIds(List.of(expectedRecord.getId())); - - assertThrows(RuntimeException.class, () -> { - memberProfileReportServices.generateFile(dto); - }); - } - - - private static void assertRecordEquals(MemberProfileRecord record, CSVRecord csvRecord) { assertEquals(record.getFirstName(), csvRecord.get("First Name")); assertEquals(record.getLastName(), csvRecord.get("Last Name")); @@ -146,45 +97,43 @@ static List parseRecordsFromFile(File file) throws IOException { return parser.getRecords(); } - private static List createSampleRecords() { + MemberProfileRecord from(MemberProfile profile) { MemberProfileRecord record1 = new MemberProfileRecord(); - record1.setId(UUID.randomUUID()); - record1.setFirstName("John"); - record1.setLastName("Doe"); - record1.setTitle("Software Engineer"); - record1.setLocation("St. Louis"); - record1.setWorkEmail("johndoe@objectcomputing.com"); - record1.setStartDate(LocalDate.of(2024, 1, 1)); - record1.setPdlName("Jane Miller"); - record1.setPdlEmail("janemiller@objectcomputing.com"); - record1.setSupervisorName("Tom Smith"); - record1.setSupervisorEmail("tomsmith@objectcomputing.com"); - - MemberProfileRecord record2 = new MemberProfileRecord(); - record2.setId(UUID.randomUUID()); - record2.setFirstName("Jane"); - record2.setLastName("Miller"); - record2.setTitle("Principal Software Engineer"); - record2.setLocation("St. Louis"); - record2.setWorkEmail("janemiller@objectcomputing.com"); - record2.setStartDate(LocalDate.of(2023, 1, 1)); - record2.setPdlName("Eve Williams"); - record2.setPdlEmail("evewilliams@objectcomputing.com"); - record2.setSupervisorName("Tom Smith"); - record2.setSupervisorEmail("tomsmith@objectcomputing.com"); - - MemberProfileRecord record3 = new MemberProfileRecord(); - record3.setId(UUID.randomUUID()); - record3.setFirstName("Tom"); - record3.setLastName("Smith"); - record3.setTitle("Manager, HR, and Head of Sales"); - record3.setLocation("New York City, New York"); - record3.setWorkEmail("tomsmith@objectcomputing.com"); - record3.setStartDate(LocalDate.of(2022, 1, 1)); - record3.setPdlName(null); - record3.setPdlEmail(null); - record3.setSupervisorName(null); - record3.setSupervisorEmail(null); + record1.setId(profile.getId()); + record1.setFirstName(profile.getFirstName()); + record1.setLastName(profile.getLastName()); + record1.setTitle(profile.getTitle()); + record1.setLocation(profile.getLocation()); + record1.setWorkEmail(profile.getWorkEmail()); + record1.setStartDate(profile.getStartDate()); + UUID pdlId = profile.getPdlId(); + if (pdlId != null) { + MemberProfile pdl = memberProfileServices.getById(pdlId); + if (pdl != null) { + record1.setPdlName(pdl.getFirstName() + " " + pdl.getLastName()); + record1.setPdlEmail(pdl.getWorkEmail()); + } + } + UUID supervisorId = profile.getSupervisorid(); + if (supervisorId != null) { + MemberProfile supervisor = + memberProfileServices.getById(supervisorId); + if (supervisor != null) { + record1.setSupervisorName(supervisor.getFirstName() + " " + + supervisor.getLastName()); + record1.setSupervisorEmail(supervisor.getWorkEmail()); + } + } + return record1; + } + + private List createSampleRecords() { + // The createADefaultMemberProfileForPdl() method actually sets both + // the PDL and Supervisor to the id of the member profile passed in. + MemberProfile pdl = createADefaultMemberProfile(); + MemberProfileRecord record1 = from(pdl); + MemberProfileRecord record2 = from(createADefaultMemberProfileForPdl(pdl)); + MemberProfileRecord record3 = from(createAThirdDefaultMemberProfile()); return List.of(record1, record2, record3); } From 78e324d92d870fbbd4ea142332653229f91210ee Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 13:25:31 -0600 Subject: [PATCH 39/64] Removed mockito from the GuildTest. --- .../checkins/services/guild/GuildTest.java | 318 ++++++++---------- 1 file changed, 133 insertions(+), 185 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/guild/GuildTest.java b/server/src/test/java/com/objectcomputing/checkins/services/guild/GuildTest.java index 4a0e27b4b0..deae25f885 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/guild/GuildTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/guild/GuildTest.java @@ -5,9 +5,13 @@ import com.objectcomputing.checkins.exceptions.PermissionException; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; +import com.objectcomputing.checkins.services.role.RoleType; +import com.objectcomputing.checkins.services.CurrentUserServicesReplacement; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.guild.member.GuildMember; import com.objectcomputing.checkins.services.guild.member.GuildMemberHistoryRepository; +import com.objectcomputing.checkins.services.guild.member.GuildMemberResponseDTO; import com.objectcomputing.checkins.services.guild.member.GuildMemberRepository; import com.objectcomputing.checkins.services.guild.member.GuildMemberServices; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; @@ -23,14 +27,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mockito; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.ArrayList; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -39,22 +42,18 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage -class GuildTest extends TestContainersSuite { +@Property(name = "replace.currentuserservices", value = StringUtils.TRUE) +class GuildTest extends TestContainersSuite + implements MemberProfileFixture { @Inject private Validator validator; + @Inject + private CurrentUserServicesReplacement currentUserServices; + @Inject private CheckInsConfiguration checkInsConfiguration; @@ -62,39 +61,12 @@ class GuildTest extends TestContainersSuite { @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; - private GuildRepository guildsRepo; - private GuildMemberRepository guildMemberRepo; - private GuildMemberHistoryRepository guildMemberHistoryRepo; - private CurrentUserServices currentUserServices; - private MemberProfileServices memberProfileServices; - private GuildMemberServices guildMemberServices; - private Environment environment; + @Inject private GuildServicesImpl guildServices; @BeforeEach - @Tag("mocked") void setUp() { - guildsRepo = Mockito.mock(GuildRepository.class); - guildMemberRepo = Mockito.mock(GuildMemberRepository.class); - guildMemberHistoryRepo = Mockito.mock(GuildMemberHistoryRepository.class); - currentUserServices = Mockito.mock(CurrentUserServices.class); - memberProfileServices = Mockito.mock(MemberProfileServices.class); - guildMemberServices = Mockito.mock(GuildMemberServices.class); - environment = Mockito.mock(Environment.class); - emailSender.reset(); - - guildServices = Mockito.spy(new GuildServicesImpl( - guildsRepo, - guildMemberRepo, - guildMemberHistoryRepo, - currentUserServices, - memberProfileServices, - guildMemberServices, - emailSender, - environment, - checkInsConfiguration) - ); } @Test @@ -182,8 +154,8 @@ void testToString() { assertTrue(s.contains(description)); assertTrue(s.contains(isCommunity)); } + @Test - @Tag("mocked") void testEmailGuildLeadersWithValidGuild() { Set guildLeadersEmails = new HashSet<>(); guildLeadersEmails.add("leader1@example.com"); @@ -204,7 +176,6 @@ void testEmailGuildLeadersWithValidGuild() { } @Test - @Tag("mocked") void testEmailGuildLeadersWithNullGuild() { Set guildLeadersEmails = new HashSet<>(); guildLeadersEmails.add("leader1@example.com"); @@ -215,7 +186,6 @@ void testEmailGuildLeadersWithNullGuild() { } @Test - @Tag("mocked") void testEmailGuildLeadersWithNullGuildName() { Set guildLeadersEmails = new HashSet<>(); guildLeadersEmails.add("leader1@example.com"); @@ -228,7 +198,6 @@ void testEmailGuildLeadersWithNullGuildName() { } @Test - @Tag("mocked") void testEmailGuildLeadersWithEmptyGuildName() { Set guildLeadersEmails = new HashSet<>(); guildLeadersEmails.add("leader1@example.com"); @@ -241,9 +210,7 @@ void testEmailGuildLeadersWithEmptyGuildName() { assertEquals(0, emailSender.events.size()); } - @Test - @Tag("mocked") void testSaveGuildWithValidData() { GuildCreateDTO guildDTO = new GuildCreateDTO(); guildDTO.setName("Test Guild"); @@ -251,48 +218,57 @@ void testSaveGuildWithValidData() { guildDTO.setLink(link); guildDTO.setCommunity(true); - UUID memberId = UUID.randomUUID(); + MemberProfile memberProfile = createADefaultMemberProfile(); GuildCreateDTO.GuildMemberCreateDTO guildMemberCreateDTO = new GuildCreateDTO.GuildMemberCreateDTO(); - guildMemberCreateDTO.setMemberId(memberId); + guildMemberCreateDTO.setMemberId(memberProfile.getId()); guildMemberCreateDTO.setLead(true); guildDTO.setGuildMembers(Collections.singletonList(guildMemberCreateDTO)); - MemberProfile memberProfile = new MemberProfile(); - memberProfile.setWorkEmail("test@example.com"); - - Guild guild = new Guild(UUID.randomUUID(), "test", "example", link, true, true); - when(guildsRepo.search(any(), any())).thenReturn(Collections.emptyList()); - when(guildsRepo.save(any())).thenReturn(guild); - when(memberProfileServices.getById(any())).thenReturn(memberProfile); - when(guildMemberRepo.save(any())).thenReturn(new GuildMember(UUID.randomUUID(), guild.getId(), guildMemberCreateDTO.getMemberId(), true)); GuildResponseDTO response = guildServices.save(guildDTO); - verify(guildsRepo, times(1)).save(any()); - assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "You have been assigned as a guild leader of test", "Congratulations, you have been assigned as a guild leader of test", memberProfile.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + String.format("You have been assigned as a guild leader of %s", guildDTO.getName()), + String.format("Congratulations, you have been assigned as a guild leader of %s", guildDTO.getName()), + memberProfile.getWorkEmail()), emailSender.events.getFirst() ); } + + GuildResponseDTO createGuild(String name, MemberProfile lead) { + GuildCreateDTO guildDTO = new GuildCreateDTO(); + guildDTO.setName(name); + GuildCreateDTO.GuildMemberCreateDTO guildMemberCreateDTO = new GuildCreateDTO.GuildMemberCreateDTO(); + guildMemberCreateDTO.setMemberId(lead.getId()); + guildMemberCreateDTO.setLead(true); + List members = new ArrayList<>(); + members.add(guildMemberCreateDTO); + guildDTO.setGuildMembers(members); + return guildServices.save(guildDTO); + } + @Test - @Tag("mocked") void testSaveGuildWithExistingName() { - GuildCreateDTO guildDTO = new GuildCreateDTO(); - guildDTO.setName("Existing Guild"); + MemberProfile memberProfile = createADefaultMemberProfile(); + createGuild("Existing Guild", memberProfile); + emailSender.reset(); - when(guildsRepo.search(any(), any())).thenReturn(Collections.singletonList(new Guild())); + GuildCreateDTO existingGuildDTO = new GuildCreateDTO(); + existingGuildDTO.setName("Existing Guild"); + GuildCreateDTO.GuildMemberCreateDTO guildMemberCreateDTO = new GuildCreateDTO.GuildMemberCreateDTO(); + guildMemberCreateDTO.setMemberId(memberProfile.getId()); + guildMemberCreateDTO.setLead(true); + existingGuildDTO.setGuildMembers(Collections.singletonList(guildMemberCreateDTO)); - assertThrows(BadArgException.class, () -> guildServices.save(guildDTO)); + assertThrows(BadArgException.class, () -> guildServices.save(existingGuildDTO)); - verify(guildsRepo, never()).save(any()); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSaveGuildWithoutLead() { GuildCreateDTO guildDTO = new GuildCreateDTO(); guildDTO.setName("Test Guild"); @@ -303,86 +279,59 @@ void testSaveGuildWithoutLead() { guildDTO.setGuildMembers(Collections.singletonList(guildMemberCreateDTO)); - when(guildsRepo.search(any(), any())).thenReturn(Collections.emptyList()); - assertThrows(BadArgException.class, () -> guildServices.save(guildDTO)); - - verify(guildsRepo, never()).save(any()); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSaveGuildWithNullDTO() { - GuildResponseDTO response = guildServices.save(null); - - verify(guildsRepo, never()).save(any()); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testUpdateGuildWithNewGuildLeaders() { - UUID guildId = UUID.randomUUID(); + // Create members involved. + MemberProfile currentUser = createADefaultMemberProfile(); + MemberProfile memberProfile1 = createASecondDefaultMemberProfile(); + MemberProfile memberProfile2 = createAThirdDefaultMemberProfile(); + + // Create the existing guild with member1 as lead. + GuildResponseDTO existing = createGuild("Test Guild", memberProfile1); + emailSender.reset(); + + // Create an update DTO. + UUID guildId = existing.getId(); GuildUpdateDTO guildDTO = new GuildUpdateDTO(); + guildDTO.setName(existing.getName()); guildDTO.setId(guildId); guildDTO.setLink("http://example.com"); - UUID currentUserId = UUID.randomUUID(); - UUID memberId1 = UUID.randomUUID(); - UUID memberId2 = UUID.randomUUID(); - + // Set the new guild members + List exMembers = existing.getGuildMembers(); GuildUpdateDTO.GuildMemberUpdateDTO guildMember1 = new GuildUpdateDTO.GuildMemberUpdateDTO(); - guildMember1.setMemberId(memberId1); - guildMember1.setLead(true); + guildMember1.setId(exMembers.get(0).getId()); + guildMember1.setMemberId(exMembers.get(0).getMemberId()); + guildMember1.setLead(exMembers.get(0).getLead()); GuildUpdateDTO.GuildMemberUpdateDTO guildMember2 = new GuildUpdateDTO.GuildMemberUpdateDTO(); - guildMember2.setMemberId(memberId2); + guildMember2.setMemberId(memberProfile2.getId()); guildMember2.setLead(true); guildDTO.setGuildMembers(Arrays.asList(guildMember1, guildMember2)); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(currentUserId); - - MemberProfile memberProfile1 = new MemberProfile(); - memberProfile1.setWorkEmail("leader1@example.com"); - - MemberProfile memberProfile2 = new MemberProfile(); - memberProfile2.setWorkEmail("leader2@example.com"); - - Guild existingGuild = new Guild(); - existingGuild.setId(guildId); - existingGuild.setName("Test Guild"); - - GuildMember existingGuildMember = new GuildMember(); - existingGuildMember.setMemberId(memberId1); - existingGuildMember.setLead(true); - - GuildMember newGuildLeader = new GuildMember(); - newGuildLeader.setMemberId(memberId2); - newGuildLeader.setLead(true); - - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(currentUserServices.isAdmin()).thenReturn(true); - when(guildsRepo.findById(guildId)).thenReturn(Optional.of(existingGuild)); - when(guildsRepo.update(any())).thenReturn(existingGuild); - when(memberProfileServices.getById(memberId1)).thenReturn(memberProfile1); - when(memberProfileServices.getById(memberId2)).thenReturn(memberProfile2); - - Set initialGuildLeaders = Collections.singleton(existingGuildMember); - Set updatedGuildLeaders = new HashSet<>(initialGuildLeaders); - updatedGuildLeaders.add(newGuildLeader); - when(guildMemberServices.findByFields(guildId, null, true)) - .thenReturn(initialGuildLeaders) - .thenReturn(updatedGuildLeaders); + // We need the current user to be logged in and admin. + currentUserServices.currentUser = currentUser; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); GuildResponseDTO response = guildServices.update(guildDTO); - assertEquals(2, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Membership Changes have been made to the Test Guild guild", "

Changes have been made to the Test Guild guild.

The following members have been added:

  • null null
  • null null
Click here to view the changes in the Check-Ins app.", memberProfile1.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + "Membership Changes have been made to the Test Guild guild", + String.format("

Changes have been made to the Test Guild guild.

The following members have been added:

  • %s %s
Click here to view the changes in the Check-Ins app.", memberProfile2.getFirstName(), memberProfile2.getLastName()), + memberProfile1.getWorkEmail()), emailSender.events.get(0) ); assertEquals( @@ -392,69 +341,62 @@ void testUpdateGuildWithNewGuildLeaders() { } @Test - @Tag("mocked") void testUpdateGuildWithNoNewGuildLeaders() { - UUID guildId = UUID.randomUUID(); + // Create members involved. + MemberProfile currentUser = createADefaultMemberProfile(); + MemberProfile memberProfile1 = createASecondDefaultMemberProfile(); + MemberProfile memberProfile2 = createAThirdDefaultMemberProfile(); + + // Create the existing guild with member1 as lead. + GuildResponseDTO existing = createGuild("Test Guild", memberProfile1); + emailSender.reset(); + + // Create an update DTO. + UUID guildId = existing.getId(); GuildUpdateDTO guildDTO = new GuildUpdateDTO(); + guildDTO.setName(existing.getName()); guildDTO.setId(guildId); guildDTO.setLink("http://example.com"); - UUID currentUserId = UUID.randomUUID(); - UUID memberId1 = UUID.randomUUID(); - UUID memberId2 = UUID.randomUUID(); - + // Set the new guild members + List exMembers = existing.getGuildMembers(); GuildUpdateDTO.GuildMemberUpdateDTO guildMember1 = new GuildUpdateDTO.GuildMemberUpdateDTO(); - guildMember1.setMemberId(memberId1); - guildMember1.setLead(true); + guildMember1.setId(exMembers.get(0).getId()); + guildMember1.setMemberId(exMembers.get(0).getMemberId()); + guildMember1.setLead(exMembers.get(0).getLead()); GuildUpdateDTO.GuildMemberUpdateDTO guildMember2 = new GuildUpdateDTO.GuildMemberUpdateDTO(); - guildMember2.setMemberId(memberId2); - guildMember2.setLead(true); + guildMember2.setMemberId(memberProfile2.getId()); + guildMember2.setLead(false); guildDTO.setGuildMembers(Arrays.asList(guildMember1, guildMember2)); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(currentUserId); - - MemberProfile memberProfile1 = new MemberProfile(); - memberProfile1.setWorkEmail("leader1@example.com"); - - MemberProfile memberProfile2 = new MemberProfile(); - memberProfile2.setWorkEmail("leader2@example.com"); - - Guild existingGuild = new Guild(); - existingGuild.setId(guildId); - existingGuild.setName("Test Guild"); - - GuildMember existingGuildMember = new GuildMember(); - existingGuildMember.setMemberId(memberId1); - existingGuildMember.setLead(true); - - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(currentUserServices.isAdmin()).thenReturn(true); - when(guildsRepo.findById(guildId)).thenReturn(Optional.of(existingGuild)); - when(guildsRepo.update(any())).thenReturn(existingGuild); - when(memberProfileServices.getById(memberId1)).thenReturn(memberProfile1); - when(memberProfileServices.getById(memberId2)).thenReturn(memberProfile2); - when(guildMemberServices.findByFields(guildId, null, true)).thenReturn(Collections.singleton(existingGuildMember)); - - guildServices.update(guildDTO); + // We need the current user to be logged in and admin. + currentUserServices.currentUser = currentUser; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); + GuildResponseDTO response = guildServices.update(guildDTO); assertEquals(1, emailSender.events.size()); assertEquals( - List.of("SEND_EMAIL", "null", "null", "Membership Changes have been made to the Test Guild guild", "

Changes have been made to the Test Guild guild.

The following members have been added:

  • null null
  • null null
Click here to view the changes in the Check-Ins app.", memberProfile1.getWorkEmail()), + List.of("SEND_EMAIL", "null", "null", + "Membership Changes have been made to the Test Guild guild", + String.format("

Changes have been made to the Test Guild guild.

The following members have been added:

  • %s %s
Click here to view the changes in the Check-Ins app.", memberProfile2.getFirstName(), memberProfile2.getLastName()), + memberProfile1.getWorkEmail()), emailSender.events.getFirst() ); } @Test - @Tag("mocked") void testUpdateGuildWithNonExistentGuildId() { GuildUpdateDTO guildDTO = new GuildUpdateDTO(); guildDTO.setId(UUID.randomUUID()); // Non-existent Guild ID - when(currentUserServices.isAdmin()).thenReturn(true); - when(guildsRepo.findById(guildDTO.getId())).thenReturn(Optional.empty()); + // We need the current user to be logged in and admin. + MemberProfile currentUser = createADefaultMemberProfile(); + currentUserServices.currentUser = currentUser; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); assertThrows(BadArgException.class, () -> guildServices.update(guildDTO)); @@ -462,18 +404,21 @@ void testUpdateGuildWithNonExistentGuildId() { } @Test - @Tag("mocked") void testUpdateGuildWithNoGuildLeads() { + // Create the existing guild with member1 as lead. + MemberProfile memberProfile1 = createASecondDefaultMemberProfile(); + GuildResponseDTO existing = createGuild("Test Guild", memberProfile1); + emailSender.reset(); + GuildUpdateDTO guildDTO = new GuildUpdateDTO(); - guildDTO.setId(UUID.randomUUID()); + guildDTO.setId(existing.getId()); guildDTO.setGuildMembers(Collections.emptyList()); // No guild members - Guild existingGuild = new Guild(); - existingGuild.setId(guildDTO.getId()); - existingGuild.setName("Test Guild"); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(guildsRepo.findById(guildDTO.getId())).thenReturn(Optional.of(existingGuild)); + // We need the current user to be logged in and admin. + MemberProfile currentUser = createADefaultMemberProfile(); + currentUserServices.currentUser = currentUser; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); assertThrows(BadArgException.class, () -> guildServices.update(guildDTO)); @@ -481,17 +426,19 @@ void testUpdateGuildWithNoGuildLeads() { } @Test - @Tag("mocked") void testUpdateGuildWithUnauthorizedUser() { - GuildUpdateDTO guildDTO = new GuildUpdateDTO(); - guildDTO.setId(UUID.randomUUID()); + // Create the existing guild with member1 as lead. + MemberProfile memberProfile1 = createASecondDefaultMemberProfile(); + GuildResponseDTO existing = createGuild("Test Guild", memberProfile1); + emailSender.reset(); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(UUID.randomUUID()); + GuildUpdateDTO guildDTO = new GuildUpdateDTO(); + guildDTO.setId(existing.getId()); + guildDTO.setGuildMembers(Collections.emptyList()); // No guild members - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(currentUserServices.isAdmin()).thenReturn(false); - when(guildMemberServices.findByFields(guildDTO.getId(), currentUser.getId(), true)).thenReturn(new HashSet<>()); + // We need the current user to be logged in and *not* admin. + MemberProfile currentUser = createADefaultMemberProfile(); + currentUserServices.currentUser = currentUser; assertThrows(PermissionException.class, () -> { guildServices.update(guildDTO); @@ -501,20 +448,21 @@ void testUpdateGuildWithUnauthorizedUser() { } @Test - @Tag("mocked") void testUpdateGuildWithInvalidLink() { + // Create the existing guild with member1 as lead. + MemberProfile memberProfile1 = createASecondDefaultMemberProfile(); + GuildResponseDTO existing = createGuild("Test Guild", memberProfile1); + emailSender.reset(); + GuildUpdateDTO guildDTO = new GuildUpdateDTO(); - guildDTO.setId(UUID.randomUUID()); + guildDTO.setId(existing.getId()); guildDTO.setLink("invalid-link"); // Invalid link - Guild existingGuild = new Guild(); - existingGuild.setId(guildDTO.getId()); - existingGuild.setName("Test Guild"); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(guildsRepo.findById(guildDTO.getId())).thenReturn(Optional.of(existingGuild)); - - doThrow(new BadArgException("Invalid link")).when(guildServices).validateLink("invalid-link"); + // We need the current user to be logged in and admin. + MemberProfile currentUser = createADefaultMemberProfile(); + currentUserServices.currentUser = currentUser; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); assertThrows(BadArgException.class, () -> guildServices.update(guildDTO)); From aa8e86b56662a21c56078d1fbae62dc6beb3a614 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 2 Jan 2025 14:55:03 -0600 Subject: [PATCH 40/64] Removed mockito from the FeedbackRequestTest. --- .../feedback_request/FeedbackRequestTest.java | 333 +++++------------- 1 file changed, 90 insertions(+), 243 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java index aa4420298f..0f83752c7c 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java @@ -6,6 +6,12 @@ import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.CurrentUserServicesReplacement; +import com.objectcomputing.checkins.services.role.RoleType; +import com.objectcomputing.checkins.services.fixture.FeedbackRequestFixture; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.ReviewPeriodFixture; +import com.objectcomputing.checkins.services.fixture.ReviewAssignmentFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; @@ -19,14 +25,8 @@ import io.micronaut.runtime.server.EmbeddedServer; import jakarta.inject.Inject; import jakarta.inject.Named; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.Mock; -import org.mockito.Mockito; import java.time.LocalDate; import java.util.*; @@ -36,111 +36,61 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage -class FeedbackRequestTest extends TestContainersSuite { - - @Mock - private FeedbackRequestRepository feedbackReqRepository; - - @Mock - private CurrentUserServices currentUserServices; - - @Mock - private MemberProfileServices memberProfileServices; - - @Mock - private ReviewPeriodRepository reviewPeriodRepository; - - @Mock - private ReviewAssignmentRepository reviewAssignmentRepository; +@Property(name = "replace.currentuserservices", value = StringUtils.TRUE) +class FeedbackRequestTest extends TestContainersSuite + implements FeedbackRequestFixture, MemberProfileFixture, ReviewPeriodFixture, ReviewAssignmentFixture { + @Inject + private CurrentUserServicesReplacement currentUserServices; + @Inject private FeedbackRequestServicesImpl feedbackRequestServices; @Inject @Named(MailJetFactory.MJML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; - @Inject - private EmbeddedServer server; - - private AutoCloseable mockFinalizer; - @Inject CheckInsConfiguration checkInsConfiguration; - @BeforeAll - void initMocks() { - mockFinalizer = openMocks(this); - feedbackRequestServices = new FeedbackRequestServicesImpl(feedbackReqRepository, currentUserServices, memberProfileServices, reviewPeriodRepository, reviewAssignmentRepository, emailSender, checkInsConfiguration); - server.getApplicationContext().inject(feedbackRequestServices); - } - @BeforeEach - @Tag("mocked") void setUp() { - Mockito.reset(feedbackReqRepository); - Mockito.reset(currentUserServices); - Mockito.reset(memberProfileServices); - Mockito.reset(reviewPeriodRepository); - Mockito.reset(reviewAssignmentRepository); emailSender.reset(); } - @AfterAll - void cleanupMocks() throws Exception { - mockFinalizer.close(); - } - @Test - @Tag("mocked") void testUpdateFeedbackRequest() { + MemberProfile creator = createADefaultMemberProfile(); + MemberProfile recipient = createASecondDefaultMemberProfile(); + MemberProfile requestee = createAThirdDefaultMemberProfile(); + FeedbackRequest feedbackRequest = + saveFeedbackRequest(creator, requestee, recipient); + UUID feedbackRequestId = UUID.randomUUID(); UUID creatorId = UUID.randomUUID(); UUID recipientId = UUID.randomUUID(); UUID requesteeId = UUID.randomUUID(); - FeedbackRequest feedbackRequest = new FeedbackRequest(); - feedbackRequest.setId(feedbackRequestId); - feedbackRequest.setCreatorId(creatorId); - feedbackRequest.setRecipientId(recipientId); - feedbackRequest.setRequesteeId(requesteeId); - feedbackRequest.setSendDate(LocalDate.now()); - feedbackRequest.setStatus("sent"); + // We need the current user to be logged in and admin. + currentUserServices.currentUser = creator; + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(creatorId); FeedbackRequestUpdateDTO updateDTO = new FeedbackRequestUpdateDTO(); updateDTO.setId(feedbackRequest.getId()); updateDTO.setDueDate(LocalDate.now().plusDays(7)); updateDTO.setStatus("submitted"); updateDTO.setRecipientId(feedbackRequest.getRecipientId()); - when(feedbackReqRepository.findById(feedbackRequest.getId())).thenReturn(Optional.of(feedbackRequest)); - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(currentUserServices.isAdmin()).thenReturn(true); - when(feedbackReqRepository.update(any(FeedbackRequest.class))).thenReturn(feedbackRequest); - when(memberProfileServices.getById(any(UUID.class))).thenReturn(new MemberProfile()); - FeedbackRequest updatedFeedbackRequest = feedbackRequestServices.update(updateDTO); assertNotNull(updatedFeedbackRequest); assertEquals("submitted", updatedFeedbackRequest.getStatus()); - verify(feedbackReqRepository, times(1)).update(any(FeedbackRequest.class)); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testUpdateFeedbackRequest_NotFound() { UUID feedbackRequestId = UUID.randomUUID(); UUID creatorId = UUID.randomUUID(); @@ -158,28 +108,25 @@ void testUpdateFeedbackRequest_NotFound() { FeedbackRequestUpdateDTO updateDTO = new FeedbackRequestUpdateDTO(); updateDTO.setId(feedbackRequest.getId()); - when(feedbackReqRepository.findById(feedbackRequest.getId())).thenReturn(Optional.empty()); - assertThrows(NotFoundException.class, () -> feedbackRequestServices.update(updateDTO)); - verify(feedbackReqRepository, never()).update(any(FeedbackRequest.class)); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testUpdateFeedbackRequest_Unauthorized() { + MemberProfile creator = createADefaultMemberProfile(); + MemberProfile recipient = createASecondDefaultMemberProfile(); + MemberProfile requestee = createAThirdDefaultMemberProfile(); + FeedbackRequest feedbackRequest = + saveFeedbackRequest(creator, requestee, recipient); + UUID feedbackRequestId = UUID.randomUUID(); UUID creatorId = UUID.randomUUID(); UUID recipientId = UUID.randomUUID(); UUID requesteeId = UUID.randomUUID(); - FeedbackRequest feedbackRequest = new FeedbackRequest(); - feedbackRequest.setId(feedbackRequestId); - feedbackRequest.setCreatorId(creatorId); - feedbackRequest.setRecipientId(recipientId); - feedbackRequest.setRequesteeId(requesteeId); - feedbackRequest.setSendDate(LocalDate.now()); - feedbackRequest.setStatus("sent"); + // We need the current user to be logged in and *not* admin. + currentUserServices.currentUser = creator; FeedbackRequestUpdateDTO updateDTO = new FeedbackRequestUpdateDTO(); updateDTO.setId(feedbackRequest.getId()); @@ -187,92 +134,33 @@ void testUpdateFeedbackRequest_Unauthorized() { updateDTO.setStatus("submitted"); updateDTO.setRecipientId(feedbackRequest.getRecipientId()); - MemberProfile requestee = new MemberProfile(); - requestee.setId(requesteeId); - - MemberProfile currentMemberProfile = new MemberProfile(); - currentMemberProfile.setId(UUID.randomUUID()); - when(feedbackReqRepository.findById(feedbackRequest.getId())).thenReturn(Optional.of(feedbackRequest)); - when(currentUserServices.getCurrentUser()).thenReturn(currentMemberProfile); - when(currentUserServices.isAdmin()).thenReturn(false); - when(memberProfileServices.getById(requesteeId)).thenReturn(requestee); - assertThrows(PermissionException.class, () -> feedbackRequestServices.update(updateDTO)); - verify(feedbackReqRepository, never()).update(any(FeedbackRequest.class)); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSendSelfReviewCompletionEmailToReviewers() { - UUID reviewAssignmentId; - ReviewAssignment reviewAssignment; - UUID creatorId = UUID.randomUUID(); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(creatorId); - - MemberProfile pdlProfile = new MemberProfile(); - pdlProfile.setId(UUID.randomUUID()); - pdlProfile.setFirstName("PDL"); - pdlProfile.setLastName("Profile"); - pdlProfile.setWorkEmail("pdl@example.com"); - - MemberProfile supervisorProfile = new MemberProfile(); - supervisorProfile.setId(UUID.randomUUID()); - supervisorProfile.setFirstName("Supervisor"); - supervisorProfile.setLastName("Profile"); - supervisorProfile.setWorkEmail("supervisor@example.com"); - - currentUser.setPdlId(pdlProfile.getId()); - currentUser.setSupervisorid(supervisorProfile.getId()); - - MemberProfile reviewer01 = new MemberProfile(); - reviewer01.setId(UUID.randomUUID()); - reviewer01.setFirstName("Reviewer01"); - reviewer01.setLastName("Profile"); - reviewer01.setWorkEmail("reviewer01@example.com"); - - MemberProfile reviewer02 = new MemberProfile(); - reviewer02.setId(UUID.randomUUID()); - reviewer02.setFirstName("Reviewer02"); - reviewer02.setLastName("Profile"); - reviewer02.setWorkEmail("reviewer02@example.com"); - - ReviewPeriod reviewPeriod = new ReviewPeriod(); - reviewPeriod.setName("Self-Review Test"); - - String firstName = "firstName"; - String lastName = "lastName"; - - currentUser.setFirstName(firstName); - currentUser.setLastName(lastName); - - UUID reviewPeriodId = UUID.randomUUID(); - FeedbackRequest feedbackRequest = new FeedbackRequest(); - feedbackRequest.setReviewPeriodId(reviewPeriodId); + MemberProfile pdlProfile = createASecondDefaultMemberProfile(); + MemberProfile supervisorProfile = createAThirdDefaultMemberProfile(); + MemberProfile currentUser = + createAProfileWithSupervisorAndPDL(supervisorProfile, pdlProfile); + + MemberProfile reviewer01 = createADefaultMemberProfile(); + MemberProfile reviewer02 = createAnUnrelatedUser(); + + ReviewPeriod reviewPeriod = createADefaultReviewPeriod(); - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(memberProfileServices.getById(pdlProfile.getId())).thenReturn(pdlProfile); - when(memberProfileServices.getById(supervisorProfile.getId())).thenReturn(supervisorProfile); - when(memberProfileServices.getById(reviewer01.getId())).thenReturn(reviewer01); - when(memberProfileServices.getById(reviewer02.getId())).thenReturn(reviewer02); - when(reviewPeriodRepository.findById(reviewPeriodId)).thenReturn(Optional.of(reviewPeriod)); + FeedbackRequest feedbackRequest = + saveFeedbackRequest(supervisorProfile, currentUser, + pdlProfile, reviewPeriod); + + currentUserServices.currentUser = currentUser; Set reviewAssignmentsSet = new HashSet(); - reviewAssignmentId = UUID.randomUUID(); - reviewAssignment= new ReviewAssignment(); - reviewAssignment.setId(reviewAssignmentId); - reviewAssignment.setReviewPeriodId(reviewPeriodId); - reviewAssignment.setReviewerId(reviewer01.getId()); - reviewAssignment.setRevieweeId(currentUser.getId()); - reviewAssignmentsSet.add(reviewAssignment); - reviewAssignmentId = UUID.randomUUID(); - reviewAssignment= new ReviewAssignment(); - reviewAssignment.setId(reviewAssignmentId); - reviewAssignment.setReviewPeriodId(reviewPeriodId); - reviewAssignment.setReviewerId(reviewer02.getId()); - reviewAssignment.setRevieweeId(currentUser.getId()); - reviewAssignmentsSet.add(reviewAssignment); + reviewAssignmentsSet.add( + createAReviewAssignmentBetweenMembers(currentUser, reviewer01, reviewPeriod, true)); + reviewAssignmentsSet.add( + createAReviewAssignmentBetweenMembers(currentUser, reviewer02, reviewPeriod, true)); feedbackRequestServices.sendSelfReviewCompletionEmailToReviewers(feedbackRequest, reviewAssignmentsSet); @@ -280,119 +168,78 @@ void testSendSelfReviewCompletionEmailToReviewers() { // The order in which emails are sent is random. We will not be // checking the recipient. assertEquals(2, emailSender.events.size()); - EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", null, emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + String.format("%s %s has finished their self-review for %s.", currentUser.getFirstName(), currentUser.getLastName(), reviewPeriod.getName()), + String.format("%s %s has completed their self-review", currentUser.getFirstName(), currentUser.getLastName()), + null, emailSender.events.getFirst()); } @Test - @Tag("mocked") void testSendSelfReviewCompletionEmailToSupervisor() { - UUID creatorId = UUID.randomUUID(); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(creatorId); - - MemberProfile pdlProfile = new MemberProfile(); - pdlProfile.setId(UUID.randomUUID()); - pdlProfile.setFirstName("PDL"); - pdlProfile.setLastName("Profile"); - pdlProfile.setWorkEmail("pdl@example.com"); + MemberProfile pdlProfile = createASecondDefaultMemberProfile(); + MemberProfile supervisorProfile = createAThirdDefaultMemberProfile(); + MemberProfile currentUser = + createAProfileWithSupervisorAndPDL(supervisorProfile, pdlProfile); - MemberProfile supervisorProfile = new MemberProfile(); - supervisorProfile.setId(UUID.randomUUID()); - supervisorProfile.setFirstName("Supervisor"); - supervisorProfile.setLastName("Profile"); - supervisorProfile.setWorkEmail("supervisor@example.com"); + ReviewPeriod reviewPeriod = createADefaultReviewPeriod(); - currentUser.setPdlId(pdlProfile.getId()); - currentUser.setSupervisorid(supervisorProfile.getId()); + FeedbackRequest feedbackRequest = + saveFeedbackRequest(supervisorProfile, currentUser, + pdlProfile, reviewPeriod); - ReviewPeriod reviewPeriod = new ReviewPeriod(); - reviewPeriod.setName("Self-Review Test"); - - String firstName = "firstName"; - String lastName = "lastName"; - - currentUser.setFirstName(firstName); - currentUser.setLastName(lastName); - - UUID reviewPeriodId = UUID.randomUUID(); - FeedbackRequest feedbackRequest = new FeedbackRequest(); - feedbackRequest.setReviewPeriodId(reviewPeriodId); - - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(memberProfileServices.getById(pdlProfile.getId())).thenReturn(pdlProfile); - when(memberProfileServices.getById(supervisorProfile.getId())).thenReturn(supervisorProfile); - when(reviewPeriodRepository.findById(reviewPeriodId)).thenReturn(Optional.of(reviewPeriod)); + currentUserServices.currentUser = currentUser; feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(feedbackRequest); assertEquals(1, emailSender.events.size()); - EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", supervisorProfile.getWorkEmail(), emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + String.format("%s %s has finished their self-review for %s.", currentUser.getFirstName(), currentUser.getLastName(), reviewPeriod.getName()), + String.format("%s %s has completed their self-review", currentUser.getFirstName(), currentUser.getLastName()), + supervisorProfile.getWorkEmail(), + emailSender.events.getFirst()); } @Test - @Tag("mocked") void testSendSelfReviewCompletionEmailToSupervisor_MissingSupervisor() { - UUID creatorId = UUID.randomUUID(); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(creatorId); - - MemberProfile pdlProfile = new MemberProfile(); - pdlProfile.setId(UUID.randomUUID()); - pdlProfile.setFirstName("PDL"); - pdlProfile.setLastName("Profile"); - pdlProfile.setWorkEmail("pdl@example.com"); + MemberProfile pdlProfile = createASecondDefaultMemberProfile(); + MemberProfile otherProfile = createAThirdDefaultMemberProfile(); + MemberProfile currentUser = createADefaultMemberProfile(); - currentUser.setPdlId(pdlProfile.getId()); + ReviewPeriod reviewPeriod = createADefaultReviewPeriod(); - String firstName = "firstName"; - String lastName = "lastName"; + FeedbackRequest feedbackRequest = + saveFeedbackRequest(otherProfile, currentUser, + pdlProfile, reviewPeriod); - currentUser.setFirstName(firstName); - currentUser.setLastName(lastName); + currentUserServices.currentUser = currentUser; - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(memberProfileServices.getById(pdlProfile.getId())).thenReturn(pdlProfile); - - feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(new FeedbackRequest()); + feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(feedbackRequest); assertEquals(0, emailSender.events.size()); } @Test - @Tag("mocked") void testSendSelfReviewCompletionEmailToSupervisor_EmailSenderException() { - UUID creatorId = UUID.randomUUID(); - MemberProfile currentUser = new MemberProfile(); - currentUser.setId(creatorId); - - MemberProfile pdlProfile = new MemberProfile(); - pdlProfile.setId(UUID.randomUUID()); - pdlProfile.setFirstName("PDL"); - pdlProfile.setLastName("Profile"); - pdlProfile.setWorkEmail("pdl@example.com"); - - MemberProfile supervisorProfile = new MemberProfile(); - supervisorProfile.setId(UUID.randomUUID()); - supervisorProfile.setFirstName("Supervisor"); - supervisorProfile.setLastName("Profile"); - supervisorProfile.setWorkEmail("supervisor@example.com"); - - currentUser.setPdlId(pdlProfile.getId()); - currentUser.setSupervisorid(supervisorProfile.getId()); + MemberProfile pdlProfile = createASecondDefaultMemberProfile(); + MemberProfile supervisorProfile = createAThirdDefaultMemberProfile(); + MemberProfile currentUser = + createAProfileWithSupervisorAndPDL(supervisorProfile, pdlProfile); - String firstName = "firstName"; - String lastName = "lastName"; + ReviewPeriod reviewPeriod = createADefaultReviewPeriod(); - currentUser.setFirstName(firstName); - currentUser.setLastName(lastName); + FeedbackRequest feedbackRequest = + saveFeedbackRequest(supervisorProfile, currentUser, + pdlProfile, reviewPeriod); - when(currentUserServices.getCurrentUser()).thenReturn(currentUser); - when(memberProfileServices.getById(pdlProfile.getId())).thenReturn(pdlProfile); - when(memberProfileServices.getById(supervisorProfile.getId())).thenReturn(supervisorProfile); + currentUserServices.currentUser = currentUser; emailSender.setException(new RuntimeException("Email sending failed")); - assertDoesNotThrow(() -> feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(new FeedbackRequest())); + assertDoesNotThrow(() -> feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(feedbackRequest)); assertEquals(1, emailSender.events.size()); - EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review.", "firstName lastName has completed their self-review", supervisorProfile.getWorkEmail(), emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + String.format("%s %s has finished their self-review for %s.", currentUser.getFirstName(), currentUser.getLastName(), reviewPeriod.getName()), + String.format("%s %s has completed their self-review", currentUser.getFirstName(), currentUser.getLastName()), + supervisorProfile.getWorkEmail(), + emailSender.events.getFirst()); } } From 219d89c641c31250430792158a8e4c0d8652fac8 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 15:58:37 -0600 Subject: [PATCH 41/64] Adjusted the configuration of the slack integration --- .../configuration/CheckInsConfiguration.java | 23 +++++++++++++++++++ .../social_media/SlackPoster.java | 6 ++++- .../social_media/SlackSearch.java | 7 +++++- server/src/main/resources/application.yml | 4 ++++ .../src/test/resources/application-test.yml | 4 ++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java index b1c707a559..6c58600b40 100644 --- a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java +++ b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java @@ -31,6 +31,9 @@ public static class ApplicationConfig { @NotNull private GoogleApiConfig googleApi; + @NotNull + private NotificationsConfig notifications; + @Getter @Setter @ConfigurationProperties("feedback") @@ -66,5 +69,25 @@ public static class ScopeConfig { private String scopeForDirectoryApi; } } + + @Getter + @Setter + @ConfigurationProperties("notifications") + public static class NotificationsConfig { + + @NotNull + private SlackConfig slack; + + @Getter + @Setter + @ConfigurationProperties("slack") + public static class SlackConfig { + @NotBlank + private String webhookUrl; + + @NotBlank + private String botToken; + } + } } } diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java index 0d561b2f0f..be08f13e14 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.notifications.social_media; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; @@ -16,9 +17,12 @@ public class SlackPoster { @Inject private HttpClient slackClient; + @Inject + private CheckInsConfiguration configuration; + public HttpResponse post(String slackBlock) { // See if we can have a webhook URL. - String slackWebHook = System.getenv("SLACK_WEBHOOK_URL"); + String slackWebHook = configuration.getApplication().getNotifications().getSlack().getWebhookUrl(); if (slackWebHook != null) { // POST it to Slack. BlockingHttpClient client = slackClient.toBlocking(); diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index 58ae30c75a..ea1494a8e9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.notifications.social_media; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.slack.api.model.block.LayoutBlock; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; @@ -16,6 +17,7 @@ import java.util.List; import java.io.IOException; +import jnr.ffi.annotations.In; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,8 +26,11 @@ public class SlackSearch { private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class); private static final String env = "SLACK_BOT_TOKEN"; + @Inject + private CheckInsConfiguration configuration; + public String findChannelId(String channelName) { - String token = System.getenv(env); + String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { try { MethodsClient client = Slack.getInstance().methods(token); diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index fd8c2ea7f9..99148dcede 100755 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -97,6 +97,10 @@ check-ins: feedback: max-suggestions: 6 request-subject: "Feedback request" + notifications: + slack: + webhook-url: ${ SLACK_WEBHOOK_URL } + bot-token: ${ SLACK_BOT_TOKEN } web-address: ${ WEB_ADDRESS } --- flyway: diff --git a/server/src/test/resources/application-test.yml b/server/src/test/resources/application-test.yml index 5ffff78794..c6fca9bd81 100644 --- a/server/src/test/resources/application-test.yml +++ b/server/src/test/resources/application-test.yml @@ -41,6 +41,10 @@ check-ins: application: google-api: delegated-user: test@objectcomputing.com + notifications: + slack: + webhook-url: https://bogus.objectcomputing.com/slack + bot-token: BOGUS_TOKEN --- aes: key: BOGUS_TEST_KEY From b48fab595bbec3da9d76b61e9d86ca388fb3cd0d Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 16:02:49 -0600 Subject: [PATCH 42/64] Fixed missed configuration point --- .../checkins/notifications/social_media/SlackSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index ea1494a8e9..8174e98052 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -55,7 +55,7 @@ public String findChannelId(String channelName) { } public String findUserId(String userEmail) { - String token = System.getenv(env); + String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { try { MethodsClient client = Slack.getInstance().methods(token); From 0040daa8e3eb48490604aad776f36a76a21dafaf Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 16:50:07 -0600 Subject: [PATCH 43/64] Injected the KudosConverter and the SlackSearch --- .../notifications/social_media/SlackSearch.java | 6 ++++-- .../checkins/services/kudos/KudosConverter.java | 14 +++++++++----- .../checkins/services/kudos/KudosServicesImpl.java | 10 +++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index 8174e98052..526fdf6a05 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -24,11 +24,13 @@ @Singleton public class SlackSearch { private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class); - private static final String env = "SLACK_BOT_TOKEN"; - @Inject private CheckInsConfiguration configuration; + public SlackSearch(CheckInsConfiguration checkInsConfiguration) { + this.configuration = checkInsConfiguration; + } + public String findChannelId(String channelName) { String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index ee3ff465f5..6742ef692b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -14,11 +14,14 @@ import com.slack.api.model.block.element.RichTextSectionElement; import com.slack.api.util.json.GsonFactory; import com.google.gson.Gson; +import io.micronaut.core.annotation.Introspected; +import jakarta.inject.Singleton; import java.util.UUID; import java.util.List; import java.util.ArrayList; +@Singleton public class KudosConverter { private record InternalBlock( List blocks @@ -26,11 +29,14 @@ private record InternalBlock( private final MemberProfileServices memberProfileServices; private final KudosRecipientServices kudosRecipientServices; + private final SlackSearch slackSearch; public KudosConverter(MemberProfileServices memberProfileServices, - KudosRecipientServices kudosRecipientServices) { + KudosRecipientServices kudosRecipientServices, + SlackSearch slackSearch) { this.memberProfileServices = memberProfileServices; this.kudosRecipientServices = kudosRecipientServices; + this.slackSearch = slackSearch; } public String toSlackBlock(Kudos kudos) { @@ -39,8 +45,7 @@ public String toSlackBlock(Kudos kudos) { // Look up the channel id from Slack String channelName = "kudos"; - SlackSearch search = new SlackSearch(); - String channelId = search.findChannelId(channelName); + String channelId = slackSearch.findChannelId(channelName); if (channelId == null) { content.add( RichTextSectionElement.Text.builder() @@ -94,9 +99,8 @@ private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() { private RichTextElement memberAsRichText(UUID memberId) { // Look up the user id by email address on Slack - SlackSearch search = new SlackSearch(); MemberProfile profile = memberProfileServices.getById(memberId); - String userId = search.findUserId(profile.getWorkEmail()); + String userId = slackSearch.findUserId(profile.getWorkEmail()); if (userId == null) { String name = MemberProfileUtils.getFullName(profile); return RichTextSectionElement.Text.builder() diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 8d429b8aa1..64750cbba0 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -54,6 +54,7 @@ class KudosServicesImpl implements KudosServices { private final RoleServices roleServices; private final MemberProfileServices memberProfileServices; private final SlackPoster slackPoster; + private final KudosConverter converter; private enum NotificationType { creation, approval @@ -69,7 +70,9 @@ private enum NotificationType { MemberProfileServices memberProfileServices, @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, CheckInsConfiguration checkInsConfiguration, - SlackPoster slackPoster) { + SlackPoster slackPoster, + KudosConverter converter + ) { this.kudosRepository = kudosRepository; this.kudosRecipientServices = kudosRecipientServices; this.kudosRecipientRepository = kudosRecipientRepository; @@ -81,6 +84,7 @@ private enum NotificationType { this.emailSender = emailSender; this.checkInsConfiguration = checkInsConfiguration; this.slackPoster = slackPoster; + this.converter = converter; } @Override @@ -376,10 +380,6 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { } private void slackApprovedKudos(Kudos kudos) { - KudosConverter converter = new KudosConverter(memberProfileServices, - kudosRecipientServices); - - String slackBlock = converter.toSlackBlock(kudos); HttpResponse httpResponse = slackPoster.post(converter.toSlackBlock(kudos)); if (httpResponse.status() != HttpStatus.OK) { From ac21685799fd0fb10118b82fba0d9261f0a63389 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 3 Jan 2025 08:28:36 -0600 Subject: [PATCH 44/64] Removed mockito from the CheckinDocumentServiceImplTest. --- .../CheckinDocumentServiceImplTest.java | 210 ++++-------------- 1 file changed, 43 insertions(+), 167 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/checkindocument/CheckinDocumentServiceImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/checkindocument/CheckinDocumentServiceImplTest.java index 595aaadc9d..1068d52b52 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/checkindocument/CheckinDocumentServiceImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/checkindocument/CheckinDocumentServiceImplTest.java @@ -2,135 +2,86 @@ import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.CheckInDocumentFixture; +import com.objectcomputing.checkins.services.fixture.CheckInFixture; import com.objectcomputing.checkins.services.checkins.CheckIn; -import com.objectcomputing.checkins.services.checkins.CheckInRepository; -import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.role.RoleType; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import java.util.Optional; +import jakarta.inject.Inject; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; + import java.util.Set; import java.util.UUID; +import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -// Disabled in nativeTest, as we get an exception from Mockito -// => org.graalvm.nativeimage.MissingReflectionRegistrationError: The program tried to reflectively access the proxy class -// inheriting [org.mockito.plugins.MockMaker] without it being registered for runtime reflection -@DisabledInNativeImage -class CheckinDocumentServiceImplTest extends TestContainersSuite { - - @Mock - private CheckInRepository checkinRepository; - - @Mock - private CheckinDocumentRepository checkinDocumentRepository; - - @Mock - private CurrentUserServices currentUserServices; - - @InjectMocks - private CheckinDocumentServicesImpl services; - private AutoCloseable mockFinalizer; +class CheckinDocumentServiceImplTest extends TestContainersSuite + implements MemberProfileFixture, CheckInFixture, CheckInDocumentFixture { + @Inject + private CheckinDocumentServicesImpl services; - @BeforeAll - void initMocks() { - mockFinalizer = MockitoAnnotations.openMocks(this); - } + private MemberProfile pdl; + private MemberProfile member; + private CheckIn checkIn; @BeforeEach - void resetMocks() { - reset(checkinRepository, checkinDocumentRepository, currentUserServices); - } - - @AfterAll - void close() throws Exception { - mockFinalizer.close(); + void reset() { + pdl = createADefaultMemberProfile(); + member = createADefaultMemberProfileForPdl(pdl); + checkIn = createADefaultCheckIn(member, pdl); } @Test void testRead() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "exampleDocId"); - Set checkinDocumentSet = Set.of( - new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "doc1"), - new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "doc2"), - new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "doc3") + createACustomCheckInDocument(checkIn, "doc1"), + createACustomCheckInDocument(checkIn, "doc2"), + createACustomCheckInDocument(checkIn, "doc3") ); - when(checkinDocumentRepository.findByCheckinsId(cd.getCheckinsId())).thenReturn(checkinDocumentSet); - - assertEquals(checkinDocumentSet, services.read(cd.getCheckinsId())); - - verify(checkinDocumentRepository, times(1)).findByCheckinsId(any(UUID.class)); + assertEquals(checkinDocumentSet, services.read(checkIn.getId())); } @Test void testReadNullId() { assertTrue(services.read(null).isEmpty()); - - verify(checkinDocumentRepository, never()).findByCheckinsId(any(UUID.class)); } @Test void testFindByUploadDocId() { - - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "exampleDocId"); - when(checkinDocumentRepository.findByUploadDocId(any(String.class))).thenReturn(Optional.of(cd)); + CheckinDocument cd = createADefaultCheckInDocument(checkIn); assertEquals(cd, services.getFindByUploadDocId(cd.getUploadDocId())); - verify(checkinDocumentRepository, times(1)).findByUploadDocId(any(String.class)); } @Test void testFindByUploadDocIdWhenRecordDoesNotExist() { - String id = "some.id"; - when(checkinDocumentRepository.findByUploadDocId(any(String.class))).thenReturn(Optional.empty()); BadArgException exception = assertThrows(BadArgException.class, () -> services.getFindByUploadDocId(id)); assertEquals(String.format("CheckinDocument with document id %s does not exist", id), exception.getMessage()); - verify(checkinDocumentRepository, times(1)).findByUploadDocId(any(String.class)); } @Test void testSave() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), "docId"); - CheckIn checkin = new CheckIn(); - - when(checkinRepository.findById(cd.getCheckinsId())).thenReturn(Optional.of(checkin)); - when(checkinDocumentRepository.save(cd)).thenReturn(cd); - + CheckinDocument cd = new CheckinDocument(checkIn.getId(), "doc1"); assertEquals(cd, services.save(cd)); - - verify(checkinRepository, times(1)).findById(any(UUID.class)); - verify(checkinDocumentRepository, times(1)).save(any(CheckinDocument.class)); } @Test void testSaveWithId() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "docId"); + CheckinDocument cd = createADefaultCheckInDocument(checkIn); BadArgException exception = assertThrows(BadArgException.class, () -> services.save(cd)); assertEquals(String.format("Found unexpected CheckinDocument id %s, please try updating instead", cd.getId()), exception.getMessage()); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, never()).findById(any(UUID.class)); } @Test @@ -139,85 +90,51 @@ void testSaveCheckinDocumentNullCheckinsId() { BadArgException exception = assertThrows(BadArgException.class, () -> services.save(cd)); assertEquals(String.format("Invalid CheckinDocument %s", cd), exception.getMessage()); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, never()).findById(any(UUID.class)); } @Test void testSaveCheckinDocumentNullUploadDocId() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), null); + CheckinDocument cd = new CheckinDocument(checkIn.getId(), null); BadArgException exception = assertThrows(BadArgException.class, () -> services.save(cd)); assertEquals(String.format("Invalid CheckinDocument %s", cd), exception.getMessage()); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, never()).findById(any(UUID.class)); } @Test void testSaveNullCheckinDocument() { assertNull(services.save(null)); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, never()).findById(any(UUID.class)); } @Test void testSaveCheckinDocumentNonExistingCheckIn() { CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), "docId"); - when(checkinRepository.findById(cd.getCheckinsId())).thenReturn(Optional.empty()); - BadArgException exception = assertThrows(BadArgException.class, () -> services.save(cd)); assertEquals(String.format("CheckIn %s doesn't exist", cd.getCheckinsId()), exception.getMessage()); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, times(1)).findById(any(UUID.class)); } @Test void testSaveCheckinDocumentExistingUploadDocId() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), "docId"); - - CheckIn checkin = new CheckIn(); - - when(checkinRepository.findById(cd.getCheckinsId())).thenReturn(Optional.of(checkin)); - when(checkinDocumentRepository.findByUploadDocId(cd.getUploadDocId())).thenReturn(Optional.of(cd)); + String docId = "doc1"; + CheckinDocument existing = createACustomCheckInDocument(checkIn, docId); + CheckinDocument cd = new CheckinDocument(checkIn.getId(), docId); BadArgException exception = assertThrows(BadArgException.class, () -> services.save(cd)); assertEquals(String.format("CheckinDocument with document ID %s already exists", cd.getUploadDocId()), exception.getMessage()); - - verify(checkinDocumentRepository, never()).save(any(CheckinDocument.class)); - verify(checkinRepository, times(1)).findById(any(UUID.class)); } @Test void testUpdate() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "docId"); - CheckIn checkin = new CheckIn(); - - when(checkinRepository.findById(eq(cd.getCheckinsId()))).thenReturn(Optional.of(checkin)); - when(checkinDocumentRepository.findById(cd.getId())).thenReturn(Optional.of(cd)); - when(checkinDocumentRepository.update(eq(cd))).thenReturn(cd); - + CheckinDocument cd = createADefaultCheckInDocument(checkIn); assertEquals(cd, services.update(cd)); - - verify(checkinRepository, times(1)).findById(any(UUID.class)); - verify(checkinDocumentRepository, times(1)).findById(cd.getId()); - verify(checkinDocumentRepository, times(1)).update(any(CheckinDocument.class)); } @Test void testUpdateWithoutId() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), "docId"); + CheckinDocument cd = new CheckinDocument(checkIn.getId(), "docId"); BadArgException exception = assertThrows(BadArgException.class, () -> services.update(cd)); assertEquals(String.format("CheckinDocument id %s not found, please try inserting instead", cd.getId()), exception.getMessage()); - - verify(checkinRepository, never()).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, never()).findById(any(UUID.class)); } @Test @@ -226,106 +143,65 @@ void testUpdateCheckinDocumentNullCheckinsId() { BadArgException exception = assertThrows(BadArgException.class, () -> services.update(cd)); assertEquals(String.format("Invalid CheckinDocument %s", cd), exception.getMessage()); - - verify(checkinRepository, never()).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, never()).findById(any(UUID.class)); } @Test void testUpdateCheckinDocumentNullUploadDocId() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), null); + CheckinDocument cd = new CheckinDocument(checkIn.getId(), null); BadArgException exception = assertThrows(BadArgException.class, () -> services.update(cd)); assertEquals(String.format("Invalid CheckinDocument %s", cd), exception.getMessage()); - - verify(checkinRepository, never()).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, never()).findById(any(UUID.class)); } @Test void testUpdateCheckinDocumentDoesNotExist() { CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "docId"); - when(checkinDocumentRepository.findById(cd.getCheckinsId())).thenReturn(Optional.empty()); BadArgException exception = assertThrows(BadArgException.class, () -> services.update(cd)); assertEquals(String.format("CheckinDocument id %s not found, please try inserting instead", cd.getId()), exception.getMessage()); - - verify(checkinRepository, never()).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, times(1)).findById(any(UUID.class)); } @Test void testUpdateCheckInDoesNotExist() { - CheckinDocument cd = new CheckinDocument(UUID.randomUUID(), UUID.randomUUID(), "docId"); - when(checkinDocumentRepository.findById(cd.getId())).thenReturn(Optional.of(cd)); - when(checkinRepository.findById(cd.getCheckinsId())).thenReturn(Optional.empty()); + CheckinDocument existing = createADefaultCheckInDocument(checkIn); + CheckinDocument cd = new CheckinDocument(existing.getId(), + UUID.randomUUID(), "docId"); BadArgException exception = assertThrows(BadArgException.class, () -> services.update(cd)); assertEquals(String.format("CheckIn %s doesn't exist", cd.getCheckinsId()), exception.getMessage()); - - verify(checkinRepository, times(1)).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, times(1)).findById(any(UUID.class)); } @Test void testUpdateNullCheckinDocument() { assertNull(services.update(null)); - - verify(checkinRepository, never()).findById(any(UUID.class)); - verify(checkinDocumentRepository, never()).update(any(CheckinDocument.class)); - verify(checkinDocumentRepository, never()).findById(any(UUID.class)); } @Test void testDeleteByCheckinId() { - when(checkinDocumentRepository.existsByCheckinsId(any(UUID.class))).thenReturn(true); - when(currentUserServices.isAdmin()).thenReturn(true); - - services.deleteByCheckinId(UUID.randomUUID()); - - verify(checkinDocumentRepository, times(1)).deleteByCheckinsId(any(UUID.class)); + CheckinDocument toBeDeleted = createADefaultCheckInDocument(checkIn); + services.deleteByCheckinId(checkIn.getId()); } @Test void testDeleteNonExistingCheckinsId() { UUID uuid = UUID.randomUUID(); - when(checkinDocumentRepository.existsByCheckinsId(any(UUID.class))).thenReturn(false); - when(currentUserServices.isAdmin()).thenReturn(true); - BadArgException exception = assertThrows(BadArgException.class, () -> services.deleteByCheckinId(uuid)); assertEquals(String.format("CheckinDocument with CheckinsId %s does not exist", uuid), exception.getMessage()); - - verify(checkinDocumentRepository, times(0)).deleteByCheckinsId(any(UUID.class)); } @Test void testDeleteByUploadDocId() { - - when(checkinDocumentRepository.existsByUploadDocId(any(String.class))).thenReturn(true); - when(currentUserServices.isAdmin()).thenReturn(true); - - services.deleteByUploadDocId("Test.Upload.Doc.Id"); - - verify(checkinDocumentRepository, times(1)).deleteByUploadDocId(any(String.class)); - verify(checkinDocumentRepository, times(1)).existsByUploadDocId(any(String.class)); + CheckinDocument toBeDeleted = createADefaultCheckInDocument(checkIn); + services.deleteByUploadDocId(toBeDeleted.getUploadDocId()); } @Test void testDeleteNonExistingUploadDocId() { String id = "Test.Id"; - when(checkinDocumentRepository.existsByUploadDocId(any(String.class))).thenReturn(false); - when(currentUserServices.isAdmin()).thenReturn(true); - BadArgException exception = assertThrows(BadArgException.class, () -> services.deleteByUploadDocId(id)); assertEquals(String.format("CheckinDocument with uploadDocId %s does not exist", id), exception.getMessage()); - verify(checkinDocumentRepository, times(0)).deleteByUploadDocId(any(String.class)); - verify(checkinDocumentRepository, times(1)).existsByUploadDocId(any(String.class)); } } From 1c415b70d42e644696480c428cb3fe33b62f3038 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 6 Jan 2025 12:40:55 -0600 Subject: [PATCH 45/64] Factored out the main FileServicesImpl functionality into FileServicesBaseImpl and left the Google specific code in FileServicesImpl. Tests were reworked to use a mock FileServicesImpl that extends FileServicesBaseImpl. --- .../services/file/FileServicesBaseImpl.java | 177 ++++ .../services/file/FileServicesImpl.java | 192 +--- .../services/reports/MarkdownGeneration.java | 2 +- .../services/FileServicesImplReplacement.java | 142 +++ .../services/file/FileControllerTest.java | 133 +-- .../services/file/FileServicesImplTest.java | 984 ++++-------------- .../reports/ReportDataControllerTest.java | 7 +- 7 files changed, 649 insertions(+), 988 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesBaseImpl.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/FileServicesImplReplacement.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesBaseImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesBaseImpl.java new file mode 100644 index 0000000000..28e841c1a4 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesBaseImpl.java @@ -0,0 +1,177 @@ +package com.objectcomputing.checkins.services.file; + +import com.objectcomputing.checkins.services.checkindocument.CheckinDocument; +import com.objectcomputing.checkins.services.checkindocument.CheckinDocumentServices; +import com.objectcomputing.checkins.services.checkins.CheckIn; +import com.objectcomputing.checkins.services.checkins.CheckInServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.multipart.CompletedFileUpload; + +import jakarta.inject.Singleton; +import jakarta.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; + +import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG; + +@Singleton +abstract public class FileServicesBaseImpl implements FileServices { + private static final Logger LOG = LoggerFactory.getLogger(FileServicesBaseImpl.class); + + protected final CheckInServices checkInServices; + protected final CheckinDocumentServices checkinDocumentServices; + protected final MemberProfileServices memberProfileServices; + protected final CurrentUserServices currentUserServices; + + public FileServicesBaseImpl(CheckInServices checkInServices, + CheckinDocumentServices checkinDocumentServices, + MemberProfileServices memberProfileServices, + CurrentUserServices currentUserServices) { + this.checkInServices = checkInServices; + this.checkinDocumentServices = checkinDocumentServices; + this.memberProfileServices = memberProfileServices; + this.currentUserServices = currentUserServices; + } + + abstract protected void getCheckinDocuments( + Set result, Set checkinDocuments) throws IOException; + abstract protected void downloadSingleFile( + String docId, FileOutputStream myWriter) throws IOException; + abstract protected FileInfoDTO uploadSingleFile( + CompletedFileUpload file, String directoryName, + Function consumer) throws IOException; + abstract protected void deleteSingleFile(String docId) throws IOException; + + @Override + public Set findFiles(@Nullable UUID checkInID) { + boolean isAdmin = currentUserServices.isAdmin(); + validate(checkInID == null && !isAdmin, NOT_AUTHORIZED_MSG); + + try { + Set result = new HashSet<>(); + if (checkInID == null && isAdmin) { + getCheckinDocuments(result, Collections.emptySet()); + } else if (checkInID != null) { + validate(!checkInServices.accessGranted(checkInID, currentUserServices.getCurrentUser().getId()), + "You are not authorized to perform this operation"); + + // If there aren't any documents, do not call + // getCheckinDocument. It assumes that an empty set means + // that it should attempt to get all documents. And, in this + // case, we just want an empty result set. + Set checkinDocuments = checkinDocumentServices.read(checkInID); + if (!checkinDocuments.isEmpty()) { + getCheckinDocuments(result, checkinDocuments); + } + } + + return result; + } catch (IOException e) { + LOG.error("Error occurred while retrieving files.", e); + throw new FileRetrievalException(e.getMessage()); + } + } + + @Override + public java.io.File downloadFiles(@NotNull String uploadDocId) { + MemberProfile currentUser = currentUserServices.getCurrentUser(); + boolean isAdmin = currentUserServices.isAdmin(); + + CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId); + validate(cd == null, String.format("Unable to find record with id %s", uploadDocId)); + + CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId()); + + if(!isAdmin) { + validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG); + } + try { + java.io.File file = java.io.File.createTempFile("tmp", ".txt"); + file.deleteOnExit(); + try( + FileOutputStream myWriter = new FileOutputStream(file) + ) { + downloadSingleFile(uploadDocId, myWriter); + return file; + } catch (IOException e) { + LOG.error("Error occurred while retrieving files.", e); + throw new FileRetrievalException(e.getMessage()); + } + } catch(IOException e) { + LOG.error("Error occurred while attempting to create a temporary file.", e); + throw new FileRetrievalException(e.getMessage()); + } + } + + @Override + public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpload file) { + MemberProfile currentUser = currentUserServices.getCurrentUser(); + boolean isAdmin = currentUserServices.isAdmin(); + validate((file.getFilename() == null || file.getFilename().equals("")), "Please select a valid file before uploading."); + + CheckIn checkIn = checkInServices.read(checkInID); + validate(checkIn == null, "Unable to find checkin record with id %s", checkInID); + if(!isAdmin) { + validate((!currentUser.getId().equals(checkIn.getTeamMemberId()) && !currentUser.getId().equals(checkIn.getPdlId())), "You are not authorized to perform this operation"); + validate(checkIn.isCompleted(), NOT_AUTHORIZED_MSG); + } + + // create folder for each team member + final String directoryName = MemberProfileUtils.getFullName(memberProfileServices.getById(checkIn.getTeamMemberId())); + + try { + return uploadSingleFile(file, directoryName, + (fileId) -> { + //create record in checkin-document service + CheckinDocument cd = new CheckinDocument(checkInID, fileId); + checkinDocumentServices.save(cd); + return cd; + }); + } catch (IOException e) { + LOG.error("Unexpected error processing file upload.", e); + throw new FileRetrievalException(e.getMessage()); + } + } + + @Override + public boolean deleteFile(@NotNull String uploadDocId) { + MemberProfile currentUser = currentUserServices.getCurrentUser(); + boolean isAdmin = currentUserServices.isAdmin(); + + CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId); + validate(cd == null, String.format("Unable to find record with id %s", uploadDocId)); + + CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId()); + if(!isAdmin) { + validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG); + } + + try { + deleteSingleFile(uploadDocId); + checkinDocumentServices.deleteByUploadDocId(uploadDocId); + return true; + } catch (IOException e) { + LOG.error("Error occurred while deleting files.", e); + throw new FileRetrievalException(e.getMessage()); + } + } + + protected void validate(boolean isError, String message, Object... args) { + if(isError) { + throw new FileRetrievalException(String.format(message, args)); + } + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java index 505f68d56c..38654da92f 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java @@ -15,11 +15,10 @@ import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; import com.objectcomputing.checkins.util.googleapiaccess.GoogleApiAccess; -import io.micronaut.core.annotation.Nullable; import io.micronaut.http.MediaType; import io.micronaut.http.multipart.CompletedFileUpload; + import jakarta.inject.Singleton; -import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,22 +28,15 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; -import java.util.HashSet; import java.util.Set; -import java.util.UUID; - -import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG; +import java.util.function.Function; @Singleton -public class FileServicesImpl implements FileServices { +public class FileServicesImpl extends FileServicesBaseImpl { private static final Logger LOG = LoggerFactory.getLogger(FileServicesImpl.class); private final GoogleApiAccess googleApiAccess; - private final CheckInServices checkInServices; - private final CheckinDocumentServices checkinDocumentServices; - private final MemberProfileServices memberProfileServices; - private final CurrentUserServices currentUserServices; private final GoogleServiceConfiguration googleServiceConfiguration; public FileServicesImpl(GoogleApiAccess googleApiAccess, @@ -53,118 +45,60 @@ public FileServicesImpl(GoogleApiAccess googleApiAccess, MemberProfileServices memberProfileServices, CurrentUserServices currentUserServices, GoogleServiceConfiguration googleServiceConfiguration) { + super(checkInServices, checkinDocumentServices, memberProfileServices, + currentUserServices); this.googleApiAccess = googleApiAccess; - this.checkInServices = checkInServices; - this.checkinDocumentServices = checkinDocumentServices; - this.memberProfileServices = memberProfileServices; - this.currentUserServices = currentUserServices; this.googleServiceConfiguration = googleServiceConfiguration; } @Override - public Set findFiles(@Nullable UUID checkInID) { - - boolean isAdmin = currentUserServices.isAdmin(); - validate(checkInID == null && !isAdmin, NOT_AUTHORIZED_MSG); + protected void getCheckinDocuments( + Set result, + Set checkinDocuments) throws IOException { + Drive drive = googleApiAccess.getDrive(); + validate(drive == null, "Unable to access Google Drive"); - try { - Set result = new HashSet<>(); - Drive drive = googleApiAccess.getDrive(); - validate(drive == null, "Unable to access Google Drive"); + String rootDirId = googleServiceConfiguration.getDirectoryId(); + validate(rootDirId == null, "No destination folder has been configured. Contact your administrator for assistance."); - String rootDirId = googleServiceConfiguration.getDirectoryId(); - validate(rootDirId == null, "No destination folder has been configured. Contact your administrator for assistance."); - - if (checkInID == null && isAdmin) { - FileList driveIndex = getFoldersInRoot(drive, rootDirId); - driveIndex.getFiles().forEach(folder -> { - try { - //find all - FileList fileList = drive.files().list().setSupportsAllDrives(true) - .setIncludeItemsFromAllDrives(true) - .setQ(String.format("'%s' in parents and mimeType != 'application/vnd.google-apps.folder' and trashed != true", folder.getId())) - .setSpaces("drive") - .setFields("files(id, name, parents, size)") - .execute(); - fileList.getFiles() - .forEach(file -> result.add(setFileInfo(file, null))); - } catch (IOException ioe) { - LOG.error("Error occurred while retrieving files from Google Drive.", ioe); - throw new FileRetrievalException(ioe.getMessage()); - } - }); - } else if (checkInID != null) { - validate(!checkInServices.accessGranted(checkInID, currentUserServices.getCurrentUser().getId()), - "You are not authorized to perform this operation"); - - Set checkinDocuments = checkinDocumentServices.read(checkInID); - for (CheckinDocument cd : checkinDocuments) { - File file = drive.files().get(cd.getUploadDocId()).setSupportsAllDrives(true).execute(); - result.add(setFileInfo(file, cd)); + if (checkinDocuments.isEmpty()) { + FileList driveIndex = getFoldersInRoot(drive, rootDirId); + driveIndex.getFiles().forEach(folder -> { + try { + //find all + FileList fileList = drive.files().list().setSupportsAllDrives(true) + .setIncludeItemsFromAllDrives(true) + .setQ(String.format("'%s' in parents and mimeType != 'application/vnd.google-apps.folder' and trashed != true", folder.getId())) + .setSpaces("drive") + .setFields("files(id, name, parents, size)") + .execute(); + fileList.getFiles() + .forEach(file -> result.add(setFileInfo(file, null))); + } catch (IOException ioe) { + LOG.error("Error occurred while retrieving files from Google Drive.", ioe); + throw new FileRetrievalException(ioe.getMessage()); } + }); + } else { + for (CheckinDocument cd : checkinDocuments) { + File file = drive.files().get(cd.getUploadDocId()).setSupportsAllDrives(true).execute(); + result.add(setFileInfo(file, cd)); } - - return result; - } catch (IOException e) { - LOG.error("Error occurred while retrieving files from Google Drive.", e); - throw new FileRetrievalException(e.getMessage()); } } @Override - public java.io.File downloadFiles(@NotNull String uploadDocId) { - - MemberProfile currentUser = currentUserServices.getCurrentUser(); - boolean isAdmin = currentUserServices.isAdmin(); + protected void downloadSingleFile(String docId, FileOutputStream myWriter) throws IOException { + Drive drive = googleApiAccess.getDrive(); + validate(drive == null, "Unable to access Google Drive"); - CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId); - validate(cd == null, String.format("Unable to find record with id %s", uploadDocId)); - - CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId()); - - if(!isAdmin) { - validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG); - } - try { - java.io.File file = java.io.File.createTempFile("tmp", ".txt"); - file.deleteOnExit(); - try( - FileOutputStream myWriter = new FileOutputStream(file) - ) { - Drive drive = googleApiAccess.getDrive(); - validate(drive == null, "Unable to access Google Drive"); - - drive.files().get(uploadDocId).setSupportsAllDrives(true).executeMediaAndDownloadTo(myWriter); - myWriter.close(); - - return file; - } catch (IOException e) { - LOG.error("Error occurred while retrieving files from Google Drive.", e); - throw new FileRetrievalException(e.getMessage()); - } - } catch(IOException e) { - LOG.error("Error occurred while attempting to create a temporary file.", e); - throw new FileRetrievalException(e.getMessage()); - } + drive.files().get(docId) + .setSupportsAllDrives(true).executeMediaAndDownloadTo(myWriter); + myWriter.close(); } @Override - public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpload file) { - - MemberProfile currentUser = currentUserServices.getCurrentUser(); - boolean isAdmin = currentUserServices.isAdmin(); - validate((file.getFilename() == null || file.getFilename().equals("")), "Please select a valid file before uploading."); - - CheckIn checkIn = checkInServices.read(checkInID); - validate(checkIn == null, "Unable to find checkin record with id %s", checkInID); - if(!isAdmin) { - validate((!currentUser.getId().equals(checkIn.getTeamMemberId()) && !currentUser.getId().equals(checkIn.getPdlId())), "You are not authorized to perform this operation"); - validate(checkIn.isCompleted(), NOT_AUTHORIZED_MSG); - } - - // create folder for each team member - final String directoryName = MemberProfileUtils.getFullName(memberProfileServices.getById(checkIn.getTeamMemberId())); - + protected FileInfoDTO uploadSingleFile(CompletedFileUpload file, String directoryName, Function consumer) throws IOException { try { Drive drive = googleApiAccess.getDrive(); validate(drive == null, "Unable to access Google Drive"); @@ -198,17 +132,12 @@ public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpl .setFields("id, size, name") .execute(); - //create record in checkin-document service - CheckinDocument cd = new CheckinDocument(checkInID, uploadedFile.getId()); - checkinDocumentServices.save(cd); - + CheckinDocument cd = + consumer.apply(uploadedFile.getId()); return setFileInfo(uploadedFile, cd); } catch (GoogleJsonResponseException e) { LOG.error("Error occurred while accessing Google Drive.", e); - throw new FileRetrievalException(e.getMessage()); - } catch (IOException e) { - LOG.error("Unexpected error processing file upload.", e); - throw new FileRetrievalException(e.getMessage()); + throw new IOException(e.getMessage()); } } @@ -317,36 +246,11 @@ private FileList getFoldersInRoot(Drive drive, String rootDirId) throws IOExcept } @Override - public boolean deleteFile(@NotNull String uploadDocId) { - - MemberProfile currentUser = currentUserServices.getCurrentUser(); - boolean isAdmin = currentUserServices.isAdmin(); - - CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId); - validate(cd == null, String.format("Unable to find record with id %s", uploadDocId)); - - CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId()); - if(!isAdmin) { - validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG); - } - - try { - Drive drive = googleApiAccess.getDrive(); - validate(drive == null, "Unable to access Google Drive"); - File file = new File().setTrashed(true); - drive.files().update(uploadDocId, file).setSupportsAllDrives(true).execute(); - checkinDocumentServices.deleteByUploadDocId(uploadDocId); - return true; - } catch (IOException e) { - LOG.error("Error occurred while retrieving files from Google Drive.", e); - throw new FileRetrievalException(e.getMessage()); - } - } - - private void validate(boolean isError, String message, Object... args) { - if(isError) { - throw new FileRetrievalException(String.format(message, args)); - } + protected void deleteSingleFile(String docId) throws IOException { + Drive drive = googleApiAccess.getDrive(); + validate(drive == null, "Unable to access Google Drive"); + File file = new File().setTrashed(true); + drive.files().update(docId, file).setSupportsAllDrives(true).execute(); } private File createNewDirectoryOnDrive(Drive drive, String directoryName, String parentId) throws IOException { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java index a1eea89b78..76315cc82c 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java @@ -82,7 +82,7 @@ public int compare(CompensationHistory.Compensation a, private static final Logger LOG = LoggerFactory.getLogger(MarkdownGeneration.class); private static final String noneAvailable = "None available during the period covered by this review."; - private static final String directory = "merit-reports"; + public static final String directory = "merit-reports"; private final ReportDataServices reportDataServices; private final KudosRepository kudosRepository; diff --git a/server/src/test/java/com/objectcomputing/checkins/services/FileServicesImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/FileServicesImplReplacement.java new file mode 100644 index 0000000000..c51d6036d3 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/FileServicesImplReplacement.java @@ -0,0 +1,142 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.services.file.FileInfoDTO; +import com.objectcomputing.checkins.services.file.FileServicesImpl; +import com.objectcomputing.checkins.services.file.FileServicesBaseImpl; +import com.objectcomputing.checkins.services.checkindocument.CheckinDocument; +import com.objectcomputing.checkins.services.checkindocument.CheckinDocumentServices; +import com.objectcomputing.checkins.services.checkins.CheckInServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; + +import io.micronaut.http.multipart.CompletedFileUpload; + +import java.io.IOException; +import java.io.FileOutputStream; +import java.nio.file.Paths; +import java.util.Set; +import java.util.HashMap; +import java.util.function.Function; + +import jakarta.inject.Singleton; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; + +@Singleton +@Replaces(FileServicesImpl.class) +@Requires(property = "replace.fileservicesimpl", value = StringUtils.TRUE) +public class FileServicesImplReplacement extends FileServicesBaseImpl { + private HashMap map = new HashMap<>(); + + public FileServicesImplReplacement( + CheckInServices checkInServices, + CheckinDocumentServices checkinDocumentServices, + MemberProfileServices memberProfileServices, + CurrentUserServices currentUserServices) { + super(checkInServices, checkinDocumentServices, memberProfileServices, + currentUserServices); + } + + // ******************************************************************* + // Test Interface + // ******************************************************************* + + public boolean shouldThrow = false; + + public void reset() { + shouldThrow = false; + map.clear(); + } + + public FileInfoDTO addFile(String key, byte[] content) { + map.put(key, content); + return setFileInfo(key, null); + } + + public FileInfoDTO addFile(String key, byte[] content, + Function consumer) { + map.put(key, content); + return setFileInfo(key, consumer.apply(key)); + } + + public String getFile(String key) { + return new String(map.get(key)); + } + + // ******************************************************************* + // Overrides for FileServicesBaseImpl + // ******************************************************************* + + @Override + public FileInfoDTO uploadDocument(String directory, + String name, String text) { + return addFile(directory + "/" + name, text.getBytes()); + } + + @Override + protected void getCheckinDocuments( + Set result, Set checkinDocuments) throws IOException { + checkThrow(); + if (checkinDocuments.isEmpty()) { + for(String key : map.keySet()) { + result.add(setFileInfo(key, null)); + } + } else { + for (CheckinDocument cd : checkinDocuments) { + result.add(setFileInfo(cd.getUploadDocId(), cd)); + } + } + } + + @Override + protected void downloadSingleFile( + String docId, FileOutputStream myWriter) throws IOException { + checkThrow(); + if (map.containsKey(docId)) { + myWriter.write(map.get(docId)); + } else { + throw new IOException("File does not exist."); + } + } + + @Override + protected FileInfoDTO uploadSingleFile( + CompletedFileUpload file, String directoryName, + Function consumer) throws IOException { + checkThrow(); + String key = directoryName + "/" + file.getFilename(); + map.put(key, file.getInputStream().readAllBytes()); + return setFileInfo(key, consumer.apply(key)); + } + + @Override + protected void deleteSingleFile(String docId) throws IOException { + checkThrow(); + if (map.containsKey(docId)) { + map.remove(docId); + } else { + throw new IOException("File does not exist."); + } + } + + private FileInfoDTO setFileInfo(String key, CheckinDocument cd) { + FileInfoDTO dto = new FileInfoDTO(); + dto.setFileId(key); + dto.setName(Paths.get(key).getFileName().toString()); + dto.setSize((long)(map.get(key).length)); + if (cd != null) { + dto.setCheckInId(cd.getCheckinsId()); + } + return dto; + } + + private void checkThrow() throws IOException { + if (shouldThrow) { + // This ensures that IOExceptions thrown from these overridden + // methods are caught in FileServicesBaseImpl and converted to + // FileRetrievalException objects. + throw new IOException("Unable to access Google Drive"); + } + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/file/FileControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/file/FileControllerTest.java index 314fa1f6e4..e547735ead 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/file/FileControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/file/FileControllerTest.java @@ -1,6 +1,13 @@ package com.objectcomputing.checkins.services.file; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.FileServicesImplReplacement; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.CheckInFixture; +import com.objectcomputing.checkins.services.fixture.CheckInDocumentFixture; +import com.objectcomputing.checkins.services.checkins.CheckIn; +import com.objectcomputing.checkins.services.checkindocument.CheckinDocument; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import io.micronaut.core.type.Argument; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; @@ -10,12 +17,14 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.client.multipart.MultipartBody; import io.micronaut.http.multipart.CompletedFileUpload; -import io.micronaut.test.annotation.MockBean; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; + import jakarta.inject.Inject; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.io.File; import java.io.FileWriter; @@ -26,22 +35,16 @@ import java.util.UUID; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; +import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito -@DisabledInNativeImage -class FileControllerTest extends TestContainersSuite { + +@Property(name = "replace.fileservicesimpl", value = StringUtils.TRUE) +class FileControllerTest extends TestContainersSuite + implements MemberProfileFixture, CheckInFixture, CheckInDocumentFixture { @Inject @Client("/services/files") @@ -51,7 +54,18 @@ class FileControllerTest extends TestContainersSuite { private final static String filePath = "testFile.txt"; @Inject - private FileServices fileServices; + private FileServicesImplReplacement fileServices; + + private CheckIn checkIn; + private MemberProfile pdl; + private MemberProfile member; + + @BeforeEach + void reset() { + pdl = createADefaultMemberProfile(); + member = createADefaultMemberProfileForPdl(pdl); + checkIn = createADefaultCheckIn(member, pdl); + } @BeforeAll void createTestFile() throws IOException { @@ -68,21 +82,12 @@ void deleteTestFile() { @Test void testFindAll() { + String fileId = "some.id"; + FileInfoDTO testFileInfoDto = fileServices.addFile(fileId, new byte[0]); - UUID testCheckinId = UUID.randomUUID(); - - FileInfoDTO testFileInfoDto = new FileInfoDTO(); - testFileInfoDto.setFileId("some.id"); - testFileInfoDto.setName(testFile.getName()); - testFileInfoDto.setCheckInId(testCheckinId); - testFileInfoDto.setSize(testFile.length()); - Set testResultFromService = new HashSet<>(); - testResultFromService.add(testFileInfoDto); - when(fileServices.findFiles(null)).thenReturn(testResultFromService); - - final HttpRequest request = HttpRequest.GET("").basicAuth("some.email.id", MEMBER_ROLE); + final HttpRequest request = HttpRequest.GET("").basicAuth(member.getWorkEmail(), ADMIN_ROLE); final HttpResponse> response = client.toBlocking().exchange(request, Argument.setOf(FileInfoDTO.class)); assertNotNull(response); @@ -92,26 +97,20 @@ void testFindAll() { assertEquals(testFileInfoDto.getFileId(), result.iterator().next().getFileId()); assertEquals(testFileInfoDto.getCheckInId(), result.iterator().next().getCheckInId()); assertEquals(testFileInfoDto.getName(), result.iterator().next().getName()); - verify(fileServices, times(1)).findFiles(null); + } + + private FileInfoDTO createUploadedDocument(String fileId) { + return fileServices.addFile(fileId, new byte[50], (id) -> { + return createACustomCheckInDocument(checkIn, id); + }); } @Test void testFindByCheckinId() { + String fileId = "some.id"; + FileInfoDTO testFileInfoDto = createUploadedDocument(fileId); - UUID testCheckinId = UUID.randomUUID(); - - FileInfoDTO testFileInfoDto = new FileInfoDTO(); - testFileInfoDto.setFileId("some.id"); - testFileInfoDto.setName(testFile.getName()); - testFileInfoDto.setCheckInId(testCheckinId); - testFileInfoDto.setSize(testFile.length()); - - Set testResultFromService = new HashSet<>(); - testResultFromService.add(testFileInfoDto); - - when(fileServices.findFiles(testCheckinId)).thenReturn(testResultFromService); - - final HttpRequest request = HttpRequest.GET(String.format("?id=%s", testCheckinId)).basicAuth("some.email.id", MEMBER_ROLE); + final HttpRequest request = HttpRequest.GET(String.format("?id=%s", checkIn.getId())).basicAuth(member.getWorkEmail(), MEMBER_ROLE); final HttpResponse> response = client.toBlocking().exchange(request, Argument.setOf(FileInfoDTO.class)); assertNotNull(response); @@ -121,61 +120,44 @@ void testFindByCheckinId() { assertEquals(testFileInfoDto.getFileId(), result.iterator().next().getFileId()); assertEquals(testFileInfoDto.getCheckInId(), result.iterator().next().getCheckInId()); assertEquals(testFileInfoDto.getName(), result.iterator().next().getName()); - verify(fileServices, times(1)).findFiles(testCheckinId); } @Test void testDownloadDocument() { String uploadDocId = "some.upload.id"; - - when(fileServices.downloadFiles(uploadDocId)).thenReturn(testFile); + FileInfoDTO testFileInfoDto = createUploadedDocument(uploadDocId); final HttpRequest request= HttpRequest.GET(String.format("/%s/download", uploadDocId)) - .basicAuth("some.email.id", MEMBER_ROLE); + .basicAuth(member.getWorkEmail(), MEMBER_ROLE); final HttpResponse response = client.toBlocking().exchange(request, File.class); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatus()); - verify(fileServices, times(1)).downloadFiles(uploadDocId); } @Test void testUploadEndpoint() { - - UUID testCheckinId = UUID.randomUUID(); - - FileInfoDTO testFileInfoDto = new FileInfoDTO(); - testFileInfoDto.setFileId("some.id"); - testFileInfoDto.setName(testFile.getName()); - testFileInfoDto.setCheckInId(testCheckinId); - testFileInfoDto.setSize(testFile.length()); - - when(fileServices.uploadFile(any(UUID.class), any(CompletedFileUpload.class))).thenReturn(testFileInfoDto); - - final HttpRequest request = HttpRequest.POST(String.format("/%s", testCheckinId), MultipartBody.builder() + final HttpRequest request = HttpRequest.POST(String.format("/%s", checkIn.getId()), MultipartBody.builder() .addPart("file", testFile).build()) - .basicAuth("some.email.id", MEMBER_ROLE) + .basicAuth(member.getWorkEmail(), MEMBER_ROLE) .contentType(MULTIPART_FORM_DATA); final HttpResponse response = client.toBlocking().exchange(request, FileInfoDTO.class); assertNotNull(response); assertEquals(HttpStatus.CREATED, response.getStatus()); FileInfoDTO result = response.getBody().get(); - assertEquals(testFileInfoDto.getSize(), result.getSize()); - assertEquals(testFileInfoDto.getFileId(), result.getFileId()); - assertEquals(testFileInfoDto.getCheckInId(), result.getCheckInId()); - assertEquals(testFileInfoDto.getName(), result.getName()); - verify(fileServices, times(1)).uploadFile(any(UUID.class), any(CompletedFileUpload.class)); + assertEquals(testFile.length(), result.getSize()); + assertEquals(checkIn.getId(), result.getCheckInId()); + assertEquals(testFile.getName(), result.getName()); } @Test void testUploadEndpointFailsForInvalidFile() { - UUID testCheckinId = UUID.randomUUID(); File badFile = new File(""); - final HttpRequest request = HttpRequest.POST(String.format("/%s", testCheckinId), MultipartBody.builder() + final HttpRequest request = HttpRequest.POST(String.format("/%s", checkIn.getId()), MultipartBody.builder() .addPart("file", badFile).build()) - .basicAuth("some.email", MEMBER_ROLE) + .basicAuth(member.getWorkEmail(), MEMBER_ROLE) .contentType(MULTIPART_FORM_DATA); final IllegalArgumentException responseException = assertThrows(IllegalArgumentException.class, () -> @@ -183,42 +165,31 @@ void testUploadEndpointFailsForInvalidFile() { // Note: exception message is different here depending on your operating system. Better to just check the type. assertTrue(responseException.getMessage().contains("java.io.FileNotFoundException")); - verify(fileServices, times(0)).uploadFile(any(UUID.class), any(CompletedFileUpload.class)); } @Test void testDeleteEndpoint() { - String uploadDocId = "some.upload.id"; - when(fileServices.deleteFile(uploadDocId)).thenReturn(true); + FileInfoDTO testFileInfoDto = createUploadedDocument(uploadDocId); final HttpRequest request = HttpRequest.DELETE(String.format("/%s", uploadDocId)) - .basicAuth("some.email.id", MEMBER_ROLE); + .basicAuth(member.getWorkEmail(), MEMBER_ROLE); final HttpResponse response = client.toBlocking().exchange(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatus()); - verify(fileServices, times(1)).deleteFile(uploadDocId); } @Test void testHandleBadArgs() { - String uploadDocId = "some.upload.id"; - doThrow(FileRetrievalException.class).when(fileServices).deleteFile(uploadDocId); final HttpRequest request = HttpRequest.DELETE(String.format("/%s", uploadDocId)) - .basicAuth("some.email.id", MEMBER_ROLE); + .basicAuth(member.getWorkEmail(), MEMBER_ROLE); final HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(request, Map.class)); assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); - verify(fileServices, times(1)).deleteFile(uploadDocId); - } - - @MockBean(FileServices.class) - public FileServices fileServices() { - return mock(FileServices.class); } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java index cbbfbd97c6..1a6e6e4ed6 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java @@ -1,13 +1,7 @@ package com.objectcomputing.checkins.services.file; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.googleapis.testing.json.GoogleJsonResponseExceptionFactoryTesting; -import com.google.api.client.http.AbstractInputStreamContent; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.testing.json.MockJsonFactory; -import com.google.api.services.drive.Drive; -import com.google.api.services.drive.model.FileList; -import com.google.common.io.ByteStreams; +import com.objectcomputing.checkins.exceptions.BadArgException; +import com.objectcomputing.checkins.exceptions.NotFoundException; import com.objectcomputing.checkins.security.GoogleServiceConfiguration; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.checkindocument.CheckinDocument; @@ -17,25 +11,32 @@ import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; -import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; -import com.objectcomputing.checkins.util.googleapiaccess.GoogleApiAccess; +import com.objectcomputing.checkins.services.CurrentUserServicesReplacement; +import com.objectcomputing.checkins.services.FileServicesImplReplacement; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.CheckInFixture; +import com.objectcomputing.checkins.services.fixture.CheckInDocumentFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.role.RoleType; + import io.micronaut.http.multipart.CompletedFileUpload; +import io.micronaut.http.MediaType; import io.micronaut.security.authentication.Authentication; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.util.StringUtils; + +import jakarta.inject.Inject; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.ByteArrayInputStream; import java.io.OutputStream; import java.time.LocalDate; import java.util.ArrayList; @@ -45,173 +46,115 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.Optional; +import java.nio.ByteBuffer; import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -// Disabled in nativeTest, as we get an exception from Mockito -// => java.lang.NoClassDefFoundError: Could not initialize class org.mockito.internal.configuration.plugins.Plugins -@DisabledInNativeImage -class FileServicesImplTest extends TestContainersSuite { - - private static File testFile; - private final static String filePath = "testFile.txt"; - final static JsonFactory jsonFactory = new MockJsonFactory(); - - @Mock - private Authentication authentication; - - @Mock - private Map mockAttributes; - - @Mock - private Drive drive; - - @Mock - private Drive.Files files; - - @Mock - private Drive.Files.List list; - - @Mock - private Drive.Files.Get get; - - @Mock - private Drive.Files.Delete delete; - - @Mock - private Drive.Files.Create create; - - @Mock - private Drive.Files.Update update; - - @Mock - private CompletedFileUpload fileToUpload; - - @Mock - private MemberProfile testMemberProfile; - - @Mock - private CheckIn testCheckIn; - @Mock - private CheckinDocument testCd; +@Property(name = "replace.fileservicesimpl", value = StringUtils.TRUE) +@Property(name = "replace.currentuserservices", value = StringUtils.TRUE) +class FileServicesImplTest extends TestContainersSuite + implements MemberProfileFixture, CheckInFixture, CheckInDocumentFixture, RoleFixture { + + private class SimpleUploadFile implements CompletedFileUpload { + String filename; + + public SimpleUploadFile(String name) { + filename = name; + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public long getDefinedSize() { + return 50; + } + + @Override + public long getSize() { + return getDefinedSize(); + } + + @Override + public String getFilename() { + return filename; + } + + @Override + public String getName() { + return getFilename(); + } + + @Override + public Optional getContentType() { + return Optional.of(MediaType.of(MediaType.TEXT_PLAIN)); + } + + @Override + public byte[] getBytes() throws IOException { + byte[] bytes = new byte[(int)getSize()]; + return bytes; + } + + @Override + public ByteBuffer getByteBuffer() throws IOException { + return ByteBuffer.wrap(getBytes()); + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(getBytes()); + } + } + + @Inject + private CurrentUserServicesReplacement currentUserServices; + + // The bulk of the functionality for FileServicesImpl has been moved to + // FileServicesBaseImpl, which this replacement extends. The Google + // related portion is the part that will not be tested (it was partially + // tested previously with the use of Mockito). + @Inject + private FileServicesImplReplacement services; + + private CheckIn checkIn; + private MemberProfile pdl; + private MemberProfile member; - @Mock - private InputStream mockInputStream; - - @Mock - private CheckInServices checkInServices; - - @Mock - private CheckinDocumentServices checkinDocumentServices; - - @Mock - private GoogleApiAccess mockGoogleApiAccess; - - @Mock - private CurrentUserServices currentUserServices; - - @Mock - private MemberProfileServices memberProfileServices; - - @Mock - private CompletedFileUpload completedFileUpload; - - @Mock - private GoogleServiceConfiguration googleServiceConfiguration; - - @InjectMocks - private FileServicesImpl services; + @BeforeEach + void reset() { + services.reset(); - private AutoCloseable mockFinalizer; + createAndAssignRoles(); - @BeforeAll - void initMocksAndInitializeFile() throws IOException { - mockFinalizer = openMocks(this); - testFile = new File(filePath); - FileWriter myWriter = new FileWriter(testFile); - myWriter.write("This.Is.A.Test.File"); - myWriter.close(); - } + pdl = createADefaultMemberProfile(); + member = createADefaultMemberProfileForPdl(pdl); - @AfterAll - void deleteTestFile() throws Exception { - testFile.deleteOnExit(); - mockFinalizer.close(); - } + currentUserServices.currentUser = createASecondDefaultMemberProfile(); + currentUserServices.roles = null; - @BeforeEach - void resetMocks() { - reset(authentication); - reset(mockAttributes); - reset(drive); - reset(files); - reset(list); - reset(get); - reset(delete); - reset(create); - reset(update); - reset(fileToUpload); - reset(testMemberProfile); - reset(testCheckIn); - reset(testCd); - reset(mockInputStream); - reset(checkInServices); - reset(checkinDocumentServices); - reset(mockGoogleApiAccess); - reset(currentUserServices); - reset(memberProfileServices); - reset(completedFileUpload); - reset(googleServiceConfiguration); - - when(authentication.getAttributes()).thenReturn(mockAttributes); - when(mockAttributes.get("email")).thenReturn(mockAttributes); - when(mockAttributes.toString()).thenReturn("test.email"); - when(currentUserServices.findOrSaveUser(any(), any(), any())).thenReturn(testMemberProfile); - when(googleServiceConfiguration.getDirectoryId()).thenReturn("testDirectoryId"); + checkIn = createADefaultCheckIn(member, pdl); } @Test - void testFindFilesForFindAll() throws IOException { - - FileList fileList = new FileList(); - List mockFiles = new ArrayList<>(); - com.google.api.services.drive.model.File file1 = new com.google.api.services.drive.model.File(); - file1.setId("some.id"); - mockFiles.add(file1); - fileList.setFiles(mockFiles); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenReturn(fileList); + void testFindFilesForFindAll() { + String fileId = "some.id"; + services.addFile(fileId, new byte[0]); + + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); final Set result = services.findFiles(null); assertNotNull(result); - assertEquals(fileList.getFiles().iterator().next().getId(), result.iterator().next().getFileId()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(0)).read(any(UUID.class)); - verify(checkInServices, times(0)).read(any(UUID.class)); + assertEquals(fileId, result.iterator().next().getFileId()); } @Test @@ -223,752 +166,273 @@ void testFindAllFailsIfNotAdmin() { } @Test - void testFindFilesForFindByCheckinId() throws IOException { - - com.google.api.services.drive.model.File file = new com.google.api.services.drive.model.File(); - file.setId("some.id"); + void testFindFilesForFindByCheckinId() { + String fileId = "some.id"; + services.addFile(fileId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, fileId); - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberProfileId = UUID.randomUUID(); - final Set testCheckinDocument = new HashSet<>(); - testCheckinDocument.add(testCd); - - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.get(any(String.class))).thenReturn(get); - when(get.setSupportsAllDrives(any())).thenReturn(get); - when(get.execute()).thenReturn(file); - when(checkInServices.accessGranted(testCheckinId, testMemberProfileId)).thenReturn(true); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberProfileId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberProfileId); - when(checkinDocumentServices.read(testCheckinId)).thenReturn(testCheckinDocument); - when(testCd.getUploadDocId()).thenReturn("some.upload.doc.id"); - - final Set result = services.findFiles(testCheckinId); + currentUserServices.currentUser = member; + final Set result = services.findFiles(checkIn.getId()); assertNotNull(result); - assertEquals(file.getId(), result.iterator().next().getFileId()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).read(testCheckinId); - verify(checkInServices, times(1)).accessGranted(testCheckinId, testMemberProfileId); + assertEquals(fileId, result.iterator().next().getFileId()); } @Test void testFindByCheckinIdWhenNoDocsAreUploadedForCheckinId() { - - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberProfileId = UUID.randomUUID(); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(checkInServices.accessGranted(testCheckinId, testMemberProfileId)).thenReturn(true); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberProfileId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberProfileId); - when(checkinDocumentServices.read(testCheckinId)).thenReturn(Collections.emptySet()); - - final Set result = services.findFiles(testCheckinId); + currentUserServices.currentUser = member; + final Set result = services.findFiles(checkIn.getId()); assertNotNull(result); assertTrue(result.isEmpty()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).read(any(UUID.class)); - verify(checkInServices, times(1)).accessGranted(testCheckinId, testMemberProfileId); } @Test void testFindByCheckinIdForUnauthorizedUser() { - UUID testCheckinId = UUID.randomUUID(); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(checkInServices.accessGranted(testCheckinId, testMemberProfile.getId())).thenReturn(false); - when(testCheckIn.getTeamMemberId()).thenReturn(UUID.randomUUID()); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(UUID.randomUUID()); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.findFiles(testCheckinId)); + services.findFiles(checkIn.getId())); assertEquals("You are not authorized to perform this operation", responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkInServices, times(1)).accessGranted(testCheckinId, testMemberProfile.getId()); - verify(checkinDocumentServices, times(0)).read(any(UUID.class)); } @Test - void testFindFilesDriveCantConnect() { - when(currentUserServices.isAdmin()).thenReturn(true); - when(mockGoogleApiAccess.getDrive()).thenReturn(null); + void testFindAllFilesThrowsException() { + String fileId = "some.id"; + services.addFile(fileId, new byte[0]); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.findFiles(null)); - - assertEquals("Unable to access Google Drive", responseException.getMessage()); - } + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); - @Test - void testFindAllFilesThrowsGoogleJsonResponseException() throws IOException { - - GoogleJsonResponseException testException = GoogleJsonResponseExceptionFactoryTesting.newMock(jsonFactory, 404, null); - when(currentUserServices.isAdmin()).thenReturn(true); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenThrow(testException); + services.shouldThrow = true; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> services.findFiles(null)); - - assertEquals(testException.getMessage(), responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); } @Test - void testFindByCheckinIdThrowsGoogleJsonResponseException() throws IOException { + void testFindByCheckinIdThrowsException() { + String fileId = "some.id"; + services.addFile(fileId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, fileId); - UUID testCheckinId = UUID.randomUUID(); - final Set testCheckinDocument = new HashSet<>(); - testCheckinDocument.add(testCd); - GoogleJsonResponseException testException = GoogleJsonResponseExceptionFactoryTesting.newMock(jsonFactory, 404, null); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(checkInServices.accessGranted(testCheckinId, testMemberProfile.getId())).thenReturn(true); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(checkinDocumentServices.read(testCheckinId)).thenReturn(testCheckinDocument); - when(testCd.getUploadDocId()).thenReturn("some.upload.doc.id"); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.get(any(String.class))).thenReturn(get); - when(get.setSupportsAllDrives(any())).thenReturn(get); - when(get.execute()).thenThrow(testException); + services.shouldThrow = true; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, - () -> services.findFiles(testCheckinId)); - - assertEquals(testException.getMessage(), responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).read(testCheckinId); - verify(checkInServices, times(1)).accessGranted(testCheckinId, testMemberProfile.getId()); + () -> services.findFiles(checkIn.getId())); } @Test - void testDownloadFiles() throws IOException { - String testUploadDocId = "some.test.id"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - when(checkinDocumentServices.getFindByUploadDocId(testUploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberId); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.get(testUploadDocId)).thenReturn(get); - when(get.setSupportsAllDrives(any())).thenReturn(get); - doAnswer(new Answer() { - public Void answer(InvocationOnMock invocation) throws IOException { - OutputStream outputStream = invocation.getArgument(0); - InputStream inputstream = new FileInputStream(testFile); - ByteStreams.copy(inputstream, outputStream); - return null; - } - }).when(get).executeMediaAndDownloadTo(any(OutputStream.class)); - - final java.io.File resultFile = services.downloadFiles(testUploadDocId); - - assertEquals(testFile.length(), resultFile.length()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkInServices, times(1)).read(any(UUID.class)); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(testUploadDocId); - verify(get, times(1)).executeMediaAndDownloadTo(any(OutputStream.class)); + void testDownloadFiles() { + String uploadDocId = "some.test.id"; + long byteCount = 50; + services.addFile(uploadDocId, new byte[(int)byteCount]); + + currentUserServices.currentUser = member; + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); + + final java.io.File resultFile = services.downloadFiles(uploadDocId); + assertEquals(byteCount, resultFile.length()); } @Test - void testDownloadFilesAdminCanAccess() throws IOException { - String testUploadDocId = "some.test.id"; - UUID testCheckinId = UUID.randomUUID(); + void testDownloadFilesAdminCanAccess() { + String uploadDocId = "some.test.id"; + long byteCount = 50; + services.addFile(uploadDocId, new byte[(int)byteCount]); - when(checkinDocumentServices.getFindByUploadDocId(testUploadDocId)).thenReturn(testCd); - when(currentUserServices.isAdmin()).thenReturn(true); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.get(testUploadDocId)).thenReturn(get); - when(get.setSupportsAllDrives(any())).thenReturn(get); - doAnswer(new Answer() { - public Void answer(InvocationOnMock invocation) throws IOException { - OutputStream outputStream = invocation.getArgument(0); - InputStream inputstream = new FileInputStream(testFile); - ByteStreams.copy(inputstream, outputStream); - return null; - } - }).when(get).executeMediaAndDownloadTo(any(OutputStream.class)); - - final java.io.File resultFile = services.downloadFiles(testUploadDocId); - - assertEquals(testFile.length(), resultFile.length()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkInServices, times(1)).read(any(UUID.class)); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(testUploadDocId); - verify(get, times(1)).executeMediaAndDownloadTo(any(OutputStream.class)); + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); + // The CheckInsServicesImpl checks with the rolePermissionServices. + assignAdminRole(currentUserServices.currentUser); + + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); + final java.io.File resultFile = services.downloadFiles(uploadDocId); + + assertEquals(byteCount, resultFile.length()); } @Test void testDownloadFilesInvalidUploadDocId() { String invalidUploadDocId = "some.test.id"; - when(checkinDocumentServices.getFindByUploadDocId(invalidUploadDocId)).thenReturn(null); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> + // This exception actually comes from the CheckinDocumentServices. + final BadArgException responseException = assertThrows(BadArgException.class, () -> services.downloadFiles(invalidUploadDocId)); - assertEquals(String.format("Unable to find record with id %s", invalidUploadDocId), responseException.getMessage()); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkInServices, times(0)).read(any(UUID.class)); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(invalidUploadDocId); + assertEquals(String.format("CheckinDocument with document id %s does not exist", invalidUploadDocId), responseException.getMessage()); } @Test void testDownloadFilesUnauthorizedUser() { - String testUploadDocId = "some.test.id"; - UUID testCheckinId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(testUploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(UUID.randomUUID()); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(UUID.randomUUID()); + String uploadDocId = "some.test.id"; + services.addFile(uploadDocId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.downloadFiles(testUploadDocId)); + // This exception actually comes from the CheckinDocumentServices. + final BadArgException responseException = assertThrows(BadArgException.class, () -> + services.downloadFiles(uploadDocId)); assertEquals(NOT_AUTHORIZED_MSG, responseException.getMessage()); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(testUploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); } @Test - void testDownloadFilesDriveCantConnect() { - String testUploadDocId = "some.test.id"; - UUID testMemberProfileId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(testUploadDocId)).thenReturn(testCd); - when(checkInServices.read(any())).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberProfileId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberProfileId); - when(mockGoogleApiAccess.getDrive()).thenReturn(null); + void testDownloadFilesThrowsException() { + String uploadDocId = "some.test.id"; + services.addFile(uploadDocId, new byte[0]); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.downloadFiles(testUploadDocId)); + currentUserServices.currentUser = member; + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); - assertEquals("Unable to access Google Drive", responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - } - - @Test - void testDownloadFilesThrowsGoogleJsonResponseException() throws IOException { - - String testUploadDocId = "some.test.id"; - UUID testMemberProfileId = UUID.randomUUID(); - GoogleJsonResponseException testException = GoogleJsonResponseExceptionFactoryTesting.newMock(jsonFactory, 404, null); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(checkinDocumentServices.getFindByUploadDocId(testUploadDocId)).thenReturn(testCd); - when(checkInServices.read(any())).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberProfileId); - when(testMemberProfile.getId()).thenReturn(testMemberProfileId); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.get(testUploadDocId)).thenThrow(testException); + services.shouldThrow = true; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, - () -> services.downloadFiles(testUploadDocId)); - - assertEquals(testException.getMessage(), responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(testUploadDocId); - verify(checkInServices, times(1)).read(any()); + () -> services.downloadFiles(uploadDocId)); } @Test - void testDeleteFile() throws IOException { - + void testDeleteFile() { String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberId); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.update(eq(uploadDocId), any())).thenReturn(update); - when(update.setSupportsAllDrives(any())).thenReturn(update); + services.addFile(uploadDocId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); + currentUserServices.currentUser = member; Boolean result = services.deleteFile(uploadDocId); - assertTrue(result); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkinDocumentServices, times(1)).deleteByUploadDocId(uploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); } @Test - void testDeleteFilesAdminCanAccess() throws IOException { + void testDeleteFilesAdminCanAccess() { String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(currentUserServices.isAdmin()).thenReturn(true); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.update(eq(uploadDocId), any())).thenReturn(update); - when(update.setSupportsAllDrives(any())).thenReturn(update); + services.addFile(uploadDocId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); - Boolean result = services.deleteFile(uploadDocId); + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); + // The CheckInsServicesImpl checks with the rolePermissionServices. + assignAdminRole(currentUserServices.currentUser); + Boolean result = services.deleteFile(uploadDocId); assertTrue(result); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkinDocumentServices, times(1)).deleteByUploadDocId(uploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); } @Test void testDeleteFileWhenCheckinDocDoesntExist() { String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(null); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> + // This exception actually comes from the CheckinDocumentServices. + final BadArgException responseException = assertThrows(BadArgException.class, () -> services.deleteFile(uploadDocId)); - assertEquals(String.format("Unable to find record with id %s", uploadDocId), responseException.getMessage()); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkInServices, times(0)).read(testCheckinId); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkinDocumentServices, times(0)).deleteByUploadDocId(uploadDocId); + assertEquals(String.format("CheckinDocument with document id %s does not exist", uploadDocId), responseException.getMessage()); } @Test - void testDeleteFileByUnauthorizedUser() throws IOException { + void testDeleteFileByUnauthorizedUser() { String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(UUID.randomUUID()); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(UUID.randomUUID()); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.delete(uploadDocId)).thenReturn(delete); + services.addFile(uploadDocId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> + final BadArgException responseException = assertThrows(BadArgException.class, () -> services.deleteFile(uploadDocId)); assertEquals(NOT_AUTHORIZED_MSG, responseException.getMessage()); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkinDocumentServices, times(0)).deleteByUploadDocId(uploadDocId); } @Test - void testDeleteFileDriveCantConnect() { + void testDeleteFileThrowsException() { String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberId); - when(mockGoogleApiAccess.getDrive()).thenReturn(null); + services.addFile(uploadDocId, new byte[0]); + CheckinDocument cd = createACustomCheckInDocument(checkIn, uploadDocId); - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.deleteFile(uploadDocId)); - - assertEquals("Unable to access Google Drive", responseException.getMessage()); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(0)).deleteByUploadDocId(uploadDocId); - } - - @Test - void testDeleteFileThrowsGoogleJsonResponseException() throws IOException { - - String uploadDocId = "Some.Upload.Doc.Id"; - UUID testCheckinId = UUID.randomUUID(); - GoogleJsonResponseException testException = GoogleJsonResponseExceptionFactoryTesting.newMock(jsonFactory, 404, null); - when(currentUserServices.isAdmin()).thenReturn(true); - when(checkinDocumentServices.getFindByUploadDocId(uploadDocId)).thenReturn(testCd); - when(testCd.getCheckinsId()).thenReturn(testCheckinId); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.update(eq(uploadDocId), any())).thenReturn(update); - when(update.setSupportsAllDrives(any())).thenReturn(update); - when(update.execute()).thenThrow(testException); + services.shouldThrow = true; //act + currentUserServices.currentUser = member; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> services.deleteFile(uploadDocId)); - - //assert - assertEquals(testException.getMessage(), responseException.getMessage()); - verify(checkinDocumentServices, times(1)).getFindByUploadDocId(uploadDocId); - verify(checkInServices, times(1)).read(testCheckinId); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(0)).deleteByUploadDocId(uploadDocId); } @Test - void testUploadFilesByCreatingNewDirectory() throws IOException { - - //arrange - String memberName = "testName"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - FileList fileList = new FileList(); - List mockFiles = new ArrayList<>(); - com.google.api.services.drive.model.File fileInService = new com.google.api.services.drive.model.File(); - fileInService.setId("some.id"); - fileInService.setName("some.file.name"); - mockFiles.add(fileInService); - fileList.setFiles(mockFiles); - - com.google.api.services.drive.model.File newFolderCreatedOnDrive = new com.google.api.services.drive.model.File(); - newFolderCreatedOnDrive.setName("new.directory.name"); - - com.google.api.services.drive.model.File fileFromDrive = new com.google.api.services.drive.model.File(); - fileFromDrive.setName("testFile"); - - Drive.Files.Create createForFileUpload = mock(Drive.Files.Create.class); - - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(testCheckIn.isCompleted()).thenReturn(false); - when(memberProfileServices.getById(any(UUID.class))).thenReturn(testMemberProfile); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn(memberName); - when(testMemberProfile.getId()).thenReturn(testMemberId); - - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenReturn(fileList); - - when(files.create(any(com.google.api.services.drive.model.File.class))).thenReturn(create); - when(create.setSupportsAllDrives(any())).thenReturn(create); - when(create.execute()).thenReturn(newFolderCreatedOnDrive); - - when(files.create(any(com.google.api.services.drive.model.File.class), any(AbstractInputStreamContent.class))).thenReturn(createForFileUpload); - when(createForFileUpload.setSupportsAllDrives(true)).thenReturn(createForFileUpload); - when(createForFileUpload.setFields(any(String.class))).thenReturn(createForFileUpload); - when(createForFileUpload.execute()).thenReturn(fileFromDrive); + void testUploadFilesByCreatingNewDirectory() { + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); //act - FileInfoDTO result = services.uploadFile(testCheckinId, fileToUpload); + currentUserServices.currentUser = member; + FileInfoDTO result = services.uploadFile(checkIn.getId(), fileToUpload); //assert assertNotNull(result); assertEquals(fileToUpload.getFilename(), result.getName()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).save(any(CheckinDocument.class)); } @Test - void testUploadFilesToExistingDirectory() throws IOException { - //arrange - String memberName = "testName"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - FileList fileList = new FileList(); - List mockFiles = new ArrayList<>(); - com.google.api.services.drive.model.File fileInService = new com.google.api.services.drive.model.File(); - fileInService.setId("some.id"); - fileInService.setName(memberName.concat(LocalDate.now().toString())); - mockFiles.add(fileInService); - fileList.setFiles(mockFiles); - - com.google.api.services.drive.model.File fileFromDrive = new com.google.api.services.drive.model.File(); - fileFromDrive.setName("testFile"); - - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(testCheckIn.isCompleted()).thenReturn(false); - when(memberProfileServices.getById(any(UUID.class))).thenReturn(testMemberProfile); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn(memberName); - when(testMemberProfile.getId()).thenReturn(testMemberId); - - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(files.create(any(com.google.api.services.drive.model.File.class), any(AbstractInputStreamContent.class))).thenReturn(create); - when(files.create(any(com.google.api.services.drive.model.File.class))).thenReturn(create); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenReturn(fileList); - when(create.setSupportsAllDrives(true)).thenReturn(create); - when(create.setFields(any(String.class))).thenReturn(create); - when(create.execute()).thenReturn(fileFromDrive); - - //act - FileInfoDTO result = services.uploadFile(testCheckinId, fileToUpload); + void testFileUploadForAdminByUploadingFileToExistingDirectory() { + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); - //assert - assertNotNull(result); - assertEquals(fileToUpload.getFilename(), result.getName()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).save(any(CheckinDocument.class)); - } - - @Test - void testFileUploadForAdminByUploadingFileToExistingDirectory() throws IOException { - //arrange - String memberName = "testName"; - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - FileList fileList = new FileList(); - List mockFiles = new ArrayList<>(); - com.google.api.services.drive.model.File fileInService = new com.google.api.services.drive.model.File(); - fileInService.setId("some.id"); - fileInService.setName(memberName.concat(LocalDate.now().toString())); - mockFiles.add(fileInService); - fileList.setFiles(mockFiles); - - com.google.api.services.drive.model.File fileFromDrive = new com.google.api.services.drive.model.File(); - fileFromDrive.setName("testFile"); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(memberProfileServices.getById(any(UUID.class))).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn(memberName); - - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(files.create(any(com.google.api.services.drive.model.File.class), any(AbstractInputStreamContent.class))).thenReturn(create); - when(files.create(any(com.google.api.services.drive.model.File.class))).thenReturn(create); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenReturn(fileList); - when(create.setSupportsAllDrives(true)).thenReturn(create); - when(create.setFields(any(String.class))).thenReturn(create); - when(create.execute()).thenReturn(fileFromDrive); + currentUserServices.roles = new ArrayList(); + currentUserServices.roles.add(RoleType.ADMIN); + // The CheckInsServicesImpl checks with the rolePermissionServices. + assignAdminRole(currentUserServices.currentUser); //act - FileInfoDTO result = services.uploadFile(testCheckinId, fileToUpload); + FileInfoDTO result = services.uploadFile(checkIn.getId(), fileToUpload); //assert assertNotNull(result); assertEquals(fileToUpload.getFilename(), result.getName()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(1)).save(any(CheckinDocument.class)); } @Test void testUploadFileThrowsErrorWhenFileNameIsEmpty() { - when(fileToUpload.getFilename()).thenReturn(null); + CompletedFileUpload fileToUpload = new SimpleUploadFile(null); + currentUserServices.currentUser = member; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.uploadFile(UUID.randomUUID(), fileToUpload)); + services.uploadFile(checkIn.getId(), fileToUpload)); assertEquals("Please select a valid file before uploading.", responseException.getMessage()); } @Test void testUploadFileThrowsErrorForInvalidCheckinId() { + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); UUID testCheckinId = UUID.randomUUID(); - when(fileToUpload.getFilename()).thenReturn("test.file.name"); - when(checkInServices.read(testCheckinId)).thenReturn(null); - - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.uploadFile(testCheckinId, fileToUpload)); - - assertEquals(String.format("Unable to find checkin record with id %s", testCheckinId), responseException.getMessage()); - } - - @Test - void testUploadFileUnauthorizedUser() throws IOException { - UUID testCheckinId = UUID.randomUUID(); - - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(UUID.randomUUID()); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(UUID.randomUUID()); - - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> + // This exception actually comes from the CheckinDocumentServices. + final NotFoundException responseException = assertThrows(NotFoundException.class, () -> services.uploadFile(testCheckinId, fileToUpload)); - assertEquals("You are not authorized to perform this operation", responseException.getMessage()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(0)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkinDocumentServices, times(0)).save(any()); + assertEquals(String.format("Checkin not found by Id: %s.", testCheckinId), responseException.getMessage()); } @Test - void testUploadFileThrowsErrorIfCheckinIsComplete() throws IOException { - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); + void testUploadFileUnauthorizedUser() { + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(currentUserServices.getCurrentUser()).thenReturn(testMemberProfile); - when(testMemberProfile.getId()).thenReturn(testMemberId); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(testCheckIn.isCompleted()).thenReturn(true); - - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.uploadFile(testCheckinId, fileToUpload)); + final BadArgException responseException = assertThrows(BadArgException.class, () -> + services.uploadFile(checkIn.getId(), fileToUpload)); assertEquals(NOT_AUTHORIZED_MSG, responseException.getMessage()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(0)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(0)).getDrive(); - verify(checkinDocumentServices, times(0)).save(any()); } @Test - void testUploadFileDriveCantConnect() throws IOException { - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(memberProfileServices.getById(testMemberId)).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn("test.name"); - when(mockGoogleApiAccess.getDrive()).thenReturn(null); + void testUploadFileThrowsErrorIfCheckinIsComplete() { + CheckIn completed = createACompletedCheckIn(member, pdl); + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); + currentUserServices.currentUser = member; final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, () -> - services.uploadFile(testCheckinId, fileToUpload)); + services.uploadFile(completed.getId(), fileToUpload)); - assertEquals("Unable to access Google Drive", responseException.getMessage()); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(0)).save(any()); - } - - @Test - void testUploadFileThrowsIOException() throws IOException { - UUID testCheckinId = UUID.randomUUID(); - UUID testMemberId = UUID.randomUUID(); - - when(currentUserServices.isAdmin()).thenReturn(true); - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(testMemberId); - when(memberProfileServices.getById(testMemberId)).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn("test.name"); - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenThrow(IOException.class); - - assertThrows(FileRetrievalException.class, () -> services.uploadFile(testCheckinId, fileToUpload)); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any(UUID.class)); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkinDocumentServices, times(0)).save(any()); + assertEquals(NOT_AUTHORIZED_MSG, responseException.getMessage()); } @Test - void testUploadFileThrowsGoogleJsonResponseException() throws IOException { - //arrange - GoogleJsonResponseException testException = GoogleJsonResponseExceptionFactoryTesting.newMock(jsonFactory, 404, null); - String memberName = "testName"; - UUID testCheckinId = UUID.randomUUID(); + void testUploadFileThrowsException() { + services.shouldThrow = true; + CompletedFileUpload fileToUpload = new SimpleUploadFile("file.name"); - when(currentUserServices.isAdmin()).thenReturn(true); - when(fileToUpload.getFilename()).thenReturn("testFile"); - when(fileToUpload.getInputStream()).thenReturn(mockInputStream); - - when(checkInServices.read(testCheckinId)).thenReturn(testCheckIn); - when(testCheckIn.getTeamMemberId()).thenReturn(UUID.randomUUID()); - when(memberProfileServices.getById(any(UUID.class))).thenReturn(testMemberProfile); - when(MemberProfileUtils.getFullName(testMemberProfile)).thenReturn(memberName); - - when(mockGoogleApiAccess.getDrive()).thenReturn(drive); - when(drive.files()).thenReturn(files); - when(files.list()).thenReturn(list); - when(list.setFields(any(String.class))).thenReturn(list); - when(list.setSupportsAllDrives(any())).thenReturn(list); - when(list.setIncludeItemsFromAllDrives(any())).thenReturn(list); - when(list.setQ(any())).thenReturn(list); - when(list.setSpaces(any())).thenReturn(list); - when(list.execute()).thenThrow(testException); - - //act - final FileRetrievalException responseException = assertThrows(FileRetrievalException.class, - () -> services.uploadFile(testCheckinId, fileToUpload)); - - //assert - assertEquals(testException.getMessage(), responseException.getMessage()); - verify(mockGoogleApiAccess, times(1)).getDrive(); - verify(checkInServices, times(1)).read(testCheckinId); - verify(memberProfileServices, times(1)).getById(any()); - verify(checkinDocumentServices, times(0)).save(any()); + currentUserServices.currentUser = member; + assertThrows(FileRetrievalException.class, () -> services.uploadFile(checkIn.getId(), fileToUpload)); } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java index 06c9cb7fa5..6a5010ff71 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java @@ -2,6 +2,7 @@ import com.objectcomputing.checkins.services.kudos.Kudos; import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.FileServicesImplReplacement; import com.objectcomputing.checkins.services.fixture.KudosFixture; import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; @@ -127,8 +128,10 @@ void processReportData() { .basicAuth(admin.getWorkEmail(), ADMIN_ROLE); client.toBlocking().exchange(request); - validateReportData(fileServices.documentName, - fileServices.documentText, target); + String documentName = target.getWorkEmail(); + String documentText = fileServices.getFile(MarkdownGeneration.directory + + "/" + documentName); + validateReportData(documentName, documentText, target); } @Test From 0ea3edf260b2f6cc3b520cbcb36ee4daa3406656 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 6 Jan 2025 12:41:55 -0600 Subject: [PATCH 46/64] Factored out the main FileServicesImpl functionality into FileServicesBaseImpl and left the Google specific code in FileServicesImpl. Tests were reworked to use a mock FileServicesImpl that extends FileServicesBaseImpl. --- .../reports/FileServicesImplReplacement.java | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java deleted file mode 100644 index 68b6ec4ea1..0000000000 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.objectcomputing.checkins.services.reports; - -/************************************************************************* - * - * This class is here due to the fact that the ReportDataController now - * references the FileServices. The real FileServicesImpl requires the - * GoogleApiAccess class that does not exist during testing. - * - * This replacement class does not require that and can help us test the - * output of the MarkdownGeneration class. - * - ************************************************************************/ - -import com.objectcomputing.checkins.services.file.FileInfoDTO; -import com.objectcomputing.checkins.services.file.FileServices; -import com.objectcomputing.checkins.services.file.FileServicesImpl; - -import io.micronaut.http.multipart.CompletedFileUpload; - -import java.io.File; -import java.util.Set; -import java.util.HashSet; -import java.util.UUID; - -import jakarta.inject.Singleton; -import io.micronaut.context.env.Environment; -import io.micronaut.context.annotation.Replaces; -import io.micronaut.context.annotation.Requires; - -@Singleton -@Replaces(FileServicesImpl.class) -@Requires(env = Environment.TEST) -public class FileServicesImplReplacement implements FileServices { - public String documentName = ""; - public String documentText = ""; - - public Set findFiles(UUID checkInId) { - return new HashSet(); - } - - public File downloadFiles(String uploadDocId) { - return null; - } - - public FileInfoDTO uploadFile(UUID checkInID, CompletedFileUpload file) { - return new FileInfoDTO(); - } - - public FileInfoDTO uploadDocument(String directory, - String name, String text) { - documentName = name; - documentText = text; - return new FileInfoDTO(); - } - - public boolean deleteFile(String uploadDocId) { - return true; - } -} From 17f97060140abe14f1630d6a2afd8440e8ab47f6 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 6 Jan 2025 12:47:53 -0600 Subject: [PATCH 47/64] Removed the import of DisabledInNativeImage --- .../member_skill/skillsreport/SkillsReportServicesImplTest.java | 1 - .../checkins/services/pulseresponse/PulseResponseTest.java | 1 - .../services/request_notifications/CheckServicesImplTest.java | 1 - 3 files changed, 3 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java index e14ea418e0..fb2e77cfff 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import jakarta.inject.Inject; import java.time.LocalDate; diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java index 206c833764..e2a55bf163 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseTest.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.time.LocalDate; import java.util.Collections; diff --git a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java index a95ec57bc0..debee294cf 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java @@ -14,7 +14,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import io.micronaut.context.annotation.Property; import io.micronaut.core.util.StringUtils; From 302142fbae395135407452e73066f98cd68b4f3a Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 6 Jan 2025 14:53:18 -0600 Subject: [PATCH 48/64] Moved the pulse responses up to the pie chart card, made them pageable, and removed the distribution bar chart. --- web-ui/src/pages/PulseReportPage.jsx | 94 ++++++++++------------------ 1 file changed, 33 insertions(+), 61 deletions(-) diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 4cf902f10a..66e5e8fcb5 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -33,7 +33,8 @@ import { MenuItem, Modal, TextField, - Typography + Typography, + Link, } from '@mui/material'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; @@ -79,6 +80,8 @@ const ScoreOptionLabel = { 'Combined': 'Both', }; +const pulsesPerPage = 15; + /* // Returns a random, integer score between 1 and 5. // We may want to uncomment this later for testing. @@ -117,6 +120,7 @@ const PulseReportPage = () => { const [expanded, setExpanded] = useState(false); const [scoreChartData, setScoreChartData] = useState([]); const [pulses, setPulses] = useState([]); + const [pulsesUpperBounds, setPulsesUpperBounds] = useState(pulsesPerPage); const [scope, setScope] = useState('Individual'); const [scoreType, setScoreType] = useState(ScoreOption.COMBINED); const [selectedPulse, setSelectedPulse] = useState(null); @@ -329,6 +333,7 @@ const PulseReportPage = () => { return compare; }); setPulses(pulses); + setPulsesUpperBounds(pulsesPerPage); }; useEffect(() => { @@ -389,63 +394,6 @@ const PulseReportPage = () => { ); - const scoreDistributionChart = () => ( - - - - - - - - - - {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.INTERNAL) && - - } - {(scoreType == ScoreOption.COMBINED || scoreType == ScoreOption.EXTERNAL) && - - } - - setExpanded(!expanded)} - aria-expanded={expanded} - aria-label={expanded ? 'show less' : 'show more'} - size="large" - /> - - {responseSummary()} - - - - ); - const handleCommentClick = pulse => { setSelectedPulse(pulse); setShowComments(true); @@ -725,16 +673,33 @@ const PulseReportPage = () => {
} + setExpanded(!expanded)} + aria-expanded={expanded} + aria-label={expanded ? 'show less' : 'show more'} + size="large" + /> + + {responseSummary()} + ); const responseSummary = () => { - let filteredPulses = pulses; + const pulsesSlice = pulses.slice(0, pulsesUpperBounds); + + let filteredPulses = pulsesSlice; const teamMemberIds = teamMembers.map(member => member.id); if (teamMemberIds.length) { - filteredPulses = pulses.filter(pulse => + filteredPulses = pulsesSlice.filter(pulse => teamMemberIds.includes(pulse.teamMemberId) ); } @@ -773,6 +738,14 @@ const PulseReportPage = () => { ); })} + {pulsesUpperBounds < pulses.length && + { + event.preventDefault(); + setPulsesUpperBounds(pulsesUpperBounds + pulsesPerPage); + }}> + Load more... + + } ); }; @@ -840,7 +813,6 @@ const PulseReportPage = () => { /> {pulseScoresChart()} {averageScores()} - {scoreDistributionChart()} setShowComments(false)}> Date: Tue, 7 Jan 2025 07:46:13 -0600 Subject: [PATCH 49/64] Changed the comment boxes to single line, expanding to 4 and changed the required title to use a red asterisk. --- web-ui/src/components/pulse/Pulse.css | 4 ++++ web-ui/src/components/pulse/Pulse.jsx | 7 +++++-- web-ui/src/pages/PulsePage.jsx | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/web-ui/src/components/pulse/Pulse.css b/web-ui/src/components/pulse/Pulse.css index 4fc7240f9e..fb73042e5a 100644 --- a/web-ui/src/components/pulse/Pulse.css +++ b/web-ui/src/components/pulse/Pulse.css @@ -20,6 +20,10 @@ margin-bottom: 1rem; padding: 1rem; } + + .title-row { + display: flex; + } } :root[data-mui-color-scheme='dark'] { diff --git a/web-ui/src/components/pulse/Pulse.jsx b/web-ui/src/components/pulse/Pulse.jsx index 379942c6d8..3382d8313d 100644 --- a/web-ui/src/components/pulse/Pulse.jsx +++ b/web-ui/src/components/pulse/Pulse.jsx @@ -44,7 +44,10 @@ const Pulse = ({ title }) => (
- {title} +
+ {title} + {commentRequired && *} +
{icons.map((sentiment, index) => ( @@ -70,7 +73,7 @@ const Pulse = ({ }} placeholder="Comment" required={commentRequired} - rows={4} + maxRows={4} value={comment} />
diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index ef6877f019..67fab04e83 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -120,10 +120,11 @@ const PulsePage = () => { Date: Tue, 7 Jan 2025 08:06:03 -0600 Subject: [PATCH 50/64] Updated test snapshots. --- .../pulse/__snapshots__/Pulse.test.jsx.snap | 13 +++--- .../__snapshots__/PulsePage.test.jsx.snap | 43 ++++++++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/web-ui/src/components/pulse/__snapshots__/Pulse.test.jsx.snap b/web-ui/src/components/pulse/__snapshots__/Pulse.test.jsx.snap index 63e99f560f..663e3ad4d4 100644 --- a/web-ui/src/components/pulse/__snapshots__/Pulse.test.jsx.snap +++ b/web-ui/src/components/pulse/__snapshots__/Pulse.test.jsx.snap @@ -6,11 +6,15 @@ exports[`renders correctly 1`] = `
-
- How are you feeling about work today? (*) -
+
+ How are you feeling about work today? (*) +
+
@@ -203,7 +207,6 @@ exports[`renders correctly 1`] = ` class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMultiline css-1sqnrkk-MuiInputBase-input-MuiOutlinedInput-input" id=":r5:" placeholder="Comment" - rows="4" style="height: 0px; overflow: hidden;" > Just testing diff --git a/web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap b/web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap index f433cc329b..55bdddcf1c 100644 --- a/web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap @@ -8,11 +8,20 @@ exports[`renders correctly 1`] = `
-
- How are you feeling about work today? (*) -
+
+ How are you feeling about work today? +
+
+ * +
+
@@ -190,12 +199,19 @@ exports[`renders correctly 1`] = ` data-testid="comment-input" >