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() + } +}