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
57 changes: 57 additions & 0 deletions src/main/java/co/lettermint/models/api/OptionalNullable.java
Original file line number Diff line number Diff line change
@@ -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<T> {
private final T value;

private OptionalNullable(T value) {
this.value = value;
}

public static <T> OptionalNullable<T> of(T value) {
return new OptionalNullable<>(value);
}

public static <T> OptionalNullable<T> nullValue() {
return new OptionalNullable<>(null);
}

@JsonValue
public T getValue() {
return value;
}

public static final class Deserializer extends JsonDeserializer<OptionalNullable<?>> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebhookBasicAuthData> basicAuth;

@JsonProperty("events")
public List<String> events;

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

@JsonProperty("events")
public List<String> events;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions src/main/java/co/lettermint/models/api/WebhookData.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class WebhookData {
@JsonProperty("url")
public String url;

@JsonProperty("has_basic_auth")
public boolean hasBasicAuth;

@JsonProperty("events")
public List<String> events;

Expand Down
3 changes: 3 additions & 0 deletions src/main/java/co/lettermint/models/api/WebhookListData.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class WebhookListData {
@JsonProperty("url")
public String url;

@JsonProperty("has_basic_auth")
public boolean hasBasicAuth;

@JsonProperty("events")
public List<String> events;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class WebhookSecretData {
@JsonProperty("url")
public String url;

@JsonProperty("has_basic_auth")
public boolean hasBasicAuth;

@JsonProperty("events")
public List<String> events;

Expand Down
59 changes: 59 additions & 0 deletions src/test/java/co/lettermint/WebhookBasicAuthTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading