Skip to content

Commit f9433d2

Browse files
authored
Convert "location overlap" tests to unit tests (#607)
These test cases validate the logic of applying proper table location overlaps checks based on various config permutations. As such, running these test on a full Polaris Server is an overkill in terms of resources. This PR constructs in-memory services local to each test case for exercising the same call paths as before.
1 parent e548601 commit f9433d2

File tree

4 files changed

+315
-393
lines changed

4 files changed

+315
-393
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.dropwizard;
20+
21+
import com.google.auth.oauth2.AccessToken;
22+
import com.google.auth.oauth2.GoogleCredentials;
23+
import jakarta.ws.rs.core.SecurityContext;
24+
import java.security.Principal;
25+
import java.time.Clock;
26+
import java.time.Instant;
27+
import java.util.Date;
28+
import java.util.Map;
29+
import java.util.Set;
30+
import org.apache.polaris.core.PolarisCallContext;
31+
import org.apache.polaris.core.PolarisDiagnostics;
32+
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
33+
import org.apache.polaris.core.auth.PolarisAuthorizer;
34+
import org.apache.polaris.core.context.CallContext;
35+
import org.apache.polaris.core.context.RealmContext;
36+
import org.apache.polaris.core.entity.PolarisEntity;
37+
import org.apache.polaris.core.entity.PrincipalEntity;
38+
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
39+
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
40+
import org.apache.polaris.core.persistence.cache.EntityCache;
41+
import org.apache.polaris.service.admin.PolarisServiceImpl;
42+
import org.apache.polaris.service.admin.api.PolarisCatalogsApi;
43+
import org.apache.polaris.service.catalog.IcebergCatalogAdapter;
44+
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi;
45+
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
46+
import org.apache.polaris.service.catalog.io.FileIOFactory;
47+
import org.apache.polaris.service.config.DefaultConfigurationStore;
48+
import org.apache.polaris.service.config.RealmEntityManagerFactory;
49+
import org.apache.polaris.service.context.CallContextCatalogFactory;
50+
import org.apache.polaris.service.context.PolarisCallContextCatalogFactory;
51+
import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory;
52+
import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
53+
import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
54+
import org.apache.polaris.service.task.TaskExecutor;
55+
import org.mockito.Mockito;
56+
57+
public record TestServices(
58+
IcebergRestCatalogApi restApi,
59+
PolarisCatalogsApi catalogsApi,
60+
RealmContext realmContext,
61+
SecurityContext securityContext) {
62+
private static final RealmContext testRealm = () -> "test-realm";
63+
64+
public static TestServices inMemory(Map<String, Object> config) {
65+
return inMemory(new TestFileIOFactory(), config);
66+
}
67+
68+
public static TestServices inMemory(FileIOFactory ioFactory) {
69+
return inMemory(ioFactory, Map.of());
70+
}
71+
72+
public static TestServices inMemory(FileIOFactory ioFactory, Map<String, Object> config) {
73+
InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory =
74+
new InMemoryPolarisMetaStoreManagerFactory();
75+
metaStoreManagerFactory.setStorageIntegrationProvider(
76+
new PolarisStorageIntegrationProviderImpl(
77+
Mockito::mock, () -> GoogleCredentials.create(new AccessToken("abc", new Date()))));
78+
79+
PolarisMetaStoreManager metaStoreManager =
80+
metaStoreManagerFactory.getOrCreateMetaStoreManager(testRealm);
81+
82+
EntityCache cache = new EntityCache(metaStoreManager);
83+
RealmEntityManagerFactory realmEntityManagerFactory =
84+
new RealmEntityManagerFactory(metaStoreManagerFactory, () -> cache) {};
85+
CallContextCatalogFactory callContextFactory =
86+
new PolarisCallContextCatalogFactory(
87+
realmEntityManagerFactory,
88+
metaStoreManagerFactory,
89+
Mockito.mock(TaskExecutor.class),
90+
ioFactory);
91+
PolarisAuthorizer authorizer = Mockito.mock(PolarisAuthorizer.class);
92+
IcebergRestCatalogApiService service =
93+
new IcebergCatalogAdapter(
94+
callContextFactory, realmEntityManagerFactory, metaStoreManagerFactory, authorizer);
95+
IcebergRestCatalogApi restApi = new IcebergRestCatalogApi(service);
96+
97+
PolarisMetaStoreSession session =
98+
metaStoreManagerFactory.getOrCreateSessionSupplier(testRealm).get();
99+
PolarisCallContext context =
100+
new PolarisCallContext(
101+
session,
102+
Mockito.mock(PolarisDiagnostics.class),
103+
new DefaultConfigurationStore(config),
104+
Clock.systemDefaultZone());
105+
PolarisMetaStoreManager.CreatePrincipalResult createdPrincipal =
106+
metaStoreManager.createPrincipal(
107+
context,
108+
new PrincipalEntity.Builder()
109+
.setName("test-principal")
110+
.setCreateTimestamp(Instant.now().toEpochMilli())
111+
.setCredentialRotationRequiredState()
112+
.build());
113+
114+
AuthenticatedPolarisPrincipal principal =
115+
new AuthenticatedPolarisPrincipal(
116+
PolarisEntity.of(createdPrincipal.getPrincipal()), Set.of());
117+
118+
SecurityContext securityContext =
119+
new SecurityContext() {
120+
@Override
121+
public Principal getUserPrincipal() {
122+
return principal;
123+
}
124+
125+
@Override
126+
public boolean isUserInRole(String s) {
127+
return false;
128+
}
129+
130+
@Override
131+
public boolean isSecure() {
132+
return true;
133+
}
134+
135+
@Override
136+
public String getAuthenticationScheme() {
137+
return "";
138+
}
139+
};
140+
141+
PolarisCatalogsApi catalogsApi =
142+
new PolarisCatalogsApi(
143+
new PolarisServiceImpl(realmEntityManagerFactory, metaStoreManagerFactory, authorizer));
144+
145+
CallContext.setCurrentContext(CallContext.of(testRealm, context));
146+
return new TestServices(restApi, catalogsApi, testRealm, securityContext);
147+
}
148+
}

dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingCatalogTest.java

Lines changed: 34 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,77 +18,36 @@
1818
*/
1919
package org.apache.polaris.service.dropwizard.admin;
2020

21-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
2221
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2323

24-
import io.dropwizard.testing.ConfigOverride;
25-
import io.dropwizard.testing.ResourceHelpers;
26-
import io.dropwizard.testing.junit5.DropwizardAppExtension;
27-
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
28-
import jakarta.ws.rs.client.Entity;
29-
import jakarta.ws.rs.client.Invocation;
3024
import jakarta.ws.rs.core.Response;
31-
import java.io.IOException;
3225
import java.util.ArrayList;
3326
import java.util.Arrays;
3427
import java.util.List;
28+
import java.util.Map;
3529
import java.util.UUID;
30+
import org.apache.iceberg.exceptions.ValidationException;
3631
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
3732
import org.apache.polaris.core.admin.model.Catalog;
3833
import org.apache.polaris.core.admin.model.CatalogProperties;
3934
import org.apache.polaris.core.admin.model.CreateCatalogRequest;
4035
import org.apache.polaris.core.admin.model.StorageConfigInfo;
41-
import org.apache.polaris.service.dropwizard.PolarisApplication;
42-
import org.apache.polaris.service.dropwizard.config.PolarisApplicationConfig;
43-
import org.apache.polaris.service.dropwizard.test.PolarisConnectionExtension;
44-
import org.apache.polaris.service.dropwizard.test.PolarisRealm;
45-
import org.apache.polaris.service.dropwizard.test.TestEnvironmentExtension;
46-
import org.junit.jupiter.api.BeforeAll;
47-
import org.junit.jupiter.api.extension.ExtendWith;
36+
import org.apache.polaris.service.dropwizard.TestServices;
37+
import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory;
4838
import org.junit.jupiter.params.ParameterizedTest;
4939
import org.junit.jupiter.params.provider.CsvSource;
5040

51-
@ExtendWith({
52-
DropwizardExtensionsSupport.class,
53-
TestEnvironmentExtension.class,
54-
PolarisConnectionExtension.class
55-
})
5641
public class PolarisOverlappingCatalogTest {
57-
private static final DropwizardAppExtension<PolarisApplicationConfig> EXT =
58-
new DropwizardAppExtension<>(
59-
PolarisApplication.class,
60-
ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"),
61-
// Bind to random port to support parallelism
62-
ConfigOverride.config("server.applicationConnectors[0].port", "0"),
63-
ConfigOverride.config("server.adminConnectors[0].port", "0"),
64-
// Block overlapping catalog paths:
65-
ConfigOverride.config("featureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS", "false"));
66-
private static String userToken;
67-
private static String realm;
68-
69-
@BeforeAll
70-
public static void setup(
71-
PolarisConnectionExtension.PolarisToken adminToken, @PolarisRealm String polarisRealm)
72-
throws IOException {
73-
userToken = adminToken.token();
74-
realm = polarisRealm;
75-
76-
// Set up the database location
77-
PolarisConnectionExtension.createTestDir(realm);
78-
}
42+
43+
static TestServices services =
44+
TestServices.inMemory(
45+
new TestFileIOFactory(), Map.of("ALLOW_OVERLAPPING_CATALOG_URLS", "false"));
7946

8047
private Response createCatalog(String prefix, String defaultBaseLocation, boolean isExternal) {
8148
return createCatalog(prefix, defaultBaseLocation, isExternal, new ArrayList<String>());
8249
}
8350

84-
private static Invocation.Builder request() {
85-
return EXT.client()
86-
.target(String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort()))
87-
.request("application/json")
88-
.header("Authorization", "Bearer " + userToken)
89-
.header(REALM_PROPERTY_KEY, realm);
90-
}
91-
9251
private Response createCatalog(
9352
String prefix,
9453
String defaultBaseLocation,
@@ -118,9 +77,10 @@ private Response createCatalog(
11877
System.currentTimeMillis(),
11978
1,
12079
config);
121-
try (Response response = request().post(Entity.json(new CreateCatalogRequest(catalog)))) {
122-
return response;
123-
}
80+
return services
81+
.catalogsApi()
82+
.createCatalog(
83+
new CreateCatalogRequest(catalog), services.realmContext(), services.securityContext());
12484
}
12585

12686
@ParameterizedTest
@@ -144,12 +104,14 @@ public void testBasicOverlappingCatalogs(boolean initiallyExternal, boolean late
144104
.returns(Response.Status.CREATED.getStatusCode(), Response::getStatus);
145105

146106
// inside `root`
147-
assertThat(createCatalog(prefix, "root/child", laterExternal))
148-
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
107+
assertThatThrownBy(() -> createCatalog(prefix, "root/child", laterExternal))
108+
.isInstanceOf(ValidationException.class)
109+
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
149110

150111
// `root` is inside this
151-
assertThat(createCatalog(prefix, "", laterExternal))
152-
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
112+
assertThatThrownBy(() -> createCatalog(prefix, "", laterExternal))
113+
.isInstanceOf(ValidationException.class)
114+
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
153115
}
154116

155117
@ParameterizedTest
@@ -166,17 +128,24 @@ public void testAllowedLocationOverlappingCatalogs(
166128
.returns(Response.Status.CREATED.getStatusCode(), Response::getStatus);
167129

168130
// This DBL overlaps with initial AL
169-
assertThat(createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs")))
170-
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
131+
assertThatThrownBy(
132+
() ->
133+
createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs")))
134+
.isInstanceOf(ValidationException.class)
135+
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
171136

172137
// This AL overlaps with initial DBL
173-
assertThat(
174-
createCatalog(
175-
prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals")))
176-
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
138+
assertThatThrownBy(
139+
() ->
140+
createCatalog(
141+
prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals")))
142+
.isInstanceOf(ValidationException.class)
143+
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
177144

178145
// This AL overlaps with an initial AL
179-
assertThat(createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats")))
180-
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
146+
assertThatThrownBy(
147+
() -> createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats")))
148+
.isInstanceOf(ValidationException.class)
149+
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
181150
}
182151
}

0 commit comments

Comments
 (0)