diff --git a/src/main/java/co/lettermint/models/api/OptionalNullable.java b/src/main/java/co/lettermint/models/api/OptionalNullable.java new file mode 100644 index 0000000..b892b92 --- /dev/null +++ b/src/main/java/co/lettermint/models/api/OptionalNullable.java @@ -0,0 +1,57 @@ +package co.lettermint.models.api; + +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import java.io.IOException; + +@JsonDeserialize(using = OptionalNullable.Deserializer.class) +public final class OptionalNullable { + private final T value; + + private OptionalNullable(T value) { + this.value = value; + } + + public static OptionalNullable of(T value) { + return new OptionalNullable<>(value); + } + + public static OptionalNullable nullValue() { + return new OptionalNullable<>(null); + } + + @JsonValue + public T getValue() { + return value; + } + + public static final class Deserializer extends JsonDeserializer> implements ContextualDeserializer { + private final JavaType valueType; + + public Deserializer() { this(null); } + + private Deserializer(JavaType valueType) { this.valueType = valueType; } + + @Override + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) { + JavaType type = property == null ? context.getContextualType() : property.getType(); + return new Deserializer(type.containedType(0)); + } + + @Override + public OptionalNullable getNullValue(DeserializationContext context) { + return OptionalNullable.nullValue(); + } + + @Override + public OptionalNullable deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return OptionalNullable.of(context.readValue(parser, valueType)); + } + } +} diff --git a/src/main/java/co/lettermint/models/api/StoreWebhookData.java b/src/main/java/co/lettermint/models/api/StoreWebhookData.java index 7c4ca5f..7f55574 100644 --- a/src/main/java/co/lettermint/models/api/StoreWebhookData.java +++ b/src/main/java/co/lettermint/models/api/StoreWebhookData.java @@ -12,6 +12,10 @@ public class StoreWebhookData { @JsonProperty("url") public String url; + @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + @JsonProperty("basic_auth") + public OptionalNullable basicAuth; + @JsonProperty("events") public List events; diff --git a/src/main/java/co/lettermint/models/api/UpdateWebhookData.java b/src/main/java/co/lettermint/models/api/UpdateWebhookData.java index ca5c569..81bf33b 100644 --- a/src/main/java/co/lettermint/models/api/UpdateWebhookData.java +++ b/src/main/java/co/lettermint/models/api/UpdateWebhookData.java @@ -12,6 +12,10 @@ public class UpdateWebhookData { @JsonProperty("url") public String url; + @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + @JsonProperty("basic_auth") + public OptionalNullable basicAuth; + @JsonProperty("events") public List events; diff --git a/src/main/java/co/lettermint/models/api/WebhookBasicAuthData.java b/src/main/java/co/lettermint/models/api/WebhookBasicAuthData.java new file mode 100644 index 0000000..6b8ea6c --- /dev/null +++ b/src/main/java/co/lettermint/models/api/WebhookBasicAuthData.java @@ -0,0 +1,13 @@ +package co.lettermint.models.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebhookBasicAuthData { + @JsonProperty("username") + public String username; + + @JsonProperty("password") + public String password; +} diff --git a/src/main/java/co/lettermint/models/api/WebhookData.java b/src/main/java/co/lettermint/models/api/WebhookData.java index 14fc792..45c54af 100644 --- a/src/main/java/co/lettermint/models/api/WebhookData.java +++ b/src/main/java/co/lettermint/models/api/WebhookData.java @@ -27,6 +27,9 @@ public class WebhookData { @JsonProperty("url") public String url; + @JsonProperty("has_basic_auth") + public boolean hasBasicAuth; + @JsonProperty("events") public List events; diff --git a/src/main/java/co/lettermint/models/api/WebhookListData.java b/src/main/java/co/lettermint/models/api/WebhookListData.java index f417c07..9c9c37b 100644 --- a/src/main/java/co/lettermint/models/api/WebhookListData.java +++ b/src/main/java/co/lettermint/models/api/WebhookListData.java @@ -27,6 +27,9 @@ public class WebhookListData { @JsonProperty("url") public String url; + @JsonProperty("has_basic_auth") + public boolean hasBasicAuth; + @JsonProperty("events") public List events; diff --git a/src/main/java/co/lettermint/models/api/WebhookSecretData.java b/src/main/java/co/lettermint/models/api/WebhookSecretData.java index 0f66c8a..85d2793 100644 --- a/src/main/java/co/lettermint/models/api/WebhookSecretData.java +++ b/src/main/java/co/lettermint/models/api/WebhookSecretData.java @@ -27,6 +27,9 @@ public class WebhookSecretData { @JsonProperty("url") public String url; + @JsonProperty("has_basic_auth") + public boolean hasBasicAuth; + @JsonProperty("events") public List events; diff --git a/src/test/java/co/lettermint/WebhookBasicAuthTest.java b/src/test/java/co/lettermint/WebhookBasicAuthTest.java new file mode 100644 index 0000000..4d09c7e --- /dev/null +++ b/src/test/java/co/lettermint/WebhookBasicAuthTest.java @@ -0,0 +1,59 @@ +package co.lettermint; + +import co.lettermint.api.ApiClient; +import co.lettermint.models.api.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class WebhookBasicAuthTest { + @Test + void createAndUpdatePreserveAllCredentialStatesAndBearerAuth() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + WebhookBasicAuthData credentials = new WebhookBasicAuthData(); + credentials.username = " fixture user "; + credentials.password = ""; + for (int state = 0; state < 3; state++) { + try (MockWebServer server = new MockWebServer()) { + for (int i = 0; i < 2; i++) server.enqueue(new MockResponse().setBody("{\"data\":{\"has_basic_auth\":true}}")); + server.start(); + ApiClient api = Lettermint.api("fixture-token", server.url("/v1").toString()); + StoreWebhookData create = new StoreWebhookData(); + create.name = "Fixture"; + create.url = "https://example.test/hook"; + UpdateWebhookData update = new UpdateWebhookData(); + if (state == 1) { create.basicAuth = OptionalNullable.of(credentials); update.basicAuth = OptionalNullable.of(credentials); } + if (state == 2) { create.basicAuth = OptionalNullable.nullValue(); update.basicAuth = OptionalNullable.nullValue(); } + assertTrue(api.webhooks().create(create).data.hasBasicAuth); + assertTrue(api.webhooks().update("webhook-id", update).data.hasBasicAuth); + for (int i = 0; i < 2; i++) { + RecordedRequest request = server.takeRequest(); + assertEquals(i == 0 ? "POST" : "PUT", request.getMethod()); + assertEquals(i == 0 ? "/v1/webhooks" : "/v1/webhooks/webhook-id", request.getPath()); + assertEquals("Bearer fixture-token", request.getHeader("Authorization")); + assertNull(request.getHeader("x-lettermint-token")); + JsonNode body = mapper.readTree(request.getBody().readUtf8()); + assertEquals(state != 0, body.has("basic_auth")); + if (state == 1) { assertEquals("", body.get("basic_auth").get("password").asText()); assertEquals(" fixture user ", body.get("basic_auth").get("username").asText()); } + if (state == 2) assertTrue(body.get("basic_auth").isNull()); + } + UpdateWebhookData decoded = mapper.readValue(mapper.writeValueAsString(update), UpdateWebhookData.class); + assertEquals(state != 0, decoded.basicAuth != null); + if (state == 1) assertEquals("", decoded.basicAuth.getValue().password); + if (state == 2) assertNull(decoded.basicAuth.getValue()); + } + } + } + + @Test + void allReadModelsExposeOnlyTheSafeFlag() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + assertTrue(mapper.readValue("{\"has_basic_auth\":true}", WebhookData.class).hasBasicAuth); + assertTrue(mapper.readValue("{\"has_basic_auth\":true}", WebhookListData.class).hasBasicAuth); + assertTrue(mapper.readValue("{\"has_basic_auth\":true}", WebhookSecretData.class).hasBasicAuth); + } +}