diff --git a/.github/workflows/TestAndCompile.yml b/.github/workflows/TestAndCompile.yml index 4d49db8..a4951cb 100644 --- a/.github/workflows/TestAndCompile.yml +++ b/.github/workflows/TestAndCompile.yml @@ -5,7 +5,7 @@ jobs: name: test and compile on self-hosted runs-on: self-hosted env: - TEST_TOKEN: ${{ secrets.MICRONAUT_HTTP_SERVICES_JUSTSERVE_TOKEN }} + JUSTSERVE_TOKEN: ${{ secrets.MICRONAUT_HTTP_SERVICES_JUSTSERVE_TOKEN }} JAVA_HOME: ${{ secrets.JAVA_HOME }} steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af8cd1e..339e76e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,16 +16,16 @@ Prerequisites: clone the codebase, then cd into the repo and install the dependencies with gradle ```sh -git clone git@github.com:JustServe-Resources/cli.git +git clone git@github.com:JustServe-Resources/devkit.git ./gradlew assemble ``` ## Branch off of main -It's a good practice not to put your changes in the main branch. Branch naming conventions aren't enforced, naming my branches with a `tag`/`task` convention are suggested, similar to [Conventional Commits] naming strategy. +Don't make any changes to the main branch - these will be denied even if you try to. Branch naming conventions aren't enforced, though naming branches with a `my-last-name`/`task-name` convention isn't a bad idea. -See our [style guide](https://github.com/Graqr#general-styling-guide) for supported coding practices. This project enforces [Conventional Commits], which is checked with each commit. +Most importantly, see our [style guide](https://github.com/Graqr#general-styling-guide) for our coding standards. This project enforces [Conventional Commits], which is checked with each commit. ## Test your change Adequate acceptance testing is to be included with pull requests for new code. See our [style guide] for our testing standards. A portion of the codebase is generated during the build process. Using gradle's `build` task will both assemble and run tests. @@ -35,12 +35,13 @@ Adequate acceptance testing is to be included with pull requests for new code. S ``` ## Validate this builds properly -This project compiles to a native executable specific to your OS. This is different from a normal java build process. Compiling this repo into an executable is not a short process. See [graal's docs] for options like quick build mode +This Micronaut application supports AOT (Ahead-of-Time) compilation to a GraalVM Native Image. This process produces a platform-specific binary with instant startup and lower memory overhead, though the compilation itself is resource-intensive. See [graal's docs] for optimization flags like quick build mode. +Running the test suite prior to the native build allows GraalVM to leverage profile-guided data for a more performant executable. ```sh -./gradlew nativeCompile +./gradlew :cli:test :cli:nativeCompile ``` > [!NOTE] -> This build may pass on your OS but may fail on the other OS for which this cli compiles. These will be built and tested during PR checks +> This build may pass on your OS but may fail on another OS for which this cli compiles. These will be built and tested during PR checks ## Submit a pull request diff --git a/README.md b/README.md index 05cce52..9d24d92 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,22 @@ -## JustServe Cli Tool +# JustServe Resources -The JustServe Cli tool is an admin tool for JustServe Specialists and administrators. +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=coverage)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=bugs)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=JustServe-Resources_cli&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=JustServe-Resources_cli) -This tool is very much under development and whose api is subject to change with each release. Standard versioning is used for this project to delineate breaking releases. +This repository is architected as a modular, multi-module Micronaut project. At its center is the `core` module, a +reusable library providing a thoroughly tested HTTP client for the JustServe API. + +Other modules leverage the `core` library to build specific applications. The `cli` module, for instance, is a GraalVM native command-line application that consumes the `core` client to provide administrative tooling. + +As the project evolves, we adhere to semantic versioning. The API is subject to change, and any breaking modifications will be clearly communicated through major version increments. + +## Cli Tool ### Install @@ -37,7 +51,7 @@ echo $env:java_home -To generate the executable for your system, run `./gradlew nativeCompile`. The executable will be generated in the build directory (`\build\native\nativeCompile\`). +To generate the executable for your system, run `./gradlew :cli:nativeCompile`. The executable will be generated in the build directory (`cli/build/native/nativeCompile/`). ### Authenticate diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 3cf53c2..09082f5 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -33,15 +33,39 @@ The name of the game is readability and [testability]. The following styling spe ### Testing +- Tests should never run against production. +- Use your development environment's auth token assigned to the `JUSTSERVE_TOKEN` environment variable in tests. - Methods and Features are to have [adequate] unit and integration tests written before any pull request can be accepted. - Because we use lombok, we don't need to test setters and getters. Using getters and setters is the preferred way to access class fields. - Unit test count is to scale appropriately according to the complexity of the method. - Features are to have [adequate] integration and end-to-end tests. - Fixes are to have [adequate] unit, integration and end-to-end tests included with the fix for the sake of [regression testing]. - Tests should only test one thing - - e.g. `Set store location with zip code.` - - e.g. `Fail to set store location using invalid zip code` - - e.g. `Set store location by city name` + - e.g. `Set project owner.` + - e.g. `Can NOT to set project owner with invalid UUID` + - e.g. `Can update an Org description` +- Use data-driven testing to validate logic across all permutations of documented behavior like all `EventType` variants below. + ```groovy + @Unroll("can set contact info for #eventType.name() event") + def "can set contact info for #eventType event"() { + given: + def event = baseEventBuilder() + .contactEmail(faker.internet().emailAddress()) + .contactName(faker.name().fullName()) + .contactPhone(faker.phoneNumber().phoneNumber()) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] + } + ``` #### Adequate Testing Coverage Adequate testing is determined by the method's documentation (this is why all methods require docs). Testing is surgical and specific; test exactly what is documented, no more and no less. If it's in the docs, then [test it]! The only exception to this is that [branches of code] should be covered in testing, which may not be documented. diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b8db30f..6327199 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -22,15 +22,16 @@ dependencies { annotationProcessor("io.micronaut.serde:micronaut-serde-processor") implementation("io.micronaut.serde:micronaut-serde-jackson") implementation("io.micronaut:micronaut-retry") - implementation("org.simplejavamail:simple-java-mail:8.12.6") - implementation("org.jsoup:jsoup:1.21.2") + implementation("org.simplejavamail:simple-java-mail:${project.properties["simpleJavaMailVersion"]}") + implementation("org.jsoup:jsoup:${project.properties["jsoupVersion"]}") + implementation("io.micronaut.reactor:micronaut-reactor") compileOnly("io.micronaut:micronaut-http-client") compileOnly("io.micronaut.openapi:micronaut-openapi-annotations") compileOnly("org.projectlombok:lombok") runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.yaml:snakeyaml") - testImplementation("net.datafaker:datafaker:2.5.1") - testImplementation("org.apache.commons:commons-lang3:3.20.0") + testImplementation("net.datafaker:datafaker:${project.properties["datafakerVersion"]}") + testImplementation("org.apache.commons:commons-lang3:${project.properties["commonsLang3Version"]}") testImplementation("io.micronaut:micronaut-http-client") } @@ -42,7 +43,7 @@ java { micronaut { testRuntime("spock2") openapi { - version = "6.20.0" + version = project.properties["micronautOpenapiSpecVersion"] as String client(file("src/main/resources/schema.yml")) { apiPackageName = "org.justserve.client" modelPackageName = "org.justserve.model" diff --git a/core/src/main/java/org/justserve/auth/JustServeClientFilter.java b/core/src/main/java/org/justserve/auth/JustServeClientFilter.java index 6a6b425..c334e72 100644 --- a/core/src/main/java/org/justserve/auth/JustServeClientFilter.java +++ b/core/src/main/java/org/justserve/auth/JustServeClientFilter.java @@ -5,6 +5,7 @@ import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.annotation.ClientFilter; import io.micronaut.http.annotation.RequestFilter; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,11 +18,10 @@ @SuppressWarnings("unused") @ClientFilter("/**") @Requires(property = "justserve.token") +@Slf4j public class JustServeClientFilter { private final String token; - private final Logger log = LoggerFactory.getLogger(JustServeClientFilter.class); - /** * Constructs a new JustServeClientFilter. * @@ -43,7 +43,9 @@ public void doFilter(MutableHttpRequest request) { log.debug("Skipping bearer token for login request ({})", request.getMethod() + " " + request.getUri()); return; } - + if (token.isEmpty()) { + log.warn("justserve.token is not set"); + } log.debug("adding bearer token to request ({})", request.getMethod() + " " + request.getUri()); request.bearerAuth(token); } diff --git a/core/src/main/java/org/justserve/client/GraphQLErrorClientFilter.java b/core/src/main/java/org/justserve/client/GraphQLErrorClientFilter.java new file mode 100644 index 0000000..d45138e --- /dev/null +++ b/core/src/main/java/org/justserve/client/GraphQLErrorClientFilter.java @@ -0,0 +1,46 @@ +package org.justserve.client; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.HttpClientFilter; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.Optional; + +/** + * GraphQL calls return 200 even if errors occur. This filter marks the response as a failure and maps the server's + * error message to an{@link HttpClientResponseException}. This does not change the status code. + * + * @author jonathan zollinger + * @since 0.1.0 + */ +@Filter("/**/graphql") +@Slf4j +public class GraphQLErrorClientFilter implements HttpClientFilter { + + @Override + public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + + return Mono.from(chain.proceed(request)).map(response -> { + Optional bodyOpt = response.getBody(Map.class); + + if (bodyOpt.isPresent()) { + Map body = bodyOpt.get(); + String err = "errors"; + if (body.containsKey(err) && body.get(err) != null) { + Object errors = body.get(err); + String errorMessage = "GraphQL returned errors: " + errors.toString(); + throw new HttpClientResponseException(errorMessage, response); + } + log.debug("GraphQL request contains no errors"); + } + return response; + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/justserve/client/RetryClientFilter.java b/core/src/main/java/org/justserve/client/RetryClientFilter.java new file mode 100644 index 0000000..735c98f --- /dev/null +++ b/core/src/main/java/org/justserve/client/RetryClientFilter.java @@ -0,0 +1,51 @@ +package org.justserve.client; + +import io.micronaut.context.annotation.Value; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.HttpClientFilter; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; + +/** + * Adds a retry to any 500 errors. The micronaut generator doesn't support this yet, so I'm adding it here instead + *
+ * You can control the retry behavior by setting the {@code micronaut.http.client.retry-attempts} + * and {@code micronaut.http.client.retry-timeout} environment variables or configuration properties. + * The default retry count is 3 and the default retry timeout is 1 second. + * + * @since 0.1.0 + * @author jonathan zollinger + */ +@Filter("/**") +public class RetryClientFilter implements HttpClientFilter { + + @Value("${micronaut.http.client.retry-attempts:3}") + int retryAttempts; + + @Value("${micronaut.http.client.retry-timeout:1s}") + Duration retryTimeout; + + @Override + public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + + return Mono.from(chain.proceed(request)) + .retryWhen(Retry.backoff(retryAttempts, retryTimeout) + .filter(throwable -> { + if (throwable instanceof HttpClientResponseException e) { + return 500 == e.getStatus().getCode(); + } + return true; + }) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> + retrySignal.failure() + ) + ); + } +} \ No newline at end of file diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml index 9bb0298..13e176b 100644 --- a/core/src/main/resources/application.yml +++ b/core/src/main/resources/application.yml @@ -10,6 +10,7 @@ micronaut: connect-timeout: 5s request-timeout: 30s retryAttempts: 3 + retry-timeout: 1s pool: enabled: true acquire-timeout: 30s diff --git a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy index 3cc02c5..d667a60 100644 --- a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy +++ b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy @@ -1,5 +1,6 @@ package org.justserve +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject import net.datafaker.Faker @@ -22,11 +23,10 @@ class GraphQLClientSpec extends Specification { @Shared Faker faker = new Faker() - @Shared Map projectIds = [:] - def setupSpec() { + def setup() { EventType.values().each { type -> def project = client.createProject(new GraphQLCreateProjectVariables() .setTitle("Test Project - ${type.name()}") @@ -47,11 +47,10 @@ class GraphQLClientSpec extends Specification { .setRedirect(redirect) when: - def response = client.createProject(args) + client.createProject(args) then: noExceptionThrown() - !response.hasErrors() where: [eventType, locationType, redirect] << [EventType.values(), ProjectLocationType.values(), ["", null, "https://google.com"]].combinations() @@ -74,11 +73,10 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() where: eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] @@ -93,11 +91,10 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() where: eventType << [EventType.MultipleDTL] @@ -113,14 +110,13 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() - where: // error reads "Only a multiple DTL project can have more than one event" - eventType << [/*EventType.DTL,*/ EventType.Ongoing, EventType.MultipleDTL] + where: + eventType << [EventType.Ongoing, EventType.MultipleDTL] } @Unroll("can set location info for #eventType.name() event") @@ -133,14 +129,13 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() - where: //error reads "Only a multiple DTL project can have more than one event, StackTrace= at JustServe.Mediators.ProjectEvents.ProjectEventMediator.InternalCreateEvent(Project project, UpdateProjectEvent updateProjectEvent, SecurityContext securityContext) in /src/src/JustServe.Mediators/ProjectEvents/ProjectEventMediator.cs:line 109" - eventType << [EventType.DTL, /*EventType.Ongoing,*/ EventType.MultipleDTL] + where: + eventType << [EventType.DTL, EventType.MultipleDTL] } @Unroll("can set schedule info for #eventType.name() event") @@ -154,11 +149,10 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() where: eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] @@ -174,11 +168,10 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() where: eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] @@ -196,11 +189,10 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: noExceptionThrown() - !response.hasErrors() where: eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] @@ -213,16 +205,35 @@ class GraphQLClientSpec extends Specification { def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) when: - def response = client.createEvent(new CreateEventMutation(vars)) + client.createEvent(new CreateEventMutation(vars)) then: - response.hasErrors() + thrown(HttpClientResponseException) where: eventType << [EventType.Recurring] } - @Unroll("can add multiple events only for #eventType.name() projects (shouldFail: #shouldFail)") + @Unroll("can NOT add multiple events only for #eventType.name() projects") + def "can add multiple events only for Multi-DTL projects"() { + given: + def firstEvent = baseEventBuilder().build() + def secondEvent = baseEventBuilder().build() + def firstVars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(firstEvent) + def secondVars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(secondEvent) + + when: + client.createEvent(new CreateEventMutation(firstVars)) + client.createEvent(new CreateEventMutation(secondVars)) + + then: + thrown(HttpClientResponseException) + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.Recurring] + } + + @Unroll("can add multiple events only for #eventType.name() project") def "can add multiple events only for Multi-DTL projects"() { given: def firstEvent = baseEventBuilder().build() @@ -232,31 +243,21 @@ class GraphQLClientSpec extends Specification { when: client.createEvent(new CreateEventMutation(firstVars)) - def secondResponse = client.createEvent(new CreateEventMutation(secondVars)) + client.createEvent(new CreateEventMutation(secondVars)) then: - secondResponse.hasErrors() == shouldFail + noExceptionThrown() where: - eventType | shouldFail - EventType.DTL | true - EventType.Ongoing | true - EventType.Recurring | true - EventType.MultipleDTL | false + eventType << [EventType.MultipleDTL] } def "can create multiple events at once for Multi-DTL projects using createEvents mutation"() { given: - def event1 = baseEventBuilder() - .shiftTitle("Morning Shift") - .build() - def event2 = baseEventBuilder() - .shiftTitle("Afternoon Shift") - .build() - def event3 = baseEventBuilder() - .shiftTitle("Evening Shift") - .build() - def vars = new CreateEventsVariables(projectIds[EventType.MultipleDTL], event1, event2, event3) + def thisEvent = baseEventBuilder().shiftTitle("Morning Shift").build() + def thatEvent = baseEventBuilder().shiftTitle("Afternoon Shift").build() + def event3 = baseEventBuilder().shiftTitle("Evening Shift").build() + def vars = new CreateEventsVariables(projectIds[EventType.MultipleDTL], thisEvent, thatEvent, event3) def mutation = new CreateEventsMutation(vars) when: @@ -264,7 +265,6 @@ class GraphQLClientSpec extends Specification { then: noExceptionThrown() - !response.hasErrors() and: "the response data should contain the newly created events" null != response.getData() @@ -272,16 +272,8 @@ class GraphQLClientSpec extends Specification { def "can create multiple recurring events at once for Recurring projects using createRecurringEvents mutation"() { given: - def time1 = new ProjectRecurringTime() - .setStartTime("10:00") - .setEndTime("12:00") - .setRecurringType(RecurringType.WEEKLY) - .setMonday(true) - def time2 = new ProjectRecurringTime() - .setStartTime("14:00") - .setEndTime("16:00") - .setRecurringType(RecurringType.MONTHLY) - .setThirdWeek(true) + def time1 = new ProjectRecurringTime().setStartTime("10:00").setEndTime("12:00").setRecurringType(RecurringType.WEEKLY).setMonday(true) + def time2 = new ProjectRecurringTime().setStartTime("14:00").setEndTime("16:00").setRecurringType(RecurringType.MONTHLY).setThirdWeek(true) def vars = new CreateRecurringEventsVariables(projectIds[EventType.Recurring], time1, time2) def mutation = new CreateRecurringEventsMutation(vars) @@ -290,7 +282,6 @@ class GraphQLClientSpec extends Specification { then: noExceptionThrown() - !response.hasErrors() and: "the response data should contain the newly created recurring events" null != response.getData() @@ -368,6 +359,7 @@ class GraphQLClientSpec extends Specification { !response.hasErrors() } + @SuppressWarnings("GroovyAssignabilityCheck") def "test searchOrganization parses the input correctly"(String searchTerm, Boolean includesAll) { given: diff --git a/core/src/test/groovy/org/justserve/GraphQLErrorClientFilterSpec.groovy b/core/src/test/groovy/org/justserve/GraphQLErrorClientFilterSpec.groovy new file mode 100644 index 0000000..d4c47e5 --- /dev/null +++ b/core/src/test/groovy/org/justserve/GraphQLErrorClientFilterSpec.groovy @@ -0,0 +1,59 @@ +package org.justserve + +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import net.datafaker.Faker +import org.justserve.client.GraphQLClient +import org.justserve.model.EventType +import org.justserve.model.GraphQLCreateProjectVariables +import org.justserve.model.ProjectLocationType +import org.justserve.model.ProjectRecurringTime +import org.justserve.model.graph.CreateRecurringEventsMutation +import org.justserve.model.graph.CreateRecurringEventsVariables +import spock.lang.Shared +import spock.lang.Specification + +@MicronautTest +class GraphQLErrorClientFilterSpec extends Specification { + + @Shared + @Inject + GraphQLClient client + + @Shared + Faker faker = new Faker() + + @SuppressWarnings("GroovyAssignabilityCheck") + void "can create Project with EventType: #eventType, LocationType: #locationType, and Redirect: #redirect"(EventType eventType, ProjectLocationType locationType, String redirect) { + given: + GraphQLCreateProjectVariables args = new GraphQLCreateProjectVariables() + .setEventType(eventType) + .setLocationType(locationType) + .setTitle("this is my title") + .setRedirect(redirect) + + when: + def response = client.createProject(args) + + then: + noExceptionThrown() + + when: "create an incompatible event" + response.getData().getCreateProject() + client.createRecurringEvents(new CreateRecurringEventsMutation(new CreateRecurringEventsVariables(response.getData().getCreateProject().getId(), new ProjectRecurringTime().setEndTime("whenever")))) + + then: + HttpClientResponseException error = thrown(HttpClientResponseException) + + and: + verifyAll { + error.getMessage().length() > 0 + error.getClass() == HttpClientResponseException.class + } + + + where: + [eventType, locationType, redirect] << [EventType.DTL, ProjectLocationType.values(), ["", null, "https://google.com"]].combinations() + } +} \ No newline at end of file diff --git a/core/src/test/groovy/org/justserve/JustServeSpec.groovy b/core/src/test/groovy/org/justserve/JustServeSpec.groovy index b5defb5..545c706 100644 --- a/core/src/test/groovy/org/justserve/JustServeSpec.groovy +++ b/core/src/test/groovy/org/justserve/JustServeSpec.groovy @@ -6,6 +6,7 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.retry.annotation.Retryable import io.micronaut.test.extensions.spock.annotation.MicronautTest import net.datafaker.Faker import org.apache.commons.lang3.RandomStringUtils @@ -66,12 +67,9 @@ class JustServeSpec extends Specification { def setupSpec() { faker = new Faker() - // if (null != System.getenv("JUSTSERVE_TOKEN")) { - // throw new IllegalStateException("JUSTSERVE_TOKEN is set. Do not define this variable in testing.") - // } ctx = ApplicationContext.builder() .environments(Environment.CLI, Environment.TEST) - .properties(["justserve.token": System.getenv("TEST_TOKEN")]) + .properties(["justserve.token": System.getProperty("justserve.token") ?: System.getenv("JUSTSERVE_TOKEN")]) .build() .start() noAuthCtx = ApplicationContext @@ -90,8 +88,6 @@ class JustServeSpec extends Specification { adminUserClient = ctx.getBean(UserClient) readOnlyUser = new TestUser(new Faker(Locale.of("en-us"))) projectClient = ctx.getBean(ProjectClient) - - // TODO: validate the user does not already exist (use the admin client user search) String customRandomEmail = RandomStringUtils.insecure().nextAlphanumeric(20) + "@fake.com" readOnlyUser.uuid = createUserFromFaker(noAuthUserClient, readOnlyUser, customRandomEmail).body().getId() readOnlyUser.email = customRandomEmail @@ -99,8 +95,9 @@ class JustServeSpec extends Specification { } void cleanupSpec() { - noAuthCtx.stop() - ctx.stop() + [noAuthCtx, ctx].each { context -> + try { context?.stop() } catch (Exception ignored) {} + } } HttpResponse createUser(UserClient client = noAuthUserClient) { @@ -120,6 +117,7 @@ class JustServeSpec extends Specification { return response } + @Retryable private static HttpResponse createUserFromFaker(UserClient client, TestUser user, String uniqueEmailInput = null) { String email = uniqueEmailInput ?: RandomStringUtils.insecure().nextAlphanumeric(20) + "@fake.com" MultipartBody requestBody = MultipartBody.builder() diff --git a/core/src/test/resources/README.md b/core/src/test/resources/README.md deleted file mode 100644 index 7286b47..0000000 --- a/core/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/gradle.properties b/gradle.properties index c118158..4d88273 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,11 @@ micronautVersion=4.8.3 justserveCliVersion=0.1.0-SNAPSHOT org.gradle.console=rich +micronautLibraryVersion=4.5.3 +micronautOpenapiVersion=4.5.3 +jarTestVersion=1.1.0 +simpleJavaMailVersion=8.12.6 +jsoupVersion=1.21.2 +datafakerVersion=2.5.1 +commonsLang3Version=3.20.0 +micronautOpenapiSpecVersion=6.20.0