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
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Smartcar, Inc. <hello@smartcar.com>
Copyright (c) 2026 Smartcar, Inc. <hello@smartcar.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,16 @@ a valid access token for the target vehicle.
// Setup
String clientId = "";
String clientSecret = "";
String redirectUri = "";
String[] scope = {};
String mode = "test";

// Initialize a new AuthClient with your credentials.
AuthClient authClient = new AuthClient.Builder
.clientId(clientId)
.clientSecret(clientSecret)
.redirectUri(redirectUri)
.mode(mode);

// Retrieve the auth URL to start the OAuth flow.
String authUrl = authClient.authUrlBuilder(scope)
String authUrl = authClient.authUrlBuilder()
.setApprovalPrompt(true)
.setState("some state")
.build();
Expand Down
5 changes: 1 addition & 4 deletions README.mdt
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,16 @@ a valid access token for the target vehicle.
// Setup
String clientId = "";
String clientSecret = "";
String redirectUri = "";
String[] scope = {};
String mode = "test";

// Initialize a new AuthClient with your credentials.
AuthClient authClient = new AuthClient.Builder
.clientId(clientId)
.clientSecret(clientSecret)
.redirectUri(redirectUri)
.mode(mode);

// Retrieve the auth URL to start the OAuth flow.
String authUrl = authClient.authUrlBuilder(scope)
String authUrl = authClient.authUrlBuilder()
.setApprovalPrompt(true)
.setState("some state")
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.smartcar.sdk;

import com.smartcar.sdk.data.*;
import com.smartcar.sdk.data.v3.VehicleAttributes;
import com.smartcar.sdk.helpers.AuthHelpers;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class SmartcarIntegrationTest {
private static final String V3_VEHICLE_ID = "tst2e255-d3c8-4f90-9fec-e6e68b98e9cb";
private static final String V3_TEST_TOKEN = "test-data-token";

public class SmartcarTest {
private String accessToken;
private VehicleIds vehicleIds;
private String clientId;
Expand Down Expand Up @@ -55,6 +61,17 @@ public void testGetVehiclesPaging() throws SmartcarException {
Assert.assertEquals(vehicleIds.getPaging().getOffset(), 0);
}

@Test
public void testGetVehicle() throws SmartcarException {
VehicleAttributes vehicle = Smartcar.getVehicle(V3_TEST_TOKEN, V3_VEHICLE_ID);

Assert.assertNotNull(vehicle);
Assert.assertEquals(vehicle.getId(), V3_VEHICLE_ID);
Assert.assertEquals(vehicle.getMake(), "TESLA");
Assert.assertEquals(vehicle.getModel(), "Model Y");
Assert.assertEquals(vehicle.getYear().intValue(), 2021);
}

@Test
public void testGetCompatibility() throws Exception {
String vin = "5YJSA1E29LF403082";
Expand Down Expand Up @@ -85,6 +102,36 @@ public void testGetCompatibility() throws Exception {
Assert.assertFalse((capable));
}

@Test
public void testGetCompatibilityMatrix() throws Exception {
String[] scope = {"read_battery", "read_charge"};

SmartcarCompatibilityMatrixRequest request = new SmartcarCompatibilityMatrixRequest.Builder()
.clientId(this.clientId)
.clientSecret(this.clientSecret)
.make("NISSAN")
.type("BEV")
.scope(scope)
.build();
CompatibilityMatrix matrix = Smartcar.getCompatibilityMatrix(request);
Map<String, List<CompatibilityMatrix.CompatibilityEntry>> results = matrix.getResults();
Assert.assertTrue(results.size() > 0);
for (Map.Entry<String, List<CompatibilityMatrix.CompatibilityEntry>> entry : results.entrySet()) {
for (CompatibilityMatrix.CompatibilityEntry result : entry.getValue()) {
Assert.assertNotNull(result.getModel());
Assert.assertNotNull(result.getStartYear());
Assert.assertNotNull(result.getEndYear());
Assert.assertNotNull(result.getType());
Assert.assertNotNull(result.getEndpoints());
Assert.assertNotNull(result.getPermissions());

Assert.assertEquals(result.getType(), "BEV");
List<String> permissions = Arrays.asList(result.getPermissions());
Assert.assertTrue(permissions.containsAll(Arrays.asList(scope)));
}
}
}

// TODO uncomment when test mode connections are returned
// @Test
// public void testGetConnections() throws SmartcarException {
Expand Down
49 changes: 49 additions & 0 deletions src/integration/java/com/smartcar/sdk/VehicleIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import javax.json.JsonArrayBuilder;

import com.smartcar.sdk.data.*;
import com.smartcar.sdk.data.v3.Signal;
import com.smartcar.sdk.data.v3.Signals;
import com.smartcar.sdk.data.v3.Signals.SignalsMeta;
import com.smartcar.sdk.helpers.AuthHelpers;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.testng.Assert;
Expand All @@ -20,6 +23,9 @@
*/
@PowerMockIgnore("javax.net.ssl.*")
public class VehicleIntegrationTest {
private static final String V3_VEHICLE_ID = "tst2e255-d3c8-4f90-9fec-e6e68b98e9cb";
private static final String V3_TEST_TOKEN = "test-data-token";

private Vehicle vehicle;
private Vehicle eVehicle;

Expand Down Expand Up @@ -371,4 +377,47 @@ public void testSubscribeUnsubscribe() throws SmartcarException {
this.vehicle.unsubscribe(
AuthHelpers.getApplicationManagementToken(), AuthHelpers.getWebhookId());
}

@Test
public void testVehicleSignals() throws SmartcarException {
Vehicle vehicle = new Vehicle(V3_VEHICLE_ID, V3_TEST_TOKEN);
Signals signals = vehicle.getSignals();
Assert.assertNotNull(signals);
Assert.assertTrue(signals.getSignals().size() > 0);

Meta meta = signals.getMeta();
Assert.assertTrue(meta instanceof SignalsMeta);
Assert.assertEquals(((SignalsMeta) meta).getTotalCount().intValue(), signals.getSignals().size());
Assert.assertEquals(((SignalsMeta) meta).getPageSize().intValue(), signals.getSignals().size());
Assert.assertEquals(((SignalsMeta) meta).getPage().intValue(), 1);

Assert.assertNotNull(signals.getLinks());
Assert.assertNotNull(signals.getIncluded());

Signal signal = signals.getSignals().stream()
.filter(s -> s.getCode().equals("odometer-traveleddistance"))
.findFirst()
.orElse(null);
Assert.assertNotNull(signal);
Assert.assertNotNull(signal);
Assert.assertEquals(signal.getCode(), "odometer-traveleddistance");
Assert.assertNotNull(signal.getBody());
Assert.assertNotNull(signal.getStatus());

Meta signalMeta = signal.getMeta();
Assert.assertNotNull(signalMeta);
Assert.assertNotNull(signalMeta.getRetrievedAt());
Assert.assertNotNull(signalMeta.getOemUpdatedAt());
}

@Test
public void testVehicleSignal() throws SmartcarException {
Vehicle vehicle = new Vehicle(V3_VEHICLE_ID, V3_TEST_TOKEN);
Signal signal = vehicle.getSignal("odometer-traveleddistance");
Assert.assertNotNull(signal);
Assert.assertEquals(signal.getCode(), "odometer-traveleddistance");
Assert.assertNotNull(signal.getBody());
Assert.assertNotNull(signal.getStatus());
Assert.assertNotNull(signal.getMeta());
}
}
27 changes: 24 additions & 3 deletions src/integration/java/com/smartcar/sdk/helpers/AuthHelpers.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.smartcar.sdk.helpers;

import com.smartcar.sdk.AuthClient;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
Expand Down Expand Up @@ -38,6 +40,10 @@ public class AuthHelpers {
private static final boolean HEADLESS = System.getenv("CI") != null || System.getenv("HEADLESS") != null;
private static final HashMap<String, String> ENV_VAR_CACHE = new HashMap<>();

public static String getSeleniumRemoteUrl() {
return safeGetEnv("SELENIUM_REMOTE_URL");
}

public static String getClientId() {
return safeGetEnv("E2E_SMARTCAR_CLIENT_ID");
}
Expand All @@ -62,6 +68,21 @@ public static WebDriver setupDriver() {
String browser = getBrowser();
WebDriver driver;

if (getSeleniumRemoteUrl() != null) {
Capabilities capabilities = "chrome".equalsIgnoreCase(browser)
? new ChromeOptions()
: new FirefoxOptions();
try {
driver = new RemoteWebDriver(
java.net.URI.create(getSeleniumRemoteUrl()).toURL(),
capabilities
);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid SELENIUM_REMOTE_URL", e);
}
return driver;
}

if ("chrome".equalsIgnoreCase(browser)) {
ChromeOptions options = new ChromeOptions();
if (HEADLESS) {
Expand All @@ -74,7 +95,7 @@ public static WebDriver setupDriver() {
if (HEADLESS) {
options.addArguments("--headless");
}

// Set Firefox binary path if available (for CI environments)
String firefoxPath = System.getenv("FIREFOX_BINARY_PATH");
if (firefoxPath == null) {
Expand All @@ -85,7 +106,7 @@ public static WebDriver setupDriver() {
"/usr/bin/firefox",
"/usr/lib/firefox/firefox"
};

for (String path : candidatePaths) {
if (new File(path).exists()) {
firefoxPath = path;
Expand All @@ -97,7 +118,7 @@ public static WebDriver setupDriver() {
System.out.println("Using Firefox binary: " + firefoxPath);
options.setBinary(firefoxPath);
}

driver = new FirefoxDriver(options);
}

Expand Down
40 changes: 31 additions & 9 deletions src/main/java/com/smartcar/sdk/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.smartcar.sdk.data.*;
import com.smartcar.sdk.data.v3.Signal;
import com.smartcar.sdk.data.v3.Signals;
import com.smartcar.sdk.deserializer.AuthDeserializer;
import com.smartcar.sdk.deserializer.BatchDeserializer;
import com.smartcar.sdk.deserializer.CompatibilityMatrixDeserializer;
import com.smartcar.sdk.deserializer.VehicleResponseDeserializer;
import com.smartcar.sdk.deserializer.v3.JsonApiDeserializer;
import com.smartcar.sdk.deserializer.v3.SignalsDeserializer;

import okhttp3.*;

import java.io.IOException;
Expand Down Expand Up @@ -55,7 +61,11 @@ private static String getSdkVersion() {
.setFieldNamingStrategy(field -> Utils.toCamelCase(field.getName()))
.registerTypeAdapter(Auth.class, new AuthDeserializer())
.registerTypeAdapter(BatchResponse.class, new BatchDeserializer())
.registerTypeAdapter(CompatibilityMatrix.class, new CompatibilityMatrixDeserializer())
.registerTypeAdapter(VehicleResponse.class, new VehicleResponseDeserializer())
.registerTypeAdapter(Signal.class, new JsonApiDeserializer<Signal>())
.registerTypeAdapter(Signals.class, new SignalsDeserializer())
.registerTypeAdapter(com.smartcar.sdk.data.v3.VehicleAttributes.class, new JsonApiDeserializer<com.smartcar.sdk.data.v3.VehicleAttributes>())
.create();

private static final Gson GSON_LOWER_CASE_WITH_UNDERSCORES = new GsonBuilder()
Expand All @@ -65,7 +75,7 @@ private static String getSdkVersion() {
/**
* Builds a request object with common headers, using provided request
* parameters
*
*
* @param url url for the request, including the query parameters
* @param method http method
* @param body request body
Expand Down Expand Up @@ -116,7 +126,7 @@ protected static Response execute(Request request) throws SmartcarException {
* @throws SmartcarException if the request is unsuccessful
*/
protected static <T extends ApiData> T execute(
Request request, Class<T> dataType) throws SmartcarException {
Request request, Class<T> dataType, String version) throws SmartcarException {
Response response = ApiClient.execute(request);
T data = null;
Meta meta;
Expand All @@ -128,6 +138,10 @@ protected static <T extends ApiData> T execute(
JsonElement jsonElement = JsonParser.parseString(bodyString);

if (jsonElement.isJsonArray()) {
// This block is for handling the service history api
// This should be refactored to a specific deserializer if more
// endpoints return arrays at the top level. I can foresee this
// being an issue down the line.
Field itemsField = dataType.getDeclaredField("items");
itemsField.setAccessible(true);

Expand All @@ -150,14 +164,17 @@ protected static <T extends ApiData> T execute(
data = GSON_CAMEL_CASE.fromJson(bodyString, dataType);
}

Headers headers = response.headers();
JsonObject headerJson = new JsonObject();
for (String header : headers.names()) {
headerJson.addProperty(header.toLowerCase(), headers.get(header));
if (!version.equals("3")) {
Headers headers = response.headers();
JsonObject headerJson = new JsonObject();
for (String header : headers.names()) {
headerJson.addProperty(header.toLowerCase(), headers.get(header));
}
String headerJsonString = headerJson.toString();
meta = GSON_CAMEL_CASE.fromJson(headerJsonString, Meta.class);
data.setMeta(meta);
}
String headerJsonString = headerJson.toString();
meta = GSON_CAMEL_CASE.fromJson(headerJsonString, Meta.class);
data.setMeta(meta);

return data;
} catch (Exception ex) {
if (bodyString.equals("")) {
Expand All @@ -172,4 +189,9 @@ protected static <T extends ApiData> T execute(
.build();
}
}

protected static <T extends ApiData> T execute(
Request request, Class<T> dataType) throws SmartcarException {
return ApiClient.execute(request, dataType, "2");
}
}
Loading