From 5e2ecb3e321f818f761c7796998da344d1e899cb Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Thu, 29 Jan 2026 11:10:37 -0700 Subject: [PATCH 1/9] chore: bump to 0.0.8-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8b86abd..ef6d0f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ micronautVersion=4.8.3 -justserveCliVersion=0.0.7 +justserveCliVersion=0.0.8-SNAPSHOT org.gradle.console=rich From fd2fd6cc83abd34bf1de9f3364a1363bc05f8dad Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Thu, 29 Jan 2026 11:27:28 -0700 Subject: [PATCH 2/9] feat: allow project to org assignment --- .../org/justserve/cli/JustServeCommand.java | 7 +-- .../cli/command/AssignOrgToProject.java | 57 +++++++++++++++++++ src/main/resources/schema.yml | 25 +++++++- .../cli/command/AssignOrgToProjectSpec.groovy | 40 +++++++++++++ .../justserve/client/ProjectClientSpec.groovy | 21 +++++++ 5 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/justserve/cli/command/AssignOrgToProject.java create mode 100644 src/test/groovy/org/justserve/cli/command/AssignOrgToProjectSpec.groovy diff --git a/src/main/java/org/justserve/cli/JustServeCommand.java b/src/main/java/org/justserve/cli/JustServeCommand.java index 4bb71a2..28905de 100644 --- a/src/main/java/org/justserve/cli/JustServeCommand.java +++ b/src/main/java/org/justserve/cli/JustServeCommand.java @@ -1,16 +1,13 @@ package org.justserve.cli; import io.micronaut.configuration.picocli.PicocliRunner; -import org.justserve.cli.command.BaseCommand; -import org.justserve.cli.command.GetTempPassword; -import org.justserve.cli.command.MakeOrgAdmin; -import org.justserve.cli.command.UnReassignProjects; +import org.justserve.cli.command.*; import org.justserve.cli.util.JustServeVersionProvider; import picocli.CommandLine.Command; import picocli.CommandLine.ParameterException; import picocli.jansi.graalvm.AnsiConsole; -@Command(subcommands = {GetTempPassword.class, MakeOrgAdmin.class, UnReassignProjects.class}, +@Command(subcommands = {GetTempPassword.class, MakeOrgAdmin.class, UnReassignProjects.class, AssignOrgToProject.class}, mixinStandardHelpOptions = true, name = "justserve", versionProvider = JustServeVersionProvider.class, description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") diff --git a/src/main/java/org/justserve/cli/command/AssignOrgToProject.java b/src/main/java/org/justserve/cli/command/AssignOrgToProject.java new file mode 100644 index 0000000..29f33e8 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/AssignOrgToProject.java @@ -0,0 +1,57 @@ +package org.justserve.cli.command; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import lombok.extern.slf4j.Slf4j; +import org.justserve.client.ProjectClient; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +import java.util.UUID; + +import static org.justserve.cli.util.JustServePrinter.printError; +import static org.justserve.cli.util.JustServePrinter.printNormal; + +@Slf4j +@Command(name = "assignOrgToProject", description = "Assigns an organization to a project", mixinStandardHelpOptions = true) +public class AssignOrgToProject extends BaseCommand implements Runnable { + + @Option(names = {"--project", "-p"}, description = "the project ID", required = true) + private UUID projectId; + + @Option(names = {"--org", "-o"}, description = "the organization ID", required = true) + private UUID orgId; + + @Inject + Provider projectClientProvider; + + @Override + public void run() { + if (isTokenInvalid()) { + return; + } + + ProjectClient client = projectClientProvider.get(); + + try { + log.atTrace().log("Assigning organization {} to project {}", orgId, projectId); + HttpResponse response = client.assignOrganizationToProject(projectId, orgId); + if (response.status() == HttpStatus.OK) { + printNormal("Successfully assigned organization %s to project %s", orgId, projectId); + log.atTrace().log("received api response status: {}", response.status()); + } else { + printError("Failed to assign organization " + orgId + " to project " + projectId + + ". Expected HTTP Status 'OK', but got " + response.status()); + log.atError().log("Failed to assign organization {} to project {}. Expected HTTP Status 'OK', but got {}", + orgId, projectId, response.status()); + } + } catch (HttpClientResponseException e) { + printError("Failed to assign organization %s to project %s. (%s: %s)", + orgId, projectId, e.getStatus().getCode(), e.getMessage()); + log.atError().setCause(e).log("Error response from API: {}", e.getResponse().body()); + } + } +} diff --git a/src/main/resources/schema.yml b/src/main/resources/schema.yml index f23e0b4..d68296f 100644 --- a/src/main/resources/schema.yml +++ b/src/main/resources/schema.yml @@ -150,6 +150,29 @@ paths: application/json: schema: type: null + /api/v1/projects/{id}/organization/{organizationId}/assign: + put: + tags: [ Project ] + description: Assigns an organization to a project. + operationId: assignOrganizationToProject + parameters: + - name: id + in: path + description: ID of the project + required: true + schema: { type: string, format: uuid } + - name: organizationId + in: path + description: ID of the organization to assign + required: true + schema: { type: string, format: uuid } + responses: + '200': + description: OK + content: + application/json: + schema: + type: null /api/v1/users: post: description: Register a new user on JustServe @@ -1231,7 +1254,7 @@ components: description: {} url: {} internalUrl: {} - organizationId: {} + organizationId: { type: string, format: uuid } reviewedBy: {} reviewedOn: {} linked: diff --git a/src/test/groovy/org/justserve/cli/command/AssignOrgToProjectSpec.groovy b/src/test/groovy/org/justserve/cli/command/AssignOrgToProjectSpec.groovy new file mode 100644 index 0000000..9c59f0b --- /dev/null +++ b/src/test/groovy/org/justserve/cli/command/AssignOrgToProjectSpec.groovy @@ -0,0 +1,40 @@ +package org.justserve.cli.command + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import org.justserve.model.ProjectCard +import spock.lang.Execution + +import static org.spockframework.runtime.model.parallel.ExecutionMode.SAME_THREAD + +@Execution(SAME_THREAD) +@MicronautTest +class AssignOrgToProjectSpec extends BaseCommandSpec { + + def "can assign an organization to a project"() { + given: + def orgSearchResponse = authOrgClient.searchByLocation(createSearchRequestForElkGrove()) + UUID orgId = orgSearchResponse.body().organizations.first().id + ProjectCard project = searchResults.first() + def args = ["assignOrgToProject", "-p", project.getId().toString(), "-o", orgId.toString()] + + when: + def (outputStream, errorStream) = executeCommand(ctx, args as String[]) + + then: + errorStream.matches(blankRegex) + outputStream.contains("Successfully assigned organization ${orgId} to project ${project.getId()}") + } + + def "fails gracefully when project or org does not exist"() { + given: + UUID fakeId = UUID.randomUUID() + def args = ["assignOrgToProject", "-p", fakeId.toString(), "-o", fakeId.toString()] + + when: + def (outputStream, errorStream) = executeCommand(ctx, args as String[]) + + then: + outputStream.matches(blankRegex) + errorStream.contains("Failed to assign organization ${fakeId} to project ${fakeId}. (400: Client 'justserve': Bad Request)") + } +} diff --git a/src/test/groovy/org/justserve/client/ProjectClientSpec.groovy b/src/test/groovy/org/justserve/client/ProjectClientSpec.groovy index f2d55c2..37a9221 100644 --- a/src/test/groovy/org/justserve/client/ProjectClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/ProjectClientSpec.groovy @@ -73,4 +73,25 @@ class ProjectClientSpec extends JustServeSpec { } } + void "can assign an organization to a project"() { + given: + def orgSearchResponse = authOrgClient.searchByLocation(createSearchRequestForElkGrove()) + UUID orgId = orgSearchResponse.body().organizations.first().id + ProjectCard project = searchResults.first() + + when: + def response = projectClient.assignOrganizationToProject(project.getId(), orgId) + + then: + verifyAll { + response.status == OK + } + + and: "validate that the reassignment worked - this is testing the underlying tech, not our codebase" + def updatedProject = projectClient.getProject(project.getId(), "en-US", new GetProjectRequest()).body() + verifyAll { + updatedProject.organization.organizationId == orgId + } + } + } From a9263de10eb535a9c635c0b88394e4a7f4a9ca30 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Thu, 29 Jan 2026 12:43:06 -0700 Subject: [PATCH 3/9] fix: don't print logs of any level to console --- src/main/resources/logback.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index b1bec03..144bb8f 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file From 9ee0c5a0d9eeb51405eb2c8b86aa616c585828c4 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 3 Feb 2026 12:38:59 -0700 Subject: [PATCH 4/9] refactor(test): rename helper method --- src/test/groovy/org/justserve/JustServeSpec.groovy | 2 +- .../groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/groovy/org/justserve/JustServeSpec.groovy b/src/test/groovy/org/justserve/JustServeSpec.groovy index 277ddad..749dc82 100644 --- a/src/test/groovy/org/justserve/JustServeSpec.groovy +++ b/src/test/groovy/org/justserve/JustServeSpec.groovy @@ -159,7 +159,7 @@ class JustServeSpec extends Specification { * @param count The number of organizations to create. * @return A list of UUIDs for the created organizations. */ - List createOrgs(int count) { + List createTestOrgs(int count) { return (1..count).collect { createOrg() } diff --git a/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy b/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy index f465bd6..86c2399 100644 --- a/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy +++ b/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy @@ -11,7 +11,7 @@ class MakeOrgAdminSpec extends BaseCommandSpec { def "can make a user an admin to #orgCount org(s) using the #orgFlag and #userFlag flags #title"() { given: UUID userID = createUser().body().id - def orgs = createOrgs(orgCount).join(",") + def orgs = createTestOrgs(orgCount).join(",") when: def (outputStream, errorStream) = executeCommand(context as ApplicationContext, ["makeOrgAdmin", orgFlag, orgs, userFlag, userID] as String[]) @@ -53,7 +53,7 @@ class MakeOrgAdminSpec extends BaseCommandSpec { if (orgCount == 1) { orgs = fakeId } else { - orgs = createOrgs(orgCount - 1).join(",") + "," + fakeId + orgs = createTestOrgs(orgCount - 1).join(",") + "," + fakeId } when: From 31eda431687d844fdad794338c691dac0f1435d5 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 3 Feb 2026 12:39:41 -0700 Subject: [PATCH 5/9] fix: handle when no org by given name is found --- .../justserve/cli/command/MakeOrgAdmin.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/justserve/cli/command/MakeOrgAdmin.java b/src/main/java/org/justserve/cli/command/MakeOrgAdmin.java index d9f382a..0702cd3 100644 --- a/src/main/java/org/justserve/cli/command/MakeOrgAdmin.java +++ b/src/main/java/org/justserve/cli/command/MakeOrgAdmin.java @@ -45,24 +45,22 @@ public void run() { } DynamicRoutingClient dynamicRoutingClient = dynamicRoutingClientProvider.get(); // since we allow submitting orgId's and orgUrl, convert any slugs to orgId's - Map orgUuidMap = Arrays.stream(orgs).distinct().parallel() - .collect(Collectors.toMap( - org -> org, - org -> org instanceof OrgId ? ((OrgId) org).getId() : dynamicRoutingClient - .getOrgIdFromSlug(((OrgSlug) org).getSlug()).body().getId() - )); - if (orgUuidMap.values().stream().anyMatch(Objects::isNull)) { - // provide a list of all invalid org slugs, not just the first failure - List invalidOrgSlugs = new ArrayList<>(); - orgUuidMap.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(entry -> (OrgSlug) entry.getKey()) - .forEach(invalidOrgSlugs::add); - printError("The following organization slugs are invalid: " + invalidOrgSlugs.stream() - .map(OrgSlug::getSlug) - .collect(Collectors.joining(", "))); - return; - } + Map orgUuidMap = Arrays.stream(orgs).distinct().parallel() + .map(org -> { + if (org instanceof OrgId) { + return new AbstractMap.SimpleEntry<>(org, ((OrgId) org).getId()); + } + try { + return new AbstractMap.SimpleEntry<>(org, dynamicRoutingClient + .getOrgIdFromSlug(((OrgSlug) org).getSlug()).body().getId()); + } catch (NullPointerException noOrgFound) { + err(String.format("The org '%s' is not found on JustServe", ((OrgSlug) org).getSlug())); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + log.atTrace().log("Finished converting any slugs to org id's."); BoundaryPermissionClient boundaryPermissionClient = boundaryPermissionClientProvider.get(); Map successfulReassignments = new HashMap<>(); From 394dbe1396367fdc87b730e7ddcddf2dc2ebbcf1 Mon Sep 17 00:00:00 2001 From: HMS-Victory <90852625+HMS-Victory@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:31:49 -0700 Subject: [PATCH 6/9] feat: add test case for group submitting url slugs --- .../cli/command/MakeOrgAdminSpec.groovy | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy b/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy index 86c2399..94082ba 100644 --- a/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy +++ b/src/test/groovy/org/justserve/cli/command/MakeOrgAdminSpec.groovy @@ -75,4 +75,35 @@ class MakeOrgAdminSpec extends BaseCommandSpec { "-o" | 1 | "--user" | ctx | "as an authorized user successfully makes the changes" | _ } + + + def "can make a user an admin to #orgCount where at least one org Slug does not exist on JustServe"() { + given: + UUID userID = createUser().body().id + String orgs + def fakeSlug = faker.internet().slug().toString() + if (orgCount == 1) { + orgs = fakeSlug + } else { + orgs=authOrgClient.searchByLocation(createSearchRequestForElkGrove()).body().getOrganizations().url.take(orgCount - 1).join(",")+","+fakeSlug + } + + when: + def (outputStream, errorStream) = executeCommand(context as ApplicationContext, ["makeOrgAdmin", orgFlag, orgs, userFlag, userID] as String[]) + + then: + verifyAll { + (outputStream as String).contains("successfully reassigned ${orgCount - 1 } orgs to user ${userID}") + (errorStream as String).contains("Error The org '${fakeSlug}' is not found on JustServe") + } + + where: + orgFlag | orgCount | userFlag | context | title | _ + "-o" | 3 | "-u" | ctx | "as an authorized user successfully makes the changes" | _ + "--org" | 3 | "-u" | ctx | "as an authorized user successfully makes the changes" | _ + "-o" | 3 | "--user" | ctx | "as an authorized user successfully makes the changes" | _ + "-o" | 1 | "-u" | ctx | "as an authorized user successfully makes the changes" | _ + "--org" | 1 | "-u" | ctx | "as an authorized user successfully makes the changes" | _ + "-o" | 1 | "--user" | ctx | "as an authorized user successfully makes the changes" | _ + } } From 790e1fcb65e126d8632764d2e33b2698b27a4131 Mon Sep 17 00:00:00 2001 From: HMS-Victory <90852625+HMS-Victory@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:08:04 -0700 Subject: [PATCH 7/9] feat: get actual org slugs dynamically DynamicRoutingClientSpec --- .../client/DynamicRoutingClientSpec.groovy | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy index 16bb270..ea4a997 100644 --- a/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy @@ -11,16 +11,19 @@ class DynamicRoutingClientSpec extends JustServeSpec { @Shared DynamicRoutingClient noAuthClient, authClient + @Shared + String realOrgSlug + def setupSpec() { noAuthClient = noAuthCtx.getBean(DynamicRoutingClient) authClient = ctx.getBean(DynamicRoutingClient) + realOrgSlug = authOrgClient.searchByLocation(createSearchRequestForElkGrove()).body().getOrganizations().url.first().toString() } def "get orgId for #url"() { when: HttpResponse response = client.getOrgIdFromSlug(url) - then: response.status() == expectedStatus if (expectedStatus == HttpStatus.OK) { @@ -28,10 +31,10 @@ class DynamicRoutingClientSpec extends JustServeSpec { } where: - url | expectedStatus | client - "accessleisure_sacramento" | HttpStatus.OK | authClient //TODO add actual orgs, not hardtyped ones - "accessleisure_sacramento" | HttpStatus.OK | noAuthClient - "1234" | HttpStatus.NOT_FOUND | authClient - "1234" | HttpStatus.NOT_FOUND | noAuthClient + url | expectedStatus | client + realOrgSlug | HttpStatus.OK | authClient //TODO add actual orgs, not hardtyped ones + realOrgSlug | HttpStatus.OK | noAuthClient + "1234" | HttpStatus.NOT_FOUND | authClient + "1234" | HttpStatus.NOT_FOUND | noAuthClient } } From 5c10e2ab17a251cb67bac4c5079e4d99d81d9305 Mon Sep 17 00:00:00 2001 From: HMS-Victory <90852625+HMS-Victory@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:10:30 -0700 Subject: [PATCH 8/9] feat: provide short descriptions for parameters inside OrganizationSlimResponse --- src/main/resources/schema.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/schema.yml b/src/main/resources/schema.yml index d68296f..04b0316 100644 --- a/src/main/resources/schema.yml +++ b/src/main/resources/schema.yml @@ -880,8 +880,8 @@ components: organizationType: { $ref: '#/components/schemas/OrganizationType' } title: { type: string, nullable: true } logo: { type: string, nullable: true } - url: { type: string, nullable: true } - internalURL: { type: string, nullable: true } + url: { type: string, nullable: true, description: "this provides only the url slug" } + internalURL: { type: string, nullable: true, description: "this currently returns null when an org is searched for by location" } website: { type: string, nullable: true } description: { type: string, nullable: true } contactName: { type: string, nullable: true } From 5fb82a9f048e48e99299562948b8e3cfad80e1dd Mon Sep 17 00:00:00 2001 From: HMS-Victory <90852625+HMS-Victory@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:11:28 -0700 Subject: [PATCH 9/9] fix: remove TODO --- .../groovy/org/justserve/client/DynamicRoutingClientSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy index ea4a997..7b5f6e9 100644 --- a/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy @@ -32,7 +32,7 @@ class DynamicRoutingClientSpec extends JustServeSpec { where: url | expectedStatus | client - realOrgSlug | HttpStatus.OK | authClient //TODO add actual orgs, not hardtyped ones + realOrgSlug | HttpStatus.OK | authClient realOrgSlug | HttpStatus.OK | noAuthClient "1234" | HttpStatus.NOT_FOUND | authClient "1234" | HttpStatus.NOT_FOUND | noAuthClient