diff --git a/src/main/java/com/bandwidth/iris/sdk/IrisClient.java b/src/main/java/com/bandwidth/iris/sdk/IrisClient.java index 93f07f0..b9d2fc9 100644 --- a/src/main/java/com/bandwidth/iris/sdk/IrisClient.java +++ b/src/main/java/com/bandwidth/iris/sdk/IrisClient.java @@ -18,8 +18,11 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; import javax.xml.stream.XMLInputFactory; +import java.util.Optional; import java.io.File; import java.net.URISyntaxException; import java.util.HashMap; @@ -37,32 +40,218 @@ public class IrisClient { private final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); protected DefaultHttpClient httpClient; + private String accessToken; + private Long accessTokenExpiration; + private String clientId; + private String clientSecret; + + /** + * Creates an IrisClient with custom URI and API version. + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + */ public IrisClient(String uri, String accountId, - String userName, String password, String version) { - this.uri = uri; - this.baseUrl = "/" + version + "/"; - this.baseAccountUrl = this.baseUrl + "accounts/" + accountId + "/"; - initHttpClient(userName, password); + String username, String password, String version) { + this(new DefaultHttpClient(), uri, accountId, username, password, version); } - public IrisClient(String accountId, String userName, String password) { - this(defaultUri, accountId, userName, password, defaultVersion); + /** + * Creates an IrisClient with default settings (dashboard.bandwidth.com, v1.0). + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + */ + public IrisClient(String accountId, String username, String password) { + this(defaultUri, accountId, username, password, defaultVersion); } + /** + * Creates an IrisClient with custom HTTP client and URI. + * @param httpClient Custom HTTP client instance + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + */ public IrisClient(DefaultHttpClient httpClient, String uri, String accountId, String username, String password) { + this(httpClient, uri, accountId, username, password, defaultVersion); + } + + /** + * Base constructor with full configuration. + * @param httpClient Custom HTTP client instance + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID (required) + * @param username API username (required) + * @param password API password (required) + * @param version API version + * @throws IllegalArgumentException if required parameters are null or empty + */ + public IrisClient(DefaultHttpClient httpClient, String uri, String accountId, String username, String password, String version) { + if (accountId == null || accountId.trim().isEmpty()) { + throw new IllegalArgumentException("accountId cannot be null or empty"); + } + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("username cannot be null or empty"); + } + if (password == null || password.trim().isEmpty()) { + throw new IllegalArgumentException("password cannot be null or empty"); + } + if (uri == null || uri.trim().isEmpty()) { + throw new IllegalArgumentException("uri cannot be null or empty"); + } + if (version == null || version.trim().isEmpty()) { + throw new IllegalArgumentException("version cannot be null or empty"); + } + this.uri = uri; - this.baseUrl = "/" + defaultVersion + "/"; + this.baseUrl = "/" + version + "/"; this.baseAccountUrl = this.baseUrl + "accounts/" + accountId + "/"; + initHttpClient(httpClient, username, password); + this.httpClient = httpClient; + } - Credentials credentials = new UsernamePasswordCredentials(username, password); - httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials); + /** + * Creates an IrisClient with custom HTTP client and OAuth client credentials. + * OAuth tokens will be automatically fetched when needed using client_credentials grant. + * @param httpClient Custom HTTP client instance + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param clientId OAuth client ID for client_credentials grant + * @param clientSecret OAuth client secret for client_credentials grant + */ + public IrisClient(DefaultHttpClient httpClient, String uri, String accountId, String username, String password, + String clientId, String clientSecret) { + this(httpClient, uri, accountId, username, password, defaultVersion); + this.clientId = clientId; + this.clientSecret = clientSecret; + } - this.httpClient = httpClient; + /** + * Creates an IrisClient with a pre-configured access token. + * Token will be used until expiration, no automatic refresh. + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param accessToken Pre-configured OAuth access token + * @param accessTokenExpiration Token expiration time (Unix timestamp in seconds) + */ + public IrisClient(String accountId, String username, String password, + String accessToken, Long accessTokenExpiration) { + this(defaultUri, accountId, username, password, defaultVersion); + this.accessToken = accessToken; + this.accessTokenExpiration = accessTokenExpiration; + } + + /** + * Creates an IrisClient with custom URI and pre-configured access token. + * Token will be used until expiration, no automatic refresh. + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + * @param accessToken Pre-configured OAuth access token + * @param accessTokenExpiration Token expiration time (Unix timestamp in seconds) + */ + public IrisClient(String uri, String accountId, String username, String password, + String version, String accessToken, Long accessTokenExpiration) { + this(uri, accountId, username, password, version); + this.accessToken = accessToken; + this.accessTokenExpiration = accessTokenExpiration; + } + + /** + * Creates an IrisClient with custom URI and OAuth client credentials. + * OAuth tokens will be automatically fetched when needed using client_credentials grant. + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + * @param clientId OAuth client ID for client_credentials grant + * @param clientSecret OAuth client secret for client_credentials grant + */ + public IrisClient(String uri, String accountId, String username, String password, + String version, String clientId, String clientSecret) { + this(uri, accountId, username, password, version); + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + /** + * Creates an IrisClient with all OAuth fields (client credentials + pre-configured token). + * Will use the provided token until expiration, then can fetch new tokens using client credentials. + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + * @param clientId OAuth client ID for client_credentials grant + * @param clientSecret OAuth client secret for client_credentials grant + * @param accessToken Pre-configured OAuth access token + * @param accessTokenExpiration Token expiration time (Unix timestamp in seconds) + */ + public IrisClient(String accountId, String username, String password, + String version, String clientId, String clientSecret, + String accessToken, Long accessTokenExpiration) { + this(defaultUri, accountId, username, password, version); + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = accessToken; + this.accessTokenExpiration = accessTokenExpiration; } - private void initHttpClient(String userName, String password) { - httpClient = new DefaultHttpClient(); - Credentials credentials = new UsernamePasswordCredentials(userName, password); + /** + * Creates an IrisClient with custom HTTP client and all OAuth fields. + * Will use the provided token until expiration, then can fetch new tokens using client credentials. + * @param httpClient Custom HTTP client instance + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + * @param clientId OAuth client ID for client_credentials grant + * @param clientSecret OAuth client secret for client_credentials grant + * @param accessToken Pre-configured OAuth access token + * @param accessTokenExpiration Token expiration time (Unix timestamp in seconds) + */ + public IrisClient(DefaultHttpClient httpClient, String accountId, String username, + String password, String version, String clientId, String clientSecret, + String accessToken, Long accessTokenExpiration) { + this(httpClient, defaultUri, accountId, username, password, version, clientId, + clientSecret, accessToken, accessTokenExpiration); + } + + /** + * Creates an IrisClient with full customization of all parameters. + * Will use the provided token until expiration, then can fetch new tokens using client credentials. + * @param httpClient Custom HTTP client instance + * @param uri Base URI for API requests + * @param accountId Bandwidth account ID + * @param username API username + * @param password API password + * @param version API version + * @param clientId OAuth client ID for client_credentials grant + * @param clientSecret OAuth client secret for client_credentials grant + * @param accessToken Pre-configured OAuth access token + * @param accessTokenExpiration Token expiration time (Unix timestamp in seconds) + */ + public IrisClient(DefaultHttpClient httpClient, String uri, String accountId, String username, + String password, String version, String clientId, String clientSecret, + String accessToken, Long accessTokenExpiration) { + this(httpClient, uri, accountId, username, password, version); + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = accessToken; + this.accessTokenExpiration = accessTokenExpiration; + } + + private void initHttpClient(DefaultHttpClient httpClient, String username, String password) { + Credentials credentials = new UsernamePasswordCredentials(username, password); httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials); } @@ -177,6 +366,7 @@ public String buildModelUri(String[] tokens) throws URISyntaxException { protected IrisResponse executeRequest(HttpUriRequest request) throws Exception { request.addHeader("User-Agent", USER_AGENT); + configureAuth(request); Map headers = new HashMap(); IrisResponse irisResponse = new IrisResponse(); @@ -205,4 +395,55 @@ public void checkResponse( IrisResponse response, BaseResponse baseResponse ) th } } + private void configureAuth (HttpUriRequest request) { + if (this.accessToken != null && (this.accessTokenExpiration == null || this.accessTokenExpiration > System.currentTimeMillis()/1000 + 60)) { + request.addHeader("Authorization", "Bearer " + this.accessToken); + } else if (this.clientId != null && this.clientSecret != null) { + HttpPost tokenRequest = new HttpPost("https://api.bandwidth.com/api/v1/oauth2/token"); + StringEntity tokenBody = new StringEntity("grant_type=client_credentials", "UTF-8"); + tokenRequest.addHeader("Content-Type", "application/x-www-form-urlencoded"); + tokenRequest.setEntity(tokenBody); + String auth = this.clientId + ":" + this.clientSecret; + String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); + tokenRequest.addHeader("Authorization", "Basic " + encodedAuth); + try { + HttpResponse tokenResponse = httpClient.execute(tokenRequest); + String responseString = EntityUtils.toString(tokenResponse.getEntity()); + JSONParser parser = new JSONParser(); + JSONObject tokenData = (JSONObject) parser.parse(responseString); + this.accessToken = (String) tokenData.get("access_token"); + Long expiresIn = (Long) tokenData.get("expires_in"); + this.accessTokenExpiration = System.currentTimeMillis()/1000 + expiresIn; + request.addHeader("Authorization", "Bearer " + this.accessToken); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public String getAccessToken() { + return accessToken; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + public Long getAccessTokenExpiration() { + return accessTokenExpiration; + } + public void setAccessTokenExpiration(Long accessTokenExpiration) { + this.accessTokenExpiration = accessTokenExpiration; + } + public String getClientId() { + return clientId; + } + public void setClientId(String clientId) { + this.clientId = clientId; + } + public String getClientSecret() { + return clientSecret; + } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + } diff --git a/src/test/java/com/bandwidth/iris/sdk/BaseModelTests.java b/src/test/java/com/bandwidth/iris/sdk/BaseModelTests.java index 707b6cd..5014773 100644 --- a/src/test/java/com/bandwidth/iris/sdk/BaseModelTests.java +++ b/src/test/java/com/bandwidth/iris/sdk/BaseModelTests.java @@ -13,30 +13,32 @@ import org.junit.Rule; import org.junit.rules.ExpectedException; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + public class BaseModelTests { @Rule - public WireMockRule wireMockRule = new WireMockRule(8090); // No-args constructor defaults to port 8080 + public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort()); @Rule public ExpectedException expectedEx = ExpectedException.none(); protected String message; protected IrisClient getDefaultClient() { - return new IrisClient("http://localhost:8090", "accountId", "username", "password", "v1.0"); + return new IrisClient("http://localhost:" + wireMockRule.port(), "accountId", "username", "password", "v1.0"); } protected IrisClient getCustomClient() { DefaultHttpClient client = new DefaultHttpClient(); - HttpHost proxy = new HttpHost("localhost",8090); - client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY,proxy); + HttpHost proxy = new HttpHost("localhost", wireMockRule.port()); + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); Credentials credentials = new UsernamePasswordCredentials("userName", "password"); CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials( new AuthScope("localhost",8080), credentials); + credsProvider.setCredentials(new AuthScope("localhost", 8080), credentials); client.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); - return new IrisClient(client, "http://localhost:8090", "accountId", "username", "password"); + return new IrisClient(client, "http://localhost:" + wireMockRule.port(), "accountId", "username", "password"); } public void setMessage(String s) { diff --git a/src/test/java/com/bandwidth/iris/sdk/OAuthTests.java b/src/test/java/com/bandwidth/iris/sdk/OAuthTests.java new file mode 100644 index 0000000..92d42b6 --- /dev/null +++ b/src/test/java/com/bandwidth/iris/sdk/OAuthTests.java @@ -0,0 +1,347 @@ +package com.bandwidth.iris.sdk; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.Rule; +import org.junit.Test; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.junit.Assert.*; + +import java.security.cert.X509Certificate; + +public class OAuthTests { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort().enableBrowserProxying(true)); + + /** + * Get the base URL for WireMock using the dynamic port. + */ + private String getBaseUrl() { + return "http://localhost:" + wireMockRule.port(); + } + + /** + * Create an HTTP client configured to use WireMock as a proxy with SSL verification disabled. + */ + private DefaultHttpClient createProxyHttpClient() throws Exception { + DefaultHttpClient httpClient = new DefaultHttpClient(); + + HttpHost proxy = new HttpHost("localhost", wireMockRule.port()); + httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + + // Disable SSL certificate verification + TrustStrategy trustAll = new TrustStrategy() { + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) { + return true; + } + }; + + SSLSocketFactory socketFactory = new SSLSocketFactory(trustAll, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + Scheme scheme = new Scheme("https", 443, socketFactory); + httpClient.getConnectionManager().getSchemeRegistry().register(scheme); + + return httpClient; + } + + @Test + public void testConstructorWithPreConfiguredAccessToken() throws Exception { + // Client with pre-configured access token + long expiration = System.currentTimeMillis() / 1000 + 3600; + IrisClient client = new IrisClient("accountId", "username", "password", + "test-access-token", expiration); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should use Bearer token + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer test-access-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithClientCredentials() throws Exception { + DefaultHttpClient httpClient = createProxyHttpClient(); + + // Client with client credentials only + IrisClient client = new IrisClient(httpClient, getBaseUrl(), "accountId", "username", "password"); + client.setClientId("test-client-id"); + client.setClientSecret("test-client-secret"); + + // Token endpoint mock + stubFor(post(urlEqualTo("/api/v1/oauth2/token")) + .withHost(equalTo("api.bandwidth.com")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"access_token\":\"fetched-token\",\"expires_in\":3600}"))); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should fetch token and use it + verify(postRequestedFor(urlEqualTo("/api/v1/oauth2/token")) + .withHost(equalTo("api.bandwidth.com")) + .withHeader("Authorization", matching("Basic .*")) + .withRequestBody(equalTo("grant_type=client_credentials"))); + + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer fetched-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithAllOAuthFields() throws Exception { + // Client with all OAuth fields (client credentials + token + expiration) - default URI + long futureExpiration = System.currentTimeMillis() / 1000 + 3600; + IrisClient client = new IrisClient("accountId", "username", "password", + "v1.0", "test-client-id", "test-client-secret", + "existing-token", futureExpiration); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should use existing token and not fetch new one + verify(0, postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token"))); + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer existing-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithAllFieldsFullCustomization() throws Exception { + // Client with ALL fields customized + long futureExpiration = System.currentTimeMillis() / 1000 + 3600; + IrisClient client = new IrisClient(new DefaultHttpClient(), getBaseUrl(), + "accountId", "username", "password", + "v1.0", "test-client-id", "test-client-secret", + "full-custom-token", futureExpiration); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should use existing token and not fetch new one + verify(0, postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token"))); + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer full-custom-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testExpiredTokenTriggersRefresh() throws Exception { + // Client with expired access token + DefaultHttpClient httpClient = createProxyHttpClient(); + long pastExpiration = System.currentTimeMillis() / 1000 - 3600; + IrisClient client = new IrisClient(httpClient, getBaseUrl(), "accountId", + "username", "password", "v1.0", "test-client-id", + "test-client-secret", "expired-token", pastExpiration); + + stubFor(post(urlPathEqualTo("/api/v1/oauth2/token")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"access_token\":\"new-token\",\"expires_in\":3600}"))); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should fetch new token and use it + verify(postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token")) + .withHeader("Authorization", matching("Basic .*")) + .withRequestBody(equalTo("grant_type=client_credentials"))); + + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer new-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testClientWithoutOAuthUsesBasicAuth() throws Exception { + // Client without OAuth configuration + IrisClient client = new IrisClient("accountId", "username", "password"); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should NOT have Bearer token + verify(0, postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token"))); + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withoutHeader("Authorization")); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testTokenExpirationWithinOneMinuteTriggersRefresh() throws Exception { + // Client with token expiring in 30 seconds + DefaultHttpClient httpClient = createProxyHttpClient(); + long soonExpiration = System.currentTimeMillis() / 1000 + 30; + IrisClient client = new IrisClient(httpClient, "accountId", "username", "password", + "v1.0", "test-client-id", "test-client-secret", + "expiring-soon-token", soonExpiration); + + stubFor(post(urlPathEqualTo("/api/v1/oauth2/token")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"access_token\":\"refreshed-token\",\"expires_in\":3600}"))); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should fetch new token and use it + verify(postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token")) + .withHeader("Authorization", matching("Basic .*")) + .withRequestBody(equalTo("grant_type=client_credentials"))); + + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer refreshed-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithCustomUriAndVersion() throws Exception { + // Client with custom URI and version + IrisClient client = new IrisClient(getBaseUrl(), "accountId", "username", "password", "v2.0"); + + stubFor(get(urlPathEqualTo("/v2.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v2.0/accounts/accountId/sites"); + + // Should NOT have Bearer token + verify(0, postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token"))); + verify(getRequestedFor(urlPathEqualTo("/v2.0/accounts/accountId/sites")) + .withoutHeader("Authorization")); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithCustomUriVersionAndToken() throws Exception { + // Client with custom URI, version, and pre-configured token + long futureExpiration = System.currentTimeMillis() / 1000 + 3600; + IrisClient client = new IrisClient(getBaseUrl(), "accountId", "username", "password", + "v2.0", "custom-uri-token", futureExpiration); + + stubFor(get(urlPathEqualTo("/v2.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v2.0/accounts/accountId/sites"); + + // Should use existing token and not fetch new one + verify(0, postRequestedFor(urlPathEqualTo("/api/v1/oauth2/token"))); + verify(getRequestedFor(urlPathEqualTo("/v2.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer custom-uri-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test + public void testConstructorWithHttpClientUriAndClientCredentials() throws Exception { + // Client with custom httpClient, URI, and OAuth client credentials + DefaultHttpClient httpClient = createProxyHttpClient(); + IrisClient client = new IrisClient(httpClient, getBaseUrl(), "accountId", + "username", "password", "http-client-id", "http-client-secret"); + + stubFor(post(urlEqualTo("/api/v1/oauth2/token")) + .withHost(equalTo("api.bandwidth.com")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"access_token\":\"fetched-token\",\"expires_in\":3600}"))); + + stubFor(get(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + IrisResponse response = client.get(getBaseUrl() + "/v1.0/accounts/accountId/sites"); + + // Should fetch token and use it + verify(postRequestedFor(urlEqualTo("/api/v1/oauth2/token")) + .withHost(equalTo("api.bandwidth.com"))); + verify(getRequestedFor(urlPathEqualTo("/v1.0/accounts/accountId/sites")) + .withHeader("Authorization", equalTo("Bearer fetched-token"))); + + assertEquals(200, response.getStatusCode()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForNullAccountId() { + new IrisClient(new DefaultHttpClient(), getBaseUrl(), null, "username", "password", "v1.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForEmptyAccountId() { + new IrisClient(new DefaultHttpClient(), getBaseUrl(), "", "username", "password", "v1.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForNullUsername() { + new IrisClient(new DefaultHttpClient(), getBaseUrl(), "accountId", null, "password", "v1.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForEmptyPassword() { + new IrisClient(new DefaultHttpClient(), getBaseUrl(), "accountId", "username", "", "v1.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForNullUri() { + new IrisClient(new DefaultHttpClient(), null, "accountId", "username", "password", "v1.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsExceptionForEmptyVersion() { + new IrisClient(new DefaultHttpClient(), getBaseUrl(), "accountId", "username", "password", ""); + } +}