diff --git a/license-generator/pom.xml b/license-generator/pom.xml
index c9a6481..ea5d2f9 100644
--- a/license-generator/pom.xml
+++ b/license-generator/pom.xml
@@ -12,19 +12,17 @@
21
UTF-8
UTF-8
-
0.12.7
1.81
2.19.2
1.5.18
2.0.17
-
- 3.14.0
-
5.13.4
- 3.5.3
0.8.13
1.2.1
+ 3.14.0
+ 3.5.4
+ 3.5.4
@@ -111,22 +109,30 @@
prepare-agent
- prepare-agent
+
+ prepare-agent
+
report
verify
- report
+
+ report
+
prepare-agent-integration
- prepare-agent-integration
+
+ prepare-agent-integration
+
report-integration
verify
- report-integration
+
+ report-integration
+
@@ -141,7 +147,7 @@
maven-failsafe-plugin
- ${maven-surefire-plugin.version}
+ ${maven-failsafe-plugin.version}
diff --git a/license-generator/src/main/java/io/github/bsayli/license/signature/SignatureDemo.java b/license-generator/src/main/java/io/github/bsayli/license/signature/SignatureDemo.java
index e36e592..9249eb4 100644
--- a/license-generator/src/main/java/io/github/bsayli/license/signature/SignatureDemo.java
+++ b/license-generator/src/main/java/io/github/bsayli/license/signature/SignatureDemo.java
@@ -68,17 +68,17 @@ static Optional readOpt(String[] argv, String name) {
private static void printUsage() {
log.info(
- """
+ """
Usage:
# Sign sample payload (encrypted license key variant)
java -cp license-generator.jar io.github.bsayli.license.signature.SignatureDemo \\
--mode sign-sample-key \\
--privateKey
-
+
# Sign sample payload (license token variant)
java -cp license-generator.jar io.github.bsayli.license.signature.SignatureDemo \\
--mode sign-sample-token \\
--privateKey
""");
}
-}
\ No newline at end of file
+}
diff --git a/licensing-service-sdk-cli/README.md b/licensing-service-sdk-cli/README.md
index 79b0dad..67b0833 100644
--- a/licensing-service-sdk-cli/README.md
+++ b/licensing-service-sdk-cli/README.md
@@ -45,7 +45,7 @@ java -jar target/licensing-service-sdk-cli-.jar \
### Options
| Flag | Long Option | Description | Required |
-| ---- | ------------------- | ---------------------------------------------------------------------------- | -------- |
+|------|---------------------|------------------------------------------------------------------------------|----------|
| `-k` | `--key` | License key string (`PREFIX~RANDOM~ENCRYPTED_USER_ID`) | Yes |
| `-s` | `--service-id` | Service identifier (e.g. `crm`) | Yes |
| `-v` | `--service-version` | Service version (e.g. `1.5.0`) | Yes |
@@ -122,7 +122,7 @@ LICENSE_SERVICE_SDK_API_PATH=/v1/licenses/access
## Exit Codes
| Code | Meaning |
-| ---- | ----------------------------------------------- |
+|------|-------------------------------------------------|
| `0` | License validated successfully |
| `1` | License validation failed (client/server error) |
| `2` | CLI usage error |
diff --git a/licensing-service/pom.xml b/licensing-service/pom.xml
index 5fe8500..2c746d5 100644
--- a/licensing-service/pom.xml
+++ b/licensing-service/pom.xml
@@ -126,6 +126,14 @@
test
+
+ com.github.codemonstur
+ embedded-redis
+ 1.4.3
+ test
+
+
+
@@ -150,6 +158,20 @@
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-failsafe-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
org.jacoco
jacoco-maven-plugin
@@ -161,6 +183,14 @@
prepare-agent
+
+
+ prepare-agent-integration
+
+ prepare-agent-integration
+
+
+
report
verify
@@ -168,6 +198,14 @@
report
+
+
+ report-integration
+ verify
+
+ report-integration
+
+
diff --git a/licensing-service/src/test/java/io/github/bsayli/licensing/api/controller/LicenseControllerIT.java b/licensing-service/src/test/java/io/github/bsayli/licensing/api/controller/LicenseControllerIT.java
new file mode 100644
index 0000000..875942e
--- /dev/null
+++ b/licensing-service/src/test/java/io/github/bsayli/licensing/api/controller/LicenseControllerIT.java
@@ -0,0 +1,142 @@
+package io.github.bsayli.licensing.api.controller;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.bsayli.licensing.api.dto.IssueAccessRequest;
+import io.github.bsayli.licensing.api.dto.LicenseAccessResponse;
+import io.github.bsayli.licensing.api.dto.ValidateAccessRequest;
+import io.github.bsayli.licensing.api.exception.LicenseControllerAdvice;
+import io.github.bsayli.licensing.common.i18n.LocalizedMessageResolver;
+import io.github.bsayli.licensing.service.LicenseOrchestrationService;
+import io.github.bsayli.licensing.testconfig.TestControllerMocksConfig;
+import io.github.bsayli.licensing.testconfig.TestWebMvcSecurityConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+@WebMvcTest(controllers = LicenseController.class)
+@Import({
+ LicenseControllerAdvice.class,
+ TestWebMvcSecurityConfig.class,
+ TestControllerMocksConfig.class
+})
+@Tag("integration")
+class LicenseControllerIT {
+
+ @Autowired private MockMvc mvc;
+ @Autowired private ObjectMapper om;
+ @Autowired private LicenseOrchestrationService service;
+ @Autowired private LocalizedMessageResolver messageResolver;
+
+ private static String fakeJwt() {
+ String p1 = "A".repeat(80);
+ String p2 = "b".repeat(80);
+ String p3 = "C".repeat(80);
+ return p1 + "." + p2 + "." + p3;
+ }
+
+ @Test
+ @DisplayName("POST /v1/licenses/access -> 200 returns token")
+ void createAccess_ok() throws Exception {
+ var req =
+ new IssueAccessRequest(
+ "L".repeat(100) + "~rnd~" + "A".repeat(64),
+ "licensing-service~demo~00:11:22:33:44:55",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a5",
+ "crm",
+ "1.5.0",
+ "Q".repeat(60));
+
+ Mockito.when(service.issueAccess(req)).thenReturn(LicenseAccessResponse.created("jwt-token"));
+ Mockito.when(messageResolver.getMessage("license.validation.success"))
+ .thenReturn("License is valid");
+
+ mvc.perform(
+ post("/v1/licenses/access")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(om.writeValueAsBytes(req)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.status").value(200))
+ .andExpect(jsonPath("$.message").value("License is valid"))
+ .andExpect(jsonPath("$.data.licenseToken").value("jwt-token"));
+ }
+
+ @Test
+ @DisplayName("POST /v1/licenses/access -> 400 validation error")
+ void createAccess_validationError() throws Exception {
+ var bad = new IssueAccessRequest("x", "short", null, "c", "1", "y");
+
+ mvc.perform(
+ post("/v1/licenses/access")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(om.writeValueAsBytes(bad)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.message").value("Request validation failed"))
+ .andExpect(jsonPath("$.errors").isArray());
+ }
+
+ @Test
+ @DisplayName("POST /v1/licenses/access/validate -> 200 returns refreshed token")
+ void validateAccess_ok_refreshed() throws Exception {
+ var req =
+ new ValidateAccessRequest(
+ "licensing-service~demo~00:11:22:33:44:55",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8a5",
+ "crm",
+ "1.5.0",
+ "Q".repeat(60));
+
+ String jwt = fakeJwt();
+
+ Mockito.when(service.validateAccess(req, jwt))
+ .thenReturn(LicenseAccessResponse.refreshed("new-jwt"));
+ Mockito.when(messageResolver.getMessage("license.validation.success"))
+ .thenReturn("License is valid");
+
+ mvc.perform(
+ post("/v1/licenses/access/validate")
+ .contentType(MediaType.APPLICATION_JSON)
+ .header("License-Token", jwt)
+ .content(om.writeValueAsBytes(req)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.licenseToken").value("new-jwt"));
+ }
+
+ @Test
+ @DisplayName("POST /v1/licenses/access/validate -> 400 invalid or missing header JWT")
+ void validateAccess_badHeader() throws Exception {
+ var req =
+ new ValidateAccessRequest(
+ "licensing-service~demo~00:11:22:33:44:55", null, "crm", "1.5.0", "Q".repeat(60));
+
+ // Missing header
+ mvc.perform(
+ post("/v1/licenses/access/validate")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(om.writeValueAsBytes(req)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.message").exists())
+ .andExpect(jsonPath("$.errors").isArray());
+
+ // Not 3-part JWT
+ mvc.perform(
+ post("/v1/licenses/access/validate")
+ .contentType(MediaType.APPLICATION_JSON)
+ .header("License-Token", "abc.def")
+ .content(om.writeValueAsBytes(req)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.errors").isArray());
+ }
+}
diff --git a/licensing-service/src/test/java/io/github/bsayli/licensing/cache/CacheConfigIT.java b/licensing-service/src/test/java/io/github/bsayli/licensing/cache/CacheConfigIT.java
new file mode 100644
index 0000000..daa174e
--- /dev/null
+++ b/licensing-service/src/test/java/io/github/bsayli/licensing/cache/CacheConfigIT.java
@@ -0,0 +1,118 @@
+package io.github.bsayli.licensing.cache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.bsayli.licensing.LicensingServiceApplication;
+import io.github.bsayli.licensing.testconfig.EmbeddedRedisConfig;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.assertj.core.api.Assumptions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(
+ classes = {LicensingServiceApplication.class, EmbeddedRedisConfig.class},
+ webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ActiveProfiles("test")
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@Tag("integration")
+class CacheConfigIT {
+
+ private static final String KEY_PREFIX = "it-key-";
+
+ @Autowired CacheManager cacheManager;
+ @Autowired StringRedisTemplate stringRedisTemplate;
+ @Autowired CacheProperties cacheProperties;
+
+ @Test
+ @DisplayName("Configured caches are registered and usable with Redis TTL applied")
+ void cachesRegisteredAndWorkingWithTtl() throws Exception {
+ assertConfiguredCachesRegistered();
+
+ for (Map.Entry e : cacheProperties.caches().entrySet()) {
+ final String cacheName = e.getKey();
+ final CacheProperties.CacheSpec spec = e.getValue();
+
+ Cache cache = cacheManager.getCache(cacheName);
+ assertThat(cache).as("cache bean should exist: %s", cacheName).isNotNull();
+
+ Optional