|
31 | 31 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
32 | 32 |
|
33 | 33 | import com.bytechef.atlas.execution.domain.Job; |
| 34 | +import com.bytechef.commons.util.JsonUtils; |
34 | 35 | import com.bytechef.platform.file.storage.TempFileStorage; |
35 | 36 | import com.bytechef.platform.workflow.execution.dto.JobDTO; |
36 | 37 | import com.bytechef.platform.workflow.test.dto.ExecutionErrorEventDTO; |
|
39 | 40 | import com.bytechef.platform.workflow.test.dto.WorkflowTestExecutionDTO; |
40 | 41 | import com.bytechef.platform.workflow.test.facade.WorkflowTestFacade; |
41 | 42 | import com.bytechef.platform.workflow.test.web.rest.model.WorkflowTestExecutionModel; |
| 43 | +import com.fasterxml.jackson.databind.DeserializationFeature; |
| 44 | +import com.fasterxml.jackson.databind.SerializationFeature; |
| 45 | +import com.fasterxml.jackson.databind.json.JsonMapper; |
| 46 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
42 | 47 | import com.github.benmanes.caffeine.cache.Cache; |
43 | 48 | import java.nio.charset.StandardCharsets; |
44 | 49 | import java.time.Duration; |
|
52 | 57 | import java.util.concurrent.TimeUnit; |
53 | 58 | import java.util.concurrent.atomic.AtomicReference; |
54 | 59 | import java.util.function.Consumer; |
| 60 | +import org.junit.jupiter.api.BeforeEach; |
55 | 61 | import org.junit.jupiter.api.Test; |
56 | 62 | import org.springframework.beans.factory.annotation.Autowired; |
57 | 63 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; |
@@ -85,6 +91,16 @@ class WorkflowTestApiControllerTest { |
85 | 91 | @Autowired |
86 | 92 | private WorkflowTestApiController controller; |
87 | 93 |
|
| 94 | + @BeforeEach |
| 95 | + public void beforeEach() { |
| 96 | + JsonUtils.setObjectMapper( |
| 97 | + JsonMapper.builder() |
| 98 | + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) |
| 99 | + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) |
| 100 | + .addModules(new JavaTimeModule()) |
| 101 | + .build()); |
| 102 | + } |
| 103 | + |
88 | 104 | @Test |
89 | 105 | void testStartStreamEmitsStartAndResult() throws Exception { |
90 | 106 | long jobId = 123L; |
@@ -189,6 +205,13 @@ void testStopAbortsActiveStreamAndInvokesFacade() throws Exception { |
189 | 205 | verify(workflowTestFacade, times(1)).stopTest(eq(jobId)); |
190 | 206 | } |
191 | 207 |
|
| 208 | + @Test |
| 209 | + void testStopWorkflowTestWithUndefinedJobId() throws Exception { |
| 210 | + mockMvc.perform( |
| 211 | + post("/internal/workflow-tests/{jobId}/stop", "undefined")) |
| 212 | + .andExpect(status().isBadRequest()); |
| 213 | + } |
| 214 | + |
192 | 215 | @Test |
193 | 216 | void testAttachWhenNotRunningEmitsErrorNotRunning() throws Exception { |
194 | 217 | // No runs have been started in this test -> attach should report not running |
@@ -279,23 +302,23 @@ void testListenerForwardingEmitsJobTaskAndErrorEvents() throws Exception { |
279 | 302 | Thread.sleep(50); |
280 | 303 | } |
281 | 304 |
|
282 | | - Consumer<JobStatusEventDTO> jobStatusEventDTOConsumer = jobListener.get(); |
283 | | - Consumer<TaskStatusEventDTO> taskStatusEventDTOConsumerStarted = taskStartedListener.get(); |
284 | | - Consumer<TaskStatusEventDTO> taskStatusEventDTOConsumerCompleted = taskCompletedListener.get(); |
285 | | - Consumer<ExecutionErrorEventDTO> executionErrorEventDTOConsumer = errorListener.get(); |
| 305 | + Consumer<JobStatusEventDTO> jobStatusEventConsumer = jobListener.get(); |
| 306 | + Consumer<TaskStatusEventDTO> taskStatusEventStartedConsumer = taskStartedListener.get(); |
| 307 | + Consumer<TaskStatusEventDTO> taskStatusEventCompletedConsumer = taskCompletedListener.get(); |
| 308 | + Consumer<ExecutionErrorEventDTO> executionErrorEventConsumer = errorListener.get(); |
286 | 309 |
|
287 | | - assertThat(jobStatusEventDTOConsumer).isNotNull(); |
288 | | - assertThat(taskStatusEventDTOConsumerStarted).isNotNull(); |
289 | | - assertThat(taskStatusEventDTOConsumerCompleted).isNotNull(); |
290 | | - assertThat(executionErrorEventDTOConsumer).isNotNull(); |
| 310 | + assertThat(jobStatusEventConsumer).isNotNull(); |
| 311 | + assertThat(taskStatusEventStartedConsumer).isNotNull(); |
| 312 | + assertThat(taskStatusEventCompletedConsumer).isNotNull(); |
| 313 | + assertThat(executionErrorEventConsumer).isNotNull(); |
291 | 314 |
|
292 | 315 | // Fire synthetic events |
293 | | - jobStatusEventDTOConsumer.accept(new JobStatusEventDTO(jobId, "STARTED", Instant.now())); |
294 | | - taskStatusEventDTOConsumerStarted.accept( |
| 316 | + jobStatusEventConsumer.accept(new JobStatusEventDTO(jobId, "STARTED", Instant.now())); |
| 317 | + taskStatusEventStartedConsumer.accept( |
295 | 318 | new TaskStatusEventDTO(jobId, 1L, STARTED, null, null, Instant.now(), null)); |
296 | | - taskStatusEventDTOConsumerCompleted.accept( |
| 319 | + taskStatusEventCompletedConsumer.accept( |
297 | 320 | new TaskStatusEventDTO(jobId, 1L, COMPLETED, "t", "type", null, Instant.now())); |
298 | | - executionErrorEventDTOConsumer.accept(new ExecutionErrorEventDTO(jobId, "Oops")); |
| 321 | + executionErrorEventConsumer.accept(new ExecutionErrorEventDTO(jobId, "Oops")); |
299 | 322 |
|
300 | 323 | // Allow a brief moment for SSE forwarding to flush before finishing |
301 | 324 | Duration duration = Duration.ofMillis(50); |
@@ -363,21 +386,19 @@ void testPendingEventsBoundedAndFlushedOnAttach() throws Exception { |
363 | 386 | // Obtain the key from the controller's 'runs' map (wait briefly if needed) |
364 | 387 | String key = waitForRunKey(); |
365 | 388 |
|
366 | | - // Remove emitters to force buffering |
367 | | - Object emitters = ReflectionTestUtils.getField(controller, "emitter"); |
| 389 | + // Remove emitter to force buffering |
| 390 | + Object emitter = ReflectionTestUtils.getField(controller, "emitter"); |
368 | 391 |
|
369 | | - assert emitters != null; |
| 392 | + assert emitter != null; |
370 | 393 |
|
371 | | - if (emitters instanceof Cache<?, ?> cache) { |
| 394 | + if (emitter instanceof Cache<?, ?> cache) { |
372 | 395 | @SuppressWarnings("unchecked") |
373 | | - Cache<String, CopyOnWriteArrayList<SseEmitter>> emitterCache = |
374 | | - (Cache<String, CopyOnWriteArrayList<SseEmitter>>) cache; |
| 396 | + Cache<String, SseEmitter> emitterCache = (Cache<String, SseEmitter>) cache; |
375 | 397 |
|
376 | 398 | emitterCache.invalidate(key); |
377 | | - } else if (emitters instanceof ConcurrentMap<?, ?>) { |
| 399 | + } else if (emitter instanceof ConcurrentMap<?, ?>) { |
378 | 400 | @SuppressWarnings("unchecked") |
379 | | - ConcurrentMap<String, CopyOnWriteArrayList<SseEmitter>> emitterMap = |
380 | | - (ConcurrentMap<String, CopyOnWriteArrayList<SseEmitter>>) emitters; |
| 401 | + ConcurrentMap<String, SseEmitter> emitterMap = (ConcurrentMap<String, SseEmitter>) emitter; |
381 | 402 |
|
382 | 403 | emitterMap.remove(key); |
383 | 404 | } |
@@ -476,14 +497,12 @@ void testPendingEventsClearedOnStop() throws Exception { |
476 | 497 |
|
477 | 498 | if (emitter instanceof Cache<?, ?> cache) { |
478 | 499 | @SuppressWarnings("unchecked") |
479 | | - Cache<String, CopyOnWriteArrayList<SseEmitter>> emitterCache = |
480 | | - (Cache<String, CopyOnWriteArrayList<SseEmitter>>) cache; |
| 500 | + Cache<String, SseEmitter> emitterCache = (Cache<String, SseEmitter>) cache; |
481 | 501 |
|
482 | 502 | emitterCache.invalidate(key); |
483 | 503 | } else if (emitter instanceof ConcurrentMap<?, ?>) { |
484 | 504 | @SuppressWarnings("unchecked") |
485 | | - ConcurrentMap<String, CopyOnWriteArrayList<SseEmitter>> emitterMap = |
486 | | - (ConcurrentMap<String, CopyOnWriteArrayList<SseEmitter>>) emitter; |
| 505 | + ConcurrentMap<String, SseEmitter> emitterMap = (ConcurrentMap<String, SseEmitter>) emitter; |
487 | 506 |
|
488 | 507 | emitterMap.remove(key); |
489 | 508 | } |
@@ -546,11 +565,18 @@ void testPendingEventsClearedOnStop() throws Exception { |
546 | 565 | } |
547 | 566 |
|
548 | 567 | private static String normalizeSse(String body) { |
549 | | - // Normalize potential double-space after 'event:' introduced by different SSE encoders |
550 | | - String normalized = body.replace("event: ", "event: "); |
| 568 | + if (body == null || body.isEmpty()) { |
| 569 | + return body; |
| 570 | + } |
551 | 571 |
|
552 | | - // Also collapse any accidental triple spaces just in case |
553 | | - return normalized.replace("event: ", "event: "); |
| 572 | + // Normalize whitespace after 'event:' at the beginning of SSE lines, regardless of encoder quirks. |
| 573 | + // This collapses any sequence of whitespace after 'event:' to a single space. |
| 574 | + // Example: |
| 575 | + // event: started |
| 576 | + // event:\tcompleted |
| 577 | + // becomes: |
| 578 | + // event: started |
| 579 | + return body.replaceAll("(?m)^(event:)(\\s*)", "$1 "); |
554 | 580 | } |
555 | 581 |
|
556 | 582 | @Configuration |
|
0 commit comments