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 .github/workflows/TestAndCompile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 7 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -37,7 +51,7 @@ echo $env:java_home
</ol>
</details>

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

Expand Down
30 changes: 27 additions & 3 deletions STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
*
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {

return Mono.from(chain.proceed(request)).map(response -> {
Optional<Map> 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;
});
}
}
51 changes: 51 additions & 0 deletions core/src/main/java/org/justserve/client/RetryClientFilter.java
Original file line number Diff line number Diff line change
@@ -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
* <br>
* 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<? extends HttpResponse<?>> 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()
)
);
}
}
1 change: 1 addition & 0 deletions core/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ micronaut:
connect-timeout: 5s
request-timeout: 30s
retryAttempts: 3
retry-timeout: 1s
pool:
enabled: true
acquire-timeout: 30s
Expand Down
Loading
Loading