From f83818c0bdd9f95f22b078c2926232fd92f196d1 Mon Sep 17 00:00:00 2001 From: PassNinja Date: Sat, 6 Jun 2026 17:14:53 -0400 Subject: [PATCH 1/3] Use passTemplate wire key for pass create Send passTemplate request param and deserialize the passTemplate response field on Pass; expose it via getPassTemplate(). Aligns the client with the API contract standardizing on passTemplate. Leaves PassTemplate.passTypeId (Apple/Google id) untouched. --- README.md | 2 +- src/main/java/com/passninja/model/Pass.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 90b4a15..bd876f0 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ PassninjaResponse response = Pass.create("demo.coupon", /* passType */ pass /* passData */); System.out.println(response.getResponseBody().getUrls()); -System.out.println(response.getResponseBody().getPassType()); +System.out.println(response.getResponseBody().getPassTemplate()); System.out.println(response.getResponseBody().getSerialNumber()); ``` diff --git a/src/main/java/com/passninja/model/Pass.java b/src/main/java/com/passninja/model/Pass.java index 5412a47..4be90e4 100644 --- a/src/main/java/com/passninja/model/Pass.java +++ b/src/main/java/com/passninja/model/Pass.java @@ -18,7 +18,7 @@ public class Pass extends ApiResource { public static final String RESOURCE = "passes"; @JsonProperty private final String id; - @JsonProperty private final String passType; + @JsonProperty private final String passTemplate; @JsonProperty private final String serialNumber; @JsonProperty private final Map pass; @JsonProperty private final Map urls; @@ -26,19 +26,19 @@ public class Pass extends ApiResource { @JsonCreator public Pass( @JsonProperty("id") final String id, - @JsonProperty("passType") final String passType, + @JsonProperty("passTemplate") final String passTemplate, @JsonProperty("serialNumber") final String serialNumber, @JsonProperty("pass") final Map pass, @JsonProperty("urls") final Map urls) { this.id = id; - this.passType = passType; + this.passTemplate = passTemplate; this.serialNumber = serialNumber; this.pass = pass; this.urls = urls; } - public String getPassType() { - return passType; + public String getPassTemplate() { + return passTemplate; } public String getSerialNumber() { @@ -56,7 +56,7 @@ public Map getUrls() { @Override public String toString() { return "Pass{" - + "passType='" + passType + '\'' + + "passTemplate='" + passTemplate + '\'' + ", serialNumber='" + serialNumber + '\'' + ", pass='" + pass + '\'' + ", urls='" + urls + '\'' @@ -71,7 +71,7 @@ public RequestBuilder() { } public RequestBuilder setPassType(String passType) { - params.put("passType", passType); + params.put("passTemplate", passType); return this; } From b0f124b3cf9fc731887a8104b8857ec4a9bed2f3 Mon Sep 17 00:00:00 2001 From: PassNinja Date: Sat, 6 Jun 2026 18:50:14 -0400 Subject: [PATCH 2/3] Add Pass.patch for partial pass updates Adds a static patch() for partial updates so a single field can be changed without a full payload (put stays a full replace). Java's HttpURLConnection cannot send PATCH, so patch posts with the server's _method=PATCH override. --- src/main/java/com/passninja/model/Pass.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/passninja/model/Pass.java b/src/main/java/com/passninja/model/Pass.java index 4be90e4..5852b3b 100644 --- a/src/main/java/com/passninja/model/Pass.java +++ b/src/main/java/com/passninja/model/Pass.java @@ -121,6 +121,19 @@ public static PassninjaResponse put(String passType, String serialNumber, return request(RequestMethod.PUT, RESOURCE + "/" + passType + "/" + serialNumber, params, Pass.class, null); } + // Partial update: only the provided fields change (PUT is a full replace that + // requires all required fields). HttpURLConnection cannot issue PATCH, so this + // sends a POST with the server's `_method=PATCH` override (POST-only). + public static PassninjaResponse patch(String passType, String serialNumber, Map pass) + throws ApiException, IOException, AuthenticationException { + Map params = new HashMap<>(); + params.put("pass", pass); + Map query = new HashMap<>(); + query.put("_method", "PATCH"); + return request(RequestMethod.POST, RESOURCE + "/" + passType + "/" + serialNumber, params, + query, Pass.class, null); + } + public static PassninjaResponse delete(String passType, String serialNumber) throws ApiException, IOException, AuthenticationException { return request(RequestMethod.DELETE, RESOURCE + "/" + passType + "/" + serialNumber, null, Pass.class, null); From 05e53cec3d84879df00bb2a58950d2a2f41b6747 Mon Sep 17 00:00:00 2001 From: PassNinja Date: Sat, 6 Jun 2026 21:01:25 -0400 Subject: [PATCH 3/3] Run tests offline against an in-process mock server Tests hit api.passninja.com directly, so they errored without live creds/network. Add a JDK HttpServer mock (no new dependency) and a passninja.apiBaseUrl system-property override so the suite runs offline and deterministically. --- .../com/passninja/net/ResponseGetter.java | 4 +- .../java/com/passninja/MockApiServer.java | 74 +++++++++++++++++++ .../com/passninja/model/PassTemplateTest.java | 14 +++- .../java/com/passninja/model/PassTest.java | 13 +++- 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/passninja/MockApiServer.java diff --git a/src/main/java/com/passninja/net/ResponseGetter.java b/src/main/java/com/passninja/net/ResponseGetter.java index 949b6d0..10a6618 100644 --- a/src/main/java/com/passninja/net/ResponseGetter.java +++ b/src/main/java/com/passninja/net/ResponseGetter.java @@ -187,7 +187,9 @@ private static PassninjaResponse requestInternal(ApiResource.RequestMetho .map(item -> urlEncodePair(item.getKey(), String.valueOf(item.getValue()))).collect(Collectors.joining()); - String passninjaUrl = String.format("%s%s", Passninja.API_BASE_URL, + String apiBase = System.getProperty("passninja.apiBaseUrl", + Passninja.API_BASE_URL); + String passninjaUrl = String.format("%s%s", apiBase, queryParams.isEmpty() ? url : url + "?" + queryParams); String encodedData = createQuery(data); diff --git a/src/test/java/com/passninja/MockApiServer.java b/src/test/java/com/passninja/MockApiServer.java new file mode 100644 index 0000000..327d5fa --- /dev/null +++ b/src/test/java/com/passninja/MockApiServer.java @@ -0,0 +1,74 @@ +package com.passninja; + +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * Minimal in-process HTTP server used by the tests so the suite runs offline + * (the JDK ships com.sun.net.httpserver, so no extra dependency is needed). + * Routes the handful of endpoints the client exercises to canned JSON. + */ +public class MockApiServer { + + private static final String PASS_JSON = + "{\"id\":\"serial123\",\"passTemplate\":\"ptk_0x2\",\"serialNumber\":\"serial123\"," + + "\"urls\":{\"landing\":\"https://i.installpass.es/p/serial123\"}}"; + private static final String PASSES_JSON = "{\"passes\":[" + PASS_JSON + "]}"; + private static final String DECRYPT_JSON = "{\"decrypted\":\"founder-id:abc123\"}"; + private static final String TEMPLATE_JSON = + "{\"id\":\"ptk_0x2\",\"name\":\"Starbucks Rewards\",\"pass_type_id\":\"pass.com.example\"," + + "\"platform\":\"apple\",\"style\":\"storeCard\",\"issued_pass_count\":0," + + "\"installed_pass_count\":0}"; + + private HttpServer server; + + /** + * Starts the server on an ephemeral port. + * + * @return the base URL (with trailing slash) to assign to Passninja.API_BASE_URL + * @throws IOException if the server cannot bind + */ + public String start() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + String path = exchange.getRequestURI().getPath(); + String method = exchange.getRequestMethod(); + int code = 200; + String body; + if (method.equals("POST") && path.equals("/v1/passes")) { + body = PASS_JSON; + } else if (method.equals("POST") && path.matches("/v1/passes/[^/]+/decrypt")) { + body = DECRYPT_JSON; + } else if (method.equals("GET") && path.matches("/v1/passes/[^/]+/[^/]+")) { + body = PASS_JSON; + } else if (method.equals("PUT") && path.matches("/v1/passes/[^/]+/[^/]+")) { + body = PASS_JSON; + } else if (method.equals("GET") && path.matches("/v1/passes/[^/]+")) { + body = PASSES_JSON; + } else if (method.equals("GET") && path.matches("/v1/pass_templates/[^/]+")) { + body = TEMPLATE_JSON; + } else { + code = 404; + body = "{\"error\":\"not found: " + method + " " + path + "\"}"; + } + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(code, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + }); + server.start(); + return "http://127.0.0.1:" + server.getAddress().getPort() + "/v1/"; + } + + public void stop() { + if (server != null) { + server.stop(0); + } + } +} diff --git a/src/test/java/com/passninja/model/PassTemplateTest.java b/src/test/java/com/passninja/model/PassTemplateTest.java index e91540a..8d20469 100644 --- a/src/test/java/com/passninja/model/PassTemplateTest.java +++ b/src/test/java/com/passninja/model/PassTemplateTest.java @@ -1,7 +1,9 @@ package com.passninja.model; +import com.passninja.MockApiServer; import com.passninja.Passninja; import com.passninja.net.PassninjaResponse; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -12,11 +14,21 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class PassTemplateTest { + private MockApiServer mock; + @BeforeEach - void setup() { + void setup() throws Exception { + mock = new MockApiServer(); + System.setProperty("passninja.apiBaseUrl", mock.start()); Passninja.init("aid_0x2", "d5247644c316194d9089e23766e08ea9"); } + @AfterEach + void teardown() { + mock.stop(); + System.clearProperty("passninja.apiBaseUrl"); + } + @Test void find_pass_template() throws Exception { PassninjaResponse response = PassTemplate.find("ptk_0x2"); diff --git a/src/test/java/com/passninja/model/PassTest.java b/src/test/java/com/passninja/model/PassTest.java index 586a706..cefde60 100644 --- a/src/test/java/com/passninja/model/PassTest.java +++ b/src/test/java/com/passninja/model/PassTest.java @@ -1,5 +1,6 @@ package com.passninja.model; +import com.passninja.MockApiServer; import com.passninja.Passninja; import com.passninja.exception.AuthenticationException; import com.passninja.net.PassninjaResponse; @@ -14,11 +15,21 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class PassTest { + private MockApiServer mock; + @BeforeEach - void setup() { + void setup() throws Exception { + mock = new MockApiServer(); + System.setProperty("passninja.apiBaseUrl", mock.start()); Passninja.init("aid_0x2", "d5247644c316194d9089e23766e08ea9"); } + @AfterEach + void teardown() { + mock.stop(); + System.clearProperty("passninja.apiBaseUrl"); + } + @Test public void should_create_a_new_pass() throws Exception { Map inputPass = new HashMap<>();