From c652ca125277a1b3c823522aa878ee329be492f3 Mon Sep 17 00:00:00 2001 From: up2up Date: Sun, 4 Jul 2021 14:19:40 +0700 Subject: [PATCH] Fix query parameter artistId and tagId on song search page #119 --- .../controllers/song_search_controller.dart | 14 +- lib/src/models/base_model.dart | 7 +- lib/src/models/entry_model.dart | 6 + lib/src/models/main_picture_model.dart | 7 +- lib/src/models/pv_model.dart | 7 +- lib/src/models/tag_group_model.dart | 6 +- lib/src/repositories/base_repository.dart | 2 +- lib/src/repositories/song_repository.dart | 16 +-- lib/src/services/http_service.dart | 5 +- .../services/shared_preference_service.dart | 3 + pubspec.lock | 126 ++++++++++++++++++ pubspec.yaml | 1 + .../song_search_controller_test.dart | 67 ++++++++++ .../song_search_controller_test.mocks.dart | 32 +++++ test/repositories/song_repository_test.dart | 43 ++++++ .../song_repository_test.mocks.dart | 21 +++ test/services/http_service_test.dart | 5 +- 17 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 test/controllers/song_search_controller_test.dart create mode 100644 test/controllers/song_search_controller_test.mocks.dart create mode 100644 test/repositories/song_repository_test.dart create mode 100644 test/repositories/song_repository_test.mocks.dart diff --git a/lib/src/controllers/song_search_controller.dart b/lib/src/controllers/song_search_controller.dart index 52c68b46..963ccc34 100644 --- a/lib/src/controllers/song_search_controller.dart +++ b/lib/src/controllers/song_search_controller.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:vocadb_app/controllers.dart'; import 'package:vocadb_app/models.dart'; @@ -20,7 +19,12 @@ class SongSearchController extends SearchPageController { final SongRepository songRepository; - SongSearchController({this.songRepository}); + final SharedPreferenceService sharedPreferenceService; + + SongSearchController( + {this.songRepository, SharedPreferenceService sharedPreferenceService}) + : sharedPreferenceService = + sharedPreferenceService ?? Get.find(); @override void onInit() { @@ -33,11 +37,11 @@ class SongSearchController extends SearchPageController { Future> fetchApi({int start}) => songRepository .findSongs( start: (start == null) ? 0 : start, - lang: SharedPreferenceService.lang, + lang: sharedPreferenceService.getContentLang, query: query.string, songType: songType.string, sort: sort.string, - artistIds: artists.toList().map((e) => e.id).join(','), - tagIds: tags.toList().map((e) => e.id).join(',')) + artistIds: artists.map((e) => e.id.toString()).toList(), + tagIds: tags.map((e) => e.id.toString()).toList()) .catchError(super.onError); } diff --git a/lib/src/models/base_model.dart b/lib/src/models/base_model.dart index 448e3a22..c7ae4b90 100644 --- a/lib/src/models/base_model.dart +++ b/lib/src/models/base_model.dart @@ -1,4 +1,6 @@ -class BaseModel { +import 'package:equatable/equatable.dart'; + +class BaseModel extends Equatable { BaseModel(); BaseModel.fromJson(Map json); @@ -6,4 +8,7 @@ class BaseModel { Map toJson() { return {}; } + + @override + List get props => throw UnimplementedError(); } diff --git a/lib/src/models/entry_model.dart b/lib/src/models/entry_model.dart index f49fb9c3..7f2cdbb4 100644 --- a/lib/src/models/entry_model.dart +++ b/lib/src/models/entry_model.dart @@ -18,6 +18,12 @@ class EntryModel extends BaseModel { List tagGroups; List webLinks; + @override + List get props => [ + id, + name, + ]; + EntryModel( {this.id, this.entryType, diff --git a/lib/src/models/main_picture_model.dart b/lib/src/models/main_picture_model.dart index 6c43e518..ce18601e 100644 --- a/lib/src/models/main_picture_model.dart +++ b/lib/src/models/main_picture_model.dart @@ -1,9 +1,14 @@ -class MainPictureModel { +import 'package:equatable/equatable.dart'; + +class MainPictureModel extends Equatable { String meme; String urlSmallThumb; String urlThumb; String urlTinyThumb; + @override + List get props => [meme, urlSmallThumb, urlThumb, urlTinyThumb]; + MainPictureModel({this.urlThumb}); MainPictureModel.fromJson(Map json) diff --git a/lib/src/models/pv_model.dart b/lib/src/models/pv_model.dart index b4e52a2b..1f9f8f41 100644 --- a/lib/src/models/pv_model.dart +++ b/lib/src/models/pv_model.dart @@ -1,4 +1,6 @@ -class PVModel { +import 'package:equatable/equatable.dart'; + +class PVModel extends Equatable { int id; String name; String service; @@ -6,6 +8,9 @@ class PVModel { String pvType; int length; + @override + List get props => [id, name, service, url, pvType, length]; + PVModel( {this.id, this.name, this.service, this.url, this.pvType, this.length}); diff --git a/lib/src/models/tag_group_model.dart b/lib/src/models/tag_group_model.dart index de03e618..521b9d08 100644 --- a/lib/src/models/tag_group_model.dart +++ b/lib/src/models/tag_group_model.dart @@ -1,9 +1,13 @@ +import 'package:equatable/equatable.dart'; import 'package:vocadb_app/models.dart'; -class TagGroupModel { +class TagGroupModel extends Equatable { int count; TagModel tag; + @override + List get props => [count, tag]; + TagGroupModel.fromJson(Map json) : count = json['count'], tag = json.containsKey('tag') ? TagModel.fromJson(json['tag']) : null; diff --git a/lib/src/repositories/base_repository.dart b/lib/src/repositories/base_repository.dart index b8f56b4a..8e1f4e09 100644 --- a/lib/src/repositories/base_repository.dart +++ b/lib/src/repositories/base_repository.dart @@ -5,7 +5,7 @@ class RestApiRepository { RestApiRepository({this.httpService}); - Future getList(String endpoint, Map params) async { + Future getList(String endpoint, Map params) async { return httpService.get('$endpoint', params).then((v) => (v is Iterable) ? v : (v.containsKey('items')) diff --git a/lib/src/repositories/song_repository.dart b/lib/src/repositories/song_repository.dart index 4b7d6c94..d723956a 100644 --- a/lib/src/repositories/song_repository.dart +++ b/lib/src/repositories/song_repository.dart @@ -11,19 +11,19 @@ class SongRepository extends RestApiRepository { String query, String songType, String sort, - String artistIds, - String tagIds, + Iterable artistIds, + Iterable tagIds, int start = 0, int maxResults = 50, String nameMatchMode = 'Auto'}) async { final String endpoint = '/api/songs'; - final Map params = Map(); + final Map params = Map(); params['query'] = query; params['fields'] = 'ThumbUrl,PVs,MainPicture'; params['songType'] = songType; params['sort'] = sort; - params['artistId'] = artistIds; - params['tagId'] = tagIds; + params['artistId[]'] = artistIds; + params['tagId[]'] = tagIds; params['languagePreference'] = lang; params['maxResults'] = maxResults.toString(); params['start'] = start.toString(); @@ -117,14 +117,14 @@ class SongRepository extends RestApiRepository { Future> getLatestSongsByTagId(int tagId, {String lang = 'Default'}) async { - return this - .findSongs(lang: lang, tagIds: tagId.toString(), sort: 'AdditionDate'); + return this.findSongs( + lang: lang, tagIds: [tagId.toString()], sort: 'AdditionDate'); } Future> getTopSongsByTagId(int tagId, {String lang = 'Default'}) async { return this - .findSongs(lang: lang, tagIds: tagId.toString(), sort: 'RatingScore'); + .findSongs(lang: lang, tagIds: [tagId.toString()], sort: 'RatingScore'); } Future rating(int songId, String rating) { diff --git a/lib/src/services/http_service.dart b/lib/src/services/http_service.dart index aa28afa7..757286f0 100644 --- a/lib/src/services/http_service.dart +++ b/lib/src/services/http_service.dart @@ -24,8 +24,9 @@ class HttpService extends GetxService { return this; } - Future get(String endpoint, Map params) async { - params?.removeWhere((key, value) => value == null || value == ''); + Future get(String endpoint, Map params) async { + params?.removeWhere( + (key, value) => value == null || (value is String && value.isEmpty)); String url = Uri.https(authority, endpoint, params).toString(); print('GET $url | $params'); final response = await _dio.get(url); diff --git a/lib/src/services/shared_preference_service.dart b/lib/src/services/shared_preference_service.dart index db152805..4f56a0fd 100644 --- a/lib/src/services/shared_preference_service.dart +++ b/lib/src/services/shared_preference_service.dart @@ -24,6 +24,9 @@ class SharedPreferenceService extends GetxService { static String get lang => Get.find().contentLang.string; + String get getContentLang => + Get.find().contentLang.string; + static bool get autoPlayValue => Get.find().autoPlay.value; diff --git a/pubspec.lock b/pubspec.lock index f8c9475c..2a872042 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.0" built_collection: dependency: transitive description: @@ -85,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" cli_util: dependency: transitive description: @@ -301,6 +343,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "9.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -327,6 +376,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" http: dependency: transitive description: @@ -334,6 +390,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" http_parser: dependency: transitive description: @@ -353,6 +416,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" js: dependency: transitive description: @@ -360,6 +430,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" logging: dependency: transitive description: @@ -486,6 +563,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -500,6 +584,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" rxdart: dependency: transitive description: @@ -514,6 +605,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.4" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" shimmer: dependency: "direct main" description: @@ -568,6 +673,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -603,6 +715,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" typed_data: dependency: transitive description: @@ -680,6 +799,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" webdriver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b7e0299b..01bd8a98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter + build_runner: ^2.0.5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/controllers/song_search_controller_test.dart b/test/controllers/song_search_controller_test.dart new file mode 100644 index 00000000..ecc1dc1b --- /dev/null +++ b/test/controllers/song_search_controller_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:vocadb_app/controllers.dart'; +import 'package:vocadb_app/models.dart'; +import 'package:vocadb_app/repositories.dart'; +import 'package:vocadb_app/services.dart'; + +import 'song_search_controller_test.mocks.dart'; + +@GenerateMocks([SongRepository, SharedPreferenceService]) +void main() { + final mockSongRepository = MockSongRepository(); + final mockSharePreference = MockSharedPreferenceService(); + + final SongSearchController controller = SongSearchController( + songRepository: mockSongRepository, + sharedPreferenceService: mockSharePreference); + + setUp(() async {}); + + test('should fetch api with all default values successfully', () async { + final mockSongModel = SongModel(id: 1, name: 'A'); + + when(mockSongRepository.findSongs( + start: 0, + lang: 'Default', + query: '', + songType: '', + sort: 'Name', + artistIds: [], + tagIds: [])).thenAnswer((_) => Future.value([mockSongModel])); + + when(mockSharePreference.getContentLang).thenReturn('Default'); + + List songs = await controller.fetchApi(); + + expect(songs.length, 1); + expect(songs[0], mockSongModel); + }); + + test('should fetch api with all non-default values successfully', () async { + final mockSongModel = SongModel(id: 1, name: 'A'); + + when(mockSongRepository.findSongs( + start: 10, + lang: 'English', + query: 'Miku', + songType: 'Remaster', + sort: 'AdditionDate', + artistIds: ['1', '2'], + tagIds: ['3', '4'])).thenAnswer((_) => Future.value([mockSongModel])); + + when(mockSharePreference.getContentLang).thenReturn('English'); + + controller.query('Miku'); + controller.songType('Remaster'); + controller.sort('AdditionDate'); + controller.artists([ArtistModel(id: 1), ArtistModel(id: 2)]); + controller.tags([TagModel(id: 3), TagModel(id: 4)]); + + List songs = await controller.fetchApi(start: 10); + + expect(songs.length, 1); + expect(songs[0], mockSongModel); + }); +} diff --git a/test/controllers/song_search_controller_test.mocks.dart b/test/controllers/song_search_controller_test.mocks.dart new file mode 100644 index 00000000..89219a78 --- /dev/null +++ b/test/controllers/song_search_controller_test.mocks.dart @@ -0,0 +1,32 @@ +// Mocks generated by Mockito 5.0.10 from annotations +// in vocadb_app/test/controllers/song_search_controller_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:vocadb_app/src/repositories/song_repository.dart' as _i2; +import 'package:vocadb_app/src/services/shared_preference_service.dart' as _i3; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: comment_references +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis + +/// A class which mocks [SongRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSongRepository extends _i1.Mock implements _i2.SongRepository { + MockSongRepository() { + _i1.throwOnMissingStub(this); + } +} + +/// A class which mocks [SharedPreferenceService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSharedPreferenceService extends _i1.Mock + implements _i3.SharedPreferenceService { + MockSharedPreferenceService() { + _i1.throwOnMissingStub(this); + } +} diff --git a/test/repositories/song_repository_test.dart b/test/repositories/song_repository_test.dart new file mode 100644 index 00000000..771a84c9 --- /dev/null +++ b/test/repositories/song_repository_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:vocadb_app/models.dart'; +import 'package:vocadb_app/repositories.dart'; +import 'package:vocadb_app/services.dart'; + +import 'song_repository_test.mocks.dart'; + +@GenerateMocks([HttpService]) +void main() { + final mockHttpService = MockHttpService(); + final songRepository = SongRepository(httpService: mockHttpService); + + Map mockResponseSuccess; + + List expectedResultSuccess; + + setUp(() async { + mockResponseSuccess = { + "items": [ + {'id': 1, 'name': 'Song A'}, + {'id': 2, 'name': 'Song B'}, + ] + }; + + expectedResultSuccess = [ + SongModel(id: 1, name: 'Song A'), + SongModel(id: 2, name: 'Song B') + ]; + + when(mockHttpService.get(any, any)) + .thenAnswer((_) => Future.value(mockResponseSuccess)); + }); + + test('should return list of song models when find songs', () async { + expect(await songRepository.findSongs(), expectedResultSuccess); + }); + + test('should return list of song models when get top rated', () async { + expect(await songRepository.getTopRated(), expectedResultSuccess); + }); +} diff --git a/test/repositories/song_repository_test.mocks.dart b/test/repositories/song_repository_test.mocks.dart new file mode 100644 index 00000000..b8159664 --- /dev/null +++ b/test/repositories/song_repository_test.mocks.dart @@ -0,0 +1,21 @@ +// Mocks generated by Mockito 5.0.10 from annotations +// in vocadb_app/test/repositories/song_repository_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:vocadb_app/src/services/http_service.dart' as _i2; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: comment_references +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis + +/// A class which mocks [HttpService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpService extends _i1.Mock implements _i2.HttpService { + MockHttpService() { + _i1.throwOnMissingStub(this); + } +} diff --git a/test/services/http_service_test.dart b/test/services/http_service_test.dart index 76e1e461..992880d7 100644 --- a/test/services/http_service_test.dart +++ b/test/services/http_service_test.dart @@ -20,7 +20,10 @@ void main() { expect( await HttpService(dio: mockDio) - .get('https://vocadb.net/api/songs/1', null), + .get('https://vocadb.net/api/songs/1', { + 'id': '1', + 'artistId[]': ['1', '2'] + }), mockResponseData); });