Skip to content

Commit 36d8a5d

Browse files
CLARIN-DSpace v9/Port #1346 + #1344 (CI test de-flakes) to the v9 base (#1379)
* test: de-flake ItemHandleCheckerIT (mock the live handle resolver) (#1346) (cherry picked from commit 867e43d on dtq-dev) Clean conflict-free pick, no v9 adaptations needed (test-only; okhttp3 mockwebserver 4.12.0 already a test-scope dep on v9-base). Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / 867e43d (BE-2, Vlna 1). * test: de-flake ORCID cache tests and ZIP-download IT (#1344) (cherry picked from commit f6f1356 on dtq-dev — Part 1 of 2) v9 adaptations / conflict resolution: - CachingOrcidRestConnectorTest.java and MetadataBitstreamControllerIT.java do not exist on dtq-dev-9-base (delete/modify conflicts) — resolved with git rm to DEFER them; when the backlog test-port items land (CLARIN_V9_PARITY_BACKLOG.md lines 217/514/530), those files MUST be sourced from f6f1356 state (== fork head), never from snapshot 8f4b80d, or the flaky byte-for-byte ZIP assert and the live ORCID sandbox dependency get re-introduced. - AuthorizationRestRepositoryIT (SSR authz de-flake via JVM system props + reloadConfig + @after clearSsrObjectResolution) and StatisticsRestRepositoryIT (solrLoggerService.commit() before TopCountries report) merged clean, no adaptation. Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / f6f1356 (BE-2, Vlna 1), ACs 8-9 DEFERRED per the card.
1 parent a7d8c71 commit 36d8a5d

3 files changed

Lines changed: 116 additions & 21 deletions

File tree

dspace-api/src/test/java/org/dspace/curate/ItemHandleCheckerIT.java

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import java.util.List;
2222
import java.util.Random;
2323

24+
import okhttp3.mockwebserver.Dispatcher;
25+
import okhttp3.mockwebserver.MockResponse;
26+
import okhttp3.mockwebserver.MockWebServer;
27+
import okhttp3.mockwebserver.RecordedRequest;
2428
import org.dspace.AbstractIntegrationTestWithDatabase;
2529
import org.dspace.authorize.AuthorizeException;
2630
import org.dspace.builder.ItemBuilder;
@@ -43,6 +47,12 @@
4347
/**
4448
* Test for checkhandles curation task.
4549
*
50+
* <p>The handle URLs are served by a local {@link MockWebServer} instead of the live handle resolver
51+
* (http://hdl.handle.net/), which the task contacts over HTTP. Hitting the live resolver made these tests
52+
* flaky: when the network was slow the HEAD request timed out and the task reported {@code 617}
53+
* (SocketTimeoutException) instead of the expected status. The mock dispatcher returns deterministic
54+
* responses keyed by path.</p>
55+
*
4656
* @author mkuchtiak
4757
*/
4858
public class ItemHandleCheckerIT extends AbstractIntegrationTestWithDatabase {
@@ -55,11 +65,10 @@ public class ItemHandleCheckerIT extends AbstractIntegrationTestWithDatabase {
5565
private static final String HANDLE_ITEM3 = HANDLE_COLLECTION + "-3";
5666
private static final String HANDLE_ITEM4 = HANDLE_COLLECTION + "-4";
5767
private static final String HANDLE_NON_EXISTING = HANDLE_COLLECTION + "-999";
58-
private static final String HANDLE_URL_REAL = "http://hdl.handle.net/20.1000/5555";
59-
private static final String HANDLE_INVALID = HANDLE_URL_REAL + "/..??^^/";
68+
// Path (relative to the mock server) of a handle that the resolver answers with a 302 redirect to a 200 page.
69+
private static final String HANDLE_REDIRECT_PATH = "20.1000/5555";
6070
private static final String HANDLE_IGNORED_1 = "11234/998";
6171
private static final String HANDLE_IGNORED_2 = "11234/999";
62-
private static final String HANDLE_URL_IGNORED = "http://hdl.handle.net/" + HANDLE_IGNORED_2;
6372

6473
protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService();
6574
protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
@@ -77,15 +86,50 @@ public class ItemHandleCheckerIT extends AbstractIntegrationTestWithDatabase {
7786
private Curator curator;
7887
private CuratorReportTest.ListReporter reporter;
7988

89+
// Local stand-in for the handle resolver. Started in setUp(); its base URL becomes handle.canonical.prefix.
90+
private MockWebServer mockHandleServer;
91+
// Previous handle.canonical.prefix, captured in setUp and restored in destroy so the shared config is not
92+
// left pointing at the now-closed mock server for later tests in the same JVM.
93+
private String originalHandlePrefix;
94+
// URLs that point at the mock server (computed from its dynamic port in setUp).
95+
private String handleUrlReal;
96+
private String handleUrlRedirectTarget;
97+
private String handleInvalid;
98+
private String handleUrlIgnored;
99+
80100
@Before
81101
@Override
82102
public void setUp() throws Exception {
83103
super.setUp();
84104
CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses();
85105
try {
106+
// Serve handle URLs from a local mock server so the task never contacts the live resolver.
107+
mockHandleServer = new MockWebServer();
108+
String baseUrl = mockHandleServer.url("/").toString();
109+
handleUrlReal = baseUrl + HANDLE_REDIRECT_PATH;
110+
handleUrlRedirectTarget = baseUrl + HANDLE_REDIRECT_PATH + "-target";
111+
handleInvalid = handleUrlReal + "/..??^^/";
112+
handleUrlIgnored = baseUrl + HANDLE_IGNORED_2;
113+
mockHandleServer.setDispatcher(new Dispatcher() {
114+
@Override
115+
public MockResponse dispatch(RecordedRequest request) {
116+
String path = request.getPath();
117+
if (("/" + HANDLE_REDIRECT_PATH).equals(path)) {
118+
// a "real" handle: 302 redirect; the task follows redirects manually via the Location header
119+
return new MockResponse().setResponseCode(302).setHeader("Location", handleUrlRedirectTarget);
120+
}
121+
if (("/" + HANDLE_REDIRECT_PATH + "-target").equals(path)) {
122+
return new MockResponse().setResponseCode(200);
123+
}
124+
// any other (well-formed, non-ignored) handle URL is treated as "not found"
125+
return new MockResponse().setResponseCode(404);
126+
}
127+
});
128+
86129
//we have to create a new community in the database
87130
context.turnOffAuthorisationSystem();
88-
cfg.setProperty("handle.canonical.prefix", "http://hdl.handle.net/");
131+
originalHandlePrefix = cfg.getProperty("handle.canonical.prefix");
132+
cfg.setProperty("handle.canonical.prefix", baseUrl);
89133
cfg.setProperty("curate.checklist.ignore", HANDLE_IGNORED_1 + "," + HANDLE_IGNORED_2);
90134

91135
this.parentCommunity = communityService.create(null, context);
@@ -131,7 +175,7 @@ public void testItemHandleNotFound() throws IOException {
131175

132176
@Test
133177
public void testItemHandleRedirected() throws IOException {
134-
replaceHandleUrl(item2, HANDLE_URL_REAL);
178+
replaceHandleUrl(item2, handleUrlReal);
135179
curator.curate(context, HANDLE_ITEM2);
136180
assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, curator.getStatus(TASK_NAME));
137181
assertTrue(curator.getResult(TASK_NAME).contains(redirectedResultForItem(item2)));
@@ -150,18 +194,18 @@ public void testNonExistingHandle() throws IOException {
150194

151195
@Test
152196
public void testInvalidHandleUrl() throws IOException {
153-
replaceHandleUrl(item3, HANDLE_INVALID);
197+
replaceHandleUrl(item3, handleInvalid);
154198
curator.curate(context, HANDLE_ITEM3);
155199
assertEquals("Curation should fail", Curator.CURATE_FAIL, curator.getStatus(TASK_NAME));
156200
String singleReport = reporter.getReport().get(0);
157-
assertTrue(singleReport.contains(HANDLE_INVALID + " = 500 - FAILED\n"));
201+
assertTrue(singleReport.contains(handleInvalid + " = 500 - FAILED\n"));
158202
assertTrue(singleReport.contains("Error: java.net.URISyntaxException: Illegal character"));
159203
reporter.getReport().clear();
160204
}
161205

162206
@Test
163207
public void testHandleUrlIgnored() throws IOException {
164-
replaceHandleUrl(item4, HANDLE_URL_IGNORED);
208+
replaceHandleUrl(item4, handleUrlIgnored);
165209
curator.curate(context, HANDLE_ITEM4);
166210
assertEquals("Curation should skip", Curator.CURATE_SKIP, curator.getStatus(TASK_NAME));
167211
assertEquals("Item: " + HANDLE_ITEM4 + "\n", reporter.getReport().get(0));
@@ -170,9 +214,9 @@ public void testHandleUrlIgnored() throws IOException {
170214

171215
@Test
172216
public void testCurateCollection() throws IOException {
173-
replaceHandleUrl(item2, HANDLE_URL_REAL);
174-
replaceHandleUrl(item3, HANDLE_INVALID);
175-
replaceHandleUrl(item4, HANDLE_URL_IGNORED);
217+
replaceHandleUrl(item2, handleUrlReal);
218+
replaceHandleUrl(item3, handleInvalid);
219+
replaceHandleUrl(item4, handleUrlIgnored);
176220
curator.curate(context, HANDLE_COLLECTION);
177221
// the final curator status is derived from the status of the latest checked item
178222
// so the final curator status is unpredictable
@@ -184,14 +228,21 @@ public void testCurateCollection() throws IOException {
184228
both(
185229
containsString("Item: " + item2.getHandle())).and(containsString(" = 200 - OK\n")
186230
), // item2
187-
containsString(HANDLE_INVALID + " = 500 - FAILED"), // item 3
231+
containsString(handleInvalid + " = 500 - FAILED"), // item 3
188232
is("Item: " + HANDLE_ITEM4 + "\n") // item 4 (ignored)
189233
));
190234
}
191235

192236
@After
193237
@Override
194238
public void destroy() throws Exception {
239+
if (mockHandleServer != null) {
240+
mockHandleServer.close();
241+
}
242+
// restore the shared config so a later test is not left pointing at the now-closed mock server
243+
if (originalHandlePrefix != null) {
244+
cfg.setProperty("handle.canonical.prefix", originalHandlePrefix);
245+
}
195246
// remove all registered handles properly
196247
identifierService.delete(context, item1, HANDLE_ITEM1);
197248
identifierService.delete(context, item2, HANDLE_ITEM2);

dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.dspace.eperson.Group;
6464
import org.dspace.services.ConfigurationService;
6565
import org.hamcrest.Matchers;
66+
import org.junit.After;
6667
import org.junit.Before;
6768
import org.junit.Test;
6869
import org.springframework.beans.factory.annotation.Autowired;
@@ -134,6 +135,42 @@ public class AuthorizationRestRepositoryIT extends AbstractControllerIntegration
134135
*/
135136
private AuthorizationFeature trueForUsersInGroupTest;
136137

138+
private static final String SSR_URL_KEY = "dspace.server.ssr.url";
139+
private static final String SSR_URL = "http://ssr.example.com/api";
140+
private static final String ALWAYS_THROW_TURNOFF_KEY =
141+
"org.dspace.app.rest.authorization.AlwaysThrowExceptionFeature.turnoff";
142+
143+
/**
144+
* Configure the SSR object-by-URI resolution used by the {@code search/object} tests (disarm the
145+
* AlwaysThrowExceptionFeature and define the SSR base URL), setting both as JVM <em>system properties</em>
146+
* (+ {@link ConfigurationService#reloadConfig()}) instead of {@code configurationService.setProperty(...)}.
147+
*
148+
* <p>A plain {@code setProperty(...)} override only lives in the in-memory combined-config view and is
149+
* silently dropped whenever that view is rebuilt by the auto-reload listener (which fires when any
150+
* reloadable cfg file's mtime changes, e.g. another test writing {@code local.cfg}). When that rebuild
151+
* lands mid-test the SSR url disappears and {@code search/object} no longer resolves the uri -> 400 (and a
152+
* dropped turnoff would let {@code alwaysexception} throw -> 500). A system property sits in the
153+
* highest-precedence override layer and is re-read on every rebuild, so it survives auto-reload; it is
154+
* cleared in {@link #clearSsrObjectResolution()}. Same pattern as
155+
* AuthenticationRestControllerIT#setAuthenticationMethodSequence.</p>
156+
*/
157+
private void enableSsrObjectResolution() {
158+
System.setProperty(ALWAYS_THROW_TURNOFF_KEY, "true");
159+
System.setProperty(SSR_URL_KEY, SSR_URL);
160+
configurationService.reloadConfig();
161+
}
162+
163+
/**
164+
* Remove the system-property overrides set by {@link #enableSsrObjectResolution()} so they do not leak into
165+
* other tests in the same JVM. Runs before the superclass {@code @After}, whose {@code reloadConfig()} then
166+
* restores the on-disk defaults.
167+
*/
168+
@After
169+
public void clearSsrObjectResolution() {
170+
System.clearProperty(SSR_URL_KEY);
171+
System.clearProperty(ALWAYS_THROW_TURNOFF_KEY);
172+
}
173+
137174
@Override
138175
@Before
139176
public void setUp() throws Exception {
@@ -852,9 +889,9 @@ public void findByObjectSSRTest() throws Exception {
852889
SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT);
853890
String siteUri = "http://ssr.example.com/api/core/sites/" + siteRest.getId();
854891

855-
// disarm the alwaysThrowExceptionFeature
856-
configurationService.setProperty("org.dspace.app.rest.authorization.AlwaysThrowExceptionFeature.turnoff", true);
857-
configurationService.setProperty("dspace.server.ssr.url", "http://ssr.example.com/api");
892+
// Disarm the alwaysThrowExceptionFeature and define the SSR base URL via system properties so the
893+
// overrides survive a mid-test config auto-reload (see enableSsrObjectResolution).
894+
enableSsrObjectResolution();
858895

859896
String adminToken = getAuthToken(admin.getEmail(), password);
860897
String epersonToken = getAuthToken(eperson.getEmail(), password);
@@ -969,9 +1006,9 @@ public void findByObjectSSRTest() throws Exception {
9691006
*/
9701007
@Test
9711008
public void findByObjectBadRequestSSRTest() throws Exception {
972-
// disarm the alwaysThrowExceptionFeature
973-
configurationService.setProperty("org.dspace.app.rest.authorization.AlwaysThrowExceptionFeature.turnoff", true);
974-
configurationService.setProperty("dspace.server.ssr.url", "http://ssr.example.com/api");
1009+
// Disarm the alwaysThrowExceptionFeature and define the SSR base URL via system properties so the
1010+
// overrides survive a mid-test config auto-reload (see enableSsrObjectResolution).
1011+
enableSsrObjectResolution();
9751012
String[] invalidUris = new String[] {
9761013
"invalid-uri",
9771014
"",
@@ -1050,9 +1087,9 @@ public void findByObjectBadRequestSSRTest() throws Exception {
10501087
public void findByNotExistingObjectSSSTest() throws Exception {
10511088
String wrongSiteUri = "http://localhost/api/core/sites/" + UUID.randomUUID();
10521089

1053-
// disarm the alwaysThrowExceptionFeature
1054-
configurationService.setProperty("org.dspace.app.rest.authorization.AlwaysThrowExceptionFeature.turnoff", true);
1055-
configurationService.setProperty("dspace.server.ssr.url", "http://ssr.example.com/api");
1090+
// Disarm the alwaysThrowExceptionFeature and define the SSR base URL via system properties so the
1091+
// overrides survive a mid-test config auto-reload (see enableSsrObjectResolution).
1092+
enableSsrObjectResolution();
10561093

10571094
String adminToken = getAuthToken(admin.getEmail(), password);
10581095

dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,13 @@ public void topCountriesReport_Community_Visited() throws Exception {
924924
.contentType(contentType))
925925
.andExpect(status().isCreated());
926926

927+
// Force a commit that waits for a new searcher so the two just-posted view events are guaranteed to be
928+
// flushed and visible to the report query below. This covers both ways the report could otherwise come
929+
// back empty: postView()'s own commit uses waitSearcher=false (can return before the searcher reopens),
930+
// and if the solr-statistics.autoCommit=false override were dropped by a mid-test config reload then
931+
// postView() skips its commit entirely (Solr's own autoCommit only fires after 10s).
932+
StatisticsServiceFactory.getInstance().getSolrLoggerService().commit();
933+
927934
// And request that collection's TopCountries report
928935
getClient(adminToken).perform(
929936
get("/api/statistics/usagereports/" + communityVisited.getID() + "_" + TOP_COUNTRIES_REPORT_ID))

0 commit comments

Comments
 (0)