Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/src/main/java/org/justserve/command/BaseCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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."));
Expand Down
33 changes: 20 additions & 13 deletions cli/src/main/java/org/justserve/command/UnReassignProjects.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<Object> reassignResponse = null;
try {
ReassignProjectRequest reassignProjectRequest = new ReassignProjectRequest(userID, project.getProjectOwnerUserId());
log.atTrace().log("Reassigning project {} ({}) to user {}", projectName, projectId, userID);
HttpResponse<Object> 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);
Expand Down
19 changes: 15 additions & 4 deletions cli/src/main/java/org/justserve/util/EmailParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions cli/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,87 @@
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<String, List> testEmails

@Shared
Map<String, String> testEmails
List<ProjectCard> 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:
def (outputStream, errorStream) = executeCommand(ctx, args as String[])

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) {
}
}
}
53 changes: 40 additions & 13 deletions cli/src/test/groovy/org/justserve/util/EmailParserSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ProjectCard> 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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<ProjectCard> 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<String, Set<UUID>> projects = EmailParser.getProjects(emailContent)

then:
projects.size() == 2
myMockProjects.every { mock -> projects.values().flatten().contains(mock.id) }

where:
urlStyle << UrlStyle.values()
}
}
Loading
Loading