mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-30 02:07:04 +00:00
Co-authored-by: Esteban Gutierrez <esteban.gutierrez@acryl.io> Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
1314 lines
53 KiB
Java
1314 lines
53 KiB
Java
package app;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import static org.mockito.Mockito.*;
|
|
import static play.mvc.Http.Status.MOVED_PERMANENTLY;
|
|
import static play.mvc.Http.Status.OK;
|
|
import static play.test.Helpers.fakeRequest;
|
|
import static play.test.Helpers.route;
|
|
|
|
import com.google.inject.AbstractModule;
|
|
import com.google.inject.Provides;
|
|
import com.google.inject.Singleton;
|
|
import com.linkedin.common.urn.Urn;
|
|
import com.linkedin.entity.Entity;
|
|
import com.linkedin.entity.client.SystemEntityClient;
|
|
import com.linkedin.metadata.aspect.CorpUserAspectArray;
|
|
import com.linkedin.metadata.snapshot.CorpUserSnapshot;
|
|
import com.linkedin.metadata.snapshot.Snapshot;
|
|
import com.linkedin.mxe.MetadataChangeProposal;
|
|
import com.linkedin.r2.RemoteInvocationException;
|
|
import com.nimbusds.jwt.JWT;
|
|
import com.nimbusds.jwt.JWTClaimsSet;
|
|
import com.nimbusds.jwt.JWTParser;
|
|
import controllers.routes;
|
|
import java.io.IOException;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.InetAddress;
|
|
import java.net.ServerSocket;
|
|
import java.net.URL;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import no.nav.security.mock.oauth2.MockOAuth2Server;
|
|
import no.nav.security.mock.oauth2.http.OAuth2HttpRequest;
|
|
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse;
|
|
import no.nav.security.mock.oauth2.http.Route;
|
|
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback;
|
|
import okhttp3.Headers;
|
|
import okhttp3.mockwebserver.MockResponse;
|
|
import okhttp3.mockwebserver.MockWebServer;
|
|
import okhttp3.mockwebserver.RecordedRequest;
|
|
import org.awaitility.Awaitility;
|
|
import org.awaitility.Durations;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.junit.jupiter.api.AfterAll;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.TestInstance;
|
|
import org.junitpioneer.jupiter.SetEnvironmentVariable;
|
|
import org.openqa.selenium.Cookie;
|
|
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import play.Application;
|
|
import play.Environment;
|
|
import play.Mode;
|
|
import play.inject.guice.GuiceApplicationBuilder;
|
|
import play.mvc.Http;
|
|
import play.mvc.Result;
|
|
import play.test.Helpers;
|
|
import play.test.TestBrowser;
|
|
import play.test.WithBrowser;
|
|
|
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
@SetEnvironmentVariable(key = "DATAHUB_SECRET", value = "test")
|
|
@SetEnvironmentVariable(key = "KAFKA_BOOTSTRAP_SERVER", value = "")
|
|
@SetEnvironmentVariable(key = "DATAHUB_ANALYTICS_ENABLED", value = "false")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_ENABLED", value = "true")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_JIT_PROVISIONING_ENABLED", value = "true")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_EXTRACT_GROUPS_ENABLED", value = "true")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_GROUPS_CLAIM_NAME", value = "groups")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_ID", value = "testclient")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_SECRET", value = "testsecret")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_CONNECT_TIMEOUT", value = "2000")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_READ_TIMEOUT", value = "2000")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_HTTP_RETRY_ATTEMPTS", value = "5")
|
|
@SetEnvironmentVariable(key = "AUTH_OIDC_HTTP_RETRY_DELAY", value = "500")
|
|
@SetEnvironmentVariable(key = "AUTH_VERBOSE_LOGGING", value = "true")
|
|
public class ApplicationTest extends WithBrowser {
|
|
private static final Logger logger = LoggerFactory.getLogger(ApplicationTest.class);
|
|
private static final String ISSUER_ID = "testIssuer";
|
|
|
|
@Override
|
|
protected Application provideApplication() {
|
|
return new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
}
|
|
|
|
@Override
|
|
protected TestBrowser provideBrowser(int port) {
|
|
HtmlUnitDriver webClient = new HtmlUnitDriver();
|
|
webClient.setJavascriptEnabled(false);
|
|
return Helpers.testBrowser(webClient, providePort());
|
|
}
|
|
|
|
/** Find an available port for testing to avoid port conflicts */
|
|
private int findAvailablePort() {
|
|
try (ServerSocket socket = new ServerSocket(0)) {
|
|
return socket.getLocalPort();
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Failed to find available port", e);
|
|
}
|
|
}
|
|
|
|
private MockOAuth2Server oauthServer;
|
|
private Thread oauthServerThread;
|
|
private CompletableFuture<Void> oauthServerStarted;
|
|
|
|
private MockWebServer gmsServer;
|
|
|
|
private String wellKnownUrl;
|
|
private int actualOauthServerPort;
|
|
private int actualGmsServerPort;
|
|
private String actualGmsServerHost;
|
|
|
|
private static final String TEST_USER = "urn:li:corpuser:testUser@myCompany.com";
|
|
private static final String TEST_TOKEN = "faketoken_YCpYIrjQH4sD3_rAc3VPPFg4";
|
|
|
|
@BeforeAll
|
|
public void init() throws IOException {
|
|
// Store actual ports to avoid dynamic allocation issues
|
|
actualOauthServerPort = findAvailablePort();
|
|
actualGmsServerPort = findAvailablePort();
|
|
|
|
// Start Mock GMS
|
|
gmsServer = new MockWebServer();
|
|
|
|
// Set up dispatcher to handle requests based on path
|
|
gmsServer.setDispatcher(
|
|
new okhttp3.mockwebserver.Dispatcher() {
|
|
@Override
|
|
public MockResponse dispatch(RecordedRequest request) {
|
|
String path = request.getPath();
|
|
|
|
// Handle user info requests
|
|
if (path.contains("/users/") && path.contains("/info")) {
|
|
return new MockResponse().setBody(String.format("{\"value\":\"%s\"}", TEST_USER));
|
|
}
|
|
|
|
// Handle token generation requests
|
|
if (path.contains("/generateSessionTokenForUser")) {
|
|
return new MockResponse()
|
|
.setBody(String.format("{\"accessToken\":\"%s\"}", TEST_TOKEN));
|
|
}
|
|
|
|
// Handle other requests (like /config endpoint requests)
|
|
if (path.contains("/config") || path.contains("/health")) {
|
|
return new MockResponse().setResponseCode(200).setBody("{}");
|
|
}
|
|
|
|
// No stored sso settings
|
|
if (path.contains("/auth/getSsoSettings")) {
|
|
return new MockResponse().setResponseCode(404);
|
|
}
|
|
|
|
// Log and return 404 for unexpected requests
|
|
logger.warn("GMS Server received unexpected request: {} {}", request.getMethod(), path);
|
|
return new MockResponse().setResponseCode(404).setBody("Not found");
|
|
}
|
|
});
|
|
|
|
gmsServer.start(InetAddress.getByName("localhost"), actualGmsServerPort);
|
|
|
|
// Start Mock Identity Provider
|
|
startMockOauthServer();
|
|
// Start Play Frontend
|
|
startServer();
|
|
// Start Browser
|
|
createBrowser();
|
|
|
|
Awaitility.await().timeout(Durations.TEN_SECONDS).until(() -> app != null);
|
|
|
|
// Wait for all servers to be fully ready to handle requests
|
|
Awaitility.await()
|
|
.timeout(Durations.TEN_SECONDS)
|
|
.until(
|
|
() -> {
|
|
try {
|
|
// Check Play application readiness
|
|
browser.goTo("/admin");
|
|
|
|
// Check mock OAuth server critical endpoints
|
|
// Test the well-known endpoint (first thing OIDC client hits)
|
|
String wellKnownUrl =
|
|
String.format(
|
|
"http://localhost:%d/%s/.well-known/openid-configuration",
|
|
actualOauthServerPort, ISSUER_ID);
|
|
java.net.URL url = new java.net.URL(wellKnownUrl);
|
|
java.net.HttpURLConnection connection =
|
|
(java.net.HttpURLConnection) url.openConnection();
|
|
connection.setRequestMethod("GET");
|
|
connection.setConnectTimeout(5000);
|
|
connection.setReadTimeout(5000);
|
|
int wellKnownResponseCode = connection.getResponseCode();
|
|
connection.disconnect();
|
|
|
|
// Test the userinfo endpoint (where groups are retrieved)
|
|
String userInfoUrl =
|
|
String.format(
|
|
"http://localhost:%d/%s/userinfo", actualOauthServerPort, ISSUER_ID);
|
|
java.net.URL userInfoUrlObj = new java.net.URL(userInfoUrl);
|
|
java.net.HttpURLConnection userInfoConnection =
|
|
(java.net.HttpURLConnection) userInfoUrlObj.openConnection();
|
|
userInfoConnection.setRequestMethod("GET");
|
|
userInfoConnection.setConnectTimeout(5000);
|
|
userInfoConnection.setReadTimeout(5000);
|
|
int userInfoResponseCode = userInfoConnection.getResponseCode();
|
|
userInfoConnection.disconnect();
|
|
|
|
logger.debug(
|
|
"All servers readiness check - Play app: OK, well-known: {}, userinfo: {}",
|
|
wellKnownResponseCode,
|
|
userInfoResponseCode);
|
|
|
|
return wellKnownResponseCode == 200 && userInfoResponseCode == 200;
|
|
} catch (Exception e) {
|
|
logger.debug("Servers not ready yet: {}", e.getMessage());
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
@BeforeEach
|
|
public void setUpTest() {
|
|
// Clear any captured proposals from previous tests to prevent interference
|
|
TestModule.clearCapturedProposals();
|
|
|
|
// Clear browser cookies and state to ensure clean test isolation
|
|
if (browser != null) {
|
|
// Clear cookies using the underlying WebDriver
|
|
browser.getDriver().manage().deleteAllCookies();
|
|
}
|
|
}
|
|
|
|
@AfterAll
|
|
public void shutdown() throws IOException {
|
|
if (gmsServer != null) {
|
|
logger.info("Shutdown Mock GMS");
|
|
gmsServer.shutdown();
|
|
}
|
|
logger.info("Shutdown Play Frontend");
|
|
stopServer();
|
|
if (oauthServer != null) {
|
|
logger.info("Shutdown MockOAuth2Server");
|
|
oauthServer.shutdown();
|
|
}
|
|
if (oauthServerThread != null && oauthServerThread.isAlive()) {
|
|
logger.info("Shutdown MockOAuth2Server thread");
|
|
oauthServerThread.interrupt();
|
|
try {
|
|
oauthServerThread.join(1000); // Wait up to 1 second for thread to finish
|
|
} catch (InterruptedException e) {
|
|
logger.warn("Shutdown MockOAuth2Server thread failed to join.");
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
// Force stop if still alive
|
|
if (oauthServerThread.isAlive()) {
|
|
logger.warn("OAuth server thread still alive after interrupt, forcing stop");
|
|
oauthServerThread.stop(); // Force stop as last resort
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startMockOauthServer() {
|
|
// Configure HEAD responses and userInfo endpoint
|
|
Route[] routes =
|
|
new Route[] {
|
|
new Route() {
|
|
@Override
|
|
public boolean match(@NotNull OAuth2HttpRequest oAuth2HttpRequest) {
|
|
return "HEAD".equals(oAuth2HttpRequest.getMethod())
|
|
&& (String.format("/%s/.well-known/openid-configuration", ISSUER_ID)
|
|
.equals(oAuth2HttpRequest.getUrl().url().getPath())
|
|
|| String.format("/%s/token", ISSUER_ID)
|
|
.equals(oAuth2HttpRequest.getUrl().url().getPath())
|
|
|| String.format("/%s/userinfo", ISSUER_ID)
|
|
.equals(oAuth2HttpRequest.getUrl().url().getPath()));
|
|
}
|
|
|
|
@Override
|
|
public OAuth2HttpResponse invoke(OAuth2HttpRequest oAuth2HttpRequest) {
|
|
String path = oAuth2HttpRequest.getUrl().url().getPath();
|
|
String responseBody = null;
|
|
|
|
if (path.equals(String.format("/%s/.well-known/openid-configuration", ISSUER_ID))) {
|
|
// Return well-known configuration with userinfo endpoint
|
|
int port = oAuth2HttpRequest.getUrl().url().getPort();
|
|
responseBody =
|
|
String.format(
|
|
"{\n"
|
|
+ " \"issuer\": \"http://localhost:%d/%s\",\n"
|
|
+ " \"authorization_endpoint\": \"http://localhost:%d/%s/authorize\",\n"
|
|
+ " \"token_endpoint\": \"http://localhost:%d/%s/token\",\n"
|
|
+ " \"userinfo_endpoint\": \"http://localhost:%d/%s/userinfo\",\n"
|
|
+ " \"jwks_uri\": \"http://localhost:%d/%s/.well-known/jwks.json\",\n"
|
|
+ " \"response_types_supported\": [\"code\"],\n"
|
|
+ " \"subject_types_supported\": [\"public\"],\n"
|
|
+ " \"id_token_signing_alg_values_supported\": [\"RS256\"]\n"
|
|
+ "}",
|
|
port, ISSUER_ID, port, ISSUER_ID, port, ISSUER_ID, port, ISSUER_ID, port,
|
|
ISSUER_ID);
|
|
} else if (path.equals(String.format("/%s/userinfo", ISSUER_ID))) {
|
|
// For HEAD requests to userinfo endpoint, just return 200 with no body
|
|
responseBody = null;
|
|
}
|
|
|
|
return new OAuth2HttpResponse(
|
|
Headers.of(
|
|
Map.of(
|
|
"Content-Type", "application/json",
|
|
"Cache-Control", "no-store",
|
|
"Pragma", "no-cache")),
|
|
200,
|
|
responseBody,
|
|
null);
|
|
}
|
|
},
|
|
// Add userInfo endpoint route
|
|
new Route() {
|
|
@Override
|
|
public boolean match(@NotNull OAuth2HttpRequest oAuth2HttpRequest) {
|
|
return "GET".equals(oAuth2HttpRequest.getMethod())
|
|
&& String.format("/%s/userinfo", ISSUER_ID)
|
|
.equals(oAuth2HttpRequest.getUrl().url().getPath());
|
|
}
|
|
|
|
@Override
|
|
public OAuth2HttpResponse invoke(OAuth2HttpRequest oAuth2HttpRequest) {
|
|
// Log the request to help debug
|
|
logger.debug(
|
|
"UserInfo endpoint called with headers: {}", oAuth2HttpRequest.getHeaders());
|
|
|
|
// Return userInfo with groups (not in ID token)
|
|
String userInfoResponse =
|
|
String.format(
|
|
"{\n"
|
|
+ " \"sub\": \"testUser\",\n"
|
|
+ " \"preferred_username\": \"testUser\",\n"
|
|
+ " \"given_name\": \"Test\",\n"
|
|
+ " \"family_name\": \"User\",\n"
|
|
+ " \"email\": \"testUser@myCompany.com\",\n"
|
|
+ " \"name\": \"Test User\",\n"
|
|
+ " \"groups\": \"myGroup\",\n"
|
|
+ " \"email_verified\": true\n"
|
|
+ "}");
|
|
|
|
return new OAuth2HttpResponse(
|
|
Headers.of(Map.of("Content-Type", "application/json")),
|
|
200,
|
|
userInfoResponse,
|
|
null);
|
|
}
|
|
}
|
|
};
|
|
oauthServer = new MockOAuth2Server(routes);
|
|
oauthServerStarted = new CompletableFuture<>();
|
|
|
|
// Create and start server in separate thread
|
|
oauthServerThread =
|
|
new Thread(
|
|
() -> {
|
|
try {
|
|
// Configure mock responses - groups are NOT in ID token, only in userInfo endpoint
|
|
// Enqueue enough callbacks for all tests with retries (22 tests * 10 retries = 220)
|
|
for (int i = 0; i < 220; i++) {
|
|
oauthServer.enqueueCallback(
|
|
new DefaultOAuth2TokenCallback(
|
|
ISSUER_ID,
|
|
"testUser",
|
|
"JWT",
|
|
List.of(),
|
|
Map.of(
|
|
"email", "testUser@myCompany.com"
|
|
// Note: groups are intentionally NOT included in ID token
|
|
// They will only be available from the userInfo endpoint
|
|
),
|
|
600));
|
|
}
|
|
|
|
oauthServer.start(InetAddress.getByName("localhost"), actualOauthServerPort);
|
|
|
|
oauthServerStarted.complete(null);
|
|
|
|
// Keep thread alive - it will be interrupted during shutdown
|
|
while (!Thread.currentThread().isInterrupted()) {
|
|
try {
|
|
Thread.sleep(1000);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
break;
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
oauthServerStarted.completeExceptionally(e);
|
|
}
|
|
});
|
|
|
|
oauthServerThread.setDaemon(true); // Ensure thread doesn't prevent JVM shutdown
|
|
oauthServerThread.start();
|
|
|
|
// Wait for server to start with timeout - block until complete
|
|
try {
|
|
oauthServerStarted.get(10, TimeUnit.SECONDS);
|
|
} catch (TimeoutException e) {
|
|
throw new RuntimeException("MockOAuth2Server failed to start within timeout", e);
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("MockOAuth2Server failed to start", e);
|
|
}
|
|
|
|
// Discovery url to authorization server metadata
|
|
wellKnownUrl =
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/"
|
|
+ ISSUER_ID
|
|
+ "/.well-known/openid-configuration";
|
|
|
|
// Wait for server to return configuration with retries
|
|
// Validate mock server returns data
|
|
int maxRetries = 20;
|
|
int retryCount = 0;
|
|
boolean serverReady = false;
|
|
|
|
while (retryCount < maxRetries && !serverReady) {
|
|
try {
|
|
URL url = new URL(wellKnownUrl);
|
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
conn.setRequestMethod("GET");
|
|
conn.setConnectTimeout(2000);
|
|
conn.setReadTimeout(2000);
|
|
int responseCode = conn.getResponseCode();
|
|
conn.disconnect();
|
|
|
|
if (responseCode == 200) {
|
|
logger.info("Successfully started MockOAuth2Server after {} attempts.", retryCount + 1);
|
|
serverReady = true;
|
|
} else {
|
|
throw new RuntimeException(
|
|
"MockOAuth2Server not accessible. Response code: " + responseCode);
|
|
}
|
|
} catch (Exception e) {
|
|
retryCount++;
|
|
if (retryCount >= maxRetries) {
|
|
throw new RuntimeException(
|
|
"Failed to connect to MockOAuth2Server after " + maxRetries + " attempts", e);
|
|
}
|
|
logger.debug("Waiting for OAuth server to be ready, attempt {}/{}", retryCount, maxRetries);
|
|
try {
|
|
Thread.sleep(1000);
|
|
} catch (InterruptedException ie) {
|
|
Thread.currentThread().interrupt();
|
|
throw new RuntimeException("Interrupted while waiting for MockOAuth2Server", ie);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Additional wait to ensure token endpoint is also fully ready
|
|
try {
|
|
Thread.sleep(2000);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
throw new RuntimeException("Interrupted during final OAuth server stabilization", e);
|
|
}
|
|
logger.info("MockOAuth2Server fully initialized and stabilized.");
|
|
}
|
|
|
|
@Test
|
|
public void testHealth() {
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.healthcheck());
|
|
|
|
Result result = route(app, request);
|
|
assertEquals(OK, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testAppConfigWithBasePath() {
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.appConfig());
|
|
|
|
Result result = route(app, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
assertTrue(
|
|
content.contains(
|
|
"\"\"")); // Default basePath is "" (empty) as configured in provideApplication()
|
|
}
|
|
|
|
@Test
|
|
public void testAppConfigWithCustomBasePath() {
|
|
// Create a new application with custom basePath
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/custom")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.appConfig());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains custom basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
assertTrue(content.contains("\"/custom\""));
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithBasePathInjection() {
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(app, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath injection
|
|
String content = Helpers.contentAsString(result);
|
|
// The HTML should contain the basePath replacement
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\"/\""));
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithCustomBasePathInjection() {
|
|
// Create a new application with custom basePath
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// Use "index.html" instead of "" to avoid matching the trailing slash redirect rule
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index("index.html"));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains custom basePath injection
|
|
String content = Helpers.contentAsString(result);
|
|
// The HTML should contain the custom basePath
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\"/datahub/\""));
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithBasePathEndingSlash() {
|
|
// Test basePath that doesn't end with slash gets slash added
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath with trailing slash
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\"/datahub/\""));
|
|
}
|
|
|
|
@Test
|
|
public void testRedirectTrailingSlash() {
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.redirectTrailingSlash("test"));
|
|
|
|
Result result = route(app, request);
|
|
assertEquals(MOVED_PERMANENTLY, result.status());
|
|
assertEquals("/test", result.redirectLocation().orElse(""));
|
|
}
|
|
|
|
@Test
|
|
public void testAppConfigWithEmptyBasePath() {
|
|
// Create a new application with empty basePath
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.appConfig());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains empty basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
assertTrue(content.contains("\"\"")); // Empty basePath
|
|
}
|
|
|
|
@Test
|
|
public void testAppConfigWithNullBasePath() {
|
|
// Create a new application without basePath configuration (should use default)
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.appConfig());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains default basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
// Should contain the default value from application.conf
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithEmptyBasePath() {
|
|
// Test with empty basePath
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// Use "index.html" instead of "" to avoid matching the trailing slash redirect rule
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index("index.html"));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response handles empty basePath correctly
|
|
String content = Helpers.contentAsString(result);
|
|
// Should still contain the basePath replacement logic
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\""));
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithRootBasePath() {
|
|
// Test with root basePath "/"
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains root basePath
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\"/\""));
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithComplexBasePath() {
|
|
// Test with complex basePath like "/datahub/v2"
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub/v2")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains complex basePath
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("@basePath") || content.contains("href=\"/datahub/v2/\""));
|
|
}
|
|
|
|
@Test
|
|
public void testPlayHttpContextIntegration() {
|
|
// Test integration with play.http.context configuration
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("play.http.context", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// When play.http.context is set, routes are prefixed with that context
|
|
Http.RequestBuilder request =
|
|
fakeRequest(Helpers.GET, "/datahub" + routes.Application.appConfig().url());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
assertTrue(content.contains("\"/datahub\""));
|
|
}
|
|
|
|
@Test
|
|
public void testPlayHttpContextWithDifferentBasePath() {
|
|
// Test when play.http.context and datahub.basePath are different
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/frontend")
|
|
.configure("play.http.context", "/api")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// When play.http.context is set, routes are prefixed with that context
|
|
Http.RequestBuilder request =
|
|
fakeRequest(Helpers.GET, "/api" + routes.Application.appConfig().url());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains the datahub.basePath, not play.http.context
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(content.contains("\"basePath\""));
|
|
assertTrue(content.contains("\"/frontend\""));
|
|
}
|
|
|
|
@Test
|
|
public void testAppConfigWithAllBasePathVariations() {
|
|
// Test various basePath configurations to ensure robustness
|
|
String[] basePaths = {"", "/", "/datahub", "/datahub/", "/custom/path", "/custom/path/"};
|
|
|
|
for (String basePath : basePaths) {
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", basePath)
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.appConfig());
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath configuration
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(
|
|
content.contains("\"basePath\""),
|
|
"BasePath not found in response for basePath: " + basePath);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWithAllBasePathVariations() {
|
|
// Test various basePath configurations in index method
|
|
String[] basePaths = {"", "/", "/datahub", "/datahub/", "/custom/path", "/custom/path/"};
|
|
|
|
for (String basePath : basePaths) {
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", basePath)
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
|
|
// Verify the response contains basePath injection
|
|
String content = Helpers.contentAsString(result);
|
|
assertTrue(
|
|
content.contains("@basePath") || content.contains("href="),
|
|
"BasePath injection not found in response for basePath: " + basePath);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testIndex() {
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(app, request);
|
|
assertEquals(OK, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testMovedPermanently() {
|
|
// We expect now to be redirected instead of returning 404
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index("/other"));
|
|
Result result = route(app, request);
|
|
assertEquals(MOVED_PERMANENTLY, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testOpenIdConfig() {
|
|
assertEquals(
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration",
|
|
wellKnownUrl);
|
|
}
|
|
|
|
@Test
|
|
public void testHappyPathOidc() throws ParseException {
|
|
// Verify the list is actually empty (cleared by @BeforeEach)
|
|
assertTrue(TestModule.getCapturedProposals().isEmpty());
|
|
|
|
browser.goTo("/authenticate");
|
|
assertEquals("", browser.url());
|
|
|
|
Cookie actorCookie = browser.getCookie("actor");
|
|
assertEquals(TEST_USER, actorCookie.getValue());
|
|
|
|
Cookie sessionCookie = browser.getCookie("PLAY_SESSION");
|
|
String jwtStr = sessionCookie.getValue();
|
|
JWT jwt = JWTParser.parse(jwtStr);
|
|
JWTClaimsSet claims = jwt.getJWTClaimsSet();
|
|
Map<String, String> data = (Map<String, String>) claims.getClaim("data");
|
|
assertEquals(TEST_TOKEN, data.get("token"));
|
|
assertEquals(TEST_USER, data.get("actor"));
|
|
// Default expiration is 24h, so should always be less than current time + 1 day since it stamps
|
|
// the time before this executes. Use a more generous tolerance to account for timezone
|
|
// differences
|
|
// and test execution time variations.
|
|
Date maxExpectedExpiration =
|
|
new Date(System.currentTimeMillis() + (25 * 60 * 60 * 1000)); // 25 hours
|
|
Date minExpectedExpiration =
|
|
new Date(System.currentTimeMillis() + (23 * 60 * 60 * 1000)); // 23 hours
|
|
Date actualExpiration = claims.getExpirationTime();
|
|
|
|
assertTrue(
|
|
actualExpiration.after(minExpectedExpiration)
|
|
&& actualExpiration.before(maxExpectedExpiration),
|
|
"JWT expiration time should be within reasonable bounds. Actual: "
|
|
+ actualExpiration
|
|
+ ", Min expected: "
|
|
+ minExpectedExpiration
|
|
+ ", Max expected: "
|
|
+ maxExpectedExpiration);
|
|
|
|
// Verify that ingestProposal was called for group membership and user status updates
|
|
List<MetadataChangeProposal> capturedProposals = TestModule.getCapturedProposals();
|
|
logger.debug("Captured {} ingestProposal calls", capturedProposals.size());
|
|
|
|
// We should have at least 2 calls: one for group membership and one for user status
|
|
assertTrue(capturedProposals.size() >= 2);
|
|
|
|
// Verify we have a group membership proposal
|
|
boolean hasGroupMembership =
|
|
capturedProposals.stream()
|
|
.anyMatch(proposal -> "groupMembership".equals(proposal.getAspectName()));
|
|
assertTrue(hasGroupMembership);
|
|
|
|
// Verify we have a user status proposal
|
|
boolean hasUserStatus =
|
|
capturedProposals.stream()
|
|
.anyMatch(proposal -> "corpUserStatus".equals(proposal.getAspectName()));
|
|
assertTrue(hasUserStatus);
|
|
|
|
// Verify the basic OIDC flow worked and the user was authenticated
|
|
assertNotNull(actorCookie);
|
|
assertNotNull(sessionCookie);
|
|
|
|
// Find and validate the group membership proposal
|
|
MetadataChangeProposal groupMembershipProposal =
|
|
capturedProposals.stream()
|
|
.filter(proposal -> "groupMembership".equals(proposal.getAspectName()))
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
assertNotNull(groupMembershipProposal);
|
|
assertEquals(
|
|
"urn:li:corpuser:testUser@myCompany.com",
|
|
groupMembershipProposal.getEntityUrn().toString());
|
|
assertEquals("corpuser", groupMembershipProposal.getEntityType());
|
|
assertEquals("groupMembership", groupMembershipProposal.getAspectName());
|
|
assertEquals("UPSERT", groupMembershipProposal.getChangeType().toString());
|
|
|
|
// Validate that the group membership proposal contains "myGroup" from the /userInfo endpoint
|
|
String aspectData =
|
|
groupMembershipProposal.getAspect().getValue().asString(StandardCharsets.UTF_8);
|
|
logger.debug("Group membership aspect data: {}", aspectData);
|
|
assertTrue(aspectData.contains("myGroup"));
|
|
}
|
|
|
|
@Test
|
|
public void testOidcRedirectToRequestedUrl() {
|
|
// Wait briefly to ensure browser is ready after @BeforeEach cleanup
|
|
Awaitility.await()
|
|
.timeout(Durations.TEN_SECONDS)
|
|
.pollDelay(Durations.ONE_HUNDRED_MILLISECONDS)
|
|
.until(() -> browser.getDriver() != null);
|
|
|
|
browser.goTo("/authenticate?redirect_uri=%2Fcontainer%2Furn%3Ali%3Acontainer%3ADATABASE");
|
|
assertEquals("container/urn:li:container:DATABASE", browser.url());
|
|
}
|
|
|
|
/**
|
|
* The Redirect Uri parameter is used to store a previous relative location within the app to be
|
|
* able to take a user back to their expected page. Redirecting to other domains should be
|
|
* blocked.
|
|
*/
|
|
@Test
|
|
public void testInvalidRedirectUrl() {
|
|
browser.goTo("/authenticate?redirect_uri=https%3A%2F%2Fwww.google.com");
|
|
assertEquals("", browser.url());
|
|
|
|
browser.goTo("/authenticate?redirect_uri=file%3A%2F%2FmyFile");
|
|
assertEquals("", browser.url());
|
|
|
|
browser.goTo("/authenticate?redirect_uri=ftp%3A%2F%2FsomeFtp");
|
|
assertEquals("", browser.url());
|
|
|
|
browser.goTo("/authenticate?redirect_uri=localhost%3A9002%2Flogin");
|
|
assertEquals("", browser.url());
|
|
}
|
|
|
|
/** Test module that provides comprehensive mocks to handle all GMS interactions */
|
|
private static class TestModule extends AbstractModule {
|
|
// Store captured ingestProposal calls for validation
|
|
private static final List<MetadataChangeProposal> capturedProposals = new ArrayList<>();
|
|
|
|
@Override
|
|
protected void configure() {
|
|
// This module will override providers for GMS interactions
|
|
}
|
|
|
|
public static List<MetadataChangeProposal> getCapturedProposals() {
|
|
return new ArrayList<>(capturedProposals);
|
|
}
|
|
|
|
public static void clearCapturedProposals() {
|
|
capturedProposals.clear();
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected SystemEntityClient provideMockSystemEntityClient() {
|
|
logger.debug("Creating mock SystemEntityClient");
|
|
SystemEntityClient mockClient = mock(SystemEntityClient.class);
|
|
|
|
try {
|
|
// Configure user provisioning mocks (mirrors tryProvisionUser)
|
|
configureUserProvisioningMocks(mockClient);
|
|
|
|
// Configure group provisioning mocks (mirrors tryProvisionGroups)
|
|
configureGroupProvisioningMocks(mockClient);
|
|
|
|
// Mock ingestProposal to capture calls for validation
|
|
doAnswer(
|
|
invocation -> {
|
|
MetadataChangeProposal proposal = invocation.getArgument(1);
|
|
logger.debug(
|
|
"ingestProposal() called with entityUrn: {}, entityType: {}, aspectName: {}, changeType: {}",
|
|
proposal.getEntityUrn(),
|
|
proposal.getEntityType(),
|
|
proposal.getAspectName(),
|
|
proposal.getChangeType());
|
|
// Capture the proposal for validation
|
|
capturedProposals.add(proposal);
|
|
return null;
|
|
})
|
|
.when(mockClient)
|
|
.ingestProposal(any(), any());
|
|
} catch (RemoteInvocationException e) {
|
|
// This should not happen with mocks, but handle it just in case
|
|
throw new RuntimeException("Failed to configure mock SystemEntityClient", e);
|
|
} catch (Exception e) {
|
|
// This should not happen with mocks, but handle it just in case
|
|
throw new RuntimeException("Failed to configure mock SystemEntityClient", e);
|
|
}
|
|
|
|
return mockClient;
|
|
}
|
|
|
|
/**
|
|
* Configures mocks for user provisioning flow (mirrors tryProvisionUser method). - First get()
|
|
* call returns null (user doesn't exist) - update() call stores the actual Entity object -
|
|
* Subsequent get() calls return the stored Entity
|
|
*/
|
|
private void configureUserProvisioningMocks(SystemEntityClient mockClient)
|
|
throws RemoteInvocationException {
|
|
final AtomicReference<Entity> storedEntity = new AtomicReference<>();
|
|
|
|
// First call to get() returns an Entity with just a key aspect (user doesn't exist),
|
|
// subsequent calls return the stored user with full aspects
|
|
doAnswer(
|
|
invocation -> {
|
|
Entity entity = storedEntity.get();
|
|
if (entity == null) {
|
|
// Return an Entity with just a key aspect (simulating non-existent user)
|
|
Entity keyOnlyEntity = new Entity();
|
|
// Create a minimal CorpUserSnapshot with just the key aspect
|
|
CorpUserSnapshot keyOnlySnapshot = new CorpUserSnapshot();
|
|
keyOnlySnapshot.setUrn(invocation.getArgument(1)); // The URN from the get() call
|
|
keyOnlySnapshot.setAspects(new CorpUserAspectArray());
|
|
keyOnlyEntity.setValue(Snapshot.create(keyOnlySnapshot));
|
|
logger.debug("get() called, returning key-only entity for non-existent user");
|
|
return keyOnlyEntity;
|
|
} else {
|
|
logger.debug("get() called, returning stored entity with full aspects");
|
|
return entity;
|
|
}
|
|
})
|
|
.when(mockClient)
|
|
.get(any(), any());
|
|
|
|
// Store the entity when update() is called
|
|
doAnswer(
|
|
invocation -> {
|
|
Entity entity = invocation.getArgument(1);
|
|
storedEntity.set(entity);
|
|
logger.debug("update() called, stored entity: {}", entity);
|
|
return null;
|
|
})
|
|
.when(mockClient)
|
|
.update(any(), any());
|
|
}
|
|
|
|
/**
|
|
* Configures mocks for group provisioning flow (mirrors tryProvisionGroups method). -
|
|
* batchGet() returns empty map initially (groups don't exist) - batchUpdate() stores the actual
|
|
* Map<Urn, Entity> of groups - Subsequent batchGet() calls return the stored groups
|
|
*/
|
|
private void configureGroupProvisioningMocks(SystemEntityClient mockClient)
|
|
throws RemoteInvocationException {
|
|
final AtomicReference<Map<Urn, Entity>> storedGroups =
|
|
new AtomicReference<>(Collections.emptyMap());
|
|
|
|
// batchGet() returns the stored groups (empty initially, then populated after batchUpdate)
|
|
doAnswer(
|
|
invocation -> {
|
|
Map<Urn, Entity> groups = storedGroups.get();
|
|
logger.debug("batchGet() called, returning {} groups", groups.size());
|
|
return groups;
|
|
})
|
|
.when(mockClient)
|
|
.batchGet(any(), any());
|
|
|
|
// Store the groups when batchUpdate() is called
|
|
doAnswer(
|
|
invocation -> {
|
|
Set<Entity> groupEntities = invocation.getArgument(1);
|
|
// Convert Set<Entity> to Map<Urn, Entity> for storage
|
|
Map<Urn, Entity> groupsMap = new HashMap<>();
|
|
for (Entity entity : groupEntities) {
|
|
Urn urn = entity.getValue().getCorpGroupSnapshot().getUrn();
|
|
groupsMap.put(urn, entity);
|
|
}
|
|
storedGroups.set(groupsMap);
|
|
logger.debug("batchUpdate() called, stored {} groups", groupEntities.size());
|
|
return null;
|
|
})
|
|
.when(mockClient)
|
|
.batchUpdate(any(), any());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testMapPathGraphQLMappingWithBasePath() {
|
|
// Test that legacy GraphQL path is mapped correctly even with base path
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// Test legacy GraphQL path mapping with base path
|
|
Http.RequestBuilder request = fakeRequest(Helpers.GET, "/datahub/api/v2/graphql");
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testMapPathRegularApiPathWithBasePath() {
|
|
// Test that regular API paths with base path are handled correctly
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
// Test regular API path with base path
|
|
Http.RequestBuilder request = fakeRequest(Helpers.GET, "/datahub/api/entities");
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testSwaggerUiPathWithBasePath() {
|
|
// Test swagger UI path with base path - should preserve full path and not strip base path
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "/datahub")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModule())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(Helpers.GET, "/datahub/openapi/swagger-ui");
|
|
|
|
Result result = route(customApp, request);
|
|
assertEquals(OK, result.status());
|
|
}
|
|
|
|
@Test
|
|
public void testIndexWhenResourceNotFound() {
|
|
// Create a new application with a custom test module that overrides the Application controller
|
|
// to simulate the case where the index.html resource cannot be loaded
|
|
Application customApp =
|
|
new GuiceApplicationBuilder()
|
|
.configure("metadataService.port", String.valueOf(actualGmsServerPort))
|
|
.configure("metadataService.host", "localhost")
|
|
.configure("datahub.basePath", "")
|
|
.configure("auth.baseUrl", "http://localhost:" + providePort())
|
|
.configure(
|
|
"auth.oidc.discoveryUri",
|
|
"http://localhost:"
|
|
+ actualOauthServerPort
|
|
+ "/testIssuer/.well-known/openid-configuration")
|
|
.overrides(new TestModuleWithFailingResource())
|
|
.in(new Environment(Mode.TEST))
|
|
.build();
|
|
|
|
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
|
|
|
|
Result result = route(customApp, request);
|
|
|
|
// When the resource cannot be loaded, the controller should return a 404 status
|
|
assertEquals(play.mvc.Http.Status.NOT_FOUND, result.status());
|
|
|
|
// Verify the response has the correct content type and cache control header
|
|
assertEquals("text/html", result.contentType().orElse(""));
|
|
assertTrue(result.headers().containsKey("Cache-Control"));
|
|
assertEquals("no-cache", result.headers().get("Cache-Control"));
|
|
}
|
|
|
|
/**
|
|
* Test module that provides a mock Application controller that simulates resource loading failure
|
|
*/
|
|
private static class TestModuleWithFailingResource extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
// This module will override the Application controller
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected controllers.Application provideFailingApplicationController(
|
|
Environment environment, com.typesafe.config.Config config) {
|
|
// Create a mock HttpClient
|
|
java.net.http.HttpClient mockHttpClient = mock(java.net.http.HttpClient.class);
|
|
|
|
// Create a mock Environment that returns null for resourceAsStream
|
|
Environment mockEnvironment = mock(Environment.class);
|
|
when(mockEnvironment.resourceAsStream("public/index.html")).thenReturn(null);
|
|
|
|
return new controllers.Application(mockHttpClient, mockEnvironment, config);
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected SystemEntityClient provideMockSystemEntityClient() {
|
|
// Reuse the same mock SystemEntityClient as the base TestModule
|
|
return new TestModule().provideMockSystemEntityClient();
|
|
}
|
|
}
|
|
}
|