|
2 | 2 |
|
3 | 3 | import static org.junit.jupiter.api.Assertions.*;
|
4 | 4 | import static org.mockito.ArgumentMatchers.any;
|
| 5 | +import static org.mockito.ArgumentMatchers.anyString; |
5 | 6 | import static org.mockito.Mockito.mock;
|
6 | 7 | import static org.mockito.Mockito.when;
|
7 | 8 |
|
8 | 9 | import cloud.eppo.api.Configuration;
|
9 | 10 | import java.io.File;
|
10 | 11 | import java.io.IOException;
|
11 | 12 | import java.nio.charset.StandardCharsets;
|
| 13 | +import java.util.ArrayList; |
| 14 | +import java.util.List; |
12 | 15 | import java.util.concurrent.CompletableFuture;
|
| 16 | +import java.util.concurrent.atomic.AtomicInteger; |
13 | 17 | import org.apache.commons.io.FileUtils;
|
| 18 | +import org.junit.jupiter.api.BeforeEach; |
14 | 19 | import org.junit.jupiter.api.Test;
|
15 | 20 | import org.mockito.Mockito;
|
16 | 21 |
|
@@ -160,4 +165,136 @@ public void testCacheWritesAfterBrokenFetch() throws IOException {
|
160 | 165 |
|
161 | 166 | assertNull(configStore.getConfiguration().getFlag("boolean_flag"));
|
162 | 167 | }
|
| 168 | + |
| 169 | + private ConfigurationStore mockConfigStore; |
| 170 | + private EppoHttpClient mockHttpClient; |
| 171 | + private ConfigurationRequestor requestor; |
| 172 | + |
| 173 | + @BeforeEach |
| 174 | + public void setup() { |
| 175 | + mockConfigStore = mock(ConfigurationStore.class); |
| 176 | + mockHttpClient = mock(EppoHttpClient.class); |
| 177 | + requestor = new ConfigurationRequestor(mockConfigStore, mockHttpClient, false, true); |
| 178 | + } |
| 179 | + |
| 180 | + @Test |
| 181 | + public void testConfigurationChangeListener() throws IOException { |
| 182 | + // Setup mock response |
| 183 | + String flagConfig = FileUtils.readFileToString(initialFlagConfigFile, StandardCharsets.UTF_8); |
| 184 | + when(mockHttpClient.get(anyString())).thenReturn(flagConfig.getBytes()); |
| 185 | + when(mockConfigStore.saveConfiguration(any())) |
| 186 | + .thenReturn(CompletableFuture.completedFuture(null)); |
| 187 | + |
| 188 | + List<Configuration> receivedConfigs = new ArrayList<>(); |
| 189 | + |
| 190 | + // Subscribe to configuration changes |
| 191 | + Runnable unsubscribe = requestor.onConfigurationChange(receivedConfigs::add); |
| 192 | + |
| 193 | + // Initial fetch should trigger the callback |
| 194 | + requestor.fetchAndSaveFromRemote(); |
| 195 | + assertEquals(1, receivedConfigs.size()); |
| 196 | + |
| 197 | + // Another fetch should trigger the callback again (fetches aren't optimized with eTag yet). |
| 198 | + requestor.fetchAndSaveFromRemote(); |
| 199 | + assertEquals(2, receivedConfigs.size()); |
| 200 | + |
| 201 | + // Unsubscribe should prevent further callbacks |
| 202 | + unsubscribe.run(); |
| 203 | + requestor.fetchAndSaveFromRemote(); |
| 204 | + assertEquals(2, receivedConfigs.size()); // Count should remain the same |
| 205 | + } |
| 206 | + |
| 207 | + @Test |
| 208 | + public void testMultipleConfigurationChangeListeners() { |
| 209 | + // Setup mock response |
| 210 | + when(mockHttpClient.get(anyString())).thenReturn("{}".getBytes()); |
| 211 | + when(mockConfigStore.saveConfiguration(any())) |
| 212 | + .thenReturn(CompletableFuture.completedFuture(null)); |
| 213 | + |
| 214 | + AtomicInteger callCount1 = new AtomicInteger(0); |
| 215 | + AtomicInteger callCount2 = new AtomicInteger(0); |
| 216 | + |
| 217 | + // Subscribe multiple listeners |
| 218 | + Runnable unsubscribe1 = requestor.onConfigurationChange(v -> callCount1.incrementAndGet()); |
| 219 | + Runnable unsubscribe2 = requestor.onConfigurationChange(v -> callCount2.incrementAndGet()); |
| 220 | + |
| 221 | + // Fetch should trigger both callbacks |
| 222 | + requestor.fetchAndSaveFromRemote(); |
| 223 | + assertEquals(1, callCount1.get()); |
| 224 | + assertEquals(1, callCount2.get()); |
| 225 | + |
| 226 | + // Unsubscribe first listener |
| 227 | + unsubscribe1.run(); |
| 228 | + requestor.fetchAndSaveFromRemote(); |
| 229 | + assertEquals(1, callCount1.get()); // Should not increase |
| 230 | + assertEquals(2, callCount2.get()); // Should increase |
| 231 | + |
| 232 | + // Unsubscribe second listener |
| 233 | + unsubscribe2.run(); |
| 234 | + requestor.fetchAndSaveFromRemote(); |
| 235 | + assertEquals(1, callCount1.get()); // Should not increase |
| 236 | + assertEquals(2, callCount2.get()); // Should not increase |
| 237 | + } |
| 238 | + |
| 239 | + @Test |
| 240 | + public void testConfigurationChangeListenerIgnoresFailedFetch() { |
| 241 | + // Setup mock response to simulate failure |
| 242 | + when(mockHttpClient.get(anyString())).thenThrow(new RuntimeException("Fetch failed")); |
| 243 | + |
| 244 | + AtomicInteger callCount = new AtomicInteger(0); |
| 245 | + requestor.onConfigurationChange(v -> callCount.incrementAndGet()); |
| 246 | + |
| 247 | + // Failed fetch should not trigger the callback |
| 248 | + try { |
| 249 | + requestor.fetchAndSaveFromRemote(); |
| 250 | + } catch (Exception e) { |
| 251 | + // Expected |
| 252 | + } |
| 253 | + assertEquals(0, callCount.get()); |
| 254 | + } |
| 255 | + |
| 256 | + @Test |
| 257 | + public void testConfigurationChangeListenerIgnoresFailedSave() { |
| 258 | + // Setup mock responses |
| 259 | + when(mockHttpClient.get(anyString())).thenReturn("{}".getBytes()); |
| 260 | + when(mockConfigStore.saveConfiguration(any())) |
| 261 | + .thenReturn( |
| 262 | + CompletableFuture.supplyAsync( |
| 263 | + () -> { |
| 264 | + throw new RuntimeException("Save failed"); |
| 265 | + })); |
| 266 | + |
| 267 | + AtomicInteger callCount = new AtomicInteger(0); |
| 268 | + requestor.onConfigurationChange(v -> callCount.incrementAndGet()); |
| 269 | + |
| 270 | + // Failed save should not trigger the callback |
| 271 | + try { |
| 272 | + requestor.fetchAndSaveFromRemote(); |
| 273 | + } catch (RuntimeException e) { |
| 274 | + // Pass |
| 275 | + } |
| 276 | + assertEquals(0, callCount.get()); |
| 277 | + } |
| 278 | + |
| 279 | + @Test |
| 280 | + public void testConfigurationChangeListenerAsyncSave() { |
| 281 | + // Setup mock responses |
| 282 | + when(mockHttpClient.getAsync(anyString())) |
| 283 | + .thenReturn(CompletableFuture.completedFuture("{\"flags\":{}}".getBytes())); |
| 284 | + |
| 285 | + CompletableFuture<Void> saveFuture = new CompletableFuture<>(); |
| 286 | + when(mockConfigStore.saveConfiguration(any())).thenReturn(saveFuture); |
| 287 | + |
| 288 | + AtomicInteger callCount = new AtomicInteger(0); |
| 289 | + requestor.onConfigurationChange(v -> callCount.incrementAndGet()); |
| 290 | + |
| 291 | + // Start fetch |
| 292 | + CompletableFuture<Void> fetch = requestor.fetchAndSaveFromRemoteAsync(); |
| 293 | + assertEquals(0, callCount.get()); // Callback should not be called yet |
| 294 | + |
| 295 | + // Complete the save |
| 296 | + saveFuture.complete(null); |
| 297 | + fetch.join(); |
| 298 | + assertEquals(1, callCount.get()); // Callback should be called after save completes |
| 299 | + } |
163 | 300 | }
|
0 commit comments