Skip to content

Commit 8ff1b38

Browse files
committed
test: Add coverage tests for async job providers
Adds tests for error handling, job cancellation, and edge cases in JobProvider and JobResultProvider. Improves branch coverage from 36% to 50% for JobProvider and from 72% to 81% for JobResultProvider.
1 parent 4e07bb8 commit 8ff1b38

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

server/src/test/java/au/csiro/pathling/async/JobProviderTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@
2525
import au.csiro.pathling.config.AsyncConfiguration;
2626
import au.csiro.pathling.config.AuthorizationConfiguration;
2727
import au.csiro.pathling.config.ServerConfiguration;
28+
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
29+
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
30+
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
2831
import jakarta.servlet.http.HttpServletResponse;
2932
import java.util.Optional;
3033
import java.util.concurrent.CompletableFuture;
34+
import java.util.concurrent.ExecutionException;
35+
import java.util.concurrent.Future;
3136
import org.apache.spark.sql.SparkSession;
3237
import org.hl7.fhir.instance.model.api.IBaseResource;
3338
import org.hl7.fhir.r4.model.Parameters;
@@ -209,4 +214,69 @@ void completedJobWithRedirectSetsCacheHeaders() {
209214

210215
assertThat(response.getHeader("Cache-Control")).isEqualTo("max-age=60");
211216
}
217+
218+
@Test
219+
void cancelledJobReturns404() {
220+
// A cancelled job should return 404 Not Found.
221+
final CompletableFuture<IBaseResource> future = new CompletableFuture<>();
222+
future.cancel(false);
223+
final Job<IBaseResource> job = new Job<>(JOB_ID, "export", future, Optional.empty());
224+
jobRegistry.register(job);
225+
226+
assertThatThrownBy(() -> jobProvider.job(JOB_ID, request, response))
227+
.isInstanceOf(ResourceNotFoundException.class)
228+
.hasMessageContaining("DELETE request cancelled this job");
229+
}
230+
231+
@Test
232+
void interruptedJobThrowsInternalError() {
233+
// A job that was interrupted should throw an InternalErrorException.
234+
@SuppressWarnings("unchecked")
235+
final Future<IBaseResource> mockFuture = mock(Future.class);
236+
try {
237+
when(mockFuture.isDone()).thenReturn(true);
238+
when(mockFuture.isCancelled()).thenReturn(false);
239+
when(mockFuture.get()).thenThrow(new InterruptedException("Thread was interrupted"));
240+
} catch (final InterruptedException | ExecutionException e) {
241+
throw new RuntimeException(e);
242+
}
243+
244+
final Job<IBaseResource> job = new Job<>(JOB_ID, "export", mockFuture, Optional.empty());
245+
jobRegistry.register(job);
246+
247+
assertThatThrownBy(() -> jobProvider.job(JOB_ID, request, response))
248+
.isInstanceOf(InternalErrorException.class)
249+
.hasMessageContaining("Job was interrupted");
250+
}
251+
252+
@Test
253+
void errorUnwrappingHandlesDirectCause() {
254+
// Test that errors with a direct cause are properly unwrapped.
255+
final CompletableFuture<IBaseResource> future = new CompletableFuture<>();
256+
future.completeExceptionally(new InvalidRequestException("Direct error"));
257+
258+
final Job<IBaseResource> job = new Job<>(JOB_ID, "export", future, Optional.empty());
259+
jobRegistry.register(job);
260+
261+
assertThatThrownBy(() -> jobProvider.job(JOB_ID, request, response))
262+
.isInstanceOf(InvalidRequestException.class)
263+
.hasMessageContaining("Direct error");
264+
}
265+
266+
@Test
267+
void errorUnwrappingHandlesNestedCause() {
268+
// Test that errors with a nested cause (wrapped in IllegalStateException) are properly
269+
// unwrapped.
270+
final CompletableFuture<IBaseResource> future = new CompletableFuture<>();
271+
future.completeExceptionally(
272+
new IllegalStateException(
273+
"Outer wrapper", new InvalidRequestException("Nested error message")));
274+
275+
final Job<IBaseResource> job = new Job<>(JOB_ID, "export", future, Optional.empty());
276+
jobRegistry.register(job);
277+
278+
assertThatThrownBy(() -> jobProvider.job(JOB_ID, request, response))
279+
.isInstanceOf(InvalidRequestException.class)
280+
.hasMessageContaining("Nested error message");
281+
}
212282
}

server/src/test/java/au/csiro/pathling/async/JobResultProviderTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
import au.csiro.pathling.config.ServerConfiguration;
2828
import au.csiro.pathling.errors.AccessDeniedError;
2929
import au.csiro.pathling.errors.ResourceNotFoundError;
30+
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
3031
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
3132
import jakarta.servlet.http.HttpServletResponse;
3233
import java.util.Optional;
3334
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.ExecutionException;
36+
import java.util.concurrent.Future;
3437
import org.hl7.fhir.instance.model.api.IBaseResource;
3538
import org.hl7.fhir.r4.model.Parameters;
3639
import org.hl7.fhir.r4.model.StringType;
@@ -235,4 +238,76 @@ void cancelledJobReturns404() {
235238
.isInstanceOf(ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException.class)
236239
.hasMessageContaining("DELETE request cancelled this job");
237240
}
241+
242+
@Test
243+
void interruptedJobThrowsInternalError() {
244+
// A job that was interrupted should throw an InternalErrorException.
245+
// Create a future that will throw InterruptedException when get() is called.
246+
@SuppressWarnings("unchecked")
247+
final Future<IBaseResource> mockFuture = mock(Future.class);
248+
try {
249+
when(mockFuture.isDone()).thenReturn(true);
250+
when(mockFuture.isCancelled()).thenReturn(false);
251+
when(mockFuture.get()).thenThrow(new InterruptedException("Thread was interrupted"));
252+
} catch (final InterruptedException | ExecutionException e) {
253+
throw new RuntimeException(e);
254+
}
255+
256+
final Job<IBaseResource> job = new Job<>(JOB_ID, "view-export", mockFuture, Optional.empty());
257+
job.setRedirectOnComplete(true);
258+
jobRegistry.register(job);
259+
260+
assertThatThrownBy(() -> jobResultProvider.jobResult(JOB_ID, request, response))
261+
.isInstanceOf(InternalErrorException.class)
262+
.hasMessageContaining("Job was interrupted");
263+
}
264+
265+
@Test
266+
void errorUnwrappingHandlesDirectCause() {
267+
// Test that errors with a direct cause are properly unwrapped.
268+
final CompletableFuture<IBaseResource> future = new CompletableFuture<>();
269+
future.completeExceptionally(new InvalidRequestException("Direct error"));
270+
271+
final Job<IBaseResource> job = new Job<>(JOB_ID, "view-export", future, Optional.empty());
272+
job.setRedirectOnComplete(true);
273+
jobRegistry.register(job);
274+
275+
assertThatThrownBy(() -> jobResultProvider.jobResult(JOB_ID, request, response))
276+
.isInstanceOf(InvalidRequestException.class)
277+
.hasMessageContaining("Direct error");
278+
}
279+
280+
@Test
281+
void errorUnwrappingHandlesNestedCause() {
282+
// Test that errors with a nested cause (wrapped in IllegalStateException) are properly
283+
// unwrapped.
284+
final CompletableFuture<IBaseResource> future = new CompletableFuture<>();
285+
future.completeExceptionally(
286+
new IllegalStateException(
287+
"Outer wrapper", new InvalidRequestException("Nested error message")));
288+
289+
final Job<IBaseResource> job = new Job<>(JOB_ID, "view-export", future, Optional.empty());
290+
job.setRedirectOnComplete(true);
291+
jobRegistry.register(job);
292+
293+
assertThatThrownBy(() -> jobResultProvider.jobResult(JOB_ID, request, response))
294+
.isInstanceOf(InvalidRequestException.class)
295+
.hasMessageContaining("Nested error message");
296+
}
297+
298+
@Test
299+
void nullJobIdReturns404() {
300+
// A null job ID should return 404 Not Found.
301+
assertThatThrownBy(() -> jobResultProvider.jobResult(null, request, response))
302+
.isInstanceOf(ResourceNotFoundError.class)
303+
.hasMessageContaining("Job ID not found");
304+
}
305+
306+
@Test
307+
void invalidJobIdFormatReturns404() {
308+
// An invalid job ID format should return 404 Not Found.
309+
assertThatThrownBy(() -> jobResultProvider.jobResult("not-a-valid-uuid", request, response))
310+
.isInstanceOf(ResourceNotFoundError.class)
311+
.hasMessageContaining("Job ID not found");
312+
}
238313
}

0 commit comments

Comments
 (0)