diff --git a/cli/src/main/java/org/justserve/command/BaseCommand.java b/cli/src/main/java/org/justserve/command/BaseCommand.java index 18bb55b..04a1658 100644 --- a/cli/src/main/java/org/justserve/command/BaseCommand.java +++ b/cli/src/main/java/org/justserve/command/BaseCommand.java @@ -24,7 +24,7 @@ public class BaseCommand implements ConsoleOutput { String token; boolean isTokenInvalid() { - if ("i-need-to-be-defined".equals(token) || null == token) { + if ("i-need-to-be-defined".equals(token) || null == token || token.isEmpty()) { printError(("NO AUTHENTICATION PROVIDED" + System.lineSeparator() + "The Authentication token is not assigned as an environment variable." + System.lineSeparator() + "Please define the environment variable \"JUSTSERVE_TOKEN\" and try again.")); diff --git a/cli/src/main/java/org/justserve/command/UnReassignProjects.java b/cli/src/main/java/org/justserve/command/UnReassignProjects.java index c8ba5b9..b1c58c4 100644 --- a/cli/src/main/java/org/justserve/command/UnReassignProjects.java +++ b/cli/src/main/java/org/justserve/command/UnReassignProjects.java @@ -90,6 +90,11 @@ public void run() { log.atError().setCause(e).log("Error getting project"); continue; } + if (null == project) { + printError("Failed to get project '" + projectName + "' (" + projectId + ")"); + log.atError().log("Project {} not found", projectId); + continue; + } if (null == project.getProjectOwnerUserId()) { warning(String.format("Project %s (%s) has no owner", projectName, projectId)); log.warn("Project {} ({}) has no owner", projectName, projectId); @@ -99,24 +104,26 @@ public void run() { log.warn("Project {} ({}) is already assigned to user {}", projectName, projectId, userID); continue; } + ReassignProjectRequest reassignProjectRequest = new ReassignProjectRequest(userID, project.getProjectOwnerUserId()); + log.atTrace().log("Reassigning project {} ({}) to user {}", projectName, projectId, userID); + HttpResponse reassignResponse = null; try { - ReassignProjectRequest reassignProjectRequest = new ReassignProjectRequest(userID, project.getProjectOwnerUserId()); - log.atTrace().log("Reassigning project {} ({}) to user {}", projectName, projectId, userID); - HttpResponse reassignResponse = client.reassignProject(projectId, reassignProjectRequest); - if (reassignResponse.status() == HttpStatus.OK) { - printNormal("Successfully reassigned project %s (%s) to user %s", projectName, projectId, userID); - log.atTrace().log("received api response status: {}", reassignResponse.status()); - successCount++; - continue; - } - printError("Failed to reassign project " + projectName + " (" + projectId + ") to user " + userID + - ". Expected HTTP Status 'OK', but got " + reassignResponse.status()); - log.atError().log("Failed to reassign project {} ({}) to user {}. Expected HTTP Status 'OK', but got {}", - projectName, projectId, userID, reassignResponse.status()); + reassignResponse = client.reassignProject(projectId, reassignProjectRequest); } catch (HttpClientResponseException e) { printError("Failed to reassign project " + projectName + " (" + projectId + ") to user " + userID); log.atError().setCause(e).log("Error response from API: {}", e.getResponse().body()); } + if (null != reassignResponse && reassignResponse.status() == HttpStatus.OK) { + printNormal("Successfully reassigned project %s (%s) to user %s", projectName, projectId, userID); + log.atTrace().log("received api response status: {}", reassignResponse.status()); + successCount++; + continue; + } + String reason = reassignResponse == null ? "response is null" : reassignResponse.status().toString(); + printError("Failed to reassign project " + projectName + " (" + projectId + ") to user " + userID + + ". Expected HTTP Status 'OK', but got " + reason); + log.atError().log("Failed to reassign project {} ({}) to user {}. Expected HTTP Status 'OK', but got {}", + projectName, projectId, userID, reason); } } printNormal("Successfully reassigned %d projects to user %s", successCount, userID); diff --git a/cli/src/main/java/org/justserve/util/EmailParser.java b/cli/src/main/java/org/justserve/util/EmailParser.java index 4fb4e13..5e9f548 100644 --- a/cli/src/main/java/org/justserve/util/EmailParser.java +++ b/cli/src/main/java/org/justserve/util/EmailParser.java @@ -65,7 +65,7 @@ public static Document parse(String emlFileContent) throws MessagingException, I * Extracts project names and their corresponding UUIDs for project ids on JustServe. * The Document is expected to represent an HTML email body from an automated JustServe email regarding reassigned projects * - * @param doc The Jsoup Document containing the HTML structure of the email. + * @param doc The Jsoup Document containing the HTML structure of the email. This is to be the html from the automated email and does not contain any other parts of the email. * @return A map where keys are project names (String) and values are project UUIDs. * @throws JustServeEmailParserError If the HTML structure does not conform to the expected format for extracting projects. */ @@ -160,11 +160,22 @@ private static String getHtmlBody(Part part) throws MessagingException, IOExcept * not contain the 'www.justserve.org*2Fprojects*2F' string. */ static UUID getProjectIDFromUglyUrl(String uglyUrl) { - String start = "www.justserve.org*2Fprojects*2F"; - if (!uglyUrl.contains(start)) { + String startAsterisk = "www.justserve.org*2Fprojects*2F"; + String startPercent = "www.justserve.org%2Fprojects%2F"; + String startSlash = "www.justserve.org/projects/"; + String splitString; + + if (uglyUrl.contains(startAsterisk)) { + splitString = startAsterisk; + } else if (uglyUrl.contains(startPercent)) { + splitString = startPercent; + } else if (uglyUrl.contains(startSlash)) { + splitString = startSlash; + } else { return null; } - String uuid = uglyUrl.split(Pattern.quote(start))[1].split(Pattern.quote("/"))[0]; + + String uuid = uglyUrl.split(Pattern.quote(splitString))[1].split(Pattern.quote("/"))[0]; return UUID.fromString(uuid); } } diff --git a/cli/src/main/resources/application.yml b/cli/src/main/resources/application.yml index 1bafcb1..e4cd39a 100644 --- a/cli/src/main/resources/application.yml +++ b/cli/src/main/resources/application.yml @@ -2,6 +2,10 @@ micronaut: application: name: justserve-cli version: "@justserveCliVersion@" + http: + services: + justserve: + url: https://www.justserve.org justserve: token: ${:i-need-to-be-defined} logger: diff --git a/cli/src/test/groovy/org/justserve/cli/command/GetTempPasswordSpec.groovy b/cli/src/test/groovy/org/justserve/cli/command/GetTempPasswordSpec.groovy index 65ffe81..c2b83bb 100644 --- a/cli/src/test/groovy/org/justserve/cli/command/GetTempPasswordSpec.groovy +++ b/cli/src/test/groovy/org/justserve/cli/command/GetTempPasswordSpec.groovy @@ -12,7 +12,7 @@ import static org.spockframework.runtime.model.parallel.ExecutionMode.SAME_THREA class GetTempPasswordSpec extends BaseCommandSpec { - @Unroll("getting temp password with '#flag' and '#email' returns ") + @Unroll("getting temp password with '#flag' and '#email' #expectation") def "commands to query temporary password should behave as expected with or without authentication"() { when: def (outputStream, errorStream) = executeCommand(context as ApplicationContext, ["getTempPassword", flag, email] as String[]) diff --git a/cli/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy b/cli/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy index fc92d8b..06a5820 100644 --- a/cli/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy +++ b/cli/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy @@ -90,16 +90,6 @@ class MakeOrgAdminSpec extends BaseCommandSpec { if (orgCount == 1) { orgs = fakeSlug } else { - // We can't easily get slugs for the sharedOrgs since they are just UUIDs in the list. - // However, the original test was searching for orgs by location to get slugs. - // To keep it fast, we can just fetch one set of slugs in setupSpec if needed, - // or just do the search once here if we really need slugs. - // But wait, createTestOrgs returns UUIDs. - // The original test used: authOrgClient.searchByLocation(...).getOrganizations().url - // Let's just do that search once in the test method, but limit it. - // Actually, better yet, let's just use the search here but only call it if orgCount > 1. - // Since we are optimizing, let's try to reuse the search result if possible, but for now - // let's stick to the original logic for the slug part but reuse the user. orgs = authOrgClient.searchByLocation(createSearchRequestForElkGrove()).body().getOrganizations().url.take(orgCount - 1).join(",") + "," + fakeSlug } diff --git a/cli/src/test/groovy/org/justserve/cli/command/UnReassignProjectsSpec.groovy b/cli/src/test/groovy/org/justserve/cli/command/UnReassignProjectsSpec.groovy index a680f2d..75053a8 100644 --- a/cli/src/test/groovy/org/justserve/cli/command/UnReassignProjectsSpec.groovy +++ b/cli/src/test/groovy/org/justserve/cli/command/UnReassignProjectsSpec.groovy @@ -1,52 +1,47 @@ package org.justserve.cli.command -import io.micronaut.core.io.ResourceResolver + import io.micronaut.test.extensions.spock.annotation.MicronautTest -import jakarta.inject.Inject +import org.justserve.TestUser +import org.justserve.model.ProjectCard import org.justserve.util.EmailParser +import org.justserve.util.TestEmailGenerator import spock.lang.Execution import spock.lang.Shared -import java.util.stream.Collectors -import java.util.stream.Stream - import static org.spockframework.runtime.model.parallel.ExecutionMode.SAME_THREAD @Execution(SAME_THREAD) @MicronautTest class UnReassignProjectsSpec extends BaseCommandSpec { - @Inject @Shared - ResourceResolver resourceResolver + File tempEmlFile @Shared - File tempEmlFile + Map testEmails @Shared - Map testEmails + List testProjects def setupSpec() { + testProjects = getProjectsByLocation(faker.location().toString()) + def newReadOnlyUser = new TestUser(faker) + newReadOnlyUser.uuid = createUser().body().id testEmails = new HashMap<>() - Stream.of("sara-anderson-email.eml", "test-with-automated-email.eml", "test-without-automated-email.eml").forEach { file -> - def resource = resourceResolver.getResourceAsStream("classpath:$file") - resource.ifPresent { stream -> - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - testEmails.put(file.replace(".eml", ""), reader.lines().collect(Collectors.joining(System.lineSeparator()))) - } catch (IOException e) { - throw new RuntimeException("Failed to read test file: $file", e) - } - } - } + testEmails.put("forwarded-reassignment-email", [TestEmailGenerator.generateMockValidEmlContent(testProjects, readOnlyUser), readOnlyUser]) + testEmails.put("automated-reassignment-email", [TestEmailGenerator.generateMockValidEmlContent(testProjects, newReadOnlyUser), newReadOnlyUser]) + testEmails.put("email-without-justserve-content", [TestEmailGenerator.generateInvalidMockEmlContent(), readOnlyUser]) } - def "can make reassignments from #title to a user"(String title, String fileContent) { + def "can make reassignments from #title to a user"(String title, String fileContent, TestUser user) { given: if (title.contains("without")) { return } - String testFile = new File(resourceResolver.getResource("classpath:${title}.eml").get().toURI()).absolutePath - def args = ["unReassignProjects", "-u", readOnlyUser.uuid.toString(), "-f", testFile] + tempEmlFile = File.createTempFile(title, ".eml") + tempEmlFile.write(fileContent) + def args = ["unReassignProjects", "-u", user.uuid.toString(), "-f", tempEmlFile.absolutePath] def projectCount = EmailParser.getProjects(fileContent).values().flatten().size() when: @@ -54,12 +49,39 @@ class UnReassignProjectsSpec extends BaseCommandSpec { then: errorStream.matches(blankRegex) - projects.each { project -> - outputStream.contains(project.id.toString()) + testProjects.each { project -> outputStream.contains(project.id.toString()) + } + outputStream.contains("Successfully reassigned ${projectCount} projects to user ${user.uuid}") + + cleanup: + try { + tempEmlFile.delete() + } catch (Exception ignored) { } - outputStream.contains("Successfully reassigned ${projectCount} projects to user ${readOnlyUser.uuid}") where: - [title, fileContent] << testEmails.collect { key, value -> [key, value] } + [title, fileContent, user] << testEmails.collect { key, value -> [key, value[0], value[1]] } + } + + def "shows error when project ID is invalid"() { + given: + def invalidProject = new ProjectCard().setId(UUID.randomUUID()).setTitle("Surprised by Joy") + def emailContent = TestEmailGenerator.generateMockValidEmlContent([invalidProject], readOnlyUser) + tempEmlFile = File.createTempFile("invalid-project", ".eml") + tempEmlFile.write(emailContent) + def args = ["unReassignProjects", "-u", readOnlyUser.uuid.toString(), "-f", tempEmlFile.absolutePath] + + when: + def (outputStream, errorStream) = executeCommand(ctx, args as String[]) + + then: + errorStream.contains("Failed to get project 'Surprised by Joy' (${invalidProject.id})") + outputStream.contains("Successfully reassigned 0 projects") + + cleanup: + try { + tempEmlFile.delete() + } catch (Exception ignored) { + } } } diff --git a/cli/src/test/groovy/org/justserve/util/EmailParserSpec.groovy b/cli/src/test/groovy/org/justserve/util/EmailParserSpec.groovy index dfedd4b..319f66e 100644 --- a/cli/src/test/groovy/org/justserve/util/EmailParserSpec.groovy +++ b/cli/src/test/groovy/org/justserve/util/EmailParserSpec.groovy @@ -3,12 +3,16 @@ package org.justserve.util import io.micronaut.core.io.ResourceResolver import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject +import net.datafaker.Faker import org.jsoup.nodes.Document +import org.justserve.TestUser +import org.justserve.model.ProjectCard +import org.justserve.util.TestEmailGenerator.UrlStyle import spock.lang.Shared import spock.lang.Specification +import spock.lang.Unroll import java.util.stream.Collectors -import java.util.stream.Stream @MicronautTest class EmailParserSpec extends Specification { @@ -26,16 +30,17 @@ class EmailParserSpec extends Specification { def setupSpec() { testEmails = new HashMap<>() - Stream.of("sara-anderson-email.eml", "test-with-automated-email.eml", "test-without-automated-email.eml").forEach { file -> - def resource = resourceResolver.getResourceAsStream("classpath:$file") - resource.ifPresent { stream -> - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - testEmails.put(file.replace(".eml", ""), reader.lines().collect(Collectors.joining(System.lineSeparator()))) - } catch (IOException e) { - throw new RuntimeException("Failed to read test file: $file", e) - } - } - } + + Faker faker = new Faker() + TestUser recipient = new TestUser(faker) + List mockProjects = [ + new ProjectCard(id: UUID.randomUUID(), title: faker.book().title()), + new ProjectCard(id: UUID.randomUUID(), title: faker.book().title()) + ] + + testEmails.put("test-with-automated-email", TestEmailGenerator.generateMockValidEmlContent(mockProjects, recipient)) + testEmails.put("test-with-automated-email-zendesk", TestEmailGenerator.generateMockZendeskEmlContent(mockProjects, recipient)) + testEmails.put("test-without-automated-email", TestEmailGenerator.generateInvalidMockEmlContent()) testTrackingUrls = new HashMap<>() def yamlResource = resourceResolver.getResourceAsStream("classpath:projects.yaml") @@ -63,7 +68,7 @@ class EmailParserSpec extends Specification { return } def error = thrown(JustServeEmailParserError) - error.message == "Email is not a JustServe generated email." + error.message == "Email does not contain an HTML body." where: @@ -112,9 +117,31 @@ class EmailParserSpec extends Specification { return } def error = thrown(JustServeEmailParserError) - error.message == "Email is not a JustServe generated email." + error.message == "Email does not contain an HTML body." where: [title, fileContent] << testEmails.collect { key, value -> [key, value] } } + + @Unroll + def "Can parse project URLs with different encoding styles"(UrlStyle urlStyle) { + given: + Faker faker = new Faker() + TestUser recipient = new TestUser(faker) + List myMockProjects = [ + new ProjectCard(id: UUID.randomUUID(), title: faker.book().title()), + new ProjectCard(id: UUID.randomUUID(), title: faker.book().title()) + ] + String emailContent = TestEmailGenerator.generateMockValidEmlContent(myMockProjects, recipient, urlStyle) + + when: + Map> projects = EmailParser.getProjects(emailContent) + + then: + projects.size() == 2 + myMockProjects.every { mock -> projects.values().flatten().contains(mock.id) } + + where: + urlStyle << UrlStyle.values() + } } diff --git a/cli/src/test/groovy/org/justserve/util/TestEmailGenerator.groovy b/cli/src/test/groovy/org/justserve/util/TestEmailGenerator.groovy index dfa67a0..822655a 100644 --- a/cli/src/test/groovy/org/justserve/util/TestEmailGenerator.groovy +++ b/cli/src/test/groovy/org/justserve/util/TestEmailGenerator.groovy @@ -4,22 +4,68 @@ import net.datafaker.Faker import org.justserve.TestUser import org.justserve.model.ProjectCard +import static java.util.concurrent.TimeUnit.DAYS + class TestEmailGenerator { - static String generateMockEmlContent(List projects, TestUser recipient) { - StringBuilder sb = new StringBuilder() - sb.append("From: JustServe \n") - sb.append("To: " + recipient.firstName + " " + recipient.lastName + " <" + recipient.email + ">\n") - sb.append("Subject: Project Reassignment\n") - sb.append("Content-Type: text/html; charset=\"UTF-8\"\n") - sb.append("Content-Transfer-Encoding: 8bit\n\n") + enum UrlStyle { + CLEAN, + ENCODED_DEFENSE, + ENCODED_AWS + } - String recipientName = recipient.firstName + " " + recipient.lastName - String recipientEmail = recipient.email + static String generateMockValidEmlContent(List projects, TestUser recipient, UrlStyle urlStyle = UrlStyle.ENCODED_AWS) { + StringBuilder sb = new StringBuilder() Faker faker = new Faker() + + String recipientName = recipient.firstName + " " + recipient.lastName String senderName = faker.name().fullName() + String senderEmail = faker.internet().emailAddress() + String boundary = "000000000000" + faker.internet().uuid().replace("-", "").substring(0, 16) + String phoneNumber = faker.phoneNumber().phoneNumber() - String htmlTemplateStart = """
Here you go.  Thx for your help.  

---------- Forwarded message ---------
From: JustServe.org <noreply-js@mail.justserve.org>
Date: Tue, Dec 16, 2025 at 8:27 AM
Subject: Project Reassignment
To: <${recipientEmail}>


+ sb.append("Return-Path: <").append(senderEmail).append(">\n") + sb.append("Date: ").append(faker.timeAndDate().past(1, DAYS)).append("\n") + sb.append("From: ").append(senderName).append(" <").append(senderEmail).append(">\n") + sb.append("To: JustServeSupport \n") + sb.append("Subject: Fwd: Project Reassignment\n") + sb.append("Mime-Version: 1.0\n") + sb.append("Content-Type: multipart/alternative; boundary=").append(boundary).append("\n\n") + + // Plain text part + sb.append("--").append(boundary).append("\n") + sb.append("Content-Type: text/plain; charset=UTF-8\n") + sb.append("Content-Transfer-Encoding: quoted-printable\n\n") + + sb.append("Hi Jonathan,=\n\n") + sb.append("Here you go. Thx for all of your help.=\n\n") + sb.append(senderName).append("=\n\n") + sb.append("---------- Forwarded message ---------\n") + sb.append("From: JustServe.org \n") + sb.append("Date: Tue, Dec 16, 2025 at 8:27 AM\n") + sb.append("Subject: Project Reassignment\n") + sb.append("To: <").append(senderEmail).append(">\n\n") + sb.append("Project Reassignment\n\n") + sb.append("The following projects have been reassigned from ").append(faker.name().fullName()).append(", to ") + .append(recipientName).append(", by ").append(senderName).append(":\n\n") + + projects.each { project -> + sb.append(" - ").append(project.title).append("\n") + sb.append(" \n") + } + + sb.append("\n").append(recipientName).append(" can now access these projects in their manage projects page for editing.\n\n") + sb.append("JustServe.org is provided as a service by The Church of Jesus Christ of Latter-day Saints.\n\n") + sb.append("© 2020 by Intellectual Reserve, Inc. All rights reserved.\n\n") + sb.append("50 East North Temple, Salt Lake City, Utah, 84150\n\n") + sb.append("This email was sent to: ").append(senderEmail).append("\n\n") + + // HTML part + sb.append("--").append(boundary).append("\n") + sb.append("Content-Type: text/html; charset=UTF-8\n") + sb.append("Content-Transfer-Encoding: quoted-printable\n\n") + + String htmlTemplateStart = """
Here you go.  Thx for your help.  

---------- Forwarded message ---------
From: JustServe.org <noreply-js@mail.justserve.org>
Date: Tue, Dec 16, 2025 at 8:27 AM
Subject: Project Reassignment
To: <${senderEmail}>


@@ -55,7 +101,7 @@ class TestEmailGenerator {

- The following projects have been reassigned from ${senderName}, to ${recipientName}, by ${senderName}: + The following projects have been reassigned from ${faker.name().fullName()}, to ${recipientName}, by ${senderName}:

-


--
${recipientName}
JustServe Assistant Area Director
Northern California 
925-699-1395  Cell and Text



+


--
${senderName}
JustServe Assistant Area Director
${faker.address().state()} 
${phoneNumber}  Cell and Text



""" sb.append(htmlTemplateStart) projects.each { project -> - String uglyUrl = "https://urldefense.com/v3/__https://v6q93rxd.r.us-east-1.awstrack.me/L0/https:*2F*2Fwww.justserve.org*2Fprojects*2F" + project.id + "/1/0100019b27fd352e-a7b03409-4c41-4863-b76a-524b7f84f180-000000/DNbIEdheshbb39W79A1Ru2ox05c=3D457__;JSUlJQ!!Oz_3W2l6Vjs!5dejAA5tmpGXmMs_vdlbrwn4wHWu5ytbEcsfO4rx9OUup3ka-dRHFZEinoeDKzwDHUqRP5WvSOsZ5sS1l875dafyqVS6\$" + String url + switch (urlStyle) { + case UrlStyle.ENCODED_DEFENSE: + url = "https://urldefense.com/v3/__https://www.justserve.org*2Fprojects*2F" + project.id + "/1/0100019cd9daccde-30d2f0ce-ca96-45a1-bd36-1cccc809eb61-000000/" + break + case UrlStyle.ENCODED_AWS: + url = "https://v6q93rxd.r.us-east-1.awstrack.me/L0/https:%2F%2Fwww.justserve.org%2Fprojects%2F" + project.id + "/1/0100019ce325c14c" + break + default: // UrlStyle.CLEAN + url = "https://www.justserve.org/projects/" + project.id + break + } + sb.append("
  • ").append(project.title).append("
  • ") + } + + sb.append(htmlTemplateEnd) + + sb.append("\n--").append(boundary).append("--\n") + return sb.toString() + } + + static String generateInvalidMockEmlContent() { + StringBuilder sb = new StringBuilder() + Faker faker = new Faker() + + String senderName = faker.name().fullName() + String senderEmail = faker.internet().emailAddress() + + sb.append("Return-Path: <").append(senderEmail).append(">\n") + sb.append("Date: ").append(faker.timeAndDate().past(1, DAYS)).append("\n") + sb.append("From: ").append(senderName).append(" <").append(senderEmail).append(">\n") + sb.append("To: JustServeSupport \n") + sb.append("Subject: Random user email without JS content\n") + sb.append("Mime-Version: 1.0\n") + sb.append("Content-Type: text/plain; charset=UTF-8\n") + sb.append("Content-Transfer-Encoding: quoted-printable\n\n") + + sb.append("Hi Jonathan,=\n\n") + sb.append("Can you help me reset my password? I forgot it.\n\n") + sb.append("Thanks,\n") + sb.append(senderName).append("\n") + + return sb.toString() + } + + static String generateMockZendeskEmlContent(List projects, TestUser recipient) { + StringBuilder sb = new StringBuilder() + Faker faker = new Faker() + + String senderName = faker.name().fullName() + String senderEmail = faker.internet().emailAddress() + String recipientName = recipient.firstName + " " + recipient.lastName + String recipientEmail = recipient.email + String boundary = "000000000000" + faker.internet().uuid().replace("-", "").substring(0, 16) + + sb.append("Return-Path: <").append(senderEmail).append(">\n") + sb.append("Date: ").append(faker.timeAndDate().past(1, DAYS)).append("\n") + sb.append("From: ").append(senderName).append(" <").append(senderEmail).append(">\n") + sb.append("To: JustServeSupport \n") + sb.append("Subject: Fwd: Project Reassignment\n") + sb.append("Mime-Version: 1.0\n") + sb.append("Content-Type: multipart/alternative; boundary=\"").append(boundary).append("\"\n\n") + + // Plain text part + sb.append("--").append(boundary).append("\n") + sb.append("Content-Type: text/plain; charset=\"UTF-8\"\n") + sb.append("Content-Transfer-Encoding: quoted-printable\n\n") + + sb.append("CCJSS ").append(faker.name().fullName()).append(" reassigned ").append(projects.size()).append(" projects to their lead JSS account, ").append(recipientName).append(".\n\n") + sb.append("---------- Forwarded message ---------\n") + sb.append("From: JustServe.org \n") + sb.append("Date: Thu, Mar 12, 2026 at 1:43 PM\n") + sb.append("Subject: Project Reassignment\n") + sb.append("To: <").append(recipientEmail).append(">\n\n") + sb.append("Project Reassignment\n\n") + sb.append("The following projects have been reassigned from ").append(faker.name().fullName()).append(", to ").append(recipientName).append(", by ").append(senderName).append(":\n\n") + + projects.each { project -> + sb.append(" - ").append(project.title).append("\n") + sb.append(" \n") + } + + sb.append("\n").append(recipientName).append(" can now access these projects in their manage projects page for editing.\n\n") + + // HTML part + sb.append("--").append(boundary).append("\n") + sb.append("Content-Type: text/html; charset=\"UTF-8\"\n") + sb.append("Content-Transfer-Encoding: quoted-printable\n\n") + + String htmlTemplateStart = """
    CCJSS ${faker.name().fullName()} reassigned ${projects.size()} projects to their lead JSS account, ${recipientName}.

    ---------- Forwarded message ---------
    From: JustServe.org <noreply-js@mail.justserve.org>
    Date: Thu, Mar 12, 2026 at 1:43 PM
    Subject: Project Reassignment
    To: <${recipientEmail}>


    +
    + + + + + + + + +
    + + + + + + + + + +
    + Logo Title +
    +
      """ + + String htmlTemplateEnd = """
    +
    +
    +
    """ + + sb.append(htmlTemplateStart) + + projects.each { project -> + String uglyUrl = "https://v6q93rxd.r.us-east-1.awstrack.me/L0/https:%2F%2Fwww.justserve.org%2Fprojects%2F" + project.id + "/1/0100019ce325c14c" sb.append("
  • ").append(project.title).append("
  • ") } sb.append(htmlTemplateEnd) + + sb.append("\n--").append(boundary).append("--\n") return sb.toString() } } diff --git a/cli/src/test/resources/README.md b/cli/src/test/resources/README.md deleted file mode 100644 index 7286b47..0000000 --- a/cli/src/test/resources/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Test Email Files - -To properly run the `EmailParserSpec` tests, you need to provide your own `.eml` files in this directory, as the original files contain Personally Identifiable Information (PII) and cannot be committed to Git. - -Please include two types of email files: - -1. **With Automated Content:** An email that **contains** the standard JustServe automated email footer. The filename for this file must include the word `with`. - * Example: `email-with-content.eml` - -2. **Without Automated Content:** An email that **does not contain** the standard JustServe automated email footer. The filename for this file must include the word `without`. - * Example: `email-without-content.eml` - -The tests are designed to dynamically find and parse any `.eml` files in this directory and will assert different outcomes based on whether "with" or "without" is present in the filename. diff --git a/core/README.md b/core/README.md deleted file mode 100644 index 9ea3d5e..0000000 --- a/core/README.md +++ /dev/null @@ -1,35 +0,0 @@ -## Micronaut 4.10.9 Documentation - -- [User Guide](https://docs.micronaut.io/4.10.9/guide/index.html) -- [API Reference](https://docs.micronaut.io/4.10.9/api/index.html) -- [Configuration Reference](https://docs.micronaut.io/4.10.9/guide/configurationreference.html) -- [Micronaut Guides](https://guides.micronaut.io/index.html) ---- - -- [Micronaut Gradle Plugin documentation](https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/) -- [GraalVM Gradle Plugin documentation](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html) -- [Shadow Gradle Plugin](https://gradleup.com/shadow/) -## Feature serialization-jackson documentation - -- [Micronaut Serialization Jackson Core documentation](https://micronaut-projects.github.io/micronaut-serialization/latest/guide/) - - -## Feature micronaut-aot documentation - -- [Micronaut AOT documentation](https://micronaut-projects.github.io/micronaut-aot/latest/guide/) - - -## Feature lombok documentation - -- [Micronaut Project Lombok documentation](https://docs.micronaut.io/latest/guide/index.html#lombok) - -- [https://projectlombok.org/features/all](https://projectlombok.org/features/all) - - -## Feature openapi documentation - -- [Micronaut OpenAPI Support documentation](https://micronaut-projects.github.io/micronaut-openapi/latest/guide/index.html) - -- [https://www.openapis.org](https://www.openapis.org) - - diff --git a/core/src/test/groovy/org/justserve/JustServeSpec.groovy b/core/src/test/groovy/org/justserve/JustServeSpec.groovy index 834f337..b5defb5 100644 --- a/core/src/test/groovy/org/justserve/JustServeSpec.groovy +++ b/core/src/test/groovy/org/justserve/JustServeSpec.groovy @@ -175,7 +175,7 @@ class JustServeSpec extends Specification { * @return A list of UUIDs for the created organizations. */ List createTestOrgs(int count) { - return (1..count).toList().parallelStream().map(this::createOrg).collect(Collectors.toList()) + return (1..count).toList().parallelStream().map({ _ -> createOrg() }).collect(Collectors.toList()) }