diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 91b11e8bd174a1262d0cc70fa6dc4da077152d6e..cde12894ac4000b3944e2d7e6099c30104849c83 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,7 @@ variables:
   IMAGE_NAME: funkwhale/funkwhale
   IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME
   IMAGE_LATEST: $IMAGE_NAME:latest
-
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
 
 
 stages:
@@ -14,40 +14,61 @@ test_api:
   services:
     - postgres:9.4
   stage: test
-  image: funkwhale/funkwhale:base
+  image: funkwhale/funkwhale:latest
+  cache:
+    key: "$CI_PROJECT_ID/pip_cache"
+    paths:
+      - "$PIP_CACHE_DIR"
   variables:
-    PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
     DATABASE_URL: "postgresql://postgres@postgres/postgres"
 
   before_script:
-    - python3 -m venv --copies virtualenv
-    - source virtualenv/bin/activate
     - cd api
     - pip install -r requirements/base.txt
     - pip install -r requirements/local.txt
     - pip install -r requirements/test.txt
   script:
     - pytest
+  tags:
+    - docker
+
+
+test_front:
+  stage: test
+  image: node:9
+  before_script:
+    - cd front
+
+  script:
+    - yarn install
+    - yarn run unit
   cache:
-    key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
+    key: "$CI_PROJECT_ID/front_dependencies"
     paths:
-      - "$CI_PROJECT_DIR/pip-cache"
+      - front/node_modules
+      - front/yarn.lock
+  artifacts:
+    name: "front_${CI_COMMIT_REF_NAME}"
+    paths:
+      - front/dist/
   tags:
     - docker
 
+
 build_front:
   stage: build
-  image: node:6-alpine
+  image: node:9
   before_script:
     - cd front
 
   script:
-    - npm install
-    - npm run build
+    - yarn install
+    - yarn run build
   cache:
-    key: "$CI_COMMIT_REF_NAME"
+    key: "$CI_PROJECT_ID/front_dependencies"
     paths:
       - front/node_modules
+      - front/yarn.lock
   artifacts:
     name: "front_${CI_COMMIT_REF_NAME}"
     paths:
diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py
index 98c0cfc08de60fc21f2ffc8d66918b638bc28c6c..08ae00b684c30bcf5df91d5438dae1c03c790259 100644
--- a/api/funkwhale_api/favorites/views.py
+++ b/api/funkwhale_api/favorites/views.py
@@ -43,7 +43,7 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin,
         favorite = models.TrackFavorite.add(track=track, user=self.request.user)
         return favorite
 
-    @list_route(methods=['delete'])
+    @list_route(methods=['delete', 'post'])
     def remove(self, request, *args, **kwargs):
         try:
             pk = int(request.data['track'])
diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py
index ef5eaf4be811f580d82378c17ab838694df52ea7..892b784caec0c7bcdd3db7927852bd83b5ad9b2b 100644
--- a/api/funkwhale_api/music/fake_data.py
+++ b/api/funkwhale_api/music/fake_data.py
@@ -4,7 +4,7 @@ Populates the database with fake data
 import random
 
 from funkwhale_api.music import models
-from funkwhale_api.music.tests import factories
+from funkwhale_api.music import factories
 
 
 def create_data(count=25):
@@ -19,4 +19,4 @@ def create_data(count=25):
 
 
 if __name__ == '__main__':
-    main()
+    create_data()
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index cff16d3f1de30ff37810d2c934923bb595d6abdf..ce0eb9b85f14bfcef9d92b3e1084ca084375c2a9 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -47,8 +47,7 @@ mutagen>=1.39,<1.40
 
 
 # Until this is merged
-#django-taggit>=0.22,<0.23
-git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5
+django-taggit>=0.22,<0.23
 # Until this is merged
 git+https://github.com/EliotBerriot/PyMemoize.git@django
 # Until this is merged
diff --git a/api/tests/test_favorites.py b/api/tests/test_favorites.py
index 418166d8e0c11aa133e87a44a55f7b1713a3ad2f..8165722eacc969a9ea651dd70815d3fa1d9c75ea 100644
--- a/api/tests/test_favorites.py
+++ b/api/tests/test_favorites.py
@@ -58,11 +58,14 @@ def test_user_can_remove_favorite_via_api(logged_in_client, factories, client):
     assert response.status_code == 204
     assert TrackFavorite.objects.count() == 0
 
-def test_user_can_remove_favorite_via_api_using_track_id(factories, logged_in_client):
+
+@pytest.mark.parametrize('method', ['delete', 'post'])
+def test_user_can_remove_favorite_via_api_using_track_id(
+        method, factories, logged_in_client):
     favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
 
     url = reverse('api:v1:favorites:tracks-remove')
-    response = logged_in_client.delete(
+    response = getattr(logged_in_client, method)(
         url, json.dumps({'track': favorite.track.pk}),
         content_type='application/json'
     )
diff --git a/dev.yml b/dev.yml
index befc4b2434848aca07b1c594f221889a3b227d3d..971e38b62ecbea864abc7d21db25547b06ba3084 100644
--- a/dev.yml
+++ b/dev.yml
@@ -3,9 +3,7 @@ version: '2'
 services:
 
   front:
-    build:
-      dockerfile: docker/Dockerfile.dev
-      context: ./front
+    build: front
     env_file: .env.dev
     environment:
       - "HOST=0.0.0.0"
diff --git a/front/Dockerfile b/front/Dockerfile
index ad05f72eb8ab839f04b841c20663db00426caeb3..cdf92446b1c5fde91d3e8511ba5dec9e1743527c 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -1,13 +1,11 @@
-FROM node:6-alpine
+FROM node:9
 
 EXPOSE 8080
-
-RUN mkdir /app
-WORKDIR /app
+WORKDIR /app/
 ADD package.json .
+RUN yarn install --only=production
+RUN yarn install --only=dev
+VOLUME ["/app/node_modules"]
+COPY . .
 
-RUN npm install
-
-ADD . .
-
-RUN npm run build
+CMD ["npm", "run", "dev"]
diff --git a/front/docker/Dockerfile.dev b/front/docker/Dockerfile.dev
deleted file mode 100644
index 1a0c90c9e0fd8bc8a578a25e797fc04a3e30f6ea..0000000000000000000000000000000000000000
--- a/front/docker/Dockerfile.dev
+++ /dev/null
@@ -1,13 +0,0 @@
-FROM node:6-alpine
-
-EXPOSE 8080
-
-RUN mkdir /app
-WORKDIR /app
-ADD package.json .
-
-RUN npm install
-
-VOLUME ["/app/node_modules"]
-
-CMD ["npm", "run", "dev"]
diff --git a/front/package.json b/front/package.json
index 58c22a4082c4d37762c32a3e23e7edf453a7fbca..66ff72d9ceabe4500f0a74a44863fbe16cee343d 100644
--- a/front/package.json
+++ b/front/package.json
@@ -9,19 +9,21 @@
     "start": "node build/dev-server.js",
     "build": "node build/build.js",
     "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
+    "unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
     "e2e": "node test/e2e/runner.js",
     "test": "npm run unit && npm run e2e",
     "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
   },
   "dependencies": {
+    "axios": "^0.17.1",
     "dateformat": "^2.0.0",
     "js-logger": "^1.3.0",
     "jwt-decode": "^2.2.0",
     "lodash": "^4.17.4",
+    "moxios": "^0.4.0",
     "semantic-ui-css": "^2.2.10",
     "vue": "^2.3.3",
     "vue-lazyload": "^1.1.4",
-    "vue-resource": "^1.3.4",
     "vue-router": "^2.3.1",
     "vue-upload-component": "^2.7.4",
     "vuedraggable": "^2.14.1",
@@ -46,6 +48,7 @@
     "cross-env": "^4.0.0",
     "cross-spawn": "^5.0.1",
     "css-loader": "^0.28.0",
+    "es6-promise": "^4.2.2",
     "eslint": "^3.19.0",
     "eslint-config-standard": "^6.2.1",
     "eslint-friendly-formatter": "^2.0.7",
@@ -67,6 +70,7 @@
     "karma-phantomjs-launcher": "^1.0.2",
     "karma-phantomjs-shim": "^1.4.0",
     "karma-sinon-chai": "^1.3.1",
+    "karma-sinon-stub-promise": "^1.0.0",
     "karma-sourcemap-loader": "^0.3.7",
     "karma-spec-reporter": "0.0.30",
     "karma-webpack": "^2.0.2",
@@ -85,6 +89,7 @@
     "shelljs": "^0.7.6",
     "sinon": "^2.1.0",
     "sinon-chai": "^2.8.0",
+    "sinon-stub-promise": "^4.0.0",
     "url-loader": "^0.5.8",
     "vue-loader": "^12.1.0",
     "vue-style-loader": "^3.0.1",
diff --git a/front/src/assets/logo/favicon.ico b/front/src/assets/logo/favicon.png
similarity index 100%
rename from front/src/assets/logo/favicon.ico
rename to front/src/assets/logo/favicon.png
diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue
index 2811c2b5c4f58ca71f8c5c55c239ffcf41c0e5d6..bb0881862397324f679296ae5cf6dfd93869cea6 100644
--- a/front/src/components/audio/Search.vue
+++ b/front/src/components/audio/Search.vue
@@ -29,13 +29,11 @@
 </template>
 
 <script>
+import axios from 'axios'
 import logger from '@/logging'
 import backend from '@/audio/backend'
 import AlbumCard from '@/components/audio/album/Card'
 import ArtistCard from '@/components/audio/artist/Card'
-import config from '@/config'
-
-const SEARCH_URL = config.API_URL + 'search'
 
 export default {
   components: {
@@ -73,17 +71,8 @@ export default {
       let params = {
         query: this.query
       }
-      this.$http.get(SEARCH_URL, {
-        params: params,
-        before (request) {
-          // abort previous request, if exists
-          if (this.previousRequest) {
-            this.previousRequest.abort()
-          }
-
-          // set previous request on Vue instance
-          this.previousRequest = request
-        }
+      axios.get('search', {
+        params: params
       }).then((response) => {
         self.results = self.castResults(response.data)
         self.isLoading = false
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
index d93373a1496937380eab3531855696289617996a..f090581ef7305927e293e3f6ffce3e6033131004 100644
--- a/front/src/components/auth/Settings.vue
+++ b/front/src/components/auth/Settings.vue
@@ -36,7 +36,7 @@
 </template>
 
 <script>
-import Vue from 'vue'
+import axios from 'axios'
 import config from '@/config'
 import logger from '@/logging'
 
@@ -61,8 +61,8 @@ export default {
         new_password1: this.new_password,
         new_password2: this.new_password
       }
-      let resource = Vue.resource(config.BACKEND_URL + 'api/auth/registration/change-password/')
-      return resource.save({}, credentials).then(response => {
+      let url = config.BACKEND_URL + 'api/auth/registration/change-password/'
+      return axios.post(url, credentials).then(response => {
         logger.default.info('Password successfully changed')
         self.$router.push('/profile/me')
       }, response => {
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index 8577e84ca5d339e7ed3ad0b10a4e99ef69411c83..c65144a93bf9c6ae1c43f0148f2df75ee4be9105 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -54,15 +54,15 @@
 </template>
 
 <script>
+import axios from 'axios'
 import $ from 'jquery'
 import logger from '@/logging'
-import config from '@/config'
 import TrackTable from '@/components/audio/track/Table'
 import RadioButton from '@/components/radios/Button'
 import Pagination from '@/components/Pagination'
 import OrderingMixin from '@/components/mixins/Ordering'
 import PaginationMixin from '@/components/mixins/Pagination'
-const FAVORITES_URL = config.API_URL + 'tracks/'
+const FAVORITES_URL = 'tracks/'
 
 export default {
   mixins: [OrderingMixin, PaginationMixin],
@@ -115,7 +115,7 @@ export default {
         ordering: this.getOrderingAsString()
       }
       logger.default.time('Loading user favorites')
-      this.$http.get(url, {params: params}).then((response) => {
+      axios.get(url, {params: params}).then((response) => {
         self.results = response.data
         self.nextLink = response.data.next
         self.previousLink = response.data.previous
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index dcfea560095064e20384dd1e77dc37707c7f42e3..65768aafe7851868b35030e2b6273dfbcfefc3b7 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -41,14 +41,13 @@
 </template>
 
 <script>
-
+import axios from 'axios'
 import logger from '@/logging'
 import backend from '@/audio/backend'
 import PlayButton from '@/components/audio/PlayButton'
 import TrackTable from '@/components/audio/track/Table'
-import config from '@/config'
 
-const FETCH_URL = config.API_URL + 'albums/'
+const FETCH_URL = 'albums/'
 
 export default {
   props: ['id'],
@@ -71,7 +70,7 @@ export default {
       this.isLoading = true
       let url = FETCH_URL + this.id + '/'
       logger.default.debug('Fetching album "' + this.id + '"')
-      this.$http.get(url).then((response) => {
+      axios.get(url).then((response) => {
         self.album = backend.Album.clean(response.data)
         self.isLoading = false
       })
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 5eab08ddf0fd9c9f7f3d82b0572f85cba4e1dc52..c2834e1de87b11fd3fdd2f9e4e8a65c5c1045e75 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -41,15 +41,14 @@
 </template>
 
 <script>
-
+import axios from 'axios'
 import logger from '@/logging'
 import backend from '@/audio/backend'
 import AlbumCard from '@/components/audio/album/Card'
 import RadioButton from '@/components/radios/Button'
 import PlayButton from '@/components/audio/PlayButton'
-import config from '@/config'
 
-const FETCH_URL = config.API_URL + 'artists/'
+const FETCH_URL = 'artists/'
 
 export default {
   props: ['id'],
@@ -74,7 +73,7 @@ export default {
       this.isLoading = true
       let url = FETCH_URL + this.id + '/'
       logger.default.debug('Fetching artist "' + this.id + '"')
-      this.$http.get(url).then((response) => {
+      axios.get(url).then((response) => {
         self.artist = response.data
         self.albums = JSON.parse(JSON.stringify(self.artist.albums)).map((album) => {
           return backend.Album.clean(album)
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 23a30e7711fda31fcbb61f7860fa70b93807c7c6..c9bea5efc0c4667f9c625a62bf12f9b820c1dbf5 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -57,10 +57,10 @@
 </template>
 
 <script>
+import axios from 'axios'
 import _ from 'lodash'
 import $ from 'jquery'
 
-import config from '@/config'
 import backend from '@/audio/backend'
 import logger from '@/logging'
 
@@ -69,7 +69,7 @@ import PaginationMixin from '@/components/mixins/Pagination'
 import ArtistCard from '@/components/audio/artist/Card'
 import Pagination from '@/components/Pagination'
 
-const FETCH_URL = config.API_URL + 'artists/'
+const FETCH_URL = 'artists/'
 
 export default {
   mixins: [OrderingMixin, PaginationMixin],
@@ -124,7 +124,7 @@ export default {
         ordering: this.getOrderingAsString()
       }
       logger.default.debug('Fetching artists')
-      this.$http.get(url, {params: params}).then((response) => {
+      axios.get(url, {params: params}).then((response) => {
         self.result = response.data
         self.result.results.map((artist) => {
           var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue
index 624da62c567e75fee65577726b5aaa0d5b2f32bd..cdcbe4b72ee7153d757da33eddb393d1d67a568e 100644
--- a/front/src/components/library/Home.vue
+++ b/front/src/components/library/Home.vue
@@ -24,14 +24,14 @@
 </template>
 
 <script>
+import axios from 'axios'
 import Search from '@/components/audio/Search'
 import backend from '@/audio/backend'
 import logger from '@/logging'
 import ArtistCard from '@/components/audio/artist/Card'
-import config from '@/config'
 import RadioCard from '@/components/radios/Card'
 
-const ARTISTS_URL = config.API_URL + 'artists/'
+const ARTISTS_URL = 'artists/'
 
 export default {
   name: 'library',
@@ -58,7 +58,7 @@ export default {
       }
       let url = ARTISTS_URL
       logger.default.time('Loading latest artists')
-      this.$http.get(url, {params: params}).then((response) => {
+      axios.get(url, {params: params}).then((response) => {
         self.artists = response.data.results
         self.artists.map((artist) => {
           var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
index 409b6b6741137f6fef494cd42a11b33648498658..1952908ff8a5a56a6731c4ca3b6c74803beca8d5 100644
--- a/front/src/components/library/Radios.vue
+++ b/front/src/components/library/Radios.vue
@@ -59,10 +59,10 @@
 </template>
 
 <script>
+import axios from 'axios'
 import _ from 'lodash'
 import $ from 'jquery'
 
-import config from '@/config'
 import logger from '@/logging'
 
 import OrderingMixin from '@/components/mixins/Ordering'
@@ -70,7 +70,7 @@ import PaginationMixin from '@/components/mixins/Pagination'
 import RadioCard from '@/components/radios/Card'
 import Pagination from '@/components/Pagination'
 
-const FETCH_URL = config.API_URL + 'radios/radios/'
+const FETCH_URL = 'radios/radios/'
 
 export default {
   mixins: [OrderingMixin, PaginationMixin],
@@ -125,7 +125,7 @@ export default {
         ordering: this.getOrderingAsString()
       }
       logger.default.debug('Fetching radios')
-      this.$http.get(url, {params: params}).then((response) => {
+      axios.get(url, {params: params}).then((response) => {
         self.result = response.data
         self.isLoading = false
       })
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index 48cd801c3d8a96fc34b4904febbacb0d6243d66f..a40409615dc75bb0e29ebe230f626a1757b7a9f4 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -60,15 +60,14 @@
 </template>
 
 <script>
-
+import axios from 'axios'
 import url from '@/utils/url'
 import logger from '@/logging'
 import backend from '@/audio/backend'
 import PlayButton from '@/components/audio/PlayButton'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
-import config from '@/config'
 
-const FETCH_URL = config.API_URL + 'tracks/'
+const FETCH_URL = 'tracks/'
 
 export default {
   props: ['id'],
@@ -94,7 +93,7 @@ export default {
       this.isLoadingTrack = true
       let url = FETCH_URL + this.id + '/'
       logger.default.debug('Fetching track "' + this.id + '"')
-      this.$http.get(url).then((response) => {
+      axios.get(url).then((response) => {
         self.track = response.data
         self.isLoadingTrack = false
       })
@@ -104,7 +103,7 @@ export default {
       this.isLoadingLyrics = true
       let url = FETCH_URL + this.id + '/lyrics/'
       logger.default.debug('Fetching lyrics for track "' + this.id + '"')
-      this.$http.get(url).then((response) => {
+      axios.get(url).then((response) => {
         self.lyrics = response.data
         self.isLoadingLyrics = false
       }, (response) => {
diff --git a/front/src/components/library/import/ArtistImport.vue b/front/src/components/library/import/ArtistImport.vue
index 870a886e1835c0d811890c9fa4e911b4f74a57a4..fb531439b4eb06c1afb1aab9be605ec5bc6219d6 100644
--- a/front/src/components/library/import/ArtistImport.vue
+++ b/front/src/components/library/import/ArtistImport.vue
@@ -37,8 +37,8 @@
 
 <script>
 import Vue from 'vue'
+import axios from 'axios'
 import logger from '@/logging'
-import config from '@/config'
 
 import ImportMixin from './ImportMixin'
 import ReleaseImport from './ReleaseImport'
@@ -92,9 +92,8 @@ export default Vue.extend({
     fetchReleaseGroupsData () {
       let self = this
       this.releaseGroups.forEach(group => {
-        let url = config.API_URL + 'providers/musicbrainz/releases/browse/' + group.id + '/'
-        let resource = Vue.resource(url)
-        resource.get({}).then((response) => {
+        let url = 'providers/musicbrainz/releases/browse/' + group.id + '/'
+        return axios.get(url).then((response) => {
           logger.default.info('successfully fetched release group', group.id)
           let release = response.data['release-list'].filter(r => {
             return r.status === 'Official'
diff --git a/front/src/components/library/import/BatchDetail.vue b/front/src/components/library/import/BatchDetail.vue
index 762074c107ff9a2f61fbccb92611041f5d9ecc6f..621078b1503e870585a8f32af0f9a6363cdaf31c 100644
--- a/front/src/components/library/import/BatchDetail.vue
+++ b/front/src/components/library/import/BatchDetail.vue
@@ -52,11 +52,10 @@
 </template>
 
 <script>
-
+import axios from 'axios'
 import logger from '@/logging'
-import config from '@/config'
 
-const FETCH_URL = config.API_URL + 'import-batches/'
+const FETCH_URL = 'import-batches/'
 
 export default {
   props: ['id'],
@@ -75,7 +74,7 @@ export default {
       this.isLoading = true
       let url = FETCH_URL + this.id + '/'
       logger.default.debug('Fetching batch "' + this.id + '"')
-      this.$http.get(url).then((response) => {
+      axios.get(url).then((response) => {
         self.batch = response.data
         self.isLoading = false
         if (self.batch.status === 'pending') {
diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue
index f6e7b03e5c06323d5033a1278626efa262d8b123..8133d8e212ebdfe09ead0ecf6d3d56c97c2f455f 100644
--- a/front/src/components/library/import/BatchList.vue
+++ b/front/src/components/library/import/BatchList.vue
@@ -42,10 +42,10 @@
 </template>
 
 <script>
+import axios from 'axios'
 import logger from '@/logging'
-import config from '@/config'
 
-const BATCHES_URL = config.API_URL + 'import-batches/'
+const BATCHES_URL = 'import-batches/'
 
 export default {
   components: {},
@@ -65,7 +65,7 @@ export default {
       var self = this
       this.isLoading = true
       logger.default.time('Loading import batches')
-      this.$http.get(url, {}).then((response) => {
+      axios.get(url, {}).then((response) => {
         self.results = response.data.results
         self.nextLink = response.data.next
         self.previousLink = response.data.previous
diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue
index 93ca75c3961d445f924bf920df7ef5e1da330ab3..35b7b636ad79200d36e68c9b53a0fff8918edd3d 100644
--- a/front/src/components/library/import/FileUpload.vue
+++ b/front/src/components/library/import/FileUpload.vue
@@ -62,10 +62,9 @@
 </template>
 
 <script>
-import Vue from 'vue'
+import axios from 'axios'
 import logger from '@/logging'
 import FileUploadWidget from './FileUploadWidget'
-import config from '@/config'
 
 export default {
   components: {
@@ -74,7 +73,7 @@ export default {
   data () {
     return {
       files: [],
-      uploadUrl: config.API_URL + 'import-jobs/',
+      uploadUrl: 'import-jobs/',
       batch: null
     }
   },
@@ -106,9 +105,7 @@ export default {
     },
     createBatch () {
       let self = this
-      let url = config.API_URL + 'import-batches/'
-      let resource = Vue.resource(url)
-      resource.save({}, {}).then((response) => {
+      return axios.post('import-batches/', {}).then((response) => {
         self.batch = response.data
       }, (response) => {
         logger.default.error('error while launching creating batch')
diff --git a/front/src/components/library/import/ImportMixin.vue b/front/src/components/library/import/ImportMixin.vue
index 475241f3d5b24415a6c5cecf11c59f597142d1b4..33c6193bd7c0a95afa6e59847dc6056a0ca38145 100644
--- a/front/src/components/library/import/ImportMixin.vue
+++ b/front/src/components/library/import/ImportMixin.vue
@@ -3,9 +3,8 @@
 </template>
 
 <script>
+import axios from 'axios'
 import logger from '@/logging'
-import config from '@/config'
-import Vue from 'vue'
 import router from '@/router'
 
 export default {
@@ -31,10 +30,9 @@ export default {
     launchImport () {
       let self = this
       this.isImporting = true
-      let url = config.API_URL + 'submit/' + self.importType + '/'
+      let url = 'submit/' + self.importType + '/'
       let payload = self.importData
-      let resource = Vue.resource(url)
-      resource.save({}, payload).then((response) => {
+      axios.post(url, payload).then((response) => {
         logger.default.info('launched import for', self.type, self.metadata.id)
         self.isImporting = false
         router.push({
diff --git a/front/src/components/library/import/TrackImport.vue b/front/src/components/library/import/TrackImport.vue
index 2275bcf34e6b9f3cf80e22a478d72f79a3e14dae..edd444d92ee071ebbb0946c16badecf21a378346 100644
--- a/front/src/components/library/import/TrackImport.vue
+++ b/front/src/components/library/import/TrackImport.vue
@@ -70,9 +70,9 @@
 </template>
 
 <script>
+import axios from 'axios'
 import Vue from 'vue'
 import time from '@/utils/time'
-import config from '@/config'
 import logger from '@/logging'
 import ImportMixin from './ImportMixin'
 
@@ -117,10 +117,8 @@ export default Vue.extend({
     search () {
       let self = this
       this.isLoading = true
-      let url = config.API_URL + 'providers/' + this.currentBackendId + '/search/'
-      let resource = Vue.resource(url)
-
-      resource.get({query: this.query}).then((response) => {
+      let url = 'providers/' + this.currentBackendId + '/search/'
+      axios.get(url, {params: {query: this.query}}).then((response) => {
         logger.default.debug('searching', self.query, 'on', self.currentBackendId)
         self.results = response.data
         self.isLoading = false
diff --git a/front/src/components/library/radios/Builder.vue b/front/src/components/library/radios/Builder.vue
index f58d5003fd5b7ac21adac9b77142fa96206e0284..8d67b61e18b5f21d67e360c3c4fe05def2e52fcc 100644
--- a/front/src/components/library/radios/Builder.vue
+++ b/front/src/components/library/radios/Builder.vue
@@ -67,7 +67,7 @@
   </div>
 </template>
 <script>
-import config from '@/config'
+import axios from 'axios'
 import $ from 'jquery'
 import _ from 'lodash'
 import BuilderFilter from './Filter'
@@ -107,8 +107,8 @@ export default {
   methods: {
     fetchFilters: function () {
       let self = this
-      let url = config.API_URL + 'radios/radios/filters/'
-      return this.$http.get(url).then((response) => {
+      let url = 'radios/radios/filters/'
+      return axios.get(url).then((response) => {
         self.availableFilters = response.data
       })
     },
@@ -130,8 +130,8 @@ export default {
     },
     fetch: function () {
       let self = this
-      let url = config.API_URL + 'radios/radios/' + this.id + '/'
-      this.$http.get(url).then((response) => {
+      let url = 'radios/radios/' + this.id + '/'
+      axios.get(url).then((response) => {
         self.filters = response.data.config.map(f => {
           return {
             config: f,
@@ -145,7 +145,7 @@ export default {
     },
     fetchCandidates: function () {
       let self = this
-      let url = config.API_URL + 'radios/radios/validate/'
+      let url = 'radios/radios/validate/'
       let final = this.filters.map(f => {
         let c = _.clone(f.config)
         c.type = f.filter.type
@@ -156,7 +156,7 @@ export default {
           {'type': 'group', filters: final}
         ]
       }
-      this.$http.post(url, final).then((response) => {
+      axios.post(url, final).then((response) => {
         self.checkResult = response.data.filters[0]
       })
     },
@@ -173,12 +173,12 @@ export default {
         'config': final
       }
       if (this.id) {
-        let url = config.API_URL + 'radios/radios/' + this.id + '/'
-        this.$http.put(url, final).then((response) => {
+        let url = 'radios/radios/' + this.id + '/'
+        axios.put(url, final).then((response) => {
         })
       } else {
-        let url = config.API_URL + 'radios/radios/'
-        this.$http.post(url, final).then((response) => {
+        let url = 'radios/radios/'
+        axios.post(url, final).then((response) => {
           self.$router.push({
             name: 'library.radios.edit',
             params: {
diff --git a/front/src/components/library/radios/Filter.vue b/front/src/components/library/radios/Filter.vue
index dd170d8b3104da08e4fbd3b93091182fe4a3a2a4..722ecbfbe93ce5afee0227ac476d034e21547705 100644
--- a/front/src/components/library/radios/Filter.vue
+++ b/front/src/components/library/radios/Filter.vue
@@ -62,6 +62,7 @@
   </tr>
 </template>
 <script>
+import axios from 'axios'
 import config from '@/config'
 import $ from 'jquery'
 import _ from 'lodash'
@@ -132,11 +133,11 @@ export default {
   methods: {
     fetchCandidates: function () {
       let self = this
-      let url = config.API_URL + 'radios/radios/validate/'
+      let url = 'radios/radios/validate/'
       let final = _.clone(this.config)
       final.type = this.filter.type
       final = {'filters': [final]}
-      this.$http.post(url, final).then((response) => {
+      axios.post(url, final).then((response) => {
         self.checkResult = response.data.filters[0]
       })
     }
diff --git a/front/src/components/metadata/CardMixin.vue b/front/src/components/metadata/CardMixin.vue
index 78aae5e7e06da465de2037f1e427e646eee88317..a7cd476f6c2d534eb04c6bf2f1038711a56dc897 100644
--- a/front/src/components/metadata/CardMixin.vue
+++ b/front/src/components/metadata/CardMixin.vue
@@ -3,11 +3,9 @@
 </template>
 
 <script>
+import axios from 'axios'
 import logger from '@/logging'
 
-import config from '@/config'
-import Vue from 'vue'
-
 export default {
   props: {
     mbId: {type: String, required: true}
@@ -25,9 +23,8 @@ export default {
     fetchData () {
       let self = this
       this.isLoading = true
-      let url = config.API_URL + 'providers/musicbrainz/' + this.type + 's/' + this.mbId + '/'
-      let resource = Vue.resource(url)
-      resource.get({}).then((response) => {
+      let url = 'providers/musicbrainz/' + this.type + 's/' + this.mbId + '/'
+      axios.get(url).then((response) => {
         logger.default.info('successfully fetched', self.type, self.mbId)
         self.data = response.data[self.type]
         this.$emit('metadata-changed', self.data)
diff --git a/front/src/config.js b/front/src/config.js
index 3ba1247acf21ec5564cff0b9975030c84bcb4908..b0ceb789226223a80c51356e7598ba81fda52819 100644
--- a/front/src/config.js
+++ b/front/src/config.js
@@ -4,7 +4,7 @@ class Config {
     if (this.BACKEND_URL === '/') {
       this.BACKEND_URL = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port
     }
-    if (!this.BACKEND_URL.endsWith('/')) {
+    if (this.BACKEND_URL.slice(-1) !== '/') {
       this.BACKEND_URL += '/'
     }
     this.API_URL = this.BACKEND_URL + 'api/v1/'
diff --git a/front/src/main.js b/front/src/main.js
index f7a6b65f4df7cdd2ddfc4b37914999b12b3e9186..92711be5933e5d7e0e10510cc8cccc0a257dc900 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -8,9 +8,10 @@ logger.default.debug('Environment variables:', process.env)
 import Vue from 'vue'
 import App from './App'
 import router from './router'
-import VueResource from 'vue-resource'
+import axios from 'axios'
 import VueLazyload from 'vue-lazyload'
 import store from './store'
+import config from './config'
 
 window.$ = window.jQuery = require('jquery')
 
@@ -19,25 +20,33 @@ window.$ = window.jQuery = require('jquery')
 // require('./semantic/semantic.css')
 require('semantic-ui-css/semantic.js')
 
-Vue.use(VueResource)
 Vue.use(VueLazyload)
 Vue.config.productionTip = false
 
-Vue.http.interceptors.push(function (request, next) {
-  // modify headers
+axios.defaults.baseURL = config.API_URL
+axios.interceptors.request.use(function (config) {
+  // Do something before request is sent
   if (store.state.auth.authenticated) {
-    request.headers.set('Authorization', store.getters['auth/header'])
+    config.headers['Authorization'] = store.getters['auth/header']
   }
-  next(function (response) {
-    // redirect to login form when we get unauthorized response from server
-    if (response.status === 401) {
-      store.commit('auth/authenticated', false)
-      logger.default.warn('Received 401 response from API, redirecting to login form')
-      router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
-    }
-  })
+  return config
+}, function (error) {
+  // Do something with request error
+  return Promise.reject(error)
 })
 
+// Add a response interceptor
+axios.interceptors.response.use(function (response) {
+  if (response.status === 401) {
+    store.commit('auth/authenticated', false)
+    logger.default.warn('Received 401 response from API, redirecting to login form')
+    router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
+  }
+  return response
+}, function (error) {
+  // Do something with response error
+  return Promise.reject(error)
+})
 store.dispatch('auth/check')
 /* eslint-disable no-new */
 new Vue({
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index d8bd197f33fabd1938fe65a5c71ca950314b663f..24dafcd7266d7fe23f2e0c60807ea29fb440db98 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -1,13 +1,8 @@
-import Vue from 'vue'
+import axios from 'axios'
 import jwtDecode from 'jwt-decode'
-import config from '@/config'
 import logger from '@/logging'
 import router from '@/router'
 
-const LOGIN_URL = config.API_URL + 'token/'
-const REFRESH_TOKEN_URL = config.API_URL + 'token/refresh/'
-const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
-
 export default {
   namespaced: true,
   state: {
@@ -54,9 +49,8 @@ export default {
   },
   actions: {
     // Send a request to the login URL and save the returned JWT
-    login ({commit, dispatch, state}, {next, credentials, onError}) {
-      let resource = Vue.resource(LOGIN_URL)
-      return resource.save({}, credentials).then(response => {
+    login ({commit, dispatch}, {next, credentials, onError}) {
+      return axios.post('token/', credentials).then(response => {
         logger.default.info('Successfully logged in as', credentials.username)
         commit('token', response.data.token)
         commit('username', credentials.username)
@@ -91,8 +85,7 @@ export default {
       }
     },
     fetchProfile ({commit, dispatch, state}) {
-      let resource = Vue.resource(USER_PROFILE_URL)
-      return resource.get({}).then((response) => {
+      return axios.get('users/users/me/').then((response) => {
         logger.default.info('Successfully fetched user profile')
         let data = response.data
         commit('profile', data)
@@ -107,8 +100,7 @@ export default {
       })
     },
     refreshToken ({commit, dispatch, state}) {
-      let resource = Vue.resource(REFRESH_TOKEN_URL)
-      return resource.save({}, {token: state.token}).then(response => {
+      return axios.post('token/refresh/', {token: state.token}).then(response => {
         logger.default.info('Refreshed auth token')
         commit('token', response.data.token)
       }, response => {
diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js
index 9337966fdf68bc84202a8d2fe3e7add193d1fa20..a4f85b235daa36567f16528d8396d259e63b1db0 100644
--- a/front/src/store/favorites.js
+++ b/front/src/store/favorites.js
@@ -1,10 +1,6 @@
-import Vue from 'vue'
-import config from '@/config'
+import axios from 'axios'
 import logger from '@/logging'
 
-const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
-const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
-
 export default {
   namespaced: true,
   state: {
@@ -35,16 +31,14 @@ export default {
     set ({commit, state}, {id, value}) {
       commit('track', {id, value})
       if (value) {
-        let resource = Vue.resource(FAVORITES_URL)
-        resource.save({}, {'track': id}).then((response) => {
+        return axios.post('favorites/tracks/', {'track': id}).then((response) => {
           logger.default.info('Successfully added track to favorites')
         }, (response) => {
           logger.default.info('Error while adding track to favorites')
           commit('track', {id, value: !value})
         })
       } else {
-        let resource = Vue.resource(REMOVE_URL)
-        resource.delete({}, {'track': id}).then((response) => {
+        return axios.post('favorites/tracks/remove/', {'track': id}).then((response) => {
           logger.default.info('Successfully removed track from favorites')
         }, (response) => {
           logger.default.info('Error while removing track from favorites')
@@ -57,9 +51,8 @@ export default {
     },
     fetch ({dispatch, state, commit}, url) {
       // will fetch favorites by batches from API to have them locally
-      url = url || FAVORITES_URL
-      let resource = Vue.resource(url)
-      resource.get().then((response) => {
+      url = url || 'favorites/tracks/'
+      return axios.get(url).then((response) => {
         logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
         response.data.results.forEach(result => {
           commit('track', {id: result.track, value: true})
diff --git a/front/src/store/player.js b/front/src/store/player.js
index 74b0b9f9ea72dcbc38d0c3cefc6fa90d1647fc49..fb348042fa107bdef406406bf67aac596da3d4b9 100644
--- a/front/src/store/player.js
+++ b/front/src/store/player.js
@@ -1,5 +1,4 @@
-import Vue from 'vue'
-import config from '@/config'
+import axios from 'axios'
 import logger from '@/logging'
 import time from '@/utils/time'
 
@@ -61,8 +60,8 @@ export default {
     }
   },
   actions: {
-    incrementVolume (context, value) {
-      context.commit('volume', context.state.volume + value)
+    incrementVolume ({commit, state}, value) {
+      commit('volume', state.volume + value)
     },
     stop (context) {
     },
@@ -70,9 +69,7 @@ export default {
       commit('playing', !state.playing)
     },
     trackListened ({commit}, track) {
-      let url = config.API_URL + 'history/listenings/'
-      let resource = Vue.resource(url)
-      resource.save({}, {'track': track.id}).then((response) => {}, (response) => {
+      return axios.post('history/listenings/', {'track': track.id}).then((response) => {}, (response) => {
         logger.default.error('Could not record track in history')
       })
     },
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
index 5dde19bd8e6f1665a826b522820e37ae3c14efaf..ac28a1e0e9cbef5b384f08517e537f56032b0c4b 100644
--- a/front/src/store/queue.js
+++ b/front/src/store/queue.js
@@ -55,33 +55,32 @@ export default {
     }
   },
   actions: {
-    append (context, {track, index, skipPlay}) {
-      index = index || context.state.tracks.length
-      if (index > context.state.tracks.length - 1) {
+    append ({commit, state, dispatch}, {track, index, skipPlay}) {
+      index = index || state.tracks.length
+      if (index > state.tracks.length - 1) {
         // we simply push to the end
-        context.commit('insert', {track, index: context.state.tracks.length})
+        commit('insert', {track, index: state.tracks.length})
       } else {
         // we insert the track at given position
-        context.commit('insert', {track, index})
+        commit('insert', {track, index})
       }
       if (!skipPlay) {
-        context.dispatch('resume')
+        dispatch('resume')
       }
-      // this.cache()
     },
 
-    appendMany (context, {tracks, index}) {
+    appendMany ({state, dispatch}, {tracks, index}) {
       logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
-      if (context.state.tracks.length === 0) {
+      if (state.tracks.length === 0) {
         index = 0
       } else {
-        index = index || context.state.tracks.length
+        index = index || state.tracks.length
       }
       tracks.forEach((t) => {
-        context.dispatch('append', {track: t, index: index, skipPlay: true})
+        dispatch('append', {track: t, index: index, skipPlay: true})
         index += 1
       })
-      context.dispatch('resume')
+      dispatch('resume')
     },
 
     cleanTrack ({state, dispatch, commit}, index) {
@@ -100,14 +99,14 @@ export default {
       }
     },
 
-    resume (context) {
-      if (context.state.ended | context.rootState.player.errored) {
-        context.dispatch('next')
+    resume ({state, dispatch, rootState}) {
+      if (state.ended | rootState.player.errored) {
+        dispatch('next')
       }
     },
-    previous (context) {
-      if (context.state.currentIndex > 0) {
-        context.dispatch('currentIndex', context.state.currentIndex - 1)
+    previous ({state, dispatch}) {
+      if (state.currentIndex > 0) {
+        dispatch('currentIndex', state.currentIndex - 1)
       }
     },
     next ({state, dispatch, commit, rootState}) {
diff --git a/front/src/store/radios.js b/front/src/store/radios.js
index 600b24b31e7fb77eafdda8a0992bdf58879f3a54..922083d8841083afbd2ddb29848c62a0e240d6f7 100644
--- a/front/src/store/radios.js
+++ b/front/src/store/radios.js
@@ -1,10 +1,6 @@
-import Vue from 'vue'
-import config from '@/config'
+import axios from 'axios'
 import logger from '@/logging'
 
-const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
-const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
-
 export default {
   namespaced: true,
   state: {
@@ -39,13 +35,12 @@ export default {
   },
   actions: {
     start ({commit, dispatch}, {type, objectId, customRadioId}) {
-      let resource = Vue.resource(CREATE_RADIO_URL)
       var params = {
         radio_type: type,
         related_object_id: objectId,
         custom_radio: customRadioId
       }
-      resource.save({}, params).then((response) => {
+      return axios.post('radios/sessions/', params).then((response) => {
         logger.default.info('Successfully started radio ', type)
         commit('current', {type, objectId, session: response.data.id, customRadioId})
         commit('running', true)
@@ -62,12 +57,10 @@ export default {
       if (!state.running) {
         return
       }
-      let resource = Vue.resource(GET_TRACK_URL)
       var params = {
         session: state.current.session
       }
-      let promise = resource.save({}, params)
-      promise.then((response) => {
+      return axios.post('radios/tracks/', params).then((response) => {
         logger.default.info('Adding track to queue from radio')
         dispatch('queue/append', {track: response.data.track}, {root: true})
       }, (response) => {
diff --git a/front/test/unit/karma.conf.js b/front/test/unit/karma.conf.js
index 8e4951c9e4ecc597be347be1fd8e163cdbab13e2..193aaff7662983b2b428af919c5b18b6e9179303 100644
--- a/front/test/unit/karma.conf.js
+++ b/front/test/unit/karma.conf.js
@@ -12,12 +12,17 @@ module.exports = function (config) {
     //    http://karma-runner.github.io/0.13/config/browsers.html
     // 2. add it to the `browsers` array below.
     browsers: ['PhantomJS'],
-    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
+    frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai', 'phantomjs-shim'],
     reporters: ['spec', 'coverage'],
-    files: ['./index.js'],
+    files: [
+      '../../node_modules/es6-promise/dist/es6-promise.auto.js',
+      './index.js'
+    ],
     preprocessors: {
       './index.js': ['webpack', 'sourcemap']
     },
+    captureTimeout: 15000,
+    retryLimit: 1,
     webpack: webpackConfig,
     webpackMiddleware: {
       noInfo: true
diff --git a/front/test/unit/specs/Hello.spec.js b/front/test/unit/specs/Hello.spec.js
deleted file mode 100644
index 80140baa9664664d35751ce9c16e0d9ccbe75427..0000000000000000000000000000000000000000
--- a/front/test/unit/specs/Hello.spec.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Vue from 'vue'
-import Hello from '@/components/Hello'
-
-describe('Hello.vue', () => {
-  it('should render correct contents', () => {
-    const Constructor = Vue.extend(Hello)
-    const vm = new Constructor().$mount()
-    expect(vm.$el.querySelector('.hello h1').textContent)
-      .to.equal('Welcome to Your Vue.js App')
-  })
-})
diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa07f9f8bfc03c5f2c766f4a333ff233058ace39
--- /dev/null
+++ b/front/test/unit/specs/store/auth.spec.js
@@ -0,0 +1,200 @@
+var sinon = require('sinon')
+import moxios from 'moxios'
+import store from '@/store/auth'
+
+import { testAction } from '../../utils'
+
+describe('store/auth', () => {
+  var sandbox
+
+  beforeEach(function () {
+    sandbox = sinon.sandbox.create()
+    moxios.install()
+  })
+  afterEach(function () {
+    sandbox.restore()
+    moxios.uninstall()
+  })
+
+  describe('mutations', () => {
+    it('profile', () => {
+      const state = {}
+      store.mutations.profile(state, {})
+      expect(state.profile).to.deep.equal({})
+    })
+    it('username', () => {
+      const state = {}
+      store.mutations.username(state, 'world')
+      expect(state.username).to.equal('world')
+    })
+    it('authenticated true', () => {
+      const state = {}
+      store.mutations.authenticated(state, true)
+      expect(state.authenticated).to.equal(true)
+    })
+    it('authenticated false', () => {
+      const state = {
+        username: 'dummy',
+        token: 'dummy',
+        tokenData: 'dummy',
+        profile: 'dummy',
+        availablePermissions: 'dummy'
+      }
+      store.mutations.authenticated(state, false)
+      expect(state.authenticated).to.equal(false)
+      expect(state.username).to.equal(null)
+      expect(state.token).to.equal(null)
+      expect(state.tokenData).to.equal(null)
+      expect(state.profile).to.equal(null)
+      expect(state.availablePermissions).to.deep.equal({})
+    })
+    it('token null', () => {
+      const state = {}
+      store.mutations.token(state, null)
+      expect(state.token).to.equal(null)
+      expect(state.tokenData).to.deep.equal({})
+    })
+    it('token real', () => {
+      // generated on http://kjur.github.io/jsjws/tool_jwt.html
+      const state = {}
+      let token = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTUxNTUzMzQyOSwiZXhwIjoxNTE1NTM3MDI5LCJpYXQiOjE1MTU1MzM0MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.'
+      let tokenData = {
+        iss: 'https://jwt-idp.example.com',
+        sub: 'mailto:mike@example.com',
+        nbf: 1515533429,
+        exp: 1515537029,
+        iat: 1515533429,
+        jti: 'id123456',
+        typ: 'https://example.com/register'
+      }
+      store.mutations.token(state, token)
+      expect(state.token).to.equal(token)
+      expect(state.tokenData).to.deep.equal(tokenData)
+    })
+    it('permissions', () => {
+      const state = { availablePermissions: {} }
+      store.mutations.permission(state, {key: 'admin', status: true})
+      expect(state.availablePermissions).to.deep.equal({admin: true})
+    })
+  })
+  describe('getters', () => {
+    it('header', () => {
+      const state = { token: 'helloworld' }
+      expect(store.getters['header'](state)).to.equal('JWT helloworld')
+    })
+  })
+  describe('actions', () => {
+    it('logout', (done) => {
+      testAction({
+        action: store.actions.logout,
+        params: {state: {}},
+        expectedMutations: [
+          { type: 'authenticated', payload: false }
+        ]
+      }, done)
+    })
+    it('check jwt null', (done) => {
+      testAction({
+        action: store.actions.check,
+        params: {state: {}},
+        expectedMutations: [
+          { type: 'authenticated', payload: false }
+        ]
+      }, done)
+    })
+    it('check jwt set', (done) => {
+      testAction({
+        action: store.actions.check,
+        params: {state: {token: 'test', username: 'user'}},
+        expectedMutations: [
+          { type: 'authenticated', payload: true },
+          { type: 'username', payload: 'user' },
+          { type: 'token', payload: 'test' }
+        ],
+        expectedActions: [
+          { type: 'fetchProfile' },
+          { type: 'refreshToken' }
+        ]
+      }, done)
+    })
+    it('login success', (done) => {
+      moxios.stubRequest('token/', {
+        status: 200,
+        response: {
+          token: 'test'
+        }
+      })
+      const credentials = {
+        username: 'bob'
+      }
+      testAction({
+        action: store.actions.login,
+        payload: {credentials: credentials},
+        expectedMutations: [
+          { type: 'token', payload: 'test' },
+          { type: 'username', payload: 'bob' },
+          { type: 'authenticated', payload: true }
+        ],
+        expectedActions: [
+          { type: 'fetchProfile' }
+        ]
+      }, done)
+    })
+    it('login error', (done) => {
+      moxios.stubRequest('token/', {
+        status: 500,
+        response: {
+          token: 'test'
+        }
+      })
+      const credentials = {
+        username: 'bob'
+      }
+      let spy = sandbox.spy()
+      testAction({
+        action: store.actions.login,
+        payload: {credentials: credentials, onError: spy}
+      }, () => {
+        expect(spy.calledOnce).to.equal(true)
+        done()
+      })
+    })
+    it('fetchProfile', (done) => {
+      const profile = {
+        username: 'bob',
+        permissions: {
+          admin: {
+            status: true
+          }
+        }
+      }
+      moxios.stubRequest('users/users/me/', {
+        status: 200,
+        response: profile
+      })
+      testAction({
+        action: store.actions.fetchProfile,
+        expectedMutations: [
+          { type: 'profile', payload: profile },
+          { type: 'permission', payload: {key: 'admin', status: true} }
+        ],
+        expectedActions: [
+          { type: 'favorites/fetch', payload: null, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('refreshToken', (done) => {
+      moxios.stubRequest('token/refresh/', {
+        status: 200,
+        response: {token: 'newtoken'}
+      })
+      testAction({
+        action: store.actions.refreshToken,
+        params: {state: {token: 'oldtoken'}},
+        expectedMutations: [
+          { type: 'token', payload: 'newtoken' }
+        ]
+      }, done)
+    })
+  })
+})
diff --git a/front/test/unit/specs/store/favorites.spec.js b/front/test/unit/specs/store/favorites.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d4314ca661a2ffbb3865d3a4edc53a2b40b1cc8
--- /dev/null
+++ b/front/test/unit/specs/store/favorites.spec.js
@@ -0,0 +1,52 @@
+import store from '@/store/favorites'
+
+import { testAction } from '../../utils'
+
+describe('store/favorites', () => {
+  describe('mutations', () => {
+    it('track true', () => {
+      const state = { tracks: [] }
+      store.mutations.track(state, {id: 1, value: true})
+      expect(state.tracks).to.deep.equal([1])
+      expect(state.count).to.deep.equal(1)
+    })
+    it('track false', () => {
+      const state = { tracks: [1] }
+      store.mutations.track(state, {id: 1, value: false})
+      expect(state.tracks).to.deep.equal([])
+      expect(state.count).to.deep.equal(0)
+    })
+  })
+  describe('getters', () => {
+    it('isFavorite true', () => {
+      const state = { tracks: [1] }
+      expect(store.getters['isFavorite'](state)(1)).to.equal(true)
+    })
+    it('isFavorite false', () => {
+      const state = { tracks: [] }
+      expect(store.getters['isFavorite'](state)(1)).to.equal(false)
+    })
+  })
+  describe('actions', () => {
+    it('toggle true', (done) => {
+      testAction({
+        action: store.actions.toggle,
+        payload: 1,
+        params: {getters: {isFavorite: () => false}},
+        expectedActions: [
+          { type: 'set', payload: {id: 1, value: true} }
+        ]
+      }, done)
+    })
+    it('toggle true', (done) => {
+      testAction({
+        action: store.actions.toggle,
+        payload: 1,
+        params: {getters: {isFavorite: () => true}},
+        expectedActions: [
+          { type: 'set', payload: {id: 1, value: false} }
+        ]
+      }, done)
+    })
+  })
+})
diff --git a/front/test/unit/specs/store/player.spec.js b/front/test/unit/specs/store/player.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..af0b6b4354394dbc9c1c62186796e62653ca52fb
--- /dev/null
+++ b/front/test/unit/specs/store/player.spec.js
@@ -0,0 +1,153 @@
+import store from '@/store/player'
+
+import { testAction } from '../../utils'
+
+describe('store/player', () => {
+  describe('mutations', () => {
+    it('set volume', () => {
+      const state = { volume: 0 }
+      store.mutations.volume(state, 0.9)
+      expect(state.volume).to.equal(0.9)
+    })
+    it('set volume max 1', () => {
+      const state = { volume: 0 }
+      store.mutations.volume(state, 2)
+      expect(state.volume).to.equal(1)
+    })
+    it('set volume min to 0', () => {
+      const state = { volume: 0.5 }
+      store.mutations.volume(state, -2)
+      expect(state.volume).to.equal(0)
+    })
+    it('increment volume', () => {
+      const state = { volume: 0 }
+      store.mutations.incrementVolume(state, 0.1)
+      expect(state.volume).to.equal(0.1)
+    })
+    it('increment volume max 1', () => {
+      const state = { volume: 0 }
+      store.mutations.incrementVolume(state, 2)
+      expect(state.volume).to.equal(1)
+    })
+    it('increment volume min to 0', () => {
+      const state = { volume: 0.5 }
+      store.mutations.incrementVolume(state, -2)
+      expect(state.volume).to.equal(0)
+    })
+    it('set duration', () => {
+      const state = { duration: 42 }
+      store.mutations.duration(state, 14)
+      expect(state.duration).to.equal(14)
+    })
+    it('set errored', () => {
+      const state = { errored: false }
+      store.mutations.errored(state, true)
+      expect(state.errored).to.equal(true)
+    })
+    it('set looping', () => {
+      const state = { looping: 1 }
+      store.mutations.looping(state, 2)
+      expect(state.looping).to.equal(2)
+    })
+    it('set playing', () => {
+      const state = { playing: false }
+      store.mutations.playing(state, true)
+      expect(state.playing).to.equal(true)
+    })
+    it('set current time', () => {
+      const state = { currentTime: 1 }
+      store.mutations.currentTime(state, 2)
+      expect(state.currentTime).to.equal(2)
+    })
+    it('toggle looping from 0', () => {
+      const state = { looping: 0 }
+      store.mutations.toggleLooping(state)
+      expect(state.looping).to.equal(1)
+    })
+    it('toggle looping from 1', () => {
+      const state = { looping: 1 }
+      store.mutations.toggleLooping(state)
+      expect(state.looping).to.equal(2)
+    })
+    it('toggle looping from 2', () => {
+      const state = { looping: 2 }
+      store.mutations.toggleLooping(state)
+      expect(state.looping).to.equal(0)
+    })
+  })
+  describe('getters', () => {
+    it('durationFormatted', () => {
+      const state = { duration: 12.51 }
+      expect(store.getters['durationFormatted'](state)).to.equal('00:13')
+    })
+    it('currentTimeFormatted', () => {
+      const state = { currentTime: 12.51 }
+      expect(store.getters['currentTimeFormatted'](state)).to.equal('00:13')
+    })
+    it('progress', () => {
+      const state = { currentTime: 4, duration: 10 }
+      expect(store.getters['progress'](state)).to.equal(40)
+    })
+  })
+  describe('actions', () => {
+    it('incrementVolume', (done) => {
+      testAction({
+        action: store.actions.incrementVolume,
+        payload: 0.2,
+        params: {state: {volume: 0.7}},
+        expectedMutations: [
+          { type: 'volume', payload: 0.7 + 0.2 }
+        ]
+      }, done)
+    })
+    it('toggle play false', (done) => {
+      testAction({
+        action: store.actions.togglePlay,
+        params: {state: {playing: false}},
+        expectedMutations: [
+          { type: 'playing', payload: true }
+        ]
+      }, done)
+    })
+    it('toggle play true', (done) => {
+      testAction({
+        action: store.actions.togglePlay,
+        params: {state: {playing: true}},
+        expectedMutations: [
+          { type: 'playing', payload: false }
+        ]
+      }, done)
+    })
+    it('trackEnded', (done) => {
+      testAction({
+        action: store.actions.trackEnded,
+        payload: {test: 'track'},
+        expectedActions: [
+          { type: 'trackListened', payload: {test: 'track'} },
+          { type: 'queue/next', payload: null, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('trackErrored', (done) => {
+      testAction({
+        action: store.actions.trackErrored,
+        payload: {test: 'track'},
+        expectedMutations: [
+          { type: 'errored', payload: true }
+        ],
+        expectedActions: [
+          { type: 'queue/next', payload: null, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('updateProgress', (done) => {
+      testAction({
+        action: store.actions.updateProgress,
+        payload: 1,
+        expectedMutations: [
+          { type: 'currentTime', payload: 1 }
+        ]
+      }, done)
+    })
+  })
+})
diff --git a/front/test/unit/specs/store/queue.spec.js b/front/test/unit/specs/store/queue.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a79c07bd982cd8df7b58231a184898b1cea3aca
--- /dev/null
+++ b/front/test/unit/specs/store/queue.spec.js
@@ -0,0 +1,333 @@
+var sinon = require('sinon')
+import _ from 'lodash'
+
+import store from '@/store/queue'
+import { testAction } from '../../utils'
+
+describe('store/queue', () => {
+  var sandbox
+
+  beforeEach(function () {
+    // Create a sandbox for the test
+    sandbox = sinon.sandbox.create()
+  })
+
+  afterEach(function () {
+    // Restore all the things made through the sandbox
+    sandbox.restore()
+  })
+  describe('mutations', () => {
+    it('currentIndex', () => {
+      const state = {}
+      store.mutations.currentIndex(state, 2)
+      expect(state.currentIndex).to.equal(2)
+    })
+    it('ended', () => {
+      const state = {}
+      store.mutations.ended(state, false)
+      expect(state.ended).to.equal(false)
+    })
+    it('tracks', () => {
+      const state = {}
+      store.mutations.tracks(state, [1, 2])
+      expect(state.tracks).to.deep.equal([1, 2])
+    })
+    it('splice', () => {
+      const state = {tracks: [1, 2, 3]}
+      store.mutations.splice(state, {start: 1, size: 2})
+      expect(state.tracks).to.deep.equal([1])
+    })
+    it('insert', () => {
+      const state = {tracks: [1, 3]}
+      store.mutations.insert(state, {track: 2, index: 1})
+      expect(state.tracks).to.deep.equal([1, 2, 3])
+    })
+    it('reorder before', () => {
+      const state = {currentIndex: 3}
+      store.mutations.reorder(state, {oldIndex: 2, newIndex: 1})
+      expect(state.currentIndex).to.equal(3)
+    })
+    it('reorder from after to before', () => {
+      const state = {currentIndex: 3}
+      store.mutations.reorder(state, {oldIndex: 4, newIndex: 1})
+      expect(state.currentIndex).to.equal(4)
+    })
+    it('reorder after', () => {
+      const state = {currentIndex: 3}
+      store.mutations.reorder(state, {oldIndex: 4, newIndex: 5})
+      expect(state.currentIndex).to.equal(3)
+    })
+    it('reorder before to after', () => {
+      const state = {currentIndex: 3}
+      store.mutations.reorder(state, {oldIndex: 1, newIndex: 5})
+      expect(state.currentIndex).to.equal(2)
+    })
+    it('reorder current', () => {
+      const state = {currentIndex: 3}
+      store.mutations.reorder(state, {oldIndex: 3, newIndex: 1})
+      expect(state.currentIndex).to.equal(1)
+    })
+  })
+  describe('getters', () => {
+    it('currentTrack', () => {
+      const state = { tracks: [1, 2, 3], currentIndex: 2 }
+      expect(store.getters['currentTrack'](state)).to.equal(3)
+    })
+    it('hasNext true', () => {
+      const state = { tracks: [1, 2, 3], currentIndex: 1 }
+      expect(store.getters['hasNext'](state)).to.equal(true)
+    })
+    it('hasNext false', () => {
+      const state = { tracks: [1, 2, 3], currentIndex: 2 }
+      expect(store.getters['hasNext'](state)).to.equal(false)
+    })
+    it('hasPrevious true', () => {
+      const state = { currentIndex: 1 }
+      expect(store.getters['hasPrevious'](state)).to.equal(true)
+    })
+    it('hasPrevious false', () => {
+      const state = { currentIndex: 0 }
+      expect(store.getters['hasPrevious'](state)).to.equal(false)
+    })
+  })
+  describe('actions', () => {
+    it('append at end', (done) => {
+      testAction({
+        action: store.actions.append,
+        payload: {track: 4, skipPlay: true},
+        params: {state: {tracks: [1, 2, 3]}},
+        expectedMutations: [
+          { type: 'insert', payload: {track: 4, index: 3} }
+        ]
+      }, done)
+    })
+    it('append at index', (done) => {
+      testAction({
+        action: store.actions.append,
+        payload: {track: 2, index: 1, skipPlay: true},
+        params: {state: {tracks: [1, 3]}},
+        expectedMutations: [
+          { type: 'insert', payload: {track: 2, index: 1} }
+        ]
+      }, done)
+    })
+    it('append and play', (done) => {
+      testAction({
+        action: store.actions.append,
+        payload: {track: 3},
+        params: {state: {tracks: [1, 2]}},
+        expectedMutations: [
+          { type: 'insert', payload: {track: 3, index: 2} }
+        ],
+        expectedActions: [
+          { type: 'resume' }
+        ]
+      }, done)
+    })
+    it('appendMany', (done) => {
+      const tracks = [{title: 1}, {title: 2}]
+      testAction({
+        action: store.actions.appendMany,
+        payload: {tracks: tracks},
+        params: {state: {tracks: []}},
+        expectedActions: [
+          { type: 'append', payload: {track: tracks[0], index: 0, skipPlay: true} },
+          { type: 'append', payload: {track: tracks[1], index: 1, skipPlay: true} },
+          { type: 'resume' }
+        ]
+      }, done)
+    })
+    it('appendMany at index', (done) => {
+      const tracks = [{title: 1}, {title: 2}]
+      testAction({
+        action: store.actions.appendMany,
+        payload: {tracks: tracks, index: 1},
+        params: {state: {tracks: [1, 2]}},
+        expectedActions: [
+          { type: 'append', payload: {track: tracks[0], index: 1, skipPlay: true} },
+          { type: 'append', payload: {track: tracks[1], index: 2, skipPlay: true} },
+          { type: 'resume' }
+        ]
+      }, done)
+    })
+    it('cleanTrack after current', (done) => {
+      testAction({
+        action: store.actions.cleanTrack,
+        payload: 3,
+        params: {state: {currentIndex: 2}},
+        expectedMutations: [
+          { type: 'splice', payload: {start: 3, size: 1} }
+        ]
+      }, done)
+    })
+    it('cleanTrack before current', (done) => {
+      testAction({
+        action: store.actions.cleanTrack,
+        payload: 1,
+        params: {state: {currentIndex: 2}},
+        expectedMutations: [
+          { type: 'splice', payload: {start: 1, size: 1} }
+        ],
+        expectedActions: [
+          { type: 'currentIndex', payload: 1 }
+        ]
+      }, done)
+    })
+    it('cleanTrack current', (done) => {
+      testAction({
+        action: store.actions.cleanTrack,
+        payload: 2,
+        params: {state: {currentIndex: 2}},
+        expectedMutations: [
+          { type: 'splice', payload: {start: 2, size: 1} }
+        ],
+        expectedActions: [
+          { type: 'player/stop', payload: null, options: {root: true} },
+          { type: 'currentIndex', payload: 2 }
+        ]
+      }, done)
+    })
+    it('resume when ended', (done) => {
+      testAction({
+        action: store.actions.resume,
+        params: {state: {ended: true}, rootState: {player: {errored: false}}},
+        expectedActions: [
+          { type: 'next' }
+        ]
+      }, done)
+    })
+    it('resume when errored', (done) => {
+      testAction({
+        action: store.actions.resume,
+        params: {state: {ended: false}, rootState: {player: {errored: true}}},
+        expectedActions: [
+          { type: 'next' }
+        ]
+      }, done)
+    })
+    it('skip resume when not ended or not error', (done) => {
+      testAction({
+        action: store.actions.resume,
+        params: {state: {ended: false}, rootState: {player: {errored: false}}},
+        expectedActions: []
+      }, done)
+    })
+    it('previous when at beginning does nothing', (done) => {
+      testAction({
+        action: store.actions.previous,
+        params: {state: {currentIndex: 0}},
+        expectedActions: []
+      }, done)
+    })
+    it('previous', (done) => {
+      testAction({
+        action: store.actions.previous,
+        params: {state: {currentIndex: 1}},
+        expectedActions: [
+          { type: 'currentIndex', payload: 0 }
+        ]
+      }, done)
+    })
+    it('next on last track when looping on queue', (done) => {
+      testAction({
+        action: store.actions.next,
+        params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 2}}},
+        expectedActions: [
+          { type: 'currentIndex', payload: 0 }
+        ]
+      }, done)
+    })
+    it('next track when last track', (done) => {
+      testAction({
+        action: store.actions.next,
+        params: {state: {tracks: [1, 2], currentIndex: 1}, rootState: {player: {looping: 0}}},
+        expectedMutations: [
+          { type: 'ended', payload: true }
+        ]
+      }, done)
+    })
+    it('next track when not last track', (done) => {
+      testAction({
+        action: store.actions.next,
+        params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {player: {looping: 0}}},
+        expectedActions: [
+          { type: 'currentIndex', payload: 1 }
+        ]
+      }, done)
+    })
+    it('currentIndex', (done) => {
+      testAction({
+        action: store.actions.currentIndex,
+        payload: 1,
+        params: {state: {tracks: [1, 2], currentIndex: 0}, rootState: {radios: {running: false}}},
+        expectedMutations: [
+          { type: 'ended', payload: false },
+          { type: 'player/currentTime', payload: 0, options: {root: true} },
+          { type: 'player/playing', payload: true, options: {root: true} },
+          { type: 'player/errored', payload: false, options: {root: true} },
+          { type: 'currentIndex', payload: 1 }
+        ]
+      }, done)
+    })
+    it('currentIndex with radio and many tracks remaining', (done) => {
+      testAction({
+        action: store.actions.currentIndex,
+        payload: 1,
+        params: {state: {tracks: [1, 2, 3, 4], currentIndex: 0}, rootState: {radios: {running: true}}},
+        expectedMutations: [
+          { type: 'ended', payload: false },
+          { type: 'player/currentTime', payload: 0, options: {root: true} },
+          { type: 'player/playing', payload: true, options: {root: true} },
+          { type: 'player/errored', payload: false, options: {root: true} },
+          { type: 'currentIndex', payload: 1 }
+        ]
+      }, done)
+    })
+    it('currentIndex with radio and less than two tracks remaining', (done) => {
+      testAction({
+        action: store.actions.currentIndex,
+        payload: 1,
+        params: {state: {tracks: [1, 2, 3], currentIndex: 0}, rootState: {radios: {running: true}}},
+        expectedMutations: [
+          { type: 'ended', payload: false },
+          { type: 'player/currentTime', payload: 0, options: {root: true} },
+          { type: 'player/playing', payload: true, options: {root: true} },
+          { type: 'player/errored', payload: false, options: {root: true} },
+          { type: 'currentIndex', payload: 1 }
+        ],
+        expectedActions: [
+          { type: 'radios/populateQueue', payload: null, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('clean', (done) => {
+      testAction({
+        action: store.actions.clean,
+        expectedMutations: [
+          { type: 'tracks', payload: [] },
+          { type: 'ended', payload: true }
+        ],
+        expectedActions: [
+          { type: 'player/stop', payload: null, options: {root: true} },
+          { type: 'currentIndex', payload: -1 }
+        ]
+      }, done)
+    })
+    it('shuffle', (done) => {
+      let _shuffle = sandbox.stub(_, 'shuffle')
+      let tracks = [1, 2, 3]
+      let shuffledTracks = [2, 3, 1]
+      _shuffle.returns(shuffledTracks)
+      testAction({
+        action: store.actions.shuffle,
+        params: {state: {tracks: tracks}},
+        expectedMutations: [
+          { type: 'tracks', payload: [] }
+        ],
+        expectedActions: [
+          { type: 'appendMany', payload: {tracks: shuffledTracks} }
+        ]
+      }, done)
+    })
+  })
+})
diff --git a/front/test/unit/specs/store/radios.spec.js b/front/test/unit/specs/store/radios.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ff8a05ed49df400cf71c588732794c1f907b144
--- /dev/null
+++ b/front/test/unit/specs/store/radios.spec.js
@@ -0,0 +1,86 @@
+var sinon = require('sinon')
+import moxios from 'moxios'
+import store from '@/store/radios'
+import { testAction } from '../../utils'
+
+describe('store/radios', () => {
+  var sandbox
+
+  beforeEach(function () {
+    sandbox = sinon.sandbox.create()
+    moxios.install()
+  })
+  afterEach(function () {
+    sandbox.restore()
+    moxios.uninstall()
+  })
+
+  describe('mutations', () => {
+    it('current', () => {
+      const state = {}
+      store.mutations.current(state, 1)
+      expect(state.current).to.equal(1)
+    })
+    it('running', () => {
+      const state = {}
+      store.mutations.running(state, false)
+      expect(state.running).to.equal(false)
+    })
+  })
+  describe('actions', () => {
+    it('start', (done) => {
+      moxios.stubRequest('radios/sessions/', {
+        status: 200,
+        response: {id: 2}
+      })
+      testAction({
+        action: store.actions.start,
+        payload: {type: 'favorites', objectId: 0, customRadioId: null},
+        expectedMutations: [
+          {
+            type: 'current',
+            payload: {
+              type: 'favorites',
+              objectId: 0,
+              customRadioId: null,
+              session: 2
+            }
+          },
+          { type: 'running', payload: true }
+        ],
+        expectedActions: [
+          { type: 'populateQueue' }
+        ]
+      }, done)
+    })
+    it('stop', (done) => {
+      testAction({
+        action: store.actions.stop,
+        expectedMutations: [
+          { type: 'current', payload: null },
+          { type: 'running', payload: false }
+        ]
+      }, done)
+    })
+    it('populateQueue', (done) => {
+      moxios.stubRequest('radios/tracks/', {
+        status: 201,
+        response: {track: {id: 1}}
+      })
+      testAction({
+        action: store.actions.populateQueue,
+        params: {state: {running: true, current: {session: 1}}},
+        expectedActions: [
+          { type: 'queue/append', payload: {track: {id: 1}}, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('populateQueue does nothing when not running', (done) => {
+      testAction({
+        action: store.actions.populateQueue,
+        params: {state: {running: false}},
+        expectedActions: []
+      }, done)
+    })
+  })
+})
diff --git a/front/test/unit/utils.js b/front/test/unit/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..233ee982e5221e0997da8bd29d33cfac17a113e3
--- /dev/null
+++ b/front/test/unit/utils.js
@@ -0,0 +1,73 @@
+// helper for testing action with expected mutations
+export const testAction = ({action, payload, params, expectedMutations, expectedActions}, done) => {
+  let mutationsCount = 0
+  let actionsCount = 0
+
+  if (!expectedMutations) {
+    expectedMutations = []
+  }
+  if (!expectedActions) {
+    expectedActions = []
+  }
+  const isOver = () => {
+    return mutationsCount >= expectedMutations.length && actionsCount >= expectedActions.length
+  }
+  // mock commit
+  const commit = (type, payload) => {
+    const mutation = expectedMutations[mutationsCount]
+
+    try {
+      expect(mutation.type).to.equal(type)
+      if (payload) {
+        expect(mutation.payload).to.deep.equal(payload)
+      }
+    } catch (error) {
+      done(error)
+    }
+
+    mutationsCount++
+    if (isOver()) {
+      done()
+    }
+  }
+  // mock dispatch
+  const dispatch = (type, payload, options) => {
+    const a = expectedActions[actionsCount]
+    try {
+      expect(a.type).to.equal(type)
+      if (payload) {
+        expect(a.payload).to.deep.equal(payload)
+      }
+      if (a.options) {
+        expect(options).to.deep.equal(a.options)
+      }
+    } catch (error) {
+      done(error)
+    }
+
+    actionsCount++
+    if (isOver()) {
+      done()
+    }
+  }
+
+  let end = function () {
+    // check if no mutations should have been dispatched
+    if (expectedMutations.length === 0) {
+      expect(mutationsCount).to.equal(0)
+    }
+    if (expectedActions.length === 0) {
+      expect(actionsCount).to.equal(0)
+    }
+    if (isOver()) {
+      done()
+    }
+  }
+  // call the action with mocked store and arguments
+  let promise = action({ commit, dispatch, ...params }, payload)
+  if (promise) {
+    return promise.then(end)
+  } else {
+    return end()
+  }
+}