Repository: porscheinformatik/sonarqube-licensecheck (java)

Issue:
NPM doesn't support transitive dependendies
Would be great that plugin can check also transitive dependencies for npm projects.

Pull request description:
Optionally resolve transitive NPM dependencies
Closes #21.

Probably has merge conflicts with #73.  I'll happily fix up conflicts on either PR after the first one is merged (assuming that happens!)

This PR performs the logic using recursion, and makes use of a `Set` to ensure we don't record duplicates where the same dependency is transitively pulled in more than once.  UTs are included to prove the behaviour.

**Update:** after running a local build of this we encountered a StackOverflowException when circular dependencies existed.  I've now made use of a `Deque` to track and warn on such instances so we don't fall down the rabbit hole.

Patch:
diff --git a/README.md b/README.md
index e3b3083..ade0866 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ The plugin scans for dependencies defined in your project including all transiti
 Currently supported formats are:
 * Maven POM files - all dependencies with scope "compile" and "runtime" are checked
 * NPM package.json files - all dependencies (except "devDependencies") are checked
+  * Note that transitive dependencies are _not_ scanned unless `licensecheck.npm.resolvetransitive` is set to `true`.
 
 ### Project Dashboard
 
diff --git a/pom.xml b/pom.xml
index 129d203..3fc4bab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
   <groupId>at.porscheinformatik.sonarqube.licensecheck</groupId>
   <artifactId>sonarqube-licensecheck-plugin</artifactId>
-  <version>3.0.0-SNAPSHOT</version>
+  <version>4.0.0-SNAPSHOT</version>
   <name>SonarQube License Check Plugin</name>
   <packaging>sonar-plugin</packaging>
 
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
index eae2ec8..e77b657 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
@@ -9,4 +9,6 @@ public class LicenseCheckPropertyKeys
     public static final String ALLOWED_DEPENDENCIES_KEY = "licensecheck.alloweddependencies";
 
     public static final String PROJECT_LICENSE_KEY = "licensecheck.projectlicense";
+
+    public static final String NPM_RESOLVE_TRANSITVE_DEPS = "licensecheck.npm.resolvetransitive";
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
index 4378ad5..83d37c4 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
@@ -2,7 +2,6 @@ package at.porscheinformatik.sonarqube.licensecheck;
 
 import static java.util.Collections.newSetFromMap;
 
-import java.util.Collections;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
@@ -44,7 +43,7 @@ public class LicenseCheckSensor implements Sensor
         this.settings = settings;
         this.validateLicenses = validateLicenses;
         this.scanners = new Scanner[]{
-            new PackageJsonDependencyScanner(),
+            new PackageJsonDependencyScanner(settings.getBoolean(LicenseCheckPropertyKeys.NPM_RESOLVE_TRANSITVE_DEPS)),
             new MavenDependencyScanner(mavenLicenseService, mavenDependencyService)};
     }
 
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
index f2494ff..2850108 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
@@ -85,15 +85,15 @@ public class ValidateLicenses
             {
                 String notAllowedLicense = "";
 
-                for(License element : licensesContainingDependency) {
-                    if(element.getStatus() == "false") {
+                for (License element : licensesContainingDependency) {
+                    if (element.getStatus() == "false") {
                         notAllowedLicense += element.getName() + " ";
                     }
                 }
                 licenseNotAllowedIssue(context, dependency, notAllowedLicense);
             }
         }
-    }    
+    }
 
     private boolean checkSpdxLicense(String spdxLicenseString, List<License> licenses)
     {
@@ -115,7 +115,7 @@ public class ValidateLicenses
             .isPresent();
     }
 
-    private boolean checkSpdxLicenseWithOr(String spdxLicenseString, List<License> licenses){
+    private boolean checkSpdxLicenseWithOr(String spdxLicenseString, List<License> licenses) {
         String[] orLicenses = spdxLicenseString.replace("(", "").replace(")", "").split(" OR ");
         return licenses
             .stream()
@@ -138,7 +138,7 @@ public class ValidateLicenses
         {
             return true;
         }
-        else if(foundLicenses.size() == count)
+        else if (foundLicenses.size() == count)
         {
             // NOT ALLOWED
             return false;
@@ -177,7 +177,7 @@ public class ValidateLicenses
         issue.save();
     }
 
-    private static boolean contains(String[] items, String valueToFind) 
+    private static boolean contains(String[] items, String valueToFind)
     {
         for (String item : items)
         {
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
index 9fdddfe..f3af084 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
@@ -1,11 +1,11 @@
 package at.porscheinformatik.sonarqube.licensecheck.interfaces;
 
 import java.io.File;
-import java.util.List;
+import java.util.Set;
 
 import at.porscheinformatik.sonarqube.licensecheck.Dependency;
 
 public interface Scanner
 {
-    List<Dependency> scan(File moduleDir);
+    Set<Dependency> scan(File moduleDir);
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
index 13ba27b..5236fdc 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
@@ -10,6 +10,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -55,13 +56,16 @@ public class MavenDependencyScanner implements Scanner
     }
 
     @Override
-    public List<Dependency> scan(File moduleDir)
+    public Set<Dependency> scan(File moduleDir)
     {
-        if(!new File(moduleDir, "pom.xml").exists())
+        if (!new File(moduleDir, "pom.xml").exists())
         {
-            return Collections.emptyList();
+            LOGGER.info("No pom.xml file found in {} - skipping Maven dependency scan", moduleDir.getPath());
+            return Collections.emptySet();
         }
 
+        LOGGER.info("Scanning for Maven dependencies");
+
         String userSettings = null;
         String globalSettings = null;
         CommandLine cmd = getCommandLineArgs();
@@ -77,13 +81,13 @@ public class MavenDependencyScanner implements Scanner
             }
         }
 
-        return this.readDependecyList(moduleDir, userSettings, globalSettings)
+        return this.readDependencyList(moduleDir, userSettings, globalSettings)
             .map(this.loadLicenseFromPom(mavenLicenseService.getLicenseMap(), userSettings, globalSettings))
             .map(this::mapMavenDependencyToLicense)
-            .collect(Collectors.toList());
+            .collect(Collectors.toSet());
     }
 
-    private Stream<Dependency> readDependecyList(File moduleDir, String userSettings, String globalSettings)
+    private Stream<Dependency> readDependencyList(File moduleDir, String userSettings, String globalSettings)
     {
         Path tempFile = createTempFile();
         if (tempFile == null)
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
index bb2e3de..785fc01 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
@@ -5,9 +5,9 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.json.Json;
 import javax.json.JsonArray;
@@ -24,89 +24,111 @@ public class PackageJsonDependencyScanner implements Scanner
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(PackageJsonDependencyScanner.class);
 
+    private boolean resolveTransitiveDeps;
+
+    public PackageJsonDependencyScanner(boolean resolveTransitiveDeps)
+    {
+        this.resolveTransitiveDeps = resolveTransitiveDeps;
+    }
+
     @Override
-    public List<Dependency> scan(File file)
+    public Set<Dependency> scan(File moduleDir)
     {
-        File packageJsonFile = new File(file, "package.json");
+        File packageJsonFile = new File(moduleDir, "package.json");
 
-        if (packageJsonFile.exists())
+        if (!packageJsonFile.exists())
         {
-            try (InputStream fis = new FileInputStream(packageJsonFile);
-                JsonReader jsonReader = Json.createReader(fis))
-            {
-                JsonObject jsonObject = jsonReader.readObject();
-                JsonObject jsonObjectDependencies = jsonObject.getJsonObject("dependencies");
-                if (jsonObjectDependencies != null)
-                {
-                    return dependencyParser(jsonObjectDependencies, packageJsonFile);
-                }
-                jsonReader.close();
-            }
-            catch (IOException e)
-            {
-                LOGGER.error("Error reading package.json", e);
-            }
+            LOGGER.info("No package.json file found in {} - skipping NPM dependency scan", moduleDir.getPath());
+            return Collections.emptySet();
         }
-        return Collections.emptyList();
+
+        LOGGER.info("Scanning for NPM dependencies");
+
+        return dependencyParser(moduleDir, packageJsonFile);
     }
 
-    private List<Dependency> dependencyParser(JsonObject jsonDependencies, File packageJsonFile)
+    private Set<Dependency> dependencyParser(File baseDir, File packageJsonFile)
     {
-        List<Dependency> dependencies = new ArrayList<>();
+        Set<Dependency> dependencies = new HashSet<>();
 
-        File nodeModulesFolder = new File(packageJsonFile.getParentFile(), "node_modules");
-        if (nodeModulesFolder.exists() && nodeModulesFolder.isDirectory())
+        try (InputStream fis = new FileInputStream(packageJsonFile);
+            JsonReader jsonReader = Json.createReader(fis))
         {
-            for (String key : jsonDependencies.keySet())
+            JsonObject packageJson = jsonReader.readObject();
+
+            JsonObject packageJsonDependencies = packageJson.getJsonObject("dependencies");
+            if (packageJsonDependencies != null)
             {
-                moduleCheck(nodeModulesFolder, key, dependencies);
+                scanDependencies(baseDir, packageJsonDependencies.keySet(), dependencies);
             }
         }
+        catch (IOException e)
+        {
+            LOGGER.error("Error reading package.json", e);
+        }
 
         return dependencies;
     }
 
-    private static void moduleCheck(File nodeModulesFolder, String identifier, List<Dependency> dependencies)
+    private void scanDependencies(File baseDir, Set<String> packageNames, Set<Dependency> dependencies)
     {
-        File moduleFolder = new File(nodeModulesFolder, identifier);
+        LOGGER.info("Scanning NPM packages " + packageNames);
 
-        if (moduleFolder.exists() && moduleFolder.isDirectory())
+        for (String packageName : packageNames)
         {
-            File packageFile = new File(moduleFolder, "package.json");
+            if (dependencies.stream().anyMatch(d -> packageName.equals(d.getName())))
+            {
+                LOGGER.debug("Package {} has already been encountered and will not be scanned again", packageName);
+                continue;
+            }
 
-            try (InputStream fis = new FileInputStream(packageFile); JsonReader jsonReader = Json.createReader(fis))
+            File packageJsonFile = new File(baseDir, "node_modules/" + packageName + "/package.json");
+            if (!packageJsonFile.exists())
             {
-                JsonObject jsonObject = jsonReader.readObject();
-                if (jsonObject != null)
+                LOGGER.warn("No package.json file found for package {} in node_modules - skipping dependency",
+                        packageName);
+                continue;
+            }
+
+            try (InputStream fis = new FileInputStream(packageJsonFile);
+                JsonReader jsonReader = Json.createReader(fis))
+            {
+                JsonObject packageJson = jsonReader.readObject();
+                if (packageJson != null)
                 {
-                    String license = null;
-                    if (jsonObject.containsKey("license"))
+                    String license = "";
+                    if (packageJson.containsKey("license"))
                     {
-                        license = jsonObject.getString("license");
+                        license = packageJson.getString("license");
                     }
-                    else if (jsonObject.containsKey("licenses"))
+                    else if (packageJson.containsKey("licenses"))
                     {
-                        JsonArray licenses = jsonObject.getJsonArray("licenses");
+                        JsonArray licenses = packageJson.getJsonArray("licenses");
                         if (licenses.size() > 0)
                         {
                             license = licenses.getJsonObject(0).getString("type");
                         }
                     }
 
-                    if (license != null)
+                    dependencies.add(new Dependency(packageName, packageJson.getString("version"), license));
+
+                    if (resolveTransitiveDeps)
                     {
-                        dependencies.add(new Dependency(identifier, jsonObject.getString("version"), license));
+                        JsonObject packageJsonDependencies = packageJson.getJsonObject("dependencies");
+                        if (packageJsonDependencies != null)
+                        {
+                            scanDependencies(baseDir, packageJsonDependencies.keySet(), dependencies);
+                        }
                     }
                 }
-                jsonReader.close();
             }
             catch (FileNotFoundException e)
             {
-                LOGGER.error("Could not find package.json", e);
+                LOGGER.error("Could not load package.json", e);
             }
             catch (Exception e)
             {
-                LOGGER.error("Error adding dependency " + identifier, e);
+                LOGGER.error("Could not check NPM package " + packageName, e);
             }
         }
     }


Test patch:
diff --git a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
index c01c268..99f929d 100644
--- a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
+++ b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
@@ -1,26 +1,66 @@
 package at.porscheinformatik.sonarqube.licensecheck;
 
 import java.io.File;
-import java.util.List;
+import java.util.Set;
 
-import org.hamcrest.CoreMatchers;
-import org.junit.Assert;
 import org.junit.Test;
 
 import at.porscheinformatik.sonarqube.licensecheck.interfaces.Scanner;
 import at.porscheinformatik.sonarqube.licensecheck.npm.PackageJsonDependencyScanner;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertThat;
+
 public class PackageJsonDependencyScannerTest
 {
-    final File folder = new File("src/test/resources");
+    private final File folder = new File("src/test/resources");
 
     @Test
     public void testHappyPath()
     {
-        Scanner scanner = new PackageJsonDependencyScanner();
+        Scanner scanner = new PackageJsonDependencyScanner(false);
+
+        Set<Dependency> dependencies = scanner.scan(folder);
+
+        assertThat(dependencies, hasSize(2));
+        assertThat(dependencies, containsInAnyOrder(
+            new Dependency("angular", "1.5.0", "MIT"),
+            new Dependency("arangojs", "5.6.0", "Apache-2.0")));
+    }
+
+    @Test
+    public void testTransitive()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(true);
+
+        Set<Dependency> dependencies = scanner.scan(folder);
+
+        assertThat(dependencies, hasSize(4));
+        assertThat(dependencies, containsInAnyOrder(
+            new Dependency("angular", "1.5.0", "MIT"),
+            new Dependency("arangojs", "5.6.0", "Apache-2.0"),
+            new Dependency("linkedlist", "1.0.1", "LGPLv3"),
+            new Dependency("retry", "0.10.1", "MIT")));
+    }
+
+    @Test
+    public void testNoPackageJson()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(false);
+
+        Set<Dependency> dependencies = scanner.scan(new File("src"));
+
+        assertThat(dependencies, hasSize(0));
+    }
+
+    @Test
+    public void testNoNodeModules()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(false);
 
-        List<Dependency> dependencies = scanner.scan(folder);
+        Set<Dependency> dependencies = scanner.scan(new File(folder, "node_modules/arangojs"));
 
-        Assert.assertThat(dependencies, CoreMatchers.hasItem(new Dependency("angular", "1.5.0", "MIT")));
+        assertThat(dependencies, hasSize(0));
     }
 }
diff --git a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
index cb89015..6894084 100644
--- a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
+++ b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
@@ -9,8 +9,8 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 import org.hamcrest.Matchers;
@@ -39,7 +39,7 @@ public class MavenDependencyScannerTest
         Scanner scanner = new MavenDependencyScanner(licenseService, dependencyService);
 
         // -
-        List<Dependency> dependencies = scanner.scan(moduleDir);
+        Set<Dependency> dependencies = scanner.scan(moduleDir);
 
         assertThat(dependencies.size(), Matchers.greaterThan(0));
 
@@ -66,7 +66,7 @@ public class MavenDependencyScannerTest
 
         File moduleDir = Files.createTempDirectory("lala").toFile();
         moduleDir.deleteOnExit();
-        List<Dependency> dependencies = scanner.scan(moduleDir);
+        Set<Dependency> dependencies = scanner.scan(moduleDir);
 
         assertThat(dependencies.size(), is(0));
     }
diff --git a/src/test/resources/node_modules/arangojs/package.json b/src/test/resources/node_modules/arangojs/package.json
new file mode 100644
index 0000000..0a4db0d
--- /dev/null
+++ b/src/test/resources/node_modules/arangojs/package.json
@@ -0,0 +1,88 @@
+{
+  "name": "arangojs",
+  "version": "5.6.0",
+  "license": "Apache-2.0",
+  "description": "The official ArangoDB JavaScript driver.",
+  "homepage": "https://github.com/arangodb/arangojs",
+  "author": "ArangoDB GmbH",
+  "keywords": [
+    "arango",
+    "arangodb",
+    "aql",
+    "nosql",
+    "client",
+    "driver",
+    "api",
+    "http",
+    "rest"
+  ],
+  "main": "lib/index.js",
+  "browser": {
+    "./lib/util/btoa.js": "./lib/util/btoa.web.js",
+    "./lib/util/bytelength.js": "./lib/util/bytelength.web.js",
+    "./lib/util/multipart.js": "./lib/util/multipart.web.js",
+    "./lib/util/request.js": "./lib/util/request.web.js"
+  },
+  "directories": {
+    "lib": "lib"
+  },
+  "files": [
+    "lib/",
+    "package.json",
+    "arangojs.d.ts",
+    "README.md",
+    "LICENSE"
+  ],
+  "typings": "arangojs.d.ts",
+  "scripts": {
+    "test": "mocha --growl",
+    "lint": "snazzy --verbose src/**/*.js test/**/*.js",
+    "ci": "mocha",
+    "watch": "npm-run-all -p watch:*",
+    "watch:browser": "npm run dist:browser -- --watch",
+    "watch:node": "watch 'npm run dist:node' ./src ./test",
+    "watch:test": "watch 'npm run lint && npm run test' ./src ./test",
+    "dist": "npm-run-all -p dist:*",
+    "dist:browser": "webpack",
+    "dist:node": "babel --compact false -d lib src",
+    "prepublish": "npm run lint && npm run dist && node -e 'require(\"./\");'"
+  },
+  "dependencies": {
+    "linkedlist": "^1.0.1",
+    "retry": "^0.10.0"
+  },
+  "devDependencies": {
+    "babel-cli": "6.22.2",
+    "babel-core": "6.22.1",
+    "babel-loader": "6.2.9",
+    "babel-plugin-add-module-exports": "0.2.1",
+    "babel-plugin-transform-builtin-extend": "1.1.0",
+    "babel-plugin-transform-es2015-modules-commonjs": "6.22.0",
+    "babel-preset-es2015": "6.22.0",
+    "babel-preset-stage-1": "6.22.0",
+    "chai": "3.5.0",
+    "core-js": "2.4.1",
+    "coveralls": "2.11.15",
+    "istanbul": "0.4.5",
+    "json-loader": "0.5.4",
+    "mocha": "3.2.0",
+    "npm-run-all": "4.0.1",
+    "snazzy": "6.0.0",
+    "standard": "8.6.0",
+    "watch": "1.0.1",
+    "webpack": "1.14.0"
+  },
+  "contributors": [
+    {
+      "name": "Alan Plum",
+      "email": "me@pluma.io"
+    }
+  ],
+  "bugs": {
+    "url": "https://github.com/arangodb/arangojs/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/arangodb/arangojs.git"
+  }
+}
diff --git a/src/test/resources/node_modules/linkedlist/package.json b/src/test/resources/node_modules/linkedlist/package.json
new file mode 100644
index 0000000..fed11ff
--- /dev/null
+++ b/src/test/resources/node_modules/linkedlist/package.json
@@ -0,0 +1,28 @@
+{
+  "author": "Kilian Ciuffolo <me@nailik.org> (http://nailik.org)",
+  "name": "linkedlist",
+  "licenses": [{
+    "type": "LGPLv3"
+  }],
+  "description": "Array like linked list with iterator",
+  "keywords": ["array", "linked", "list", "linkedlist", "iterator"],
+  "version": "1.0.1",
+  "repository": {
+    "type": "git",
+    "url": "http://github.com/kilianc/node-linkedlist.git"
+  },
+  "main" : "./",
+  "scripts": {
+    "test": "make test"
+  },
+  "engines": {
+    "node": ">= v0.6.x"
+  },
+  "dependencies": {
+    "retry": "^1.2.3"
+  },
+  "devDependencies": {
+    "mocha": "1.0.x",
+    "should": "0.6.1"
+  }
+}
diff --git a/src/test/resources/node_modules/retry/package.json b/src/test/resources/node_modules/retry/package.json
new file mode 100644
index 0000000..77a7d12
--- /dev/null
+++ b/src/test/resources/node_modules/retry/package.json
@@ -0,0 +1,26 @@
+{
+  "author": "Tim Koschützki <tim@debuggable.com> (http://debuggable.com/)",
+  "name": "retry",
+  "description": "Abstraction for exponential and custom retry strategies for failed operations.",
+  "license": "MIT",
+  "version": "0.10.1",
+  "homepage": "https://github.com/tim-kos/node-retry",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/tim-kos/node-retry.git"
+  },
+  "directories": {
+    "lib": "./lib"
+  },
+  "main": "index",
+  "engines": {
+    "node": "*"
+  },
+  "dependencies": {
+    "linkedlist": "^1.0.1"
+  },
+  "devDependencies": {
+    "fake": "0.2.0",
+    "far": "0.0.1"
+  }
+}
diff --git a/src/test/resources/package.json b/src/test/resources/package.json
index 909f0c9..af1d969 100644
--- a/src/test/resources/package.json
+++ b/src/test/resources/package.json
@@ -6,10 +6,11 @@
   "dependencies": {
     "angular": "^1.4.3",
     "angular-ui-router" : "~0.2.18",
-    "angular-ui-bootstrap" : "1.1.2"
+    "angular-ui-bootstrap" : "1.1.2",
+    "arangojs" : "5.6.0"
   },
   "devDependencies": {
     "gulp": "^3.9.1"
   },
   "license": "MIT"
-}
\ No newline at end of file
+}