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
14 changes: 11 additions & 3 deletions src/main/java/com/chargebee/v4/internal/JsonUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
Expand Down Expand Up @@ -135,12 +136,19 @@ public static Timestamp getTimestamp(JsonObject obj, String key) {
return epochSeconds != null ? new Timestamp(epochSeconds * 1000) : null;
}

/** Returns a nested JsonObject for the given key, or null. */
/** Returns a nested or string-encoded JsonObject for the given key, or null. */
public static JsonObject getJsonObject(JsonObject obj, String key) {
if (obj == null || key == null) return null;
JsonElement elem = obj.get(key);
if (elem == null || elem.isJsonNull() || !elem.isJsonObject()) return null;
return elem.getAsJsonObject();
if (elem == null || elem.isJsonNull()) return null;
if (elem.isJsonPrimitive() && elem.getAsJsonPrimitive().isString()) {
try {
elem = JsonParser.parseString(elem.getAsString());
} catch (JsonParseException e) {
return null;
}
}
return elem.isJsonObject() ? elem.getAsJsonObject() : null;
}

/** Returns a nested JsonArray for the given key, or null. */
Expand Down
24 changes: 24 additions & 0 deletions src/test/java/com/chargebee/v4/internal/JsonUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.math.BigDecimal;
import java.sql.Timestamp;
Expand Down Expand Up @@ -286,6 +288,28 @@ class GetObjectTests {
assertNull(JsonUtil.getJsonObject(obj, "name"));
}

@Test void getJsonObjectDecodesString() {
JsonObject root = new JsonObject();
root.addProperty("metadata", "{\"reason\":\"test-promo-claim\"}");
JsonObject metadata = JsonUtil.getJsonObject(root, "metadata");
assertNotNull(metadata);
assertEquals("test-promo-claim", metadata.get("reason").getAsString());
}

@Test void getJsonObjectDecodesEmptyObjectString() {
JsonObject root = new JsonObject();
root.addProperty("metadata", "{}");
assertEquals(new JsonObject(), JsonUtil.getJsonObject(root, "metadata"));
}

@ParameterizedTest
@ValueSource(strings = {"", " ", "not json", "{", "{\"reason\":", "[]", "[{}]", "null", "true", "42", "\"text\""})
void getJsonObjectRejectsInvalidOrNonObjectStrings(String value) {
JsonObject root = new JsonObject();
root.addProperty("metadata", value);
assertNull(JsonUtil.getJsonObject(root, "metadata"));
}

@Test void getObjectReturnsJsonString() {
JsonObject root = JsonUtil.parse("{\"meta\": {\"key\": \"val\"}}");
String meta = JsonUtil.getObject(root, "meta");
Expand Down
107 changes: 107 additions & 0 deletions src/test/java/com/chargebee/v4/internal/MetadataParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.chargebee.v4.internal;

import com.chargebee.v4.client.ChargebeeClient;
import com.chargebee.v4.models.grantBlock.GrantBlock;
import com.chargebee.v4.models.item.Item;
import com.chargebee.v4.models.itemPrice.ItemPrice;
import com.chargebee.v4.models.ledgerOperation.params.LedgerOperationAllocateParams;
import com.chargebee.v4.models.ledgerOperation.responses.LedgerOperationAllocateResponse;
import com.google.gson.JsonObject;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

class MetadataParserTest {

private static final Map<String, Object> METADATA = Map.of(
"grantor", "local-test",
"grantor_type", "test_operator",
"reason", "test-promo-claim",
"nested", Map.of("eligible", true, "amount", 25L),
"labels", Arrays.asList("café", null),
"quoted", "a \"quote\"\\path\nline",
"opaque", "{\"keep\":\"string\"}");

@ParameterizedTest
@CsvSource({"false,false", "false,true", "true,false", "true,true"})
void allocatePreservesMetadataOverLocalHttp(boolean stringEncoded, boolean async) throws Exception {
JsonObject block = metadataModel(stringEncoded);
block.addProperty("id", "local-grant");
block.addProperty("grant_source", "promotional");
String body = "{\"grant_blocks\":[" + block + "]}";
byte[] responseBytes = body.getBytes(StandardCharsets.UTF_8);
AtomicReference<String> requestBody = new AtomicReference<>();
AtomicReference<String> requestMethod = new AtomicReference<>();
AtomicReference<String> requestPath = new AtomicReference<>();
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v2/ledger_operations/allocate", exchange -> {
requestBody.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
requestMethod.set(exchange.getRequestMethod());
requestPath.set(exchange.getRequestURI().getPath());
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(200, responseBytes.length);
exchange.getResponseBody().write(responseBytes);
exchange.close();
});
server.start();
try (ChargebeeClient client = ChargebeeClient.builder("local-test-key", "local-test")
.endpoint("http://127.0.0.1:" + server.getAddress().getPort() + "/api/v2")
.build()) {
LedgerOperationAllocateParams params = LedgerOperationAllocateParams.builder()
.subscriptionId("local-subscription").unitId("local-unit")
.id("local-allocation").amount("25").metadata(METADATA).build();
LedgerOperationAllocateResponse response = async
? client.ledgerOperations().allocateAsync(params).get(10, TimeUnit.SECONDS)
: client.ledgerOperations().allocate(params);

assertEquals("POST", requestMethod.get());
assertEquals("/api/v2/ledger_operations/allocate", requestPath.get());
assertEquals(JsonUtil.parse(JsonUtil.toJson(METADATA)),
JsonUtil.parse(requestBody.get()).get("metadata"));
assertEquals(1, response.getGrantBlocks().size());
GrantBlock grant = response.getGrantBlocks().get(0);
assertEquals("local-grant", grant.getId());
assertEquals("test-promo-claim", grant.getMetadata().get("reason"));
assertEquals(METADATA, grant.getMetadata());
} finally {
server.stop(0);
}
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void itemAndItemPricePreserveMetadata(boolean stringEncoded) {
JsonObject model = metadataModel(stringEncoded);
assertEquals(METADATA, Item.fromJson(model).getMetadata());
assertEquals(METADATA, ItemPrice.fromJson(model).getMetadata());
}

@ParameterizedTest
@ValueSource(strings = {"{}", "{\"metadata\":null}", "{\"metadata\":{}}", "{\"metadata\":\"{}\"}",
"{\"metadata\":\"{\"}", "{\"metadata\":[]}", "{\"metadata\":42}", "{\"metadata\":true}"})
void absentOrInvalidMetadataRemainsEmpty(String json) {
assertTrue(GrantBlock.fromJson(json).getMetadata().isEmpty());
assertTrue(Item.fromJson(json).getMetadata().isEmpty());
assertTrue(ItemPrice.fromJson(json).getMetadata().isEmpty());
}

private static JsonObject metadataModel(boolean stringEncoded) {
JsonObject model = new JsonObject();
String json = JsonUtil.toJson(METADATA);
if (stringEncoded) {
model.addProperty("metadata", json);
} else {
model.add("metadata", JsonUtil.parse(json));
}
return model;
}
}
Loading