Repository: Unleash/unleash-client-java
Pull request #131 (2021-03-16 22:24:21)

Add a FeatureToggle bootstrapper.
Since our normal backup file location must be writable since we write back. 
This adds support for reading toggles from a place where Unleash does not have write access.
First implementation only adds support for accepting absolute paths to filesystem, but the interface should be extensible enough so we can add `s3` or `cloud-storage` support, though we probably want to keep that out of client-core, to avoid dragging in aws or google dependencies

fixes: #129

checketts: It would be nice if the default absolute filesystem path could also support resources on the classpath. That way a resource that was compiled into a 'fat jar' would also be easily usable.
chriswk: Good point, I'll add support for `classpath:` resources with this.

I do seem to remember that fat jar resources came with some extra configuration issues since the embedded file most likely is zipped inside a jar. I'll start by making sure we can find resources from classpath.
ivarconr: > we can add s3 or cloud-storage support, though we probably want to keep that out of client-core, to avoid dragging in aws or google dependencies

I was thinking it might be enough to support fetching a json file from a URL, maybe with some authentication as part of that. 

Maybe you should be allowed to just push in an input stream? Then you can fetch it however you like, and you can just pass on the input stream as part of the configuration? Or would it be better to then say it must be a json string?
chriswk: I think you're right. We should probably simplify the BootstrapHandler to accept a string which it parses to a set of toggles. Then it would be up to the user to figure out how they want to provide the String. Seeing as how the current FileHandler only uses Java core APIs providing it could be a starter for how to write a ToggleBootstrapHandler. I'll have a look at changing the API a bit
ivarconr: I really like the inversion of control where the user now can just specify a `string` producer. This means the user can read their warmup state from anywhere (url, redis, S3, memcached, etc) without injecting support for everything in to the SDK. 

Would be nice to see a Howto guide at some point where you curate a hot file in a S3 bucket and read it at startup. 
chriswk: I can look into adding a how-to for that

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0678370..2b38da66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 4.2.0
+- feat: Add ability to bootstrap features from files via UNLEASH_BOOTSTRAP_FILE (#129)
+- feat: Add ability to provide bootstrap via custom ToggleBootstrapProvider (#129)
+
 ## 4.1.0
 - feat: Add ability to disable metrics via UnleashContext (#125)
 - fix: Add logging for when a matching strategy is not found (#123)
diff --git a/README.md b/README.md
index 69f2ac70..6fcad4bb 100644
--- a/README.md
+++ b/README.md
@@ -221,6 +221,20 @@ based on the values stored in `unleash-repo.json`. As a result of this, the seco
 * When `unleash-repo.json` does not exists
 * When the named feature toggle does not exist in `unleash-repo.json`
 
+## Bootstrapping
+* Unleash supports bootstrapping from a JSON string.
+* Configure your own custom provider implementing the `ToggleBootstrapProvider` interface's single method `String read()`.
+  This should return a JSON string in the same format returned from `/api/client/features`
+* Example bootstrap files can be found in the json files located in [src/test/resources](src/test/resources)
+* Our assumption is this can be use for applications deployed to ephemeral containers or more locked down file systems where Unleash's need to write the backup file is not desirable or possible.
+
+### Provided Bootstrappers
+#### ToggleBootstrapFileProvider
+* Unleash comes configured with a `ToggleBootstrapFileProvider` which implements the `ToggleBootstrapProvider` interface.
+* It is the default implementation used if not overridden via the `setToggleBootstrapProvider` on UnleashConfig.
+##### Configure ToggleBootstrapFileProvider
+* The `ToggleBootstrapFileProvider` reads the file located at the path defined by the `UNLEASH_BOOTSTRAP_FILE` environment variable.
+* It supports both `classpath:` paths and absolute file paths.
 
 ## Unit testing
 You might want to control the state of the toggles during unit-testing.
diff --git a/pom.xml b/pom.xml
index f6837c70..de826dc0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
     <properties>
         <version.slf4j>1.7.30</version.slf4j>
         <version.log4j2>2.13.3</version.log4j2>
-        <version.junit5>5.7.0</version.junit5>
+        <version.junit5>5.7.1</version.junit5>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <version.unleash.specification>3.3.0</version.unleash.specification>
         <arguments />
diff --git a/src/main/java/no/finn/unleash/event/UnleashSubscriber.java b/src/main/java/no/finn/unleash/event/UnleashSubscriber.java
index 184902bd..8ef0c473 100644
--- a/src/main/java/no/finn/unleash/event/UnleashSubscriber.java
+++ b/src/main/java/no/finn/unleash/event/UnleashSubscriber.java
@@ -29,4 +29,6 @@ public interface UnleashSubscriber {
     default void togglesBackedUp(ToggleCollection toggleCollection) {}
 
     default void toggleBackupRestored(ToggleCollection toggleCollection) {}
+
+    default void togglesBootstrapped(ToggleCollection toggleCollection) {}
 }
diff --git a/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java b/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
index 04f9881c..a73f6e31 100644
--- a/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
+++ b/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
@@ -12,6 +12,7 @@ import no.finn.unleash.util.UnleashScheduledExecutor;
 
 public final class FeatureToggleRepository implements ToggleRepository {
     private final ToggleBackupHandler toggleBackupHandler;
+    private final ToggleBootstrapHandler toggleBootstrapHandler;
     private final ToggleFetcher toggleFetcher;
     private final EventDispatcher eventDispatcher;
 
@@ -29,7 +30,6 @@ public final class FeatureToggleRepository implements ToggleRepository {
                 toggleBackupHandler);
     }
 
-    @Deprecated
     public FeatureToggleRepository(
             UnleashConfig unleashConfig,
             UnleashScheduledExecutor executor,
@@ -39,8 +39,11 @@ public final class FeatureToggleRepository implements ToggleRepository {
         this.toggleBackupHandler = toggleBackupHandler;
         this.toggleFetcher = toggleFetcher;
         this.eventDispatcher = new EventDispatcher(unleashConfig);
-
-        toggleCollection = toggleBackupHandler.read();
+        this.toggleBootstrapHandler = new ToggleBootstrapHandler(unleashConfig);
+        this.toggleCollection = toggleBackupHandler.read();
+        if (this.toggleCollection.getFeatures().isEmpty()) {
+            this.toggleCollection = toggleBootstrapHandler.read();
+        }
 
         if (unleashConfig.isSynchronousFetchOnInitialisation()) {
             updateToggles().run();
diff --git a/src/main/java/no/finn/unleash/repository/ToggleBootstrapFileProvider.java b/src/main/java/no/finn/unleash/repository/ToggleBootstrapFileProvider.java
new file mode 100644
index 00000000..ef526d2d
--- /dev/null
+++ b/src/main/java/no/finn/unleash/repository/ToggleBootstrapFileProvider.java
@@ -0,0 +1,76 @@
+package no.finn.unleash.repository;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import no.finn.unleash.lang.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ToggleBootstrapFileProvider implements ToggleBootstrapProvider {
+    private static final Logger LOG = LoggerFactory.getLogger(ToggleBootstrapFileProvider.class);
+    final String path;
+
+    /**
+     * Accepts path to file to read either as constructor parameter or as environment variable in
+     * "UNLEASH_BOOTSTRAP_FILE"
+     *
+     * @param path - path to toggles file
+     */
+    public ToggleBootstrapFileProvider(@Nullable String path) {
+        this.path =
+                Optional.ofNullable(path)
+                        .orElseGet(() -> getEnvOrProperty("UNLEASH_BOOTSTRAP_FILE"));
+    }
+
+    @Override
+    @Nullable
+    public String read() {
+        LOG.info("Trying to read feature toggles from bootstrap file found at {}", path);
+        try {
+            File file = getFile(path);
+            return fileAsString(file);
+        } catch (FileNotFoundException ioEx) {
+            LOG.warn("Could not find file {}", path, ioEx);
+        } catch (IOException ioEx) {
+            LOG.warn("Generic IOException when trying to read file at {}", path, ioEx);
+        }
+        return null;
+    }
+
+    @Nullable
+    private String getEnvOrProperty(String envName) {
+        return Optional.ofNullable(System.getenv(envName))
+                .orElseGet(() -> System.getProperty(envName));
+    }
+
+    private String fileAsString(File file) throws IOException {
+        return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+    }
+
+    @Nullable
+    private File getFile(String path) {
+        if (path.startsWith("classpath:")) {
+            try {
+                URL resource =
+                        getClass()
+                                .getClassLoader()
+                                .getResource(path.substring("classpath:".length()));
+                if (resource != null) {
+                    return Paths.get(resource.toURI()).toFile();
+                }
+                return null;
+            } catch (URISyntaxException e) {
+                return null;
+            }
+        } else {
+            return Paths.get(path).toFile();
+        }
+    }
+}
diff --git a/src/main/java/no/finn/unleash/repository/ToggleBootstrapHandler.java b/src/main/java/no/finn/unleash/repository/ToggleBootstrapHandler.java
new file mode 100644
index 00000000..44ed46c3
--- /dev/null
+++ b/src/main/java/no/finn/unleash/repository/ToggleBootstrapHandler.java
@@ -0,0 +1,51 @@
+package no.finn.unleash.repository;
+
+import java.io.StringReader;
+import java.util.Collections;
+import no.finn.unleash.UnleashException;
+import no.finn.unleash.event.EventDispatcher;
+import no.finn.unleash.event.UnleashEvent;
+import no.finn.unleash.event.UnleashSubscriber;
+import no.finn.unleash.lang.Nullable;
+import no.finn.unleash.util.UnleashConfig;
+
+public class ToggleBootstrapHandler {
+    private final EventDispatcher eventDispatcher;
+    @Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
+
+    public ToggleBootstrapHandler(UnleashConfig unleashConfig) {
+        this.toggleBootstrapProvider = unleashConfig.getToggleBootstrapProvider();
+        this.eventDispatcher = new EventDispatcher(unleashConfig);
+    }
+
+    public ToggleCollection parse(String jsonString) {
+        try (StringReader stringReader = new StringReader(jsonString)) {
+            ToggleCollection toggleCollection = JsonToggleParser.fromJson(stringReader);
+            eventDispatcher.dispatch(new ToggleBootstrapRead(toggleCollection));
+            return toggleCollection;
+        } catch (IllegalStateException ise) {
+            eventDispatcher.dispatch(new UnleashException("Failed to read toggle bootstrap", ise));
+        }
+        return new ToggleCollection(Collections.emptyList());
+    }
+
+    public ToggleCollection read() {
+        if (toggleBootstrapProvider != null) {
+            return parse(toggleBootstrapProvider.read());
+        }
+        return new ToggleCollection(Collections.emptyList());
+    }
+
+    private static class ToggleBootstrapRead implements UnleashEvent {
+        private final ToggleCollection toggleCollection;
+
+        private ToggleBootstrapRead(ToggleCollection toggleCollection) {
+            this.toggleCollection = toggleCollection;
+        }
+
+        @Override
+        public void publishTo(UnleashSubscriber unleashSubscriber) {
+            unleashSubscriber.togglesBootstrapped(toggleCollection);
+        }
+    }
+}
diff --git a/src/main/java/no/finn/unleash/repository/ToggleBootstrapProvider.java b/src/main/java/no/finn/unleash/repository/ToggleBootstrapProvider.java
new file mode 100644
index 00000000..696c8311
--- /dev/null
+++ b/src/main/java/no/finn/unleash/repository/ToggleBootstrapProvider.java
@@ -0,0 +1,13 @@
+package no.finn.unleash.repository;
+
+public interface ToggleBootstrapProvider {
+    /**
+     * Should return JSON string parseable to /api/client/features format Look in
+     * src/test/resources/features-v1.json or src/test/resources/unleash-repo-v1.json for example
+     * Example in {@link no.finn.unleash.repository.ToggleBootstrapFileProvider}
+     *
+     * @return JSON string that can be sent to {@link
+     *     no.finn.unleash.repository.ToggleBootstrapHandler#parse(String)}
+     */
+    String read();
+}
diff --git a/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java b/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java
index 0e8809dd..39c7c05e 100644
--- a/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java
+++ b/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java
@@ -2,7 +2,6 @@ package no.finn.unleash.strategy;
 
 import java.util.Map;
 import java.util.Optional;
-
 import no.finn.unleash.UnleashContext;
 
 /**
diff --git a/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java b/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java
index 8e9b205d..d6068df2 100644
--- a/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java
+++ b/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java
@@ -2,7 +2,6 @@ package no.finn.unleash.strategy;
 
 import java.util.Map;
 import java.util.Optional;
-
 import no.finn.unleash.UnleashContext;
 
 /**
diff --git a/src/main/java/no/finn/unleash/util/UnleashConfig.java b/src/main/java/no/finn/unleash/util/UnleashConfig.java
index a39c79f1..82d9a6df 100644
--- a/src/main/java/no/finn/unleash/util/UnleashConfig.java
+++ b/src/main/java/no/finn/unleash/util/UnleashConfig.java
@@ -1,5 +1,7 @@
 package no.finn.unleash.util;
 
+import static no.finn.unleash.DefaultUnleash.UNKNOWN_STRATEGY;
+
 import java.io.File;
 import java.net.*;
 import java.util.HashMap;
@@ -11,10 +13,9 @@ import no.finn.unleash.UnleashContextProvider;
 import no.finn.unleash.event.NoOpSubscriber;
 import no.finn.unleash.event.UnleashSubscriber;
 import no.finn.unleash.lang.Nullable;
+import no.finn.unleash.repository.ToggleBootstrapProvider;
 import no.finn.unleash.strategy.Strategy;
 
-import static no.finn.unleash.DefaultUnleash.UNKNOWN_STRATEGY;
-
 public class UnleashConfig {
 
     static final String UNLEASH_APP_NAME_HEADER = "UNLEASH-APPNAME";
@@ -39,6 +40,7 @@ public class UnleashConfig {
     private final UnleashScheduledExecutor unleashScheduledExecutor;
     private final UnleashSubscriber unleashSubscriber;
     @Nullable private final Strategy fallbackStrategy;
+    @Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
 
     private UnleashConfig(
             @Nullable URI unleashAPI,
@@ -58,7 +60,8 @@ public class UnleashConfig {
             boolean synchronousFetchOnInitialisation,
             @Nullable UnleashScheduledExecutor unleashScheduledExecutor,
             @Nullable UnleashSubscriber unleashSubscriber,
-            @Nullable Strategy fallbackStrategy) {
+            @Nullable Strategy fallbackStrategy,
+            @Nullable ToggleBootstrapProvider unleashBootstrapProvider) {
 
         if (appName == null) {
             throw new IllegalStateException("You are required to specify the unleash appName");
@@ -80,7 +83,7 @@ public class UnleashConfig {
             throw new IllegalStateException("You are required to specify a subscriber");
         }
 
-        if ( fallbackStrategy == null) {
+        if (fallbackStrategy == null) {
             this.fallbackStrategy = UNKNOWN_STRATEGY;
         } else {
             this.fallbackStrategy = fallbackStrategy;
@@ -108,6 +111,7 @@ public class UnleashConfig {
         this.synchronousFetchOnInitialisation = synchronousFetchOnInitialisation;
         this.unleashScheduledExecutor = unleashScheduledExecutor;
         this.unleashSubscriber = unleashSubscriber;
+        this.toggleBootstrapProvider = unleashBootstrapProvider;
     }
 
     public static Builder builder() {
@@ -205,6 +209,11 @@ public class UnleashConfig {
         return fallbackStrategy;
     }
 
+    @Nullable
+    public ToggleBootstrapProvider getToggleBootstrapProvider() {
+        return toggleBootstrapProvider;
+    }
+
     static class ProxyAuthenticator extends Authenticator {
 
         @Override
@@ -249,6 +258,7 @@ public class UnleashConfig {
         private @Nullable UnleashSubscriber unleashSubscriber;
         private boolean isProxyAuthenticationByJvmProperties;
         private Strategy fallbackStrategy;
+        private @Nullable ToggleBootstrapProvider toggleBootstrapProvider;
 
         private static String getHostname() {
             String hostName = System.getProperty("hostname");
@@ -355,6 +365,12 @@ public class UnleashConfig {
             return this;
         }
 
+        public Builder toggleBootstrapProvider(
+                @Nullable ToggleBootstrapProvider toggleBootstrapProvider) {
+            this.toggleBootstrapProvider = toggleBootstrapProvider;
+            return this;
+        }
+
         private String getBackupFile() {
             if (backupFile != null) {
                 return backupFile;
@@ -384,7 +400,8 @@ public class UnleashConfig {
                     Optional.ofNullable(scheduledExecutor)
                             .orElseGet(UnleashScheduledExecutorImpl::getInstance),
                     Optional.ofNullable(unleashSubscriber).orElseGet(NoOpSubscriber::new),
-                    fallbackStrategy);
+                    fallbackStrategy,
+                    toggleBootstrapProvider);
         }
 
         public String getDefaultSdkVersion() {


Test patch:
diff --git a/src/test/java/no/finn/unleash/integration/ClientSpecificationTest.java b/src/test/java/no/finn/unleash/integration/ClientSpecificationTest.java
index c4846b3a..0f21f2bc 100644
--- a/src/test/java/no/finn/unleash/integration/ClientSpecificationTest.java
+++ b/src/test/java/no/finn/unleash/integration/ClientSpecificationTest.java
@@ -142,6 +142,7 @@ public class ClientSpecificationTest {
                                         .withStatus(200)
                                         .withHeader("Content-Type", "application/json")
                                         .withBody(definition.getState().toString())));
+        stubFor(post(urlEqualTo("/api/client/register")).willReturn(aResponse().withStatus(200)));
     }
 
     private TestDefinition getTestDefinition(String fileName) throws IOException {
diff --git a/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java b/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
index fbb979dd..9094e2b4 100644
--- a/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
+++ b/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
@@ -1,12 +1,18 @@
 package no.finn.unleash.repository;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.*;
 
+import java.io.File;
+import java.io.IOException;
 import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import no.finn.unleash.ActivationStrategy;
 import no.finn.unleash.FeatureToggle;
@@ -42,6 +48,7 @@ public class FeatureToggleRepositoryTest {
                         .build();
 
         ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read()).thenReturn(new ToggleCollection(Collections.emptyList()));
         ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
         new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
@@ -106,6 +113,8 @@ public class FeatureToggleRepositoryTest {
         ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
 
         ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read()).thenReturn(new ToggleCollection(Collections.emptyList()));
+
         ToggleCollection toggleCollection =
                 populatedToggleCollection(
                         new FeatureToggle(
@@ -135,6 +144,7 @@ public class FeatureToggleRepositoryTest {
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
         ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
         ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read()).thenReturn(new ToggleCollection(Collections.emptyList()));
 
         // setup fetcher
         ToggleCollection toggleCollection = populatedToggleCollection();
@@ -158,6 +168,7 @@ public class FeatureToggleRepositoryTest {
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
         ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
         ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read()).thenReturn(new ToggleCollection(Collections.emptyList()));
 
         // setup fetcher
         ToggleCollection toggleCollection = populatedToggleCollection();
@@ -175,4 +186,66 @@ public class FeatureToggleRepositoryTest {
         list.addAll(Arrays.asList(featureToggles));
         return new ToggleCollection(list);
     }
+
+    @Test
+    public void should_read_from_bootstrap_location_if_backup_was_empty()
+            throws URISyntaxException, IOException {
+        File file =
+                new File(getClass().getClassLoader().getResource("unleash-repo-v1.json").toURI());
+        ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class);
+        when(toggleBootstrapProvider.read()).thenReturn(fileToString(file));
+        UnleashConfig config =
+                UnleashConfig.builder()
+                        .synchronousFetchOnInitialisation(false)
+                        .appName("test-sync-update")
+                        .unleashAPI("http://localhost:8080")
+                        .toggleBootstrapProvider(toggleBootstrapProvider)
+                        .build();
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
+        ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read()).thenReturn(new ToggleCollection(Collections.emptyList()));
+        FeatureToggleRepository repo =
+                new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
+        assertThat(repo.getFeatureNames()).hasSize(3);
+    }
+
+    @Test
+    public void should_not_read_bootstrap_if_backup_was_found()
+            throws IOException, URISyntaxException {
+        File file =
+                new File(getClass().getClassLoader().getResource("unleash-repo-v1.json").toURI());
+        ToggleBootstrapProvider toggleBootstrapProvider = mock(ToggleBootstrapProvider.class);
+        when(toggleBootstrapProvider.read()).thenReturn(fileToString(file));
+        UnleashConfig config =
+                UnleashConfig.builder()
+                        .synchronousFetchOnInitialisation(false)
+                        .appName("test-sync-update")
+                        .unleashAPI("http://localhost:8080")
+                        .toggleBootstrapProvider(toggleBootstrapProvider)
+                        .build();
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
+        ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
+        when(toggleBackupHandler.read())
+                .thenReturn(
+                        populatedToggleCollection(
+                                new FeatureToggle(
+                                        "toggleFeatureName1",
+                                        true,
+                                        Collections.singletonList(
+                                                new ActivationStrategy("custom", null))),
+                                new FeatureToggle(
+                                        "toggleFeatureName2",
+                                        true,
+                                        Collections.singletonList(
+                                                new ActivationStrategy("custom", null)))));
+        FeatureToggleRepository repo =
+                new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
+        verify(toggleBootstrapProvider, times(0)).read();
+    }
+
+    private String fileToString(File f) throws IOException {
+        return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
+    }
 }
diff --git a/src/test/java/no/finn/unleash/repository/ToggleBootstrapFileProviderTest.java b/src/test/java/no/finn/unleash/repository/ToggleBootstrapFileProviderTest.java
new file mode 100644
index 00000000..a2704a46
--- /dev/null
+++ b/src/test/java/no/finn/unleash/repository/ToggleBootstrapFileProviderTest.java
@@ -0,0 +1,37 @@
+package no.finn.unleash.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import org.junit.jupiter.api.Test;
+
+class ToggleBootstrapFileProviderTest {
+
+    @Test
+    public void shouldBeAbleToLoadFilePassedInAsArgument() {
+        File exampleRepoFile =
+                new File(getClass().getClassLoader().getResource("unleash-repo-v0.json").getFile());
+        ToggleBootstrapFileProvider toggleBootstrapFileProvider =
+                new ToggleBootstrapFileProvider(exampleRepoFile.getAbsolutePath());
+        assertThat(toggleBootstrapFileProvider.read()).isNotEmpty();
+    }
+
+    @Test
+    public void shouldBeAbleToLoadFilePassedInEnvironment() {
+        File exampleRepoFile =
+                new File(getClass().getClassLoader().getResource("unleash-repo-v0.json").getFile());
+        System.setProperty("UNLEASH_BOOTSTRAP_FILE", exampleRepoFile.getAbsolutePath());
+        ToggleBootstrapFileProvider toggleBootstrapFileProvider =
+                new ToggleBootstrapFileProvider(null);
+        assertThat(toggleBootstrapFileProvider.read()).isNotEmpty();
+    }
+
+    @Test
+    public void shouldBeAbleToLoadfileFromClasspathReference() {
+        System.setProperty("UNLEASH_BOOTSTRAP_FILE", "classpath:unleash-repo-v0.json");
+        ToggleBootstrapFileProvider bootstrap = new ToggleBootstrapFileProvider(null);
+
+        String read = bootstrap.read();
+        assertThat(read).isNotBlank();
+    }
+}
diff --git a/src/test/java/no/finn/unleash/repository/ToggleBootstrapHandlerTest.java b/src/test/java/no/finn/unleash/repository/ToggleBootstrapHandlerTest.java
new file mode 100644
index 00000000..9cd23de1
--- /dev/null
+++ b/src/test/java/no/finn/unleash/repository/ToggleBootstrapHandlerTest.java
@@ -0,0 +1,32 @@
+package no.finn.unleash.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import no.finn.unleash.util.UnleashConfig;
+import org.junit.jupiter.api.Test;
+
+public class ToggleBootstrapHandlerTest {
+    @Test
+    public void can_parse_toggle_collection_from_string() throws URISyntaxException, IOException {
+        File file =
+                new File(getClass().getClassLoader().getResource("unleash-repo-v1.json").toURI());
+        UnleashConfig config =
+                UnleashConfig.builder()
+                        .appName("test")
+                        .unleashAPI("http://http://unleash.org")
+                        .build();
+        ToggleBootstrapHandler handler = new ToggleBootstrapHandler(config);
+
+        ToggleCollection parse = handler.parse(fileToString(file));
+        assertThat(parse.getFeatures()).hasSize(3);
+    }
+
+    private String fileToString(File f) throws IOException {
+        return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
+    }
+}