Skip to content

Commit fbc15e3

Browse files
committed
- Improved AuthInterceptor to conditionally skip Content-Type header for DELETE requests to /releases/{release_uid}.
- Added unit tests for `AuthInterceptor` to verify header behavior for different request types. - Updated error messages in `ContentstackUnitTest` for clarity. - Introduced a new `UnitTestSuite` to run all unit tests without API access. - Adjusted assertions in various unit tests to ensure correct header presence.
1 parent 38d2c8b commit fbc15e3

File tree

15 files changed

+342
-31
lines changed

15 files changed

+342
-31
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@
245245
<version>3.0.0-M5</version>
246246
<configuration>
247247
<includes>
248+
<!-- Run all test files following Maven naming conventions -->
249+
<include>**/Test*.java</include>
250+
<include>**/*Test.java</include>
251+
<include>**/*Tests.java</include>
252+
<include>**/*TestCase.java</include>
248253
<include>**/*TestSuite.java</include>
249254
</includes>
250255
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>

src/main/java/com/contentstack/cms/Contentstack.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,12 @@ private OkHttpClient httpClient(Contentstack contentstack, Boolean retryOnFailur
858858
builder.addInterceptor(this.oauthInterceptor);
859859
} else {
860860
this.authInterceptor = contentstack.interceptor = new AuthInterceptor();
861+
862+
// Configure early access if needed
863+
if (this.earlyAccess != null) {
864+
this.authInterceptor.setEarlyAccess(this.earlyAccess);
865+
}
866+
861867
builder.addInterceptor(this.authInterceptor);
862868
}
863869

src/main/java/com/contentstack/cms/core/AuthInterceptor.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package com.contentstack.cms.core;
22

3+
import java.io.IOException;
4+
5+
import org.jetbrains.annotations.NotNull;
6+
37
import okhttp3.Interceptor;
48
import okhttp3.Request;
59
import okhttp3.Response;
6-
import org.jetbrains.annotations.NotNull;
7-
8-
import java.io.IOException;
910

1011
/**
1112
* <b>The type Header interceptor that extends Interceptor</b>
@@ -73,16 +74,42 @@ public void setEarlyAccess(String[] earlyAccess) {
7374
@Override
7475
public Response intercept(Chain chain) throws IOException {
7576
final String xUserAgent = Util.SDK_NAME + "/v" + Util.SDK_VERSION;
76-
Request.Builder request = chain.request().newBuilder().header(Util.X_USER_AGENT, xUserAgent).header(Util.USER_AGENT, Util.defaultUserAgent()).header(Util.CONTENT_TYPE, Util.CONTENT_TYPE_VALUE);
77+
Request originalRequest = chain.request();
78+
Request.Builder request = originalRequest.newBuilder()
79+
.header(Util.X_USER_AGENT, xUserAgent)
80+
.header(Util.USER_AGENT, Util.defaultUserAgent());
81+
82+
// Skip Content-Type header for DELETE /releases/{release_uid} request
83+
// to avoid "Body cannot be empty when content-type is set to 'application/json'" error
84+
if (!isDeleteReleaseRequest(originalRequest)) {
85+
request.header(Util.CONTENT_TYPE, Util.CONTENT_TYPE_VALUE);
86+
}
7787

7888
if (this.authtoken != null) {
7989
request.addHeader(Util.AUTHTOKEN, this.authtoken);
8090
}
81-
if (this.earlyAccess!=null && this.earlyAccess.length > 0) {
91+
92+
if (this.earlyAccess != null && this.earlyAccess.length > 0) {
8293
String commaSeparated = String.join(", ", earlyAccess);
8394
request.addHeader(Util.EARLY_ACCESS_HEADER, commaSeparated);
8495
}
8596
return chain.proceed(request.build());
8697
}
8798

99+
/**
100+
* Checks if the request is a DELETE request to /releases/{release_uid} endpoint.
101+
* This endpoint should not have Content-Type header as it doesn't accept a body.
102+
*
103+
* @param request The HTTP request to check
104+
* @return true if this is a DELETE /releases/{release_uid} request
105+
*/
106+
private boolean isDeleteReleaseRequest(Request request) {
107+
if (!"DELETE".equals(request.method())) {
108+
return false;
109+
}
110+
String path = request.url().encodedPath();
111+
// Match pattern: /v3/releases/{release_uid} (no trailing path segments)
112+
return path.matches(".*/releases/[^/]+$");
113+
}
114+
88115
}

src/main/java/com/contentstack/cms/models/OAuthConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ public String getFormattedAuthorizationEndpoint() {
8282
if (hostname.contains("contentstack")) {
8383
hostname = hostname
8484
.replaceAll("-api\\.", "-app.") // eu-api.contentstack.com -> eu-app.contentstack.com
85+
.replaceAll("-api-", "-app-") // eu-api-contentstack.com -> eu-app-contentstack.com
8586
.replaceAll("^api\\.", "app.") // api.contentstack.io -> app.contentstack.io
87+
.replaceAll("^api-", "app-") // api-contentstack.com -> app-contentstack.com
8688
.replaceAll("\\.io$", ".com"); // *.io -> *.com
8789
} else {
8890
hostname = Util.OAUTH_APP_HOST;
@@ -107,7 +109,9 @@ public String getTokenEndpoint() {
107109
if (hostname.contains("contentstack")) {
108110
hostname = hostname
109111
.replaceAll("-api\\.", "-developerhub-api.") // eu-api.contentstack.com -> eu-developerhub-api.contentstack.com
112+
.replaceAll("-api-", "-developerhub-api-") // eu-api-contentstack.com -> eu-developerhub-api-contentstack.com
110113
.replaceAll("^api\\.", "developerhub-api.") // api.contentstack.io -> developerhub-api.contentstack.io
114+
.replaceAll("^api-", "developerhub-api-") // api-contentstack.com -> developerhub-api-contentstack.com
111115
.replaceAll("\\.io$", ".com"); // *.io -> *.com
112116
} else {
113117
hostname = Util.OAUTH_API_HOST;

src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,44 @@ public Response intercept(Chain chain) throws IOException {
4242
Request.Builder requestBuilder = originalRequest.newBuilder()
4343
.header("X-User-Agent", Util.defaultUserAgent())
4444
.header("User-Agent", Util.defaultUserAgent())
45-
.header("Content-Type", originalRequest.url().toString().contains("/token") ? "application/x-www-form-urlencoded" : "application/json")
4645
.header("x-header-ea", earlyAccess != null ? String.join(",", earlyAccess) : "true");
46+
47+
// Skip Content-Type header for DELETE /releases/{release_uid} request
48+
// to avoid "Body cannot be empty when content-type is set to 'application/json'" error
49+
if (!isDeleteReleaseRequest(originalRequest)) {
50+
String contentType = originalRequest.url().toString().contains("/token")
51+
? "application/x-www-form-urlencoded"
52+
: "application/json";
53+
requestBuilder.header("Content-Type", contentType);
54+
}
55+
4756
// Skip auth header for token endpoints
4857
if (!originalRequest.url().toString().contains("/token")) {
4958
if (oauthHandler.getTokens() != null && oauthHandler.getTokens().hasAccessToken()) {
5059
requestBuilder.header("Authorization", "Bearer " + oauthHandler.getAccessToken());
51-
5260
}
5361
}
5462

5563
// Execute request with retry and refresh handling
5664
return executeRequest(chain, requestBuilder.build(), 0);
5765
}
5866

67+
/**
68+
* Checks if the request is a DELETE request to /releases/{release_uid} endpoint.
69+
* This endpoint should not have Content-Type header as it doesn't accept a body.
70+
*
71+
* @param request The HTTP request to check
72+
* @return true if this is a DELETE /releases/{release_uid} request
73+
*/
74+
private boolean isDeleteReleaseRequest(Request request) {
75+
if (!"DELETE".equals(request.method())) {
76+
return false;
77+
}
78+
String path = request.url().encodedPath();
79+
// Match pattern: /v3/releases/{release_uid} (no trailing path segments)
80+
return path.matches(".*/releases/[^/]+$");
81+
}
82+
5983
private Response executeRequest(Chain chain, Request request, int retryCount) throws IOException {
6084
// Skip token refresh for token endpoints to avoid infinite loops
6185
if (request.url().toString().contains("/token")) {

src/test/java/com/contentstack/cms/ContentstackUnitTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ void testSetOrganizations() {
191191
client.organization();
192192
} catch (Exception e) {
193193
System.out.println(e.getLocalizedMessage());
194-
Assertions.assertEquals("Please Login to access user instance", e.getLocalizedMessage());
194+
Assertions.assertEquals("Login or configure OAuth to continue. organization", e.getLocalizedMessage());
195195
}
196196
}
197197

@@ -203,7 +203,7 @@ void testSetAuthtokenLogin() {
203203
try {
204204
client.login("[email protected]", "fake@password");
205205
} catch (Exception e) {
206-
Assertions.assertEquals("User is already loggedIn, Please logout then try to login again", e.getMessage());
206+
Assertions.assertEquals("Operation not allowed. You are already logged in.", e.getMessage());
207207
}
208208
Assertions.assertEquals("fake@authtoken", client.authtoken);
209209
}
@@ -216,7 +216,7 @@ void testSetAuthtokenLoginWithTfa() {
216216
params.put("tfaToken", "fake@tfa");
217217
client.login("[email protected]", "fake@password", params);
218218
} catch (Exception e) {
219-
Assertions.assertEquals("User is already loggedIn, Please logout then try to login again", e.getMessage());
219+
Assertions.assertEquals("Operation not allowed. You are already logged in.", e.getMessage());
220220
}
221221
Assertions.assertEquals("fake@authtoken", client.authtoken);
222222
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.contentstack.cms;
2+
3+
import com.contentstack.cms.core.AuthInterceptorTest;
4+
import com.contentstack.cms.stack.EnvironmentUnitTest;
5+
import com.contentstack.cms.stack.GlobalFieldUnitTests;
6+
import com.contentstack.cms.stack.LocaleUnitTest;
7+
import com.contentstack.cms.stack.ReleaseUnitTest;
8+
import org.junit.platform.runner.JUnitPlatform;
9+
import org.junit.platform.suite.api.SelectClasses;
10+
import org.junit.runner.RunWith;
11+
12+
/**
13+
* Unit Test Suite for running all unit tests
14+
* These tests don't require API access or credentials
15+
*
16+
* Note: Only public test classes can be included here.
17+
* Many unit test classes in the project are package-private and
18+
* cannot be referenced in this suite.
19+
*/
20+
@SuppressWarnings("deprecation")
21+
@RunWith(JUnitPlatform.class)
22+
@SelectClasses({
23+
// Core tests
24+
AuthInterceptorTest.class,
25+
ContentstackUnitTest.class,
26+
27+
// Stack module tests (only public classes)
28+
EnvironmentUnitTest.class,
29+
GlobalFieldUnitTests.class,
30+
LocaleUnitTest.class,
31+
ReleaseUnitTest.class
32+
})
33+
public class UnitTestSuite {
34+
}
35+

0 commit comments

Comments
 (0)