From 10459f06187bf7863850436e28755cb6c50e9624 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 15 Aug 2023 15:57:02 +0200 Subject: [PATCH] Improve tests and so on. --- .fernignore | 11 +- .github/workflows/check-updates.yml | 23 ++++ .github/workflows/ci.yml | 37 ++++++- .../java/com/squidex/api/AccessToken.java | 37 +++++++ .../java/com/squidex/api/AuthInterceptor.java | 84 +++++++++++++++ .../com/squidex/api/InMemoryTokenStore.java | 20 ++++ .../squidex/api/SquidexApiClientBuilder.java | 102 ++++++++++++++++-- src/main/java/com/squidex/api/TokenStore.java | 10 ++ .../com/squidex/api/core/ClientOptions.java | 15 ++- .../java/com/squidex/api/ClientProvider.java | 23 ++++ .../java/com/squidex/api/SchemasTests.java | 2 +- src/test/java/com/squidex/api/TestClient.java | 8 -- src/test/java/com/squidex/api/TestSetup.java | 7 ++ .../com/squidex/api/UserManagementTests.java | 2 +- src/test/java/com/squidex/api/Utils.java | 5 - 15 files changed, 353 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/check-updates.yml create mode 100644 src/main/java/com/squidex/api/AccessToken.java create mode 100644 src/main/java/com/squidex/api/AuthInterceptor.java create mode 100644 src/main/java/com/squidex/api/InMemoryTokenStore.java create mode 100644 src/main/java/com/squidex/api/TokenStore.java create mode 100644 src/test/java/com/squidex/api/ClientProvider.java delete mode 100644 src/test/java/com/squidex/api/TestClient.java diff --git a/.fernignore b/.fernignore index f1ea577..b894140 100644 --- a/.fernignore +++ b/.fernignore @@ -1,17 +1,14 @@ README.md # Wrappers -ClientOptions.java +**/ClientOptions.java -AccessToken.java -AuthInterceptor.java -InMemoryTokenStore.java -SquidexApiClient.java -SquidexApiClientBuilder.java -TokenStore.java +# Client files +src/main/java/com/squidex/api/*.* # Workflows .github/workflows/check-updates.yml +.github/workflows/ci.yml .github/workflows/test.yml #tests diff --git a/.github/workflows/check-updates.yml b/.github/workflows/check-updates.yml new file mode 100644 index 0000000..813c791 --- /dev/null +++ b/.github/workflows/check-updates.yml @@ -0,0 +1,23 @@ +name: Check Update +concurrency: check + +on: + workflow_dispatch: + schedule: + # Automatically run on every Sunday + - cron: '0 0 * * 0' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.WORKFLOW_SECRET }} + + - name: Check for Update + uses: saadmk11/github-actions-version-updater@v0.7.4 + with: + token: ${{ secrets.WORKFLOW_SECRET }} + release_types: 'major' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0414818..1ed1c74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,25 @@ jobs: java-version: "11" architecture: x64 + - name: Test - Start Compose + run: docker-compose up -d + working-directory: tests + - name: Test run: ./gradlew test + + - name: Test - Dump docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 + with: + images: 'squidex,squidex/resizer' + tail: '100' + + - name: Test - Cleanup + if: always() + run: docker-compose down + working-directory: tests + publish: needs: [ compile, test ] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') @@ -52,10 +69,26 @@ jobs: java-version: "11" architecture: x64 + - name: Test - Start Compose + run: docker-compose up -d + working-directory: tests + - name: Publish to maven run: | - ./gradlew publish + ./gradlew publish env: MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - MAVEN_PUBLISH_REGISTRY_URL: "https://s01.oss.sonatype.org/content/repositories/releases/" \ No newline at end of file + MAVEN_PUBLISH_REGISTRY_URL: "https://s01.oss.sonatype.org/content/repositories/releases/" + + - name: Test - Dump docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2.2.1 + with: + images: 'squidex,squidex/resizer' + tail: '100' + + - name: Test - Cleanup + if: always() + run: docker-compose down + working-directory: tests \ No newline at end of file diff --git a/src/main/java/com/squidex/api/AccessToken.java b/src/main/java/com/squidex/api/AccessToken.java new file mode 100644 index 0000000..3a023d3 --- /dev/null +++ b/src/main/java/com/squidex/api/AccessToken.java @@ -0,0 +1,37 @@ +package com.squidex.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; + +public final class AccessToken { + private final String accessToken; + private final int expiresIn; + private final Instant expiresAt; + + @JsonCreator() + public AccessToken( + @JsonProperty("access_token")String accessToken, + @JsonProperty("expires_in")int expiresIn) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.expiresAt = Instant.now().plusSeconds(expiresIn); + } + + + public String getAccessToken() { + return accessToken; + } + + + public int getExpiresIn() { + return expiresIn; + } + + @JsonIgnore() + public Instant getExpiresAt() { + return expiresAt; + } +} diff --git a/src/main/java/com/squidex/api/AuthInterceptor.java b/src/main/java/com/squidex/api/AuthInterceptor.java new file mode 100644 index 0000000..e2f5bc0 --- /dev/null +++ b/src/main/java/com/squidex/api/AuthInterceptor.java @@ -0,0 +1,84 @@ +package com.squidex.api; + +import com.squidex.api.core.Environment; +import com.squidex.api.core.ObjectMappers; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; + +public final class AuthInterceptor implements Interceptor { + private final Environment environment; + private final String clientId; + private final String clientSecret; + private final TokenStore tokenStore; + private final OkHttpClient httpClient; + + public AuthInterceptor(OkHttpClient httpClient, Environment environment, String clientId, String clientSecret, TokenStore tokenStore) { + this.httpClient = httpClient; + this.environment = environment; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.tokenStore = tokenStore; + } + + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request originalRequest = chain.request(); + + AccessToken token = this.tokenStore.get(); + if (token != null && token.getExpiresAt().isBefore(Instant.now())) { + // The token has been expired, therefore also remove it from the store for other calls. + token = null; + this.tokenStore.clear(); + } + + if (token == null) { + token = acquireToken(); + this.tokenStore.set(token); + } + + Request requestWithHeader = originalRequest.newBuilder() + .header("Authorization", String.format("Bearer %s", token.getAccessToken())) + .build(); + + Response response = chain.proceed(requestWithHeader); + if (response.code() == 401) { + this.tokenStore.clear(); + + return intercept(chain); + } + + return response; + } + + private AccessToken acquireToken() throws IOException { + RequestBody formBody = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", this.clientId) + .add("client_secret", this.clientSecret) + .add("scope", "squidex-api") + .build(); + + HttpUrl tokenUrl = Objects.requireNonNull(HttpUrl.parse(this.environment.getUrl())) + .newBuilder() + .addPathSegments("identity-server/connect/token") + .build(); + + Request tokenRequest = new Request.Builder() + .url(tokenUrl.url()) + .post(formBody) + .build(); + + AccessToken token; + try (Response response = this.httpClient.newCall(tokenRequest).execute()) { + assert response.body() != null; + token = ObjectMappers.JSON_MAPPER.readValue(response.body().string(), AccessToken.class); + } + + return token; + } +} \ No newline at end of file diff --git a/src/main/java/com/squidex/api/InMemoryTokenStore.java b/src/main/java/com/squidex/api/InMemoryTokenStore.java new file mode 100644 index 0000000..1d074e9 --- /dev/null +++ b/src/main/java/com/squidex/api/InMemoryTokenStore.java @@ -0,0 +1,20 @@ +package com.squidex.api; + +public class InMemoryTokenStore implements TokenStore { + private AccessToken currentToken; + + @Override + public AccessToken get() { + return this.currentToken; + } + + @Override + public void set(AccessToken token) { + this.currentToken = token; + } + + @Override + public void clear() { + this.currentToken = null; + } +} diff --git a/src/main/java/com/squidex/api/SquidexApiClientBuilder.java b/src/main/java/com/squidex/api/SquidexApiClientBuilder.java index fc6ea73..6d4bced 100644 --- a/src/main/java/com/squidex/api/SquidexApiClientBuilder.java +++ b/src/main/java/com/squidex/api/SquidexApiClientBuilder.java @@ -2,16 +2,21 @@ import com.squidex.api.core.ClientOptions; import com.squidex.api.core.Environment; +import okhttp3.OkHttpClient; + +import javax.net.ssl.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; public final class SquidexApiClientBuilder { - private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + private final ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); private Environment environment = Environment.DEFAULT; - - public SquidexApiClientBuilder token(String token) { - this.clientOptionsBuilder.addHeader("Authorization", "Bearer " + token); - return this; - } + private String clientId; + private String clientSecret; + private TokenStore tokenStore; + private OkHttpClient httpClient; + private boolean trustAllCerts; public SquidexApiClientBuilder environment(Environment environment) { this.environment = environment; @@ -23,13 +28,96 @@ public SquidexApiClientBuilder url(String url) { return this; } + public SquidexApiClientBuilder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public String clientId() { + return this.clientId; + } + + public SquidexApiClientBuilder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public String clientSecret() { + return this.clientSecret; + } + + public SquidexApiClientBuilder tokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + return this; + } + + public SquidexApiClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + public SquidexApiClientBuilder appName(String appName) { - clientOptionsBuilder.appName(appName); + this.clientOptionsBuilder.appName(appName); + return this; + } + + public SquidexApiClientBuilder trustAllCerts() { + this.trustAllCerts = true; return this; } public SquidexApiClient build() { clientOptionsBuilder.environment(this.environment); + + if (this.tokenStore == null) { + this.tokenStore = new InMemoryTokenStore(); + } + + if (this.httpClient == null) { + this.httpClient = new OkHttpClient(); + } + + if (this.trustAllCerts) { + X509TrustManager trustAllCerts = new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[] {}; + } + }; + + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[] { trustAllCerts }, new java.security.SecureRandom()); + + this.httpClient = this.httpClient.newBuilder() + .sslSocketFactory(sslContext.getSocketFactory(), trustAllCerts) + .build(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + AuthInterceptor interceptor = new AuthInterceptor( + this.httpClient, + this.environment, + this.clientId, + this.clientSecret, + this.tokenStore); + + this.httpClient = this.httpClient.newBuilder() + .addInterceptor(interceptor) + .build(); + + clientOptionsBuilder.httpClient(this.httpClient); + return new SquidexApiClient(clientOptionsBuilder.build()); } } diff --git a/src/main/java/com/squidex/api/TokenStore.java b/src/main/java/com/squidex/api/TokenStore.java new file mode 100644 index 0000000..4b95ef6 --- /dev/null +++ b/src/main/java/com/squidex/api/TokenStore.java @@ -0,0 +1,10 @@ +package com.squidex.api; + +public interface TokenStore { + AccessToken get(); + + void set(AccessToken token); + + void clear(); +} + diff --git a/src/main/java/com/squidex/api/core/ClientOptions.java b/src/main/java/com/squidex/api/core/ClientOptions.java index 06f542c..3398069 100644 --- a/src/main/java/com/squidex/api/core/ClientOptions.java +++ b/src/main/java/com/squidex/api/core/ClientOptions.java @@ -29,7 +29,7 @@ private ClientOptions( "X-Fern-SDK-Name", "com.squidex.fern:api-sdk", "X-Fern-SDK-Version", - "0.0.8", + "0.0.7", "X-Fern-Language", "JAVA")); this.headerSuppliers = headerSuppliers; @@ -67,6 +67,8 @@ public static Builder builder() { public static final class Builder { private Environment environment; + private OkHttpClient httpClient; + private final Map headers = new HashMap<>(); private final Map> headerSuppliers = new HashMap<>(); @@ -93,8 +95,17 @@ public Builder appName(String appName) { return this; } + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + public ClientOptions build() { - return new ClientOptions(environment, headers, headerSuppliers, new OkHttpClient(), this.appName); + if (this.httpClient == null) { + this.httpClient = new OkHttpClient(); + } + + return new ClientOptions(environment, headers, headerSuppliers, this.httpClient, this.appName); } } } diff --git a/src/test/java/com/squidex/api/ClientProvider.java b/src/test/java/com/squidex/api/ClientProvider.java new file mode 100644 index 0000000..0836755 --- /dev/null +++ b/src/test/java/com/squidex/api/ClientProvider.java @@ -0,0 +1,23 @@ +package com.squidex.api; + +public final class ClientProvider { + private final SquidexApiClientBuilder builder; + private final SquidexApiClient client; + + public ClientProvider(SquidexApiClientBuilder builder, SquidexApiClient client) { + this.builder = builder; + this.client = client; + } + + public SquidexApiClient client(String appName) { + return this.builder.appName(appName).build(); + } + + public SquidexApiClient client() { + return client; + } + + public SquidexApiClientBuilder builder() { + return builder; + } +} diff --git a/src/test/java/com/squidex/api/SchemasTests.java b/src/test/java/com/squidex/api/SchemasTests.java index 098c1fc..f8eed5b 100644 --- a/src/test/java/com/squidex/api/SchemasTests.java +++ b/src/test/java/com/squidex/api/SchemasTests.java @@ -15,7 +15,7 @@ public class SchemasTests extends TestBase { @Test public void should_create_and_fetch_schema() { CreateSchemaDto request = CreateSchemaDto.builder() - .name("schema-%s".formatted(UUID.randomUUID())) + .name(String.format("schema-%%s%s", UUID.randomUUID())) .fields(Collections.singletonList( UpsertSchemaFieldDto.builder() .name("field1") diff --git a/src/test/java/com/squidex/api/TestClient.java b/src/test/java/com/squidex/api/TestClient.java deleted file mode 100644 index 5e2fe67..0000000 --- a/src/test/java/com/squidex/api/TestClient.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.squidex.api; - -public final class TestClient { - public void test() { - // Add tests here and mark this file in .fernignore - assert true; - } -} diff --git a/src/test/java/com/squidex/api/TestSetup.java b/src/test/java/com/squidex/api/TestSetup.java index 623f685..32a1d73 100644 --- a/src/test/java/com/squidex/api/TestSetup.java +++ b/src/test/java/com/squidex/api/TestSetup.java @@ -1,5 +1,6 @@ package com.squidex.api; +import com.squidex.api.core.ApiError; import com.squidex.api.resources.apps.requests.CreateAppDto; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -53,6 +54,12 @@ private void waitForServer() throws InterruptedException { try { client.client().ping().getPing(); break; + } catch (ApiError ex) { + if (ex.statusCode() == 400) { + break; + } else { + throw ex; + } } catch (Exception ex) { if (expires.isBefore(Instant.now())) { throw new Error(String.format("Cannot connect to test system with: %s.", ex.getMessage())); diff --git a/src/test/java/com/squidex/api/UserManagementTests.java b/src/test/java/com/squidex/api/UserManagementTests.java index 41e27e1..b05c05f 100644 --- a/src/test/java/com/squidex/api/UserManagementTests.java +++ b/src/test/java/com/squidex/api/UserManagementTests.java @@ -13,7 +13,7 @@ public class UserManagementTests extends TestBase { @Test() public void Should_create_and_fetch_user() { - String email = "user%s@email.com".formatted(UUID.randomUUID()); + String email = String.format("user%s@email.com", UUID.randomUUID()); CreateUserDto request = CreateUserDto.builder() .email(email) diff --git a/src/test/java/com/squidex/api/Utils.java b/src/test/java/com/squidex/api/Utils.java index 428abfc..1d7d4d6 100644 --- a/src/test/java/com/squidex/api/Utils.java +++ b/src/test/java/com/squidex/api/Utils.java @@ -44,8 +44,3 @@ public static ClientProvider getClient() { } } -record ClientProvider(SquidexApiClientBuilder builder, SquidexApiClient client) { - public SquidexApiClient client(String appName) { - return this.builder.appName(appName).build(); - } -} \ No newline at end of file