|
| 1 | +package com.launchdarkly.sdk.server.integrations; |
| 2 | + |
| 3 | +import com.google.common.collect.ImmutableMap; |
| 4 | +import com.launchdarkly.sdk.ContextKind; |
| 5 | +import com.launchdarkly.sdk.LDValue; |
| 6 | +import com.launchdarkly.sdk.server.DataModel; |
| 7 | +import com.launchdarkly.sdk.server.LDConfig; |
| 8 | +import com.launchdarkly.sdk.server.ModelBuilders; |
| 9 | +import com.launchdarkly.sdk.server.datasources.FDv2SourceResult; |
| 10 | +import com.launchdarkly.sdk.server.datasources.SelectorSource; |
| 11 | +import com.launchdarkly.sdk.server.datasources.Synchronizer; |
| 12 | +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; |
| 13 | +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; |
| 14 | +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; |
| 15 | +import com.launchdarkly.sdk.server.subsystems.ClientContext; |
| 16 | +import com.launchdarkly.sdk.server.subsystems.DataSourceBuildInputs; |
| 17 | +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSet; |
| 18 | +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSetType; |
| 19 | +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; |
| 20 | +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; |
| 21 | +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; |
| 22 | + |
| 23 | +import com.launchdarkly.sdk.internal.fdv2.sources.Selector; |
| 24 | +import org.junit.Test; |
| 25 | + |
| 26 | +import java.util.Map; |
| 27 | +import java.util.concurrent.BlockingQueue; |
| 28 | +import java.util.concurrent.CompletableFuture; |
| 29 | +import java.util.concurrent.LinkedBlockingQueue; |
| 30 | +import java.util.concurrent.TimeUnit; |
| 31 | +import java.util.function.Function; |
| 32 | + |
| 33 | +import static com.google.common.collect.Iterables.get; |
| 34 | +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; |
| 35 | +import static com.launchdarkly.sdk.server.TestComponents.clientContext; |
| 36 | +import static com.launchdarkly.sdk.server.TestComponents.nullLogger; |
| 37 | +import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; |
| 38 | +import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; |
| 39 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 40 | +import static org.hamcrest.Matchers.emptyIterable; |
| 41 | +import static org.hamcrest.Matchers.equalTo; |
| 42 | +import static org.hamcrest.Matchers.is; |
| 43 | +import static org.hamcrest.Matchers.iterableWithSize; |
| 44 | +import static org.hamcrest.Matchers.not; |
| 45 | +import static org.hamcrest.Matchers.notNullValue; |
| 46 | +import static org.hamcrest.Matchers.nullValue; |
| 47 | + |
| 48 | +@SuppressWarnings("javadoc") |
| 49 | +public class TestDataV2Test { |
| 50 | + private static final LDValue[] THREE_STRING_VALUES = |
| 51 | + new LDValue[] { LDValue.of("red"), LDValue.of("green"), LDValue.of("blue") }; |
| 52 | + |
| 53 | + private final CapturingDataSourceUpdates updates = new CapturingDataSourceUpdates(); |
| 54 | + |
| 55 | + private DataSourceBuildInputs dataSourceBuildInputs() { |
| 56 | + ClientContext context = clientContext("", new LDConfig.Builder().build(), updates); |
| 57 | + SelectorSource selectorSource = () -> Selector.EMPTY; |
| 58 | + return new DataSourceBuildInputs( |
| 59 | + nullLogger, |
| 60 | + 0, |
| 61 | + updates, |
| 62 | + context.getServiceEndpoints(), |
| 63 | + context.getHttp(), |
| 64 | + sharedExecutor, |
| 65 | + null, |
| 66 | + selectorSource); |
| 67 | + } |
| 68 | + |
| 69 | + @Test |
| 70 | + public void initializesWithEmptyData() throws Exception { |
| 71 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 72 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 73 | + |
| 74 | + FDv2SourceResult result = sync.next().get(5, TimeUnit.SECONDS); |
| 75 | + |
| 76 | + assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 77 | + ChangeSet<ItemDescriptor> changeSet = result.getChangeSet(); |
| 78 | + assertThat(changeSet, notNullValue()); |
| 79 | + assertThat(changeSet.getType(), equalTo(ChangeSetType.Full)); |
| 80 | + assertThat(changeSet.getData(), iterableWithSize(1)); |
| 81 | + assertThat(get(changeSet.getData(), 0).getKey(), equalTo(DataModel.FEATURES)); |
| 82 | + assertThat(get(changeSet.getData(), 0).getValue().getItems(), emptyIterable()); |
| 83 | + } |
| 84 | + |
| 85 | + @Test |
| 86 | + public void initializesWithFlags() throws Exception { |
| 87 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 88 | + td.update(td.flag("flag1").on(true)) |
| 89 | + .update(td.flag("flag2").on(false)); |
| 90 | + |
| 91 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 92 | + FDv2SourceResult result = sync.next().get(5, TimeUnit.SECONDS); |
| 93 | + |
| 94 | + assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 95 | + ChangeSet<ItemDescriptor> changeSet = result.getChangeSet(); |
| 96 | + assertThat(changeSet.getType(), equalTo(ChangeSetType.Full)); |
| 97 | + assertThat(changeSet.getData(), iterableWithSize(1)); |
| 98 | + assertThat(get(changeSet.getData(), 0).getKey(), equalTo(DataModel.FEATURES)); |
| 99 | + assertThat(get(changeSet.getData(), 0).getValue().getItems(), iterableWithSize(2)); |
| 100 | + |
| 101 | + ModelBuilders.FlagBuilder expectedFlag1 = flagBuilder("flag1").version(1).salt("") |
| 102 | + .on(true).offVariation(1).fallthroughVariation(0).variations(true, false); |
| 103 | + ModelBuilders.FlagBuilder expectedFlag2 = flagBuilder("flag2").version(1).salt("") |
| 104 | + .on(false).offVariation(1).fallthroughVariation(0).variations(true, false); |
| 105 | + |
| 106 | + Map<String, ItemDescriptor> flags = ImmutableMap.copyOf(get(changeSet.getData(), 0).getValue().getItems()); |
| 107 | + ItemDescriptor flag1 = flags.get("flag1"); |
| 108 | + ItemDescriptor flag2 = flags.get("flag2"); |
| 109 | + assertThat(flag1, not(nullValue())); |
| 110 | + assertThat(flag2, not(nullValue())); |
| 111 | + |
| 112 | + assertJsonEquals(flagJson(expectedFlag1, 1), flagJson(flag1)); |
| 113 | + assertJsonEquals(flagJson(expectedFlag2, 1), flagJson(flag2)); |
| 114 | + } |
| 115 | + |
| 116 | + @Test |
| 117 | + public void addsFlag() throws Exception { |
| 118 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 119 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 120 | + |
| 121 | + FDv2SourceResult initResult = sync.next().get(5, TimeUnit.SECONDS); |
| 122 | + assertThat(initResult.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 123 | + assertThat(initResult.getChangeSet().getType(), equalTo(ChangeSetType.Full)); |
| 124 | + |
| 125 | + td.update(td.flag("flag1").on(true)); |
| 126 | + |
| 127 | + FDv2SourceResult updateResult = sync.next().get(5, TimeUnit.SECONDS); |
| 128 | + assertThat(updateResult.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 129 | + ChangeSet<ItemDescriptor> changeSet = updateResult.getChangeSet(); |
| 130 | + assertThat(changeSet.getType(), equalTo(ChangeSetType.Partial)); |
| 131 | + assertThat(changeSet.getData(), iterableWithSize(1)); |
| 132 | + KeyedItems<ItemDescriptor> keyedItems = get(changeSet.getData(), 0).getValue(); |
| 133 | + Map<String, ItemDescriptor> items = ImmutableMap.copyOf(keyedItems.getItems()); |
| 134 | + assertThat(items.size(), equalTo(1)); |
| 135 | + ItemDescriptor flag1 = items.get("flag1"); |
| 136 | + assertThat(flag1, not(nullValue())); |
| 137 | + |
| 138 | + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flag1").version(1).salt("") |
| 139 | + .on(true).offVariation(1).fallthroughVariation(0).variations(true, false); |
| 140 | + assertJsonEquals(flagJson(expectedFlag, 2), flagJson(flag1)); |
| 141 | + } |
| 142 | + |
| 143 | + @Test |
| 144 | + public void updatesFlag() throws Exception { |
| 145 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 146 | + td.update(td.flag("flag1") |
| 147 | + .on(false) |
| 148 | + .variationForUser("a", true) |
| 149 | + .ifMatch("name", LDValue.of("Lucy")).thenReturn(true)); |
| 150 | + |
| 151 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 152 | + FDv2SourceResult initResult = sync.next().get(5, TimeUnit.SECONDS); |
| 153 | + assertThat(initResult.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 154 | + |
| 155 | + td.update(td.flag("flag1").on(true)); |
| 156 | + |
| 157 | + FDv2SourceResult updateResult = sync.next().get(5, TimeUnit.SECONDS); |
| 158 | + ChangeSet<ItemDescriptor> changeSet = updateResult.getChangeSet(); |
| 159 | + Map<String, ItemDescriptor> items = ImmutableMap.copyOf(get(changeSet.getData(), 0).getValue().getItems()); |
| 160 | + ItemDescriptor flag1 = items.get("flag1"); |
| 161 | + |
| 162 | + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flag1").version(2).salt("") |
| 163 | + .on(true).offVariation(1).fallthroughVariation(0).variations(true, false) |
| 164 | + .addTarget(0, "a").addContextTarget(ContextKind.DEFAULT, 0) |
| 165 | + .addRule("rule0", 0, "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}"); |
| 166 | + assertJsonEquals(flagJson(expectedFlag, 2), flagJson(flag1)); |
| 167 | + } |
| 168 | + |
| 169 | + @Test |
| 170 | + public void deletesFlag() throws Exception { |
| 171 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 172 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 173 | + |
| 174 | + sync.next().get(5, TimeUnit.SECONDS); |
| 175 | + |
| 176 | + td.update(td.flag("foo").on(false).valueForAll(LDValue.of("bar"))); |
| 177 | + FDv2SourceResult addResult = sync.next().get(5, TimeUnit.SECONDS); |
| 178 | + assertThat(addResult.getChangeSet().getType(), equalTo(ChangeSetType.Partial)); |
| 179 | + Map<String, ItemDescriptor> addItems = ImmutableMap.copyOf(get(addResult.getChangeSet().getData(), 0).getValue().getItems()); |
| 180 | + assertThat(addItems.get("foo").getVersion(), equalTo(1)); |
| 181 | + assertThat(addItems.get("foo").getItem(), notNullValue()); |
| 182 | + |
| 183 | + td.delete("foo"); |
| 184 | + FDv2SourceResult deleteResult = sync.next().get(5, TimeUnit.SECONDS); |
| 185 | + assertThat(deleteResult.getChangeSet().getType(), equalTo(ChangeSetType.Partial)); |
| 186 | + Map<String, ItemDescriptor> deleteItems = ImmutableMap.copyOf(get(deleteResult.getChangeSet().getData(), 0).getValue().getItems()); |
| 187 | + assertThat(deleteItems.get("foo").getVersion(), equalTo(2)); |
| 188 | + assertThat(deleteItems.get("foo").getItem(), nullValue()); |
| 189 | + |
| 190 | + sync.close(); |
| 191 | + } |
| 192 | + |
| 193 | + @Test |
| 194 | + public void flagConfigSimpleBoolean() throws Exception { |
| 195 | + Function<ModelBuilders.FlagBuilder, ModelBuilders.FlagBuilder> expectedBooleanFlag = fb -> |
| 196 | + fb.on(true).variations(true, false).offVariation(1).fallthroughVariation(0); |
| 197 | + |
| 198 | + verifyFlag(f -> f, expectedBooleanFlag); |
| 199 | + verifyFlag(f -> f.booleanFlag(), expectedBooleanFlag); |
| 200 | + verifyFlag(f -> f.on(true), expectedBooleanFlag); |
| 201 | + verifyFlag(f -> f.on(false), fb -> expectedBooleanFlag.apply(fb).on(false)); |
| 202 | + verifyFlag(f -> f.variationForAll(false), fb -> expectedBooleanFlag.apply(fb).fallthroughVariation(1)); |
| 203 | + verifyFlag(f -> f.variationForAll(true), expectedBooleanFlag); |
| 204 | + } |
| 205 | + |
| 206 | + @Test |
| 207 | + public void flagConfigStringVariations() throws Exception { |
| 208 | + verifyFlag( |
| 209 | + f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2), |
| 210 | + fb -> fb.variations("red", "green", "blue").on(true).offVariation(0).fallthroughVariation(2) |
| 211 | + ); |
| 212 | + } |
| 213 | + |
| 214 | + @Test |
| 215 | + public void userTargets() throws Exception { |
| 216 | + Function<ModelBuilders.FlagBuilder, ModelBuilders.FlagBuilder> expectedBooleanFlag = fb -> |
| 217 | + fb.variations(true, false).on(true).offVariation(1).fallthroughVariation(0); |
| 218 | + |
| 219 | + verifyFlag( |
| 220 | + f -> f.variationForUser("a", true).variationForUser("b", true), |
| 221 | + fb -> expectedBooleanFlag.apply(fb).addTarget(0, "a", "b") |
| 222 | + .addContextTarget(ContextKind.DEFAULT, 0) |
| 223 | + ); |
| 224 | + } |
| 225 | + |
| 226 | + @Test |
| 227 | + public void flagRules() throws Exception { |
| 228 | + Function<ModelBuilders.FlagBuilder, ModelBuilders.FlagBuilder> expectedBooleanFlag = fb -> |
| 229 | + fb.variations(true, false).on(true).offVariation(1).fallthroughVariation(0); |
| 230 | + |
| 231 | + verifyFlag( |
| 232 | + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(true), |
| 233 | + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, |
| 234 | + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}") |
| 235 | + ); |
| 236 | + } |
| 237 | + |
| 238 | + private void verifyFlag( |
| 239 | + Function<TestData.FlagBuilder, TestData.FlagBuilder> configureFlag, |
| 240 | + Function<ModelBuilders.FlagBuilder, ModelBuilders.FlagBuilder> configureExpectedFlag |
| 241 | + ) throws Exception { |
| 242 | + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flagkey").version(1).salt(""); |
| 243 | + expectedFlag = configureExpectedFlag.apply(expectedFlag); |
| 244 | + |
| 245 | + TestDataV2 td = TestDataV2.synchronizer(); |
| 246 | + Synchronizer sync = td.build(dataSourceBuildInputs()); |
| 247 | + sync.next().get(5, TimeUnit.SECONDS); |
| 248 | + |
| 249 | + td.update(configureFlag.apply(td.flag("flagkey"))); |
| 250 | + |
| 251 | + FDv2SourceResult result = sync.next().get(5, TimeUnit.SECONDS); |
| 252 | + assertThat(result.getResultType(), equalTo(FDv2SourceResult.ResultType.CHANGE_SET)); |
| 253 | + ChangeSet<ItemDescriptor> changeSet = result.getChangeSet(); |
| 254 | + Map<String, ItemDescriptor> items = ImmutableMap.copyOf(get(changeSet.getData(), 0).getValue().getItems()); |
| 255 | + ItemDescriptor flag = items.get("flagkey"); |
| 256 | + assertJsonEquals(flagJson(expectedFlag, 1), flagJson(flag)); |
| 257 | + } |
| 258 | + |
| 259 | + private static String flagJson(ModelBuilders.FlagBuilder flagBuilder, int version) { |
| 260 | + return DataModel.FEATURES.serialize(new ItemDescriptor(version, flagBuilder.build())); |
| 261 | + } |
| 262 | + |
| 263 | + private static String flagJson(ItemDescriptor flag) { |
| 264 | + return DataModel.FEATURES.serialize(flag); |
| 265 | + } |
| 266 | + |
| 267 | + private static class CapturingDataSourceUpdates implements com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink, |
| 268 | + com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSinkV2 { |
| 269 | + BlockingQueue<com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet<ItemDescriptor>> inits = |
| 270 | + new LinkedBlockingQueue<>(); |
| 271 | + BlockingQueue<UpsertParams> upserts = new LinkedBlockingQueue<>(); |
| 272 | + BlockingQueue<ChangeSet<ItemDescriptor>> applies = new LinkedBlockingQueue<>(); |
| 273 | + boolean valid; |
| 274 | + |
| 275 | + @Override |
| 276 | + public boolean init(com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet<ItemDescriptor> allData) { |
| 277 | + inits.add(allData); |
| 278 | + return true; |
| 279 | + } |
| 280 | + |
| 281 | + @Override |
| 282 | + public boolean upsert(DataKind kind, String key, ItemDescriptor item) { |
| 283 | + upserts.add(new UpsertParams(kind, key, item)); |
| 284 | + return true; |
| 285 | + } |
| 286 | + |
| 287 | + @Override |
| 288 | + public DataStoreStatusProvider getDataStoreStatusProvider() { |
| 289 | + return null; |
| 290 | + } |
| 291 | + |
| 292 | + @Override |
| 293 | + public void updateStatus(State newState, ErrorInfo newError) { |
| 294 | + valid = newState == State.VALID; |
| 295 | + } |
| 296 | + |
| 297 | + @Override |
| 298 | + public boolean apply(ChangeSet<ItemDescriptor> changeSet) { |
| 299 | + applies.add(changeSet); |
| 300 | + return true; |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + private static class UpsertParams { |
| 305 | + final DataKind kind; |
| 306 | + final String key; |
| 307 | + final ItemDescriptor item; |
| 308 | + |
| 309 | + UpsertParams(DataKind kind, String key, ItemDescriptor item) { |
| 310 | + this.kind = kind; |
| 311 | + this.key = key; |
| 312 | + this.item = item; |
| 313 | + } |
| 314 | + } |
| 315 | +} |
0 commit comments