|
3 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.*; |
4 | 4 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; |
5 | 5 | import static org.assertj.core.api.Assertions.assertThat; |
| 6 | +import static org.mockito.Mockito.doThrow; |
6 | 7 | import static org.mockito.Mockito.mock; |
7 | 8 | import static org.mockito.Mockito.times; |
8 | 9 |
|
9 | 10 | import com.github.tomakehurst.wiremock.junit5.WireMockExtension; |
| 11 | +import com.github.tomakehurst.wiremock.verification.LoggedRequest; |
10 | 12 | import io.getunleash.DefaultUnleash; |
11 | 13 | import io.getunleash.SynchronousTestExecutor; |
12 | 14 | import io.getunleash.Unleash; |
13 | 15 | import io.getunleash.engine.UnleashEngine; |
| 16 | +import io.getunleash.engine.YggdrasilInvalidInputException; |
14 | 17 | import io.getunleash.event.ClientFeaturesResponse; |
15 | 18 | import io.getunleash.event.EventDispatcher; |
16 | 19 | import io.getunleash.event.GatedEventEmitter; |
|
22 | 25 | import java.util.List; |
23 | 26 | import java.util.concurrent.CountDownLatch; |
24 | 27 | import java.util.concurrent.TimeUnit; |
| 28 | +import java.util.stream.Collectors; |
25 | 29 | import org.junit.jupiter.api.BeforeEach; |
26 | 30 | import org.junit.jupiter.api.Test; |
27 | 31 | import org.junit.jupiter.api.extension.RegisterExtension; |
@@ -178,6 +182,69 @@ public void should_write_backup_file_when_streaming_update_received() throws Exc |
178 | 182 | assertThat(savedBackupContent).doesNotContain("\"events\""); // store state |
179 | 183 | } |
180 | 184 |
|
| 185 | + @Test |
| 186 | + void should_reconnect_without_last_event_id_header() throws Exception { |
| 187 | + String hydration = |
| 188 | + "{\"events\":[{\"type\":\"hydration\",\"eventId\":1,\"features\":[{\"name\":\"deltaFeature\",\"enabled\":true,\"strategies\":[],\"variants\":[]}],\"segments\":[]}]}"; |
| 189 | + |
| 190 | + String badUpdate = "{not-json"; |
| 191 | + |
| 192 | + stubFor( |
| 193 | + get(urlEqualTo("/api/client/streaming")) |
| 194 | + .willReturn( |
| 195 | + aResponse() |
| 196 | + .withStatus(200) |
| 197 | + .withHeader("Content-Type", "text/event-stream") |
| 198 | + .withBody( |
| 199 | + "event: unleash-connected\n" |
| 200 | + + "data: " |
| 201 | + + hydration |
| 202 | + + "\n\n" |
| 203 | + + "event: unleash-updated\n" |
| 204 | + + "data: " |
| 205 | + + badUpdate |
| 206 | + + "\n\n"))); |
| 207 | + |
| 208 | + FeatureBackupHandlerFile backupHandler = mock(FeatureBackupHandlerFile.class); |
| 209 | + UnleashEngine mockEngine = mock(UnleashEngine.class); |
| 210 | + |
| 211 | + doThrow(new YggdrasilInvalidInputException("Invalid input")) |
| 212 | + .when(mockEngine) |
| 213 | + .takeState(badUpdate); |
| 214 | + |
| 215 | + StreamingFeatureFetcherImpl streamingFetcher = |
| 216 | + new StreamingFeatureFetcherImpl( |
| 217 | + config, |
| 218 | + new GatedEventEmitter(new EventDispatcher(config)), |
| 219 | + mockEngine, |
| 220 | + backupHandler, |
| 221 | + null); |
| 222 | + streamingFetcher.start(); |
| 223 | + |
| 224 | + // Wait for reconnect to happen by polling WireMock's request log |
| 225 | + long deadline = System.currentTimeMillis() + 5_000; |
| 226 | + while (System.currentTimeMillis() < deadline) { |
| 227 | + int count = serverMock.getAllServeEvents().size(); |
| 228 | + if (count >= 2) break; |
| 229 | + Thread.sleep(50); |
| 230 | + } |
| 231 | + |
| 232 | + var serveEvents = serverMock.getAllServeEvents(); |
| 233 | + assertThat(serveEvents.size()).isGreaterThanOrEqualTo(2); |
| 234 | + |
| 235 | + List<LoggedRequest> requests = |
| 236 | + serveEvents.stream() |
| 237 | + .map(se -> se.getRequest()) |
| 238 | + .filter(req -> req.getMethod().getName().equals("GET")) |
| 239 | + .filter(req -> req.getUrl().equals("/api/client/streaming")) |
| 240 | + .collect(Collectors.toList()); |
| 241 | + |
| 242 | + LoggedRequest reconnectionRequest = requests.get(1); |
| 243 | + // The important part - a reconnection should never include the last-event-id header so that |
| 244 | + // we get a fresh hydration |
| 245 | + assertThat(reconnectionRequest.getHeader("Last-Event-ID")).isNull(); |
| 246 | + } |
| 247 | + |
181 | 248 | private static class TestSubscriber implements UnleashSubscriber { |
182 | 249 | private final CountDownLatch togglesFetchedLatch = new CountDownLatch(1); |
183 | 250 | private final CountDownLatch multipleTogglesFetchedLatch = new CountDownLatch(2); |
|
0 commit comments