diff --git a/frontend/lib/auth/auth_service.dart b/frontend/lib/auth/auth_service.dart index b4b9749..5ed47e4 100644 --- a/frontend/lib/auth/auth_service.dart +++ b/frontend/lib/auth/auth_service.dart @@ -1,4 +1,3 @@ - import 'dart:convert'; import 'dart:core'; import 'package:flutter/material.dart'; @@ -11,65 +10,16 @@ import 'dart:math'; import '/database/database.dart'; import 'package:intl/intl.dart'; - - class AuthService { - final FirebaseAuth _auth = FirebaseAuth.instance; - final GoogleSignIn googleSignIn = GoogleSignIn( - clientId: - '717450671046-s8e21c4eu14ebejnnc3varjpues2g2s2.apps.googleusercontent.com', - ); - - Future?> getSpotifyUserDetails() async { - final String endpoint = - 'https://api.spotify.com/v1/me'; // Replace with your backend endpoint - try { - final response = await http.get(Uri.parse(endpoint)); - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else { - print('Failed to load user details'); - return null; - } - } catch (e) { - print('Error: $e'); - return null; - } - } - - Future>?> getSpotifyPlaylists() async { - final String endpoint = - 'http://localhost:5002/spotify-playlists'; // Replace with your backend endpoint - - try { - final response = await http.get(Uri.parse(endpoint)); - if (response.statusCode == 200) { - Map jsonResponse = jsonDecode(response.body); - List playlists = jsonResponse['items']; - List> formattedPlaylists = playlists - .map((playlist) => { - 'id': playlist['id'], - 'name': playlist['name'], - 'description': playlist['description'], - 'image': playlist['images'].isNotEmpty - ? playlist['images'][0]['url'] - : null, - 'owner': playlist['owner']['display_name'], - 'tracks': playlist['tracks']['total'], - 'public': playlist['public'], - 'href': playlist['external_urls']['spotify'], - }) - .toList(); - return formattedPlaylists; - } else { - print('Failed to load Spotify playlists: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error: $e'); - return null; - } - } + final FirebaseAuth _auth; + final GoogleSignIn _googleSignIn; + + // Constructor for dependency injection + AuthService({FirebaseAuth? auth, GoogleSignIn? googleSignIn}) + : _auth = auth ?? FirebaseAuth.instance, + _googleSignIn = googleSignIn ?? GoogleSignIn( + clientId: '717450671046-s8e21c4eu14ebejnnc3varjpues2g2s2.apps.googleusercontent.com', + ); Future registration({ required String email, @@ -78,50 +28,17 @@ class AuthService { }) async { try { UserCredential userCredential = - await _auth.createUserWithEmailAndPassword( - email: email, - password: password, - ); - - await userCredential.user?.updateProfile(displayName: username); - await userCredential.user?.reload(); + await _auth.createUserWithEmailAndPassword(email: email, password: password); + await _updateProfile(userCredential.user, username); return 'Success'; } on FirebaseAuthException catch (e) { - if (e.code == 'weak-password') { - return 'The password provided is too weak.'; - } else if (e.code == 'email-already-in-use') { - return 'The account already exists for that email.'; - } else { - return e.message; - } + return _handleAuthException(e); } catch (e) { return e.toString(); } } - Future signInWithGoogle() async { - try { - final GoogleSignInAccount? googleSignInAccount = - await googleSignIn.signIn(); - if (googleSignInAccount != null) { - // final GoogleSignInAuthentication googleSignInAuthentication = - await googleSignInAccount.authentication; - - // final AuthCredential credential = GoogleAuthProvider.credential( - // accessToken: googleSignInAuthentication.accessToken, - // idToken: googleSignInAuthentication.idToken, - // ); - - return 'Success'; - } else { - // User cancelled Google sign-in - return 'Google sign-in cancelled.'; - } - } catch (error) { - return error.toString(); - } - } Future login({ required String email, @@ -170,40 +87,29 @@ class AuthService { } } - Future authenticateWithSpotify(BuildContext context) async { - final url = - 'https://accounts.spotify.com/authorize?client_id=4a35390dc3c74e85abfd35698529a7f8&response_type=code&redirect_uri=http://localhost:5001/callback&scope=user-read-email'; - - final result = await FlutterWebAuth.authenticate( - url: url, - callbackUrlScheme: 'myapp', - ); - - final code = Uri.parse(result).queryParameters['code']; - - if (code != null) { - await _linkSpotifyAccountToFirebase(code); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Spotify account linked successfully!'), - )); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Failed to link Spotify account.'), - )); + Future _updateProfile(User? user, String username) async { + if (user != null) { + await user.updateProfile(displayName: username); + await user.reload(); } } - Future _linkSpotifyAccountToFirebase(String code) async { - final User? user = FirebaseAuth.instance.currentUser; - final OAuthCredential credential = OAuthProvider('spotify.com').credential( - accessToken: code, - ); - await user?.linkWithCredential(credential); + String? _handleAuthException(FirebaseAuthException e) { + switch (e.code) { + case 'weak-password': + return 'The password provided is too weak.'; + case 'email-already-in-use': + return 'The account already exists for that email.'; + case 'user-not-found': + return 'No user found for that email.'; + case 'wrong-password': + return 'Wrong password provided for that user.'; + default: + return e.message; + } } } - - class SpotifyUser { final String id; final String displayName; @@ -696,86 +602,6 @@ class SpotifyAuth { } - - //My Idea for PlaylistGeneration is to get a Users most listened to Artists and add their songs to the playlist - //then also add the spotify Recommendations - - //Fetches Top Tracks of Artists from a Specific Genre - // static Future> fetchTopTracks() async{ - // if (_accessToken == null) { - // print('Access token is not available'); - // return []; - // } - // - // List trackIds = []; - // - // final String topArtistsEndpoint = 'https://api.spotify.com/v1/me/top/artists'; - // - // try { - // final topArtistsResponse = await http.get( - // Uri.parse(topArtistsEndpoint), - // headers: {'Authorization': 'Bearer $_accessToken'}, - // ); - // - // if (topArtistsResponse.statusCode == 200) { - // // Parse the top artists - // final Map artistsData = jsonDecode(topArtistsResponse.body); - // - // //If Users does not listen to that Genre - // // bool flag = false; - // - // // Finding your most listened to artists of that genre - // List artistC = []; - // int i = 0; - // final Map chosenArtist = {}; - // if(selectedGenres != ''){ - // for(Map g in artistsData['items']){ - // if(g['genres'].contains(selectedGenres.toLowerCase())){ - // chosenArtist.addAll(g); - // artistC[i] = g['id']; - // i++; - // // flag = true; - // } - // } - // } - // - // //Pulling Multiple Artists Top Tracks and adding it to the List - // for(int j = 0; j < i; j++){ - // - // String artistID = artistC[j]; - // final String artistsTopEndpoint = "https://api.spotify.com/v1/artists/$artistID/top-tracks"; - // - // final topTracksResponse = await http.get( - // Uri.parse(artistsTopEndpoint), - // headers: {'Authorization': 'Bearer $_accessToken'}, - // ); - // - // if (topTracksResponse.statusCode == 200) { - // // Parse the top tracks - // final Map tracksData = jsonDecode(topTracksResponse.body); - // for (var track in tracksData['tracks']) { - // trackIds.add(track['id']); - // } - // - // // Return the combined object - // return trackIds; - // } else { - // print('Failed to fetch top tracks for artist'); - // return []; - // } - // } - // - // } else { - // print('Failed to fetch top artists'); - // return []; - // } - // } catch (e) { - // print('Error occurred: $e'); - // return []; - // } - // return trackIds; - // } - //Filter the Top Artists Tracks based on the mood static Future> moodOfTrackIDs({ required List tracks, diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 5cb7911..d722d49 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -12,6 +12,7 @@ import 'package:frontend/pages/user_profile.dart'; import 'package:frontend/pages/link_spotify.dart'; import 'package:frontend/theme/theme_provider.dart'; import 'package:provider/provider.dart'; +import 'auth/auth_service.dart'; import 'firebase_options.dart'; import 'package:frontend/pages/log_in.dart'; import 'package:frontend/pages/sing_up.dart'; @@ -61,8 +62,8 @@ class MyApp extends StatelessWidget { initialRoute: '/', routes: { '/': (context) => const Welcome(), - '/signup': (context) => const SignUp(), - '/login': (context) => const LogIn(), + '/signup': (context) => SignUp(authService: AuthService(),), + '/login': (context) => LogIn(authService: AuthService(),), '/userprofile': (context) => const UserProfile(), '/linkspotify': (context) => const LinkSpotify(), '/userplaylist': (context) => const PlaylistPage(), diff --git a/frontend/lib/pages/camera.dart b/frontend/lib/pages/camera.dart index 01bf034..56e04d4 100644 --- a/frontend/lib/pages/camera.dart +++ b/frontend/lib/pages/camera.dart @@ -360,6 +360,7 @@ class _CameraPageState extends State { ), ), IconButton( + key: Key('flipCamButton'), icon: Icon(Icons.switch_camera_outlined), color: Colors.white, onPressed: _switchCamera, @@ -388,6 +389,7 @@ class _CameraPageState extends State { ), ), IconButton( + key: Key('drawerButton'), icon: Icon(Icons.tune_rounded), color: Colors.white, onPressed: () => Scaffold.of(context).openDrawer(), // Open the drawer diff --git a/frontend/lib/pages/log_in.dart b/frontend/lib/pages/log_in.dart index 2aa5737..2dae2f3 100644 --- a/frontend/lib/pages/log_in.dart +++ b/frontend/lib/pages/log_in.dart @@ -5,15 +5,17 @@ import '../auth/auth_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LogIn extends StatefulWidget { - const LogIn({Key? key}) : super(key: key); + final AuthService authService; + + const LogIn({Key? key, required this.authService}) : super(key: key); @override State createState() => _LogInState(); } class _LogInState extends State { - final AuthService _authService = AuthService(); - final TextEditingController _usernameOrEmailController = TextEditingController(); + final TextEditingController _usernameOrEmailController = + TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final FocusNode _emailFocusNode = FocusNode(); @@ -29,7 +31,8 @@ class _LogInState extends State { _isLoading = true; }); - final result = await _authService.login(email: email, password: password); + final result = await widget.authService + .login(email: email, password: password); // Use widget.authService setState(() { _isLoading = false; @@ -52,18 +55,6 @@ class _LogInState extends State { } } - // Future _checkCredentials() async { - // SharedPreferences prefs = await SharedPreferences.getInstance(); - // final email = prefs.getString('email') ?? ''; - // final password = prefs.getString('password') ?? ''; - // - // if (email.isNotEmpty && password.isNotEmpty) { - // await _authService.login(email: email, password: password); - // return true; - // } - // return false; - // } - Future _handleForgotPassword() async { setState(() { _isPasswordFieldEnabled = false; @@ -73,7 +64,8 @@ class _LogInState extends State { final email = _usernameOrEmailController.text.trim(); if (email.isNotEmpty) { - final result = await _authService.sendPasswordResetEmail(email); + final result = await widget.authService + .sendPasswordResetEmail(email); // Use widget.authService if (result == 'Success') { ScaffoldMessenger.of(context).showSnackBar( @@ -102,10 +94,8 @@ class _LogInState extends State { @override Widget build(BuildContext context) { - final screenWidth = MediaQuery - .of(context) - .size - .width; + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; if (_shouldNavigate) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -114,198 +104,156 @@ class _LogInState extends State { } return Scaffold( - backgroundColor: Theme - .of(context) - .colorScheme - .primary, - resizeToAvoidBottomInset: false, + backgroundColor: Theme.of(context).colorScheme.primary, + resizeToAvoidBottomInset: false, // Allow resizing to avoid overflow body: SafeArea( child: _isLoading ? Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Theme - .of(context) - .colorScheme - .secondary), + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.secondary), ), ) : Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, // Distribute space children: [ - Container( - padding: EdgeInsets.only(top: 25, left: 20, right: 20), - width: screenWidth, - child: Stack( - children: [ - Align( - alignment: Alignment.centerLeft, - child: IconButton( - iconSize: 35, - onPressed: () { - Navigator.pop(context); - }, - icon: Icon( - Icons.arrow_back, - color: Theme - .of(context) - .colorScheme - .secondary, + Expanded( // Use Expanded to take available height + child: SingleChildScrollView( + child: Column( + children: [ + Container( + padding: EdgeInsets.only(top: 25, left: 20, right: 20), + width: screenWidth, + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: IconButton( + iconSize: 35, + onPressed: () { + Navigator.pop(context); + }, + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + Align( + alignment: Alignment.center, + child: SvgPicture.asset( + 'assets/images/SimpleLogo.svg', + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], ), ), - ), - Align( - alignment: Alignment.center, - child: SvgPicture.asset( - 'assets/images/SimpleLogo.svg', - color: Theme - .of(context) - .colorScheme - .secondary, - ), - ), - ], - ), - ), - SizedBox(height: MediaQuery - .of(context) - .size - .height * 0.2), - Container( - width: screenWidth * 0.75, - child: Text( - 'Log in', - style: TextStyle( - fontSize: screenWidth * 0.065, - fontFamily: 'Roboto', - fontWeight: FontWeight.w900, - ), - textAlign: TextAlign.left, - ), - ), - SizedBox(height: 35), - // Email input - Container( - width: screenWidth * 0.75, - child: TextField( - onSubmitted: (value) { - if (!_isPasswordFieldEnabled) { - _handleForgotPassword(); - } - }, - focusNode: _emailFocusNode, - cursorColor: Theme - .of(context) - .colorScheme - .secondary, - style: TextStyle( - fontSize: 20, - fontFamily: 'Roboto', - fontWeight: FontWeight.w500, - color: Theme - .of(context) - .colorScheme - .secondary, - ), - controller: _usernameOrEmailController, - decoration: InputDecoration( - hintText: 'Email', - hintStyle: TextStyle( - fontSize: 20, - color: Theme - .of(context) - .colorScheme - .secondary, - fontFamily: 'Roboto', - fontWeight: FontWeight.w400, - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - width: 2, - color: Theme - .of(context) - .colorScheme - .secondary, - ), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - width: 2, - color: Theme - .of(context) - .colorScheme - .secondary, + SizedBox(height: MediaQuery.of(context).size.height * 0.2), + Container( + width: screenWidth * 0.75, + child: Text( + 'Log in', + style: TextStyle( + fontSize: screenWidth * 0.065, + fontFamily: 'Roboto', + fontWeight: FontWeight.w900, + ), + textAlign: TextAlign.left, + ), ), - ), - ), - ), - ), - SizedBox(height: 50), - // Password input - Container( - width: screenWidth * 0.75, - child: TextField( - enabled: _isPasswordFieldEnabled, - cursorColor: Theme - .of(context) - .colorScheme - .secondary, - style: TextStyle( - fontSize: 20, - fontFamily: 'Roboto', - fontWeight: FontWeight.w500, - color: Theme - .of(context) - .colorScheme - .secondary, - ), - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - hintText: 'Password', - hintStyle: TextStyle( - fontSize: 20, - color: Theme - .of(context) - .colorScheme - .secondary, - fontFamily: 'Roboto', - fontWeight: FontWeight.w400, - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - width: 2, - color: Theme - .of(context) - .colorScheme - .secondary, + SizedBox(height: 35), + // Email input + Container( + width: screenWidth * 0.75, + child: TextField( + onSubmitted: (value) { + if (!_isPasswordFieldEnabled) { + _handleForgotPassword(); + } + }, + focusNode: _emailFocusNode, + cursorColor: Theme.of(context).colorScheme.secondary, + style: TextStyle( + fontSize: 20, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.secondary, + ), + controller: _usernameOrEmailController, + decoration: InputDecoration( + hintText: 'Email', + hintStyle: TextStyle( + fontSize: 20, + color: Theme.of(context).colorScheme.secondary, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ), ), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - width: 2, - color: Theme - .of(context) - .colorScheme - .secondary, + SizedBox(height: 50), + // Password input + Container( + width: screenWidth * 0.75, + child: TextField( + enabled: _isPasswordFieldEnabled, + cursorColor: Theme.of(context).colorScheme.secondary, + style: TextStyle( + fontSize: 20, + fontFamily: 'Roboto', + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.secondary, + ), + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + hintText: 'Password', + hintStyle: TextStyle( + fontSize: 20, + color: Theme.of(context).colorScheme.secondary, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ), ), - ), + ], ), ), ), - Spacer(), // This will push the login button section to the bottom Container( - color: Theme - .of(context) - .colorScheme - .primary, + color: Theme.of(context).colorScheme.primary, height: 200, child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, children: [ Divider( - color: Theme - .of(context) - .colorScheme - .secondary, + color: Theme.of(context).colorScheme.secondary, thickness: 2, ), SizedBox(height: 20), @@ -319,7 +267,8 @@ class _LogInState extends State { horizontal: screenWidth * 0.06), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ TextButton( onPressed: _handleForgotPassword, @@ -327,8 +276,7 @@ class _LogInState extends State { "Forgot your password?", style: TextStyle( fontSize: 16, - color: Theme - .of(context) + color: Theme.of(context) .colorScheme .secondary, fontFamily: 'Roboto', @@ -349,8 +297,7 @@ class _LogInState extends State { "Terms and Conditions", style: TextStyle( fontSize: 16, - color: Theme - .of(context) + color: Theme.of(context) .colorScheme .secondary, fontFamily: 'Roboto', @@ -367,13 +314,11 @@ class _LogInState extends State { flex: 5, child: Container( height: 70, - padding: EdgeInsets.fromLTRB(0, 0, screenWidth * 0.1, - 0), + padding: EdgeInsets.fromLTRB( + 0, 0, screenWidth * 0.1, 0), child: FloatingActionButton.extended( - backgroundColor: Theme - .of(context) - .colorScheme - .secondary, + backgroundColor: + Theme.of(context).colorScheme.secondary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), @@ -382,8 +327,7 @@ class _LogInState extends State { 'Log In', style: TextStyle( fontSize: 20, - color: Theme - .of(context) + color: Theme.of(context) .colorScheme .tertiary, fontFamily: 'Roboto', @@ -395,6 +339,7 @@ class _LogInState extends State { ), ], ), + SizedBox(height: 20), ], ), ), diff --git a/frontend/lib/pages/sing_up.dart b/frontend/lib/pages/sing_up.dart index 9ace2ca..205d54a 100644 --- a/frontend/lib/pages/sing_up.dart +++ b/frontend/lib/pages/sing_up.dart @@ -4,14 +4,14 @@ import 'package:url_launcher/url_launcher.dart'; import '../auth/auth_service.dart'; class SignUp extends StatefulWidget { - const SignUp({Key? key}) : super(key: key); + final AuthService authService; + const SignUp({Key? key, required this.authService}) : super(key: key); @override State createState() => _SignUpState(); } class _SignUpState extends State { - final AuthService _authService = AuthService(); final TextEditingController _usernameController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @@ -39,7 +39,7 @@ class _SignUpState extends State { return; } - final result = await _authService.registration( + final result = await widget.authService.registration( email: email, password: password, username: username, @@ -260,9 +260,8 @@ class _SignUpState extends State { ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Container( + + Container( color: Theme.of(context).colorScheme.primary, height: 180, child: Column( @@ -327,6 +326,7 @@ class _SignUpState extends State { height: 70, padding: EdgeInsets.fromLTRB(0, 0, screenWidth * 0.1, 0), child: FloatingActionButton.extended( + key: Key('createAccountButton'), backgroundColor: Theme.of(context).colorScheme.secondary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), onPressed: _createAccount, @@ -347,7 +347,7 @@ class _SignUpState extends State { ], ), ), - ), + ], ), ), diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 1b6d99a..b8cdffa 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,6 +16,8 @@ import google_sign_in_ios import just_audio import network_info_plus import path_provider_foundation +import shared_preferences_foundation +import sqflite import url_launcher_macos import window_to_front @@ -31,6 +33,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index d85c233..11fb537 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -121,6 +121,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" built_collection: dependency: transitive description: @@ -185,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -414,6 +462,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_sound: + dependency: "direct main" + description: + name: flutter_sound + sha256: "1f40b26b92907a433afe877c927cd48f5a2e4d0f7188e5d39eb5756008aa51ab" + url: "https://pub.dev" + source: hosted + version: "9.6.0" + flutter_sound_platform_interface: + dependency: transitive + description: + name: flutter_sound_platform_interface + sha256: "2e218521d8187b9a4c65063ad9c79bfe88119531ff68047a2eaa6b027cb276bb" + url: "https://pub.dev" + source: hosted + version: "9.6.0" + flutter_sound_web: + dependency: transitive + description: + name: flutter_sound_web + sha256: "5013a15e4e69a4bdc8badd729130eef43c3305e30ba8e6933f863436ce969932" + url: "https://pub.dev" + source: hosted + version: "9.6.0" flutter_svg: dependency: "direct main" description: @@ -456,6 +528,14 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" glob: dependency: transitive description: @@ -512,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.4+1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" http: dependency: "direct main" description: @@ -520,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -528,6 +624,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -572,18 +684,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -600,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logger: + dependency: transitive + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: transitive description: @@ -620,18 +740,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" mockito: dependency: "direct main" description: @@ -760,6 +888,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + url: "https://pub.dev" + source: hosted + version: "10.4.5" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + url: "https://pub.dev" + source: hosted + version: "10.3.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: @@ -784,6 +952,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" provider: dependency: "direct main" description: @@ -800,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" random_string: dependency: transitive description: @@ -808,14 +992,110 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" - rxdart: + recase: dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + remove_emoji: + dependency: transitive + description: + name: remove_emoji + sha256: ed9e8463e8c9ca05b86fcddd4c0dbd2c2605a50d267f4ffa05496607924809e3 + url: "https://pub.dev" + source: hosted + version: "0.0.10" + rxdart: + dependency: "direct dev" description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted version: "0.28.0" + sentiment_dart: + dependency: "direct main" + description: + name: sentiment_dart + sha256: ddac8742cf5141f531eb1510b074ce715b9958cb02a763a4cc0a918768e4a0c8 + url: "https://pub.dev" + source: hosted + version: "0.0.5" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -853,6 +1133,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 + url: "https://pub.dev" + source: hosted + version: "2.3.3+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4+4" stack_trace: dependency: transitive description: @@ -905,10 +1201,18 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "1.0.1" typed_data: dependency: transitive description: @@ -1025,10 +1329,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" watcher: dependency: transitive description: @@ -1045,6 +1349,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" win32: dependency: transitive description: @@ -1086,5 +1406,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.1 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 24f2c70..218169d 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -58,11 +58,17 @@ dependencies: flutter_sound: ^9.2.13 permission_handler: ^10.3.0 sentiment_dart: + sqflite_common_ffi: + dev_dependencies: flutter_test: sdk: flutter + mockito: ^5.0.0 + rxdart: + build_runner: + mocktail: # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/frontend/test/audio_player_page_test.dart b/frontend/test/audio_player_page_test.dart deleted file mode 100644 index bfdf4e1..0000000 --- a/frontend/test/audio_player_page_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:audioplayers/audioplayers.dart'; -//import 'package:frontend/pages/audio_player_page.dart'; // Adjust import path - -// Mock class for AudioPlayer -class MockAudioPlayer extends Mock implements AudioPlayer {} - -void main() { - group('AudioPlayerPage', () { - late MockAudioPlayer mockAudioPlayer; - - setUp(() { - mockAudioPlayer = MockAudioPlayer(); - }); - - testWidgets('Play/Pause Button Test', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp()); - - expect(find.byIcon(Icons.play_arrow), findsOneWidget); - - await tester.tap(find.byIcon(Icons.play_arrow)); - await tester.pump(); - - expect(find.byIcon(Icons.pause), findsOneWidget); - - await tester.tap(find.byIcon(Icons.pause)); - await tester.pump(); - - verify(mockAudioPlayer.pause()).called(1); - - expect(find.byIcon(Icons.play_arrow), findsOneWidget); - - - }); - testWidgets('Navigate Back Test', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - )); - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - verify(mockAudioPlayer.pause()).called(1); - - expect(find.text('Welcome to Homepage'), findsOneWidget); - }); - }); -} diff --git a/frontend/test/camera.test.dart b/frontend/test/camera.test.dart new file mode 100644 index 0000000..a139a75 --- /dev/null +++ b/frontend/test/camera.test.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:frontend/components/audio_service.dart'; +import 'package:frontend/pages/camera.dart'; +import 'package:mockito/mockito.dart'; +import 'package:camera/camera.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mockito/annotations.dart'; +import 'camera.test.mocks.dart'; + +@GenerateMocks([AuthService, CameraController, CameraDescription, AudioRecorder]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockAuthService mockAuthService; + late MockCameraController mockCameraController; + late MockAudioRecorder mockAudioRecorder; + List mockCameras = []; + + setUp(() async { + mockAuthService = MockAuthService(); + mockCameraController = MockCameraController(); + mockAudioRecorder = MockAudioRecorder(); + mockCameras = [ + CameraDescription( + name: 'TestCamera', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ]; + + when(mockCameraController.initialize()).thenAnswer((_) async { + return Future.value(); + }); + + when(mockCameraController.value).thenReturn(CameraValue( + isInitialized: true, + previewSize: const Size(1920, 1080), + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: false, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + description: mockCameras[0], + )); + when(mockCameraController.initialize()).thenAnswer((_) async {}); + }); + + group('CameraPage Widget Tests', () { + testWidgets('Camera initializes successfully with valid token', (WidgetTester tester) async { + when(mockCameraController.initialize()).thenAnswer((_) async { + return Future.value(); + }); + + await tester.pumpWidget( + MaterialApp( + home: CameraPage(cameras: mockCameras), + ), + ); + + await tester.pump(); + expect(find.byType(CameraPage), findsOneWidget); + }); + + testWidgets('Switching camera updates the controller with valid token', (WidgetTester tester) async { + when(mockCameraController.value).thenReturn(CameraValue( + isInitialized: true, + previewSize: const Size(1920, 1080), + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: false, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + description: mockCameras[0], + )); + when(mockCameraController.initialize()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget( + MaterialApp( + home: CameraPage(cameras: mockCameras), + ), + ); + + await tester.tap(find.byKey(const Key('flipCamButton'))); + await tester.pump(); + expect(find.byType(CameraPage), findsOneWidget); + verifyNever(mockCameraController.dispose()).called(0); + }); + + testWidgets('Taking a photo triggers the mood detection with valid token', (WidgetTester tester) async { + when(mockCameraController.value).thenReturn(CameraValue( + isInitialized: true, + previewSize: const Size(1920, 1080), + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: false, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + description: mockCameras[0], + )); + when(mockCameraController.initialize()).thenAnswer((_) async => Future.value()); + when(mockCameraController.takePicture()).thenAnswer((_) async => XFile('test_path')); + + await tester.pumpWidget( + MaterialApp( + home: CameraPage(cameras: mockCameras), + ), + ); + + await tester.pump(); + expect(find.byType(CameraPage), findsOneWidget); + verifyNever(mockCameraController.takePicture()).called(0); + }); + + testWidgets('Audio recording starts and stops correctly with valid token', (WidgetTester tester) async { + when(mockAudioRecorder.openRecorder()).thenAnswer((_) async => Future.value()); + when(mockAudioRecorder.record()).thenAnswer((_) async => Future.value()); + when(mockAudioRecorder.stopRecorder()).thenAnswer((_) async => 'test_path'); + when(mockAudioRecorder.mood).thenReturn(['Happy']); + when(mockAudioRecorder.transcription).thenReturn('Test transcription'); + + await tester.pumpWidget( + MaterialApp( + home: CameraPage(cameras: mockCameras), + ), + ); + + await tester.tap(find.text('Audio')); + await tester.pump(); + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(); + + verifyNever(mockAudioRecorder.record()).called(0); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(); + + verifyNever(mockAudioRecorder.stopRecorder()).called(0); + }); + + testWidgets('Cannot switch modes while recording with valid token', (WidgetTester tester) async { + when(mockCameraController.value).thenReturn(CameraValue( + isInitialized: true, + previewSize: const Size(1920, 1080), + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: false, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + description: mockCameras[0], + )); + when(mockCameraController.initialize()).thenAnswer((_) async => Future.value()); + + await tester.pumpWidget( + MaterialApp( + home: CameraPage(cameras: mockCameras), + ), + ); + + await tester.tap(find.text('Video')); + await tester.pump(); + verifyNever(mockAudioRecorder.stopRecorder()); + }); + }); +} diff --git a/frontend/test/camera.test.mocks.dart b/frontend/test/camera.test.mocks.dart new file mode 100644 index 0000000..d2368e8 --- /dev/null +++ b/frontend/test/camera.test.mocks.dart @@ -0,0 +1,685 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/camera.test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; +import 'dart:ui' as _i8; + +import 'package:camera/camera.dart' as _i3; +import 'package:camera_platform_interface/camera_platform_interface.dart' + as _i2; +import 'package:firebase_auth/firebase_auth.dart' as _i7; +import 'package:flutter/material.dart' as _i4; +import 'package:flutter/services.dart' as _i9; +import 'package:frontend/auth/auth_service.dart' as _i5; +import 'package:frontend/components/audio_service.dart' as _i11; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeMediaSettings_0 extends _i1.SmartFake implements _i2.MediaSettings { + _FakeMediaSettings_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCameraDescription_1 extends _i1.SmartFake + implements _i2.CameraDescription { + _FakeCameraDescription_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCameraValue_2 extends _i1.SmartFake implements _i3.CameraValue { + _FakeCameraValue_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeXFile_3 extends _i1.SmartFake implements _i2.XFile { + _FakeXFile_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_4 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i4.DiagnosticLevel? minLevel = _i4.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [AuthService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthService extends _i1.Mock implements _i5.AuthService { + MockAuthService() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future registration({ + required String? email, + required String? password, + required String? username, + }) => + (super.noSuchMethod( + Invocation.method( + #registration, + [], + { + #email: email, + #password: password, + #username: username, + }, + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future login({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future sendPasswordResetEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [email], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i7.User?> getCurrentUser() => (super.noSuchMethod( + Invocation.method( + #getCurrentUser, + [], + ), + returnValue: _i6.Future<_i7.User?>.value(), + ) as _i6.Future<_i7.User?>); +} + +/// A class which mocks [CameraController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraController extends _i1.Mock implements _i3.CameraController { + MockCameraController() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.MediaSettings get mediaSettings => (super.noSuchMethod( + Invocation.getter(#mediaSettings), + returnValue: _FakeMediaSettings_0( + this, + Invocation.getter(#mediaSettings), + ), + ) as _i2.MediaSettings); + + @override + _i2.CameraDescription get description => (super.noSuchMethod( + Invocation.getter(#description), + returnValue: _FakeCameraDescription_1( + this, + Invocation.getter(#description), + ), + ) as _i2.CameraDescription); + + @override + _i2.ResolutionPreset get resolutionPreset => (super.noSuchMethod( + Invocation.getter(#resolutionPreset), + returnValue: _i2.ResolutionPreset.low, + ) as _i2.ResolutionPreset); + + @override + bool get enableAudio => (super.noSuchMethod( + Invocation.getter(#enableAudio), + returnValue: false, + ) as bool); + + @override + int get cameraId => (super.noSuchMethod( + Invocation.getter(#cameraId), + returnValue: 0, + ) as int); + + @override + _i3.CameraValue get value => (super.noSuchMethod( + Invocation.getter(#value), + returnValue: _FakeCameraValue_2( + this, + Invocation.getter(#value), + ), + ) as _i3.CameraValue); + + @override + set value(_i3.CameraValue? newValue) => super.noSuchMethod( + Invocation.setter( + #value, + newValue, + ), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + void debugCheckIsDisposed() => super.noSuchMethod( + Invocation.method( + #debugCheckIsDisposed, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Future initialize() => (super.noSuchMethod( + Invocation.method( + #initialize, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future prepareForVideoRecording() => (super.noSuchMethod( + Invocation.method( + #prepareForVideoRecording, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future pausePreview() => (super.noSuchMethod( + Invocation.method( + #pausePreview, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future resumePreview() => (super.noSuchMethod( + Invocation.method( + #resumePreview, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setDescription(_i2.CameraDescription? description) => + (super.noSuchMethod( + Invocation.method( + #setDescription, + [description], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i2.XFile> takePicture() => (super.noSuchMethod( + Invocation.method( + #takePicture, + [], + ), + returnValue: _i6.Future<_i2.XFile>.value(_FakeXFile_3( + this, + Invocation.method( + #takePicture, + [], + ), + )), + ) as _i6.Future<_i2.XFile>); + + @override + _i6.Future startImageStream(_i3.onLatestImageAvailable? onAvailable) => + (super.noSuchMethod( + Invocation.method( + #startImageStream, + [onAvailable], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future stopImageStream() => (super.noSuchMethod( + Invocation.method( + #stopImageStream, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future startVideoRecording( + {_i3.onLatestImageAvailable? onAvailable}) => + (super.noSuchMethod( + Invocation.method( + #startVideoRecording, + [], + {#onAvailable: onAvailable}, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i2.XFile> stopVideoRecording() => (super.noSuchMethod( + Invocation.method( + #stopVideoRecording, + [], + ), + returnValue: _i6.Future<_i2.XFile>.value(_FakeXFile_3( + this, + Invocation.method( + #stopVideoRecording, + [], + ), + )), + ) as _i6.Future<_i2.XFile>); + + @override + _i6.Future pauseVideoRecording() => (super.noSuchMethod( + Invocation.method( + #pauseVideoRecording, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future resumeVideoRecording() => (super.noSuchMethod( + Invocation.method( + #resumeVideoRecording, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i4.Widget buildPreview() => (super.noSuchMethod( + Invocation.method( + #buildPreview, + [], + ), + returnValue: _FakeWidget_4( + this, + Invocation.method( + #buildPreview, + [], + ), + ), + ) as _i4.Widget); + + @override + _i6.Future getMaxZoomLevel() => (super.noSuchMethod( + Invocation.method( + #getMaxZoomLevel, + [], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future getMinZoomLevel() => (super.noSuchMethod( + Invocation.method( + #getMinZoomLevel, + [], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future setZoomLevel(double? zoom) => (super.noSuchMethod( + Invocation.method( + #setZoomLevel, + [zoom], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setFlashMode(_i2.FlashMode? mode) => (super.noSuchMethod( + Invocation.method( + #setFlashMode, + [mode], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setExposureMode(_i2.ExposureMode? mode) => + (super.noSuchMethod( + Invocation.method( + #setExposureMode, + [mode], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setExposurePoint(_i8.Offset? point) => (super.noSuchMethod( + Invocation.method( + #setExposurePoint, + [point], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getMinExposureOffset() => (super.noSuchMethod( + Invocation.method( + #getMinExposureOffset, + [], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future getMaxExposureOffset() => (super.noSuchMethod( + Invocation.method( + #getMaxExposureOffset, + [], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future getExposureOffsetStepSize() => (super.noSuchMethod( + Invocation.method( + #getExposureOffsetStepSize, + [], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future setExposureOffset(double? offset) => (super.noSuchMethod( + Invocation.method( + #setExposureOffset, + [offset], + ), + returnValue: _i6.Future.value(0.0), + ) as _i6.Future); + + @override + _i6.Future lockCaptureOrientation( + [_i9.DeviceOrientation? orientation]) => + (super.noSuchMethod( + Invocation.method( + #lockCaptureOrientation, + [orientation], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setFocusMode(_i2.FocusMode? mode) => (super.noSuchMethod( + Invocation.method( + #setFocusMode, + [mode], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future unlockCaptureOrientation() => (super.noSuchMethod( + Invocation.method( + #unlockCaptureOrientation, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setFocusPoint(_i8.Offset? point) => (super.noSuchMethod( + Invocation.method( + #setFocusPoint, + [point], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future dispose() => (super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CameraDescription]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockCameraDescription extends _i1.Mock implements _i2.CameraDescription { + MockCameraDescription() { + _i1.throwOnMissingStub(this); + } + + @override + String get name => (super.noSuchMethod( + Invocation.getter(#name), + returnValue: _i10.dummyValue( + this, + Invocation.getter(#name), + ), + ) as String); + + @override + _i2.CameraLensDirection get lensDirection => (super.noSuchMethod( + Invocation.getter(#lensDirection), + returnValue: _i2.CameraLensDirection.front, + ) as _i2.CameraLensDirection); + + @override + int get sensorOrientation => (super.noSuchMethod( + Invocation.getter(#sensorOrientation), + returnValue: 0, + ) as int); +} + +/// A class which mocks [AudioRecorder]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAudioRecorder extends _i1.Mock implements _i11.AudioRecorder { + MockAudioRecorder() { + _i1.throwOnMissingStub(this); + } + + @override + List get mood => (super.noSuchMethod( + Invocation.getter(#mood), + returnValue: [], + ) as List); + + @override + set mood(List? _mood) => super.noSuchMethod( + Invocation.setter( + #mood, + _mood, + ), + returnValueForMissingStub: null, + ); + + @override + String get transcription => (super.noSuchMethod( + Invocation.getter(#transcription), + returnValue: _i10.dummyValue( + this, + Invocation.getter(#transcription), + ), + ) as String); + + @override + set transcription(String? _transcription) => super.noSuchMethod( + Invocation.setter( + #transcription, + _transcription, + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Future init() => (super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future openRecorder() => (super.noSuchMethod( + Invocation.method( + #openRecorder, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future record() => (super.noSuchMethod( + Invocation.method( + #record, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future stopRecorder() => (super.noSuchMethod( + Invocation.method( + #stopRecorder, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future transcribeAudioAndAnalyze(String? filePath) => + (super.noSuchMethod( + Invocation.method( + #transcribeAudioAndAnalyze, + [filePath], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/frontend/test/database_test.dart b/frontend/test/database_test.dart new file mode 100644 index 0000000..b9edad9 --- /dev/null +++ b/frontend/test/database_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite_common/sqflite.dart'; +import 'package:frontend/database/database.dart'; // Make sure this path is correct + +void main() { + late DatabaseHelper dbHelper; + late Database db; // Making sure this is properly initialized + + setUpAll(() { + // Initialize the ffi-based database for testing + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + setUp(() async { + // Initialize an in-memory database to avoid issues with Android file storage + dbHelper = DatabaseHelper(); + db = await openDatabase(inMemoryDatabasePath, version: 1, onCreate: (db, version) async { + await db.execute(''' + CREATE TABLE playlists( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playlistId TEXT, + mood TEXT, + userId TEXT, + dateCreated TEXT + ) + '''); + }); + }); + + tearDown(() async { + // Ensure database is closed after each test + if (db.isOpen) { + await db.close(); + } + }); + + test('Database is created', () async { + var tables = await db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='playlists';"); + expect(tables.isNotEmpty, true, reason: 'playlists table should exist'); + print("Playlist table does exist"); + }); + + test('Insert playlist into database', () async { + Map playlist = { + 'playlistId': 'test123', + 'mood': 'happy', + 'userId': 'user1', + 'dateCreated': '2024-09-30', + }; + int result = await db.insert('playlists', playlist); + print("Playlists inserted into database"); + expect(result, isNonZero, reason: 'Playlist should be inserted'); + }); + + test('Query all playlists', () async { + List> playlists = await db.query('playlists'); + expect(playlists, isList); + }); + + test('Insert and Query playlist by userId', () async { + // Insert a test playlist + Map playlist = { + 'playlistId': 'test123', + 'mood': 'happy', + 'userId': 'user1', + 'dateCreated': '2024-09-30', + }; + int insertResult = await db.insert('playlists', playlist); + expect(insertResult, isNonZero, reason: 'Playlist should be inserted'); + + // Debugging step: Print out all playlists to verify data + List> allPlaylists = await db.query('playlists'); + print("All Playlists in DB: $allPlaylists"); + + // Now query by userId + String testUserId = 'user1'; + List> result = await db.query( + 'playlists', + where: 'userId = ?', + whereArgs: [testUserId], + ); + + // Debugging step: Print the result of the query by userId + print("Playlists for userId = $testUserId: $result"); + + expect(result.isNotEmpty, true, reason: 'Should return playlists for the given user'); + }); +} \ No newline at end of file diff --git a/frontend/test/log_in.test.dart b/frontend/test/log_in.test.dart index edad835..bd4b535 100644 --- a/frontend/test/log_in.test.dart +++ b/frontend/test/log_in.test.dart @@ -1,66 +1,101 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; import 'package:frontend/pages/log_in.dart'; -//import 'package:mockito/mockito.dart'; -// import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mockito/annotations.dart'; +import 'log_in.test.mocks.dart'; +@GenerateMocks([AuthService]) void main() { - group('LogIn Page Tests', () { - testWidgets('LogIn page UI test', (WidgetTester tester) async { - // Build the LogIn widget - await tester.pumpWidget(MaterialApp(home: LogIn())); - - // Expect to find the 'Log In' text twice - expect(find.text('Log In'), findsNWidgets(2)); - - // Expect to find the 'Username or Email' TextField - expect( - find.byWidgetPredicate((widget) => - widget is TextField && - widget.decoration?.hintText == 'Username or Email'), - findsOneWidget); - - // Expect to find the 'Password' TextField - expect( - find.byWidgetPredicate((widget) => - widget is TextField && widget.decoration?.hintText == 'Password'), - findsOneWidget); - - // Expect to find the 'Log In' button - expect(find.widgetWithText(OutlinedButton, 'Log In'), findsOneWidget); - - // Expect to find the 'Create Account' text span - expect( - find.byWidgetPredicate((widget) => - widget is RichText && - widget.text.toPlainText() == - 'Don\'t have an account?\nCreate Account\n\nTerms and Conditions'), - findsOneWidget); + TestWidgetsFlutterBinding.ensureInitialized(); + late MockAuthService mockAuthService; + + setUp(() { + mockAuthService = MockAuthService(); + }); + + group('LogIn Widget Tests', () { + testWidgets('Successful login shows success message', (WidgetTester tester) async { + // Arrange + when(mockAuthService.login(email: anyNamed('email'), password: anyNamed('password'))) + .thenAnswer((_) async => 'Success'); + + await tester.pumpWidget( + MaterialApp( + home: LogIn(authService: mockAuthService), + ), + ); + + // Act + await tester.enterText(find.byType(TextField).first, 'test@example.com'); + await tester.enterText(find.byType(TextField).at(1), 'password123'); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); // Allow time for the Snackbar to appear + + // Assert + verify(mockAuthService.login(email: 'test@example.com', password: 'password123')).called(1); }); - // testWidgets('LogIn with mock Firebase Auth', (WidgetTester tester) async { - // final mockUser = MockUser( - // isAnonymous: false, - // uid: 'someuid', - // email: 'test@example.com', - // ); - // final auth = MockFirebaseAuth(mockUser: mockUser); - - // // Inject the mock Firebase Auth into the LogIn widget - // await tester.pumpWidget(MaterialApp( - // home: LogIn(), - // )); - - // // Find and enter text into the email and password fields - // await tester.enterText(find.byHintText('Username or Email'), 'test@example.com'); - // await tester.enterText(find.byHintText('Password'), 'password'); - - // // Tap the 'Log In' button - // await tester.tap(find.widgetWithText(OutlinedButton, 'Log In')); - // await tester.pumpAndSettle(); - - // // Verify that the login method was called - // verify(auth.signInWithEmailAndPassword(email: 'test@example.com', password: 'password')).called(1); - // }); + testWidgets('Login fails with wrong password', (WidgetTester tester) async { + // Arrange + when(mockAuthService.login(email: anyNamed('email'), password: anyNamed('password'))) + .thenAnswer((_) async => 'Wrong password provided for that user.'); + + await tester.pumpWidget( + MaterialApp( + home: LogIn(authService: mockAuthService), + ), + ); + + // Act + await tester.enterText(find.byType(TextField).first, 'test@example.com'); + await tester.enterText(find.byType(TextField).at(1), 'wrongpassword'); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(SnackBar), findsOneWidget); // Check if the Snackbar is shown + expect(find.text('Wrong password provided for that user.'), findsOneWidget); + verify(mockAuthService.login(email: 'test@example.com', password: 'wrongpassword')).called(1); + }); + + testWidgets('Password reset sends email', (WidgetTester tester) async { + // Arrange + when(mockAuthService.sendPasswordResetEmail(any)) + .thenAnswer((_) async => 'Success'); + + await tester.pumpWidget( + MaterialApp( + home: LogIn(authService: mockAuthService), + ), + ); + + // Act + await tester.enterText(find.byType(TextField).first, 'test@example.com'); + await tester.tap(find.byType(TextButton).first); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Password reset email sent'), findsOneWidget); + verify(mockAuthService.sendPasswordResetEmail('test@example.com')).called(1); + }); + + testWidgets('Forgot password without email shows error', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + MaterialApp( + home: LogIn(authService: mockAuthService), + ), + ); + + // Act + await tester.tap(find.byType(TextButton).first); // Trigger forgot password + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Please enter your email'), findsOneWidget); + }); }); } diff --git a/frontend/test/log_in.test.mocks.dart b/frontend/test/log_in.test.mocks.dart new file mode 100644 index 0000000..4e7159d --- /dev/null +++ b/frontend/test/log_in.test.mocks.dart @@ -0,0 +1,87 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/log_in.test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:firebase_auth/firebase_auth.dart' as _i4; +import 'package:frontend/auth/auth_service.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [AuthService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthService extends _i1.Mock implements _i2.AuthService { + MockAuthService() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future registration({ + required String? email, + required String? password, + required String? username, + }) => + (super.noSuchMethod( + Invocation.method( + #registration, + [], + { + #email: email, + #password: password, + #username: username, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future login({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future sendPasswordResetEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [email], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future<_i4.User?> getCurrentUser() => (super.noSuchMethod( + Invocation.method( + #getCurrentUser, + [], + ), + returnValue: _i3.Future<_i4.User?>.value(), + ) as _i3.Future<_i4.User?>); +} diff --git a/frontend/test/main_test.dart b/frontend/test/main_test.dart deleted file mode 100644 index b89316a..0000000 --- a/frontend/test/main_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -//import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:frontend/main.dart' as app; -import 'package:frontend/pages/log_in.dart'; -import 'package:frontend/pages/sing_up.dart'; -import 'package:frontend/pages/welcome.dart'; - -void main() { - testWidgets('Initial Route Test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(app.MyApp()); - - // Verify that the initial route is the Loading page. - expect(find.byType(Welcome), findsNothing); - expect(find.byType(SignUp), findsNothing); - expect(find.byType(LogIn), findsNothing); - }); - - testWidgets('Navigate To Welcome', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(app.MyApp()); - - // Loading navigates to the Welcome page. - await tester.pumpAndSettle(); - - // Verify that the Welcome page is displayed. - expect(find.byType(Welcome), findsOneWidget); - expect(find.byType(SignUp), findsNothing); - expect(find.byType(LogIn), findsNothing); - }); -} diff --git a/frontend/test/model/auth_service_test.dart b/frontend/test/model/auth_service_test.dart new file mode 100644 index 0000000..0f032bf --- /dev/null +++ b/frontend/test/model/auth_service_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +import 'auth_service_test.mocks.dart'; + + + +@GenerateMocks([FirebaseAuth, UserCredential, User, GoogleSignIn]) +void main() { + late AuthService authService; + late MockFirebaseAuth mockFirebaseAuth; + late MockUserCredential mockUserCredential; + late MockUser mockUser; + + setUp(() { + mockFirebaseAuth = MockFirebaseAuth(); + mockUserCredential = MockUserCredential(); + mockUser = MockUser(); + + // Inject mocks into the AuthService + authService = AuthService(auth: mockFirebaseAuth, googleSignIn: MockGoogleSignIn()); + }); + + group('AuthService Tests', () { + test('User Registration - Success', () async { + // Arrange + final String email = 'test@example.com'; + final String password = 'password123'; + final String username = 'testuser'; + + when(mockFirebaseAuth.createUserWithEmailAndPassword( + email: email, + password: password, + )).thenAnswer((_) async => mockUserCredential); + + when(mockUserCredential.user).thenReturn(mockUser); + + // Act + final result = await authService.registration(email: email, password: password, username: username); + + // Assert + expect(result, 'Success'); + verify(mockFirebaseAuth.createUserWithEmailAndPassword(email: email, password: password)).called(1); + verify(mockUser.updateProfile(displayName: username)).called(1); + }); + + test('User Registration - Weak Password', () async { + // Arrange + final String email = 'test@example.com'; + final String password = '123'; + final String username = 'testuser'; + + when(mockFirebaseAuth.createUserWithEmailAndPassword( + email: email, + password: password, + )).thenThrow(FirebaseAuthException(code: 'weak-password')); + + // Act + final result = await authService.registration(email: email, password: password, username: username); + + // Assert + expect(result, 'The password provided is too weak.'); + }); + + test('User Login - Success', () async { + // Arrange + final String email = 'test@example.com'; + final String password = 'password123'; + + when(mockFirebaseAuth.signInWithEmailAndPassword( + email: email, + password: password, + )).thenAnswer((_) async => mockUserCredential); + + // Act + final result = await authService.login(email: email, password: password); + + // Assert + expect(result, 'Success'); + verify(mockFirebaseAuth.signInWithEmailAndPassword(email: email, password: password)).called(1); + }); + + test('User Login - User Not Found', () async { + // Arrange + final String email = 'test@example.com'; + final String password = 'password123'; + + when(mockFirebaseAuth.signInWithEmailAndPassword( + email: email, + password: password, + )).thenThrow(FirebaseAuthException(code: 'user-not-found')); + + // Act + final result = await authService.login(email: email, password: password); + + // Assert + expect(result, 'No user found for that email.'); + }); + + test('Send Password Reset Email - Success', () async { + // Arrange + final String email = 'test@example.com'; + + when(mockFirebaseAuth.sendPasswordResetEmail(email: email)).thenAnswer((_) async => {}); + + // Act + final result = await authService.sendPasswordResetEmail(email); + + // Assert + expect(result, 'Success'); + verify(mockFirebaseAuth.sendPasswordResetEmail(email: email)).called(1); + }); + + test('Send Password Reset Email - User Not Found', () async { + // Arrange + final String email = 'test@example.com'; + + when(mockFirebaseAuth.sendPasswordResetEmail(email: email)).thenThrow(FirebaseAuthException(code: 'user-not-found')); + + // Act + final result = await authService.sendPasswordResetEmail(email); + + // Assert + expect(result, 'No user found for that email.'); + }); + + test('Get Current User - Success', () async { + // Arrange + when(mockFirebaseAuth.currentUser).thenReturn(mockUser); + + // Act + final result = await authService.getCurrentUser(); + + // Assert + expect(result, mockUser); + }); + }); +} diff --git a/frontend/test/model/auth_service_test.mocks.dart b/frontend/test/model/auth_service_test.mocks.dart new file mode 100644 index 0000000..adaf20a --- /dev/null +++ b/frontend/test/model/auth_service_test.mocks.dart @@ -0,0 +1,1123 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/model/auth_service_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:firebase_auth/firebase_auth.dart' as _i4; +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' + as _i3; +import 'package:firebase_core/firebase_core.dart' as _i2; +import 'package:google_sign_in/google_sign_in.dart' as _i7; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + as _i8; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeFirebaseApp_0 extends _i1.SmartFake implements _i2.FirebaseApp { + _FakeFirebaseApp_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeActionCodeInfo_1 extends _i1.SmartFake + implements _i3.ActionCodeInfo { + _FakeActionCodeInfo_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserCredential_2 extends _i1.SmartFake + implements _i4.UserCredential { + _FakeUserCredential_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeConfirmationResult_3 extends _i1.SmartFake + implements _i4.ConfirmationResult { + _FakeConfirmationResult_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserMetadata_4 extends _i1.SmartFake implements _i3.UserMetadata { + _FakeUserMetadata_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeMultiFactor_5 extends _i1.SmartFake implements _i4.MultiFactor { + _FakeMultiFactor_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIdTokenResult_6 extends _i1.SmartFake implements _i3.IdTokenResult { + _FakeIdTokenResult_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUser_7 extends _i1.SmartFake implements _i4.User { + _FakeUser_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [FirebaseAuth]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseAuth extends _i1.Mock implements _i4.FirebaseAuth { + MockFirebaseAuth() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_0( + this, + Invocation.getter(#app), + ), + ) as _i2.FirebaseApp); + + @override + set app(_i2.FirebaseApp? _app) => super.noSuchMethod( + Invocation.setter( + #app, + _app, + ), + returnValueForMissingStub: null, + ); + + @override + set tenantId(String? tenantId) => super.noSuchMethod( + Invocation.setter( + #tenantId, + tenantId, + ), + returnValueForMissingStub: null, + ); + + @override + set customAuthDomain(String? customAuthDomain) => super.noSuchMethod( + Invocation.setter( + #customAuthDomain, + customAuthDomain, + ), + returnValueForMissingStub: null, + ); + + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) as Map); + + @override + _i5.Future useAuthEmulator( + String? host, + int? port, { + bool? automaticHostMapping = true, + }) => + (super.noSuchMethod( + Invocation.method( + #useAuthEmulator, + [ + host, + port, + ], + {#automaticHostMapping: automaticHostMapping}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future applyActionCode(String? code) => (super.noSuchMethod( + Invocation.method( + #applyActionCode, + [code], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i3.ActionCodeInfo> checkActionCode(String? code) => + (super.noSuchMethod( + Invocation.method( + #checkActionCode, + [code], + ), + returnValue: _i5.Future<_i3.ActionCodeInfo>.value(_FakeActionCodeInfo_1( + this, + Invocation.method( + #checkActionCode, + [code], + ), + )), + ) as _i5.Future<_i3.ActionCodeInfo>); + + @override + _i5.Future confirmPasswordReset({ + required String? code, + required String? newPassword, + }) => + (super.noSuchMethod( + Invocation.method( + #confirmPasswordReset, + [], + { + #code: code, + #newPassword: newPassword, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i4.UserCredential> createUserWithEmailAndPassword({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #createUserWithEmailAndPassword, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #createUserWithEmailAndPassword, + [], + { + #email: email, + #password: password, + }, + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future> fetchSignInMethodsForEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #fetchSignInMethodsForEmail, + [email], + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + _i5.Future<_i4.UserCredential> getRedirectResult() => (super.noSuchMethod( + Invocation.method( + #getRedirectResult, + [], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #getRedirectResult, + [], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + bool isSignInWithEmailLink(String? emailLink) => (super.noSuchMethod( + Invocation.method( + #isSignInWithEmailLink, + [emailLink], + ), + returnValue: false, + ) as bool); + + @override + _i5.Stream<_i4.User?> authStateChanges() => (super.noSuchMethod( + Invocation.method( + #authStateChanges, + [], + ), + returnValue: _i5.Stream<_i4.User?>.empty(), + ) as _i5.Stream<_i4.User?>); + + @override + _i5.Stream<_i4.User?> idTokenChanges() => (super.noSuchMethod( + Invocation.method( + #idTokenChanges, + [], + ), + returnValue: _i5.Stream<_i4.User?>.empty(), + ) as _i5.Stream<_i4.User?>); + + @override + _i5.Stream<_i4.User?> userChanges() => (super.noSuchMethod( + Invocation.method( + #userChanges, + [], + ), + returnValue: _i5.Stream<_i4.User?>.empty(), + ) as _i5.Stream<_i4.User?>); + + @override + _i5.Future sendPasswordResetEmail({ + required String? email, + _i3.ActionCodeSettings? actionCodeSettings, + }) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [], + { + #email: email, + #actionCodeSettings: actionCodeSettings, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future sendSignInLinkToEmail({ + required String? email, + required _i3.ActionCodeSettings? actionCodeSettings, + }) => + (super.noSuchMethod( + Invocation.method( + #sendSignInLinkToEmail, + [], + { + #email: email, + #actionCodeSettings: actionCodeSettings, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setLanguageCode(String? languageCode) => (super.noSuchMethod( + Invocation.method( + #setLanguageCode, + [languageCode], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setSettings({ + bool? appVerificationDisabledForTesting = false, + String? userAccessGroup, + String? phoneNumber, + String? smsCode, + bool? forceRecaptchaFlow, + }) => + (super.noSuchMethod( + Invocation.method( + #setSettings, + [], + { + #appVerificationDisabledForTesting: + appVerificationDisabledForTesting, + #userAccessGroup: userAccessGroup, + #phoneNumber: phoneNumber, + #smsCode: smsCode, + #forceRecaptchaFlow: forceRecaptchaFlow, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setPersistence(_i3.Persistence? persistence) => + (super.noSuchMethod( + Invocation.method( + #setPersistence, + [persistence], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i4.UserCredential> signInAnonymously() => (super.noSuchMethod( + Invocation.method( + #signInAnonymously, + [], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInAnonymously, + [], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> signInWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #signInWithCredential, + [credential], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithCredential, + [credential], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> signInWithCustomToken(String? token) => + (super.noSuchMethod( + Invocation.method( + #signInWithCustomToken, + [token], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithCustomToken, + [token], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> signInWithEmailAndPassword({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #signInWithEmailAndPassword, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithEmailAndPassword, + [], + { + #email: email, + #password: password, + }, + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> signInWithEmailLink({ + required String? email, + required String? emailLink, + }) => + (super.noSuchMethod( + Invocation.method( + #signInWithEmailLink, + [], + { + #email: email, + #emailLink: emailLink, + }, + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithEmailLink, + [], + { + #email: email, + #emailLink: emailLink, + }, + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> signInWithProvider( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #signInWithProvider, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithProvider, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.ConfirmationResult> signInWithPhoneNumber( + String? phoneNumber, [ + _i4.RecaptchaVerifier? verifier, + ]) => + (super.noSuchMethod( + Invocation.method( + #signInWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + returnValue: + _i5.Future<_i4.ConfirmationResult>.value(_FakeConfirmationResult_3( + this, + Invocation.method( + #signInWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + )), + ) as _i5.Future<_i4.ConfirmationResult>); + + @override + _i5.Future<_i4.UserCredential> signInWithPopup(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #signInWithPopup, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #signInWithPopup, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future signInWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #signInWithRedirect, + [provider], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future verifyPasswordResetCode(String? code) => + (super.noSuchMethod( + Invocation.method( + #verifyPasswordResetCode, + [code], + ), + returnValue: _i5.Future.value(_i6.dummyValue( + this, + Invocation.method( + #verifyPasswordResetCode, + [code], + ), + )), + ) as _i5.Future); + + @override + _i5.Future verifyPhoneNumber({ + String? phoneNumber, + _i3.PhoneMultiFactorInfo? multiFactorInfo, + required _i3.PhoneVerificationCompleted? verificationCompleted, + required _i3.PhoneVerificationFailed? verificationFailed, + required _i3.PhoneCodeSent? codeSent, + required _i3.PhoneCodeAutoRetrievalTimeout? codeAutoRetrievalTimeout, + String? autoRetrievedSmsCodeForTesting, + Duration? timeout = const Duration(seconds: 30), + int? forceResendingToken, + _i3.MultiFactorSession? multiFactorSession, + }) => + (super.noSuchMethod( + Invocation.method( + #verifyPhoneNumber, + [], + { + #phoneNumber: phoneNumber, + #multiFactorInfo: multiFactorInfo, + #verificationCompleted: verificationCompleted, + #verificationFailed: verificationFailed, + #codeSent: codeSent, + #codeAutoRetrievalTimeout: codeAutoRetrievalTimeout, + #autoRetrievedSmsCodeForTesting: autoRetrievedSmsCodeForTesting, + #timeout: timeout, + #forceResendingToken: forceResendingToken, + #multiFactorSession: multiFactorSession, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future revokeTokenWithAuthorizationCode( + String? authorizationCode) => + (super.noSuchMethod( + Invocation.method( + #revokeTokenWithAuthorizationCode, + [authorizationCode], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [UserCredential]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserCredential extends _i1.Mock implements _i4.UserCredential { + MockUserCredential() { + _i1.throwOnMissingStub(this); + } +} + +/// A class which mocks [User]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUser extends _i1.Mock implements _i4.User { + MockUser() { + _i1.throwOnMissingStub(this); + } + + @override + bool get emailVerified => (super.noSuchMethod( + Invocation.getter(#emailVerified), + returnValue: false, + ) as bool); + + @override + bool get isAnonymous => (super.noSuchMethod( + Invocation.getter(#isAnonymous), + returnValue: false, + ) as bool); + + @override + _i3.UserMetadata get metadata => (super.noSuchMethod( + Invocation.getter(#metadata), + returnValue: _FakeUserMetadata_4( + this, + Invocation.getter(#metadata), + ), + ) as _i3.UserMetadata); + + @override + List<_i3.UserInfo> get providerData => (super.noSuchMethod( + Invocation.getter(#providerData), + returnValue: <_i3.UserInfo>[], + ) as List<_i3.UserInfo>); + + @override + String get uid => (super.noSuchMethod( + Invocation.getter(#uid), + returnValue: _i6.dummyValue( + this, + Invocation.getter(#uid), + ), + ) as String); + + @override + _i4.MultiFactor get multiFactor => (super.noSuchMethod( + Invocation.getter(#multiFactor), + returnValue: _FakeMultiFactor_5( + this, + Invocation.getter(#multiFactor), + ), + ) as _i4.MultiFactor); + + @override + _i5.Future delete() => (super.noSuchMethod( + Invocation.method( + #delete, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getIdToken([bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdToken, + [forceRefresh], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i3.IdTokenResult> getIdTokenResult( + [bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + returnValue: _i5.Future<_i3.IdTokenResult>.value(_FakeIdTokenResult_6( + this, + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + )), + ) as _i5.Future<_i3.IdTokenResult>); + + @override + _i5.Future<_i4.UserCredential> linkWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #linkWithCredential, + [credential], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #linkWithCredential, + [credential], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> linkWithProvider(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithProvider, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #linkWithProvider, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> reauthenticateWithProvider( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future<_i4.UserCredential> reauthenticateWithPopup( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future reauthenticateWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithRedirect, + [provider], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i4.UserCredential> linkWithPopup(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithPopup, + [provider], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #linkWithPopup, + [provider], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future linkWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithRedirect, + [provider], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i4.ConfirmationResult> linkWithPhoneNumber( + String? phoneNumber, [ + _i4.RecaptchaVerifier? verifier, + ]) => + (super.noSuchMethod( + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + returnValue: + _i5.Future<_i4.ConfirmationResult>.value(_FakeConfirmationResult_3( + this, + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + )), + ) as _i5.Future<_i4.ConfirmationResult>); + + @override + _i5.Future<_i4.UserCredential> reauthenticateWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + returnValue: _i5.Future<_i4.UserCredential>.value(_FakeUserCredential_2( + this, + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + )), + ) as _i5.Future<_i4.UserCredential>); + + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future sendEmailVerification( + [_i3.ActionCodeSettings? actionCodeSettings]) => + (super.noSuchMethod( + Invocation.method( + #sendEmailVerification, + [actionCodeSettings], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i4.User> unlink(String? providerId) => (super.noSuchMethod( + Invocation.method( + #unlink, + [providerId], + ), + returnValue: _i5.Future<_i4.User>.value(_FakeUser_7( + this, + Invocation.method( + #unlink, + [providerId], + ), + )), + ) as _i5.Future<_i4.User>); + + @override + _i5.Future updateEmail(String? newEmail) => (super.noSuchMethod( + Invocation.method( + #updateEmail, + [newEmail], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future updatePassword(String? newPassword) => (super.noSuchMethod( + Invocation.method( + #updatePassword, + [newPassword], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future updatePhoneNumber( + _i3.PhoneAuthCredential? phoneCredential) => + (super.noSuchMethod( + Invocation.method( + #updatePhoneNumber, + [phoneCredential], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future updateDisplayName(String? displayName) => + (super.noSuchMethod( + Invocation.method( + #updateDisplayName, + [displayName], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future updatePhotoURL(String? photoURL) => (super.noSuchMethod( + Invocation.method( + #updatePhotoURL, + [photoURL], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future updateProfile({ + String? displayName, + String? photoURL, + }) => + (super.noSuchMethod( + Invocation.method( + #updateProfile, + [], + { + #displayName: displayName, + #photoURL: photoURL, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future verifyBeforeUpdateEmail( + String? newEmail, [ + _i3.ActionCodeSettings? actionCodeSettings, + ]) => + (super.noSuchMethod( + Invocation.method( + #verifyBeforeUpdateEmail, + [ + newEmail, + actionCodeSettings, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [GoogleSignIn]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGoogleSignIn extends _i1.Mock implements _i7.GoogleSignIn { + MockGoogleSignIn() { + _i1.throwOnMissingStub(this); + } + + @override + _i8.SignInOption get signInOption => (super.noSuchMethod( + Invocation.getter(#signInOption), + returnValue: _i8.SignInOption.standard, + ) as _i8.SignInOption); + + @override + List get scopes => (super.noSuchMethod( + Invocation.getter(#scopes), + returnValue: [], + ) as List); + + @override + bool get forceCodeForRefreshToken => (super.noSuchMethod( + Invocation.getter(#forceCodeForRefreshToken), + returnValue: false, + ) as bool); + + @override + _i5.Stream<_i7.GoogleSignInAccount?> get onCurrentUserChanged => + (super.noSuchMethod( + Invocation.getter(#onCurrentUserChanged), + returnValue: _i5.Stream<_i7.GoogleSignInAccount?>.empty(), + ) as _i5.Stream<_i7.GoogleSignInAccount?>); + + @override + _i5.Future<_i7.GoogleSignInAccount?> signInSilently({ + bool? suppressErrors = true, + bool? reAuthenticate = false, + }) => + (super.noSuchMethod( + Invocation.method( + #signInSilently, + [], + { + #suppressErrors: suppressErrors, + #reAuthenticate: reAuthenticate, + }, + ), + returnValue: _i5.Future<_i7.GoogleSignInAccount?>.value(), + ) as _i5.Future<_i7.GoogleSignInAccount?>); + + @override + _i5.Future isSignedIn() => (super.noSuchMethod( + Invocation.method( + #isSignedIn, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future<_i7.GoogleSignInAccount?> signIn() => (super.noSuchMethod( + Invocation.method( + #signIn, + [], + ), + returnValue: _i5.Future<_i7.GoogleSignInAccount?>.value(), + ) as _i5.Future<_i7.GoogleSignInAccount?>); + + @override + _i5.Future<_i7.GoogleSignInAccount?> signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i5.Future<_i7.GoogleSignInAccount?>.value(), + ) as _i5.Future<_i7.GoogleSignInAccount?>); + + @override + _i5.Future<_i7.GoogleSignInAccount?> disconnect() => (super.noSuchMethod( + Invocation.method( + #disconnect, + [], + ), + returnValue: _i5.Future<_i7.GoogleSignInAccount?>.value(), + ) as _i5.Future<_i7.GoogleSignInAccount?>); + + @override + _i5.Future requestScopes(List? scopes) => (super.noSuchMethod( + Invocation.method( + #requestScopes, + [scopes], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future canAccessScopes( + List? scopes, { + String? accessToken, + }) => + (super.noSuchMethod( + Invocation.method( + #canAccessScopes, + [scopes], + {#accessToken: accessToken}, + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); +} diff --git a/frontend/test/model/spotify_auth_test.dart b/frontend/test/model/spotify_auth_test.dart new file mode 100644 index 0000000..0a6a08d --- /dev/null +++ b/frontend/test/model/spotify_auth_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:http/testing.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +@GenerateMocks([SpotifyAuth]) +void main() { + +} diff --git a/frontend/test/model/spotify_auth_test.mocks.dart b/frontend/test/model/spotify_auth_test.mocks.dart new file mode 100644 index 0000000..123830d --- /dev/null +++ b/frontend/test/model/spotify_auth_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/model/spotify_auth_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:frontend/auth/auth_service.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [SpotifyAuth]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSpotifyAuth extends _i1.Mock implements _i2.SpotifyAuth { + MockSpotifyAuth() { + _i1.throwOnMissingStub(this); + } + + @override + void setSelectedGenres(List? genres) => super.noSuchMethod( + Invocation.method( + #setSelectedGenres, + [genres], + ), + returnValueForMissingStub: null, + ); +} diff --git a/frontend/test/navbar_test.dart b/frontend/test/navbar_test.dart new file mode 100644 index 0000000..2f9277f --- /dev/null +++ b/frontend/test/navbar_test.dart @@ -0,0 +1,70 @@ +import 'package:frontend/components/navbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class MockNavigatorObserver extends Mock implements NavigatorObserver {} + +void main() { + testWidgets('NavBar navigation test', (WidgetTester tester) async { + // Define a mock navigation observer to verify navigation calls. + final mockObserver = MockNavigatorObserver(); + + // Build the widget tree with MaterialApp to allow navigation. + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: NavBar( + currentIndex: 1, + onTap: (index) { + switch (index) { + case 0: + Navigator.pushReplacementNamed(tester.element(find.byType(NavBar)), '/camera'); + break; + case 1: + Navigator.pushReplacementNamed(tester.element(find.byType(NavBar)), '/userplaylist'); + break; + case 2: + Navigator.pushReplacementNamed(tester.element(find.byType(NavBar)), '/userprofile'); + break; + case 3: + Navigator.pushReplacementNamed(tester.element(find.byType(NavBar)), '/settings'); + break; + } + }, + ), + ), + navigatorObservers: [mockObserver], + routes: { + '/camera': (context) => const Scaffold(body: Text('HOME')), + '/userplaylist': (context) => const Scaffold(body: Text('PLAYLISTS')), + '/userprofile': (context) => const Scaffold(body: Text('PROFILE')), + '/settings': (context) => const Scaffold(body: Text('HELP')), + }, + ), + ); + + // Verify initial state. + expect(find.text('HOME'), findsOneWidget); + + // Tap on the bottom navigation item at index 0 (Camera). + // await tester.tap(find.byIcon(Icons.camera_alt)); + await tester.tap(find.byKey(Key('CAMERAICON'))); + await tester.pumpAndSettle(); // Wait for navigation to complete. + verify(mockObserver.didReplace( + oldRoute: anyNamed('oldRoute'), newRoute: anyNamed('newRoute'))); + expect(find.text('HOME'), findsOneWidget); + + // Tap on the bottom navigation item at index 2 (User Profile). + // await tester.tap(find.byIcon(Icons.person)); + // await tester.pumpAndSettle(); + // await tester.tap(find.byKey(Key('PROFILEICON'))); + // expect(find.text('PROFILE'), findsOneWidget); + // + // Tap on the bottom navigation item at index 3 (Settings). + // await tester.tap(find.byIcon(Icons.settings)); + // await tester.tap(find.byKey(Key('HELPICON'))); + // await tester.pumpAndSettle(); + // expect(find.text('HELP'), findsOneWidget); + }); +} \ No newline at end of file diff --git a/frontend/test/sign_up_test.dart b/frontend/test/sign_up_test.dart index e2b3afc..1470cc1 100644 --- a/frontend/test/sign_up_test.dart +++ b/frontend/test/sign_up_test.dart @@ -1,37 +1,108 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:frontend/pages/sing_up.dart'; // Import your SignUp widget +import 'package:frontend/auth/auth_service.dart'; +import 'package:frontend/pages/sing_up.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'sign_up_test.mocks.dart'; + +@GenerateMocks([AuthService]) void main() { - testWidgets('SignUp page UI test', (WidgetTester tester) async { - tester.binding.window.physicalSizeTestValue = Size(1080, 1920); - tester.binding.window.devicePixelRatioTestValue = 1.0; - - // Build the SignUp widget - await tester.pumpWidget(MaterialApp(home: SignUp())); + TestWidgetsFlutterBinding.ensureInitialized(); + late MockAuthService mockAuthService; + + setUp(() { + mockAuthService = MockAuthService(); + }); + + // Function to create the widget under test + Widget createWidgetUnderTest() { + return MaterialApp( + routes: { + '/': (context) => SignUp(authService: mockAuthService), + '/linkspotify': (context) => Placeholder(), // Mock target screen + }, + ); + } + + + group('SignUp Widget Tests', () { + testWidgets('renders input fields and buttons', (WidgetTester tester) async { + await tester.pumpWidget(createWidgetUnderTest()); + + expect(find.byType(TextField), findsNWidgets(4)); // 4 text fields + expect(find.byKey(Key('createAccountButton')), findsOneWidget); // Create button + expect(find.text('Create your account'), findsOneWidget); // Title text + }); + + testWidgets('shows snackbar if passwords do not match', (WidgetTester tester) async { + await tester.pumpWidget(createWidgetUnderTest()); + + await tester.enterText(find.byType(TextField).at(0), 'username'); // Username + await tester.enterText(find.byType(TextField).at(1), 'email@example.com'); // Email + await tester.enterText(find.byType(TextField).at(2), 'password123'); // Password + await tester.enterText(find.byType(TextField).at(3), 'password'); // Confirm Password (not matching) + + // Scroll down by dragging + await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300)); + await tester.pumpAndSettle(); // Let the scroll animation settle + + await tester.tap(find.byKey(Key('createAccountButton'))); + await tester.pump(); // Rebuild the widget after the state has changed + + expect(find.text('Passwords do not match'), findsOneWidget); + }); + + testWidgets('navigates to /linkspotify on successful registration', (WidgetTester tester) async { + when(mockAuthService.registration( + email: anyNamed('email'), + password: anyNamed('password'), + username: anyNamed('username'), + )).thenAnswer((_) async => 'Success'); // Mocking the registration response + + await tester.pumpWidget(createWidgetUnderTest()); + + // Fill in the text fields + await tester.enterText(find.byType(TextField).at(0), 'username'); + await tester.enterText(find.byType(TextField).at(1), 'email@example.com'); + await tester.enterText(find.byType(TextField).at(2), 'password123'); + await tester.enterText(find.byType(TextField).at(3), 'password123'); // Matching password + + // Scroll down by dragging (if necessary) + await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300)); + await tester.pumpAndSettle(); // Let the scroll animation settle + + // Tap the create account button + await tester.tap(find.byKey(Key('createAccountButton'))); + await tester.pumpAndSettle(); // Wait for animations - // Expect to find the 'Create Your Account' text - expect(find.text('Create Your\nAccount'), findsOneWidget); + // Check that we navigate to the new screen + expect(find.byType(Placeholder), findsOneWidget); // Verify the target screen + }); - // Expect to find the 'Username' TextField - expect(find.widgetWithText(TextField, 'Username'), findsOneWidget); + testWidgets('shows snackbar if registration fails', (WidgetTester tester) async { + when(mockAuthService.registration( + email: anyNamed('email'), + password: anyNamed('password'), + username: anyNamed('username'), + )).thenAnswer((_) async => 'Registration failed'); // Mocking the registration response - // Expect to find the 'Email' TextField - expect(find.widgetWithText(TextField, 'Email'), findsOneWidget); + await tester.pumpWidget(createWidgetUnderTest()); - // Expect to find the 'Password' TextField - expect(find.widgetWithText(TextField, 'Password'), findsOneWidget); + await tester.enterText(find.byType(TextField).at(0), 'username'); + await tester.enterText(find.byType(TextField).at(1), 'email@example.com'); + await tester.enterText(find.byType(TextField).at(2), 'password123'); + await tester.enterText(find.byType(TextField).at(3), 'password123'); // Matching password - // Expect to find the 'Confirm Password' TextField - expect(find.widgetWithText(TextField, 'Confirm Password'), findsOneWidget); + // Scroll down by dragging + await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -300)); + await tester.pumpAndSettle(); // Let the scroll animation settle - // Expect to find the 'Create' button - expect(find.widgetWithText(OutlinedButton, 'Create'), findsOneWidget); + await tester.tap(find.byKey(Key('createAccountButton'))); + await tester.pump(); // Rebuild the widget after the state has changed - expect(find.byWidgetPredicate( - (widget) => - widget is RichText && - widget.text.toPlainText() == 'Already have an account?\nLog In\n\nTerms and Conditions', - ), findsOneWidget); + expect(find.text('Registration failed'), findsOneWidget); + }); }); } \ No newline at end of file diff --git a/frontend/test/sign_up_test.mocks.dart b/frontend/test/sign_up_test.mocks.dart new file mode 100644 index 0000000..e1d82b7 --- /dev/null +++ b/frontend/test/sign_up_test.mocks.dart @@ -0,0 +1,87 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/sign_up_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:firebase_auth/firebase_auth.dart' as _i4; +import 'package:frontend/auth/auth_service.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [AuthService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthService extends _i1.Mock implements _i2.AuthService { + MockAuthService() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future registration({ + required String? email, + required String? password, + required String? username, + }) => + (super.noSuchMethod( + Invocation.method( + #registration, + [], + { + #email: email, + #password: password, + #username: username, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future login({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future sendPasswordResetEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [email], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future<_i4.User?> getCurrentUser() => (super.noSuchMethod( + Invocation.method( + #getCurrentUser, + [], + ), + returnValue: _i3.Future<_i4.User?>.value(), + ) as _i3.Future<_i4.User?>); +} diff --git a/frontend/test/spotify_auth_non_static.dart b/frontend/test/spotify_auth_non_static.dart new file mode 100644 index 0000000..a022c87 --- /dev/null +++ b/frontend/test/spotify_auth_non_static.dart @@ -0,0 +1,1188 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_auth/flutter_web_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:frontend/database/database.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:http/http.dart' as http; +import 'dart:math'; +import 'package:intl/intl.dart'; + +class SpotifyAuthNonStatic { + MethodChannel _channel = MethodChannel('spotify_auth'); + String? _accessToken; // variable to hold the access token + Function(String)? _onSuccessCallback; // Callback function + SpotifyUser? currentUser; + String selectedGenres = ''; + List realtimeArtists = []; + + Future authenticate() async { + try { + final String? accessToken = await _channel.invokeMethod( + 'authenticate'); // Calls native method + return accessToken; + } on PlatformException catch (e) { + print("Failed to authenticate: ${e.message}"); + return null; + } + } + + // Initialize the service and set the method call handler + void initialize(Function(String) onSuccessCallback) { + _onSuccessCallback = onSuccessCallback; + _channel.setMethodCallHandler(_handleMethodCall); + } + + // Method to handle method calls from native code + Future _handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'onSuccess': + String accessToken = call.arguments; + _handleSuccess(accessToken); + break; + case 'onError': + String error = call.arguments; + _handleError(error); + break; + default: + throw MissingPluginException('Not implemented: ${call.method}'); + } + } + + void _handleSuccess(String accessToken) { + // Handle the access token (e.g., save it, use it for API calls, etc.) + print('Login was a success and flutter has the recieved the token:'); + print('+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'); + print('Access Token: $accessToken'); + _accessToken = accessToken; + if (_onSuccessCallback != null) { + _onSuccessCallback!(accessToken); // Call the success callback + } + } + + void _handleError(String error) { + // Handle the error (e.g., show an error message to the user) + print('Error: $error'); + } + + String? getAccessToken() { + return _accessToken; + } + + void setAccessToken(String a) { + _accessToken = a; + } + + String? getUserId(){ + + if (currentUser == null) { + return "User not found"; + }else{ + return currentUser?.id; + } + } + + Future?> fetchUserDetails() async { + if (_accessToken == null) { + print('Access token is not available'); + return null; + } + + final String endpoint = 'https://api.spotify.com/v1/me'; + try { + final response = await http.get( + Uri.parse(endpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + if (response.statusCode == 200) { + final userdetails = jsonDecode(response.body); + print('User details fetched and stored:'); + print(userdetails); + + + //this will store everything into an object for later use so that we can fetch the ID. + currentUser = SpotifyUser.fromJson(userdetails); + + + return userdetails; + } else { + print('Failed to fetch user details: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error: $e'); + return null; + } + } + + Future>?> fetchUserPlaylists(String? userId) async { + if (_accessToken == null) { + print('Access token is not available'); + return null; + } + + // Fetch playlists from the local database + List> localPlaylists = await DatabaseHelper.getPlaylistsByUserId(userId); + final List localPlaylistIds = localPlaylists.map((playlist) => playlist['playlistId'] as String).toList(); + + final String endpoint = 'https://api.spotify.com/v1/me/playlists'; + try { + final response = await http.get( + Uri.parse(endpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final playlistDetails = jsonDecode(response.body); + final List fetchedPlaylists = playlistDetails['items']; + final List> matchingPlaylists = []; + + // Loop through fetched playlists and compare with local database + for (var playlist in fetchedPlaylists) { + final String playlistId = playlist['id']; + + // Check if the fetched playlist ID matches any in the local database + if (localPlaylistIds.contains(playlistId)) { + // Find the corresponding local playlist entry + final matchingLocalPlaylist = localPlaylists.firstWhere( + (localPlaylist) => localPlaylist['playlistId'] == playlistId, + ); + + // Add the mood from the local database to the playlist object + playlist['mood'] = matchingLocalPlaylist['mood']; + playlist['dateCreated'] = matchingLocalPlaylist['dateCreated']; + matchingPlaylists.add(playlist); + } + } + print("matching playlists are as follows:"); + print(matchingPlaylists); + return matchingPlaylists; + } else { + print('Failed to fetch user playlists: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error: $e'); + return null; + } + } + + Future?> fetchPlaylistTracks( + String playlistId) async { + if (_accessToken == null) { + print('Access token is not available'); + return null; + } + + final String endpoint = 'https://api.spotify.com/v1/playlists/$playlistId/tracks'; + try { + final response = await http.get( + Uri.parse(endpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + if (response.statusCode == 200) { + final playlistData = jsonDecode(response.body); + print('Playlist tracks fetched:'); + //print(playlistData); + return playlistData; + } else { + print('Failed to fetch playlist tracks: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error: $e'); + return null; + } + } + + Future>?> fetchTrackAudioFeatures( + List trackIds) async { + if (_accessToken == null) { + print('Access token is not available'); + return null; + } + + final String ids = trackIds.join(','); + final String endpoint = 'https://api.spotify.com/v1/audio-features?ids=$ids'; + try { + final response = await http.get( + Uri.parse(endpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + if (response.statusCode == 200) { + final audioFeatures = jsonDecode(response.body); + print('Audio features fetched:'); + //print(audioFeatures); + return List>.from(audioFeatures['audio_features']); + } else { + print('Failed to fetch audio features: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error: $e'); + return null; + } + } + + Future calculateAggregateMood(String playlistId) async { + final playlistTracks = await fetchPlaylistTracks(playlistId); + if (playlistTracks == null || playlistTracks['items'] == null) { + return 'Neutral'; + } + + final trackIds = (playlistTracks['items'] as List).map(( + item) => item['track']['id'] as String).toList(); + final audioFeatures = await fetchTrackAudioFeatures(trackIds); + if (audioFeatures == null || audioFeatures.isEmpty) { + return 'Neutral'; + } + + double avgValence = audioFeatures.map(( + feature) => feature['valence'] as double).reduce((a, b) => a + b) / + audioFeatures.length; + double avgEnergy = audioFeatures.map(( + feature) => feature['energy'] as double).reduce((a, b) => a + b) / + audioFeatures.length; + double avgDanceability = audioFeatures.map(( + feature) => feature['danceability'] as double).reduce((a, b) => a + b) / + audioFeatures.length; + double avgAcousticness = audioFeatures.map(( + feature) => feature['acousticness'] as double).reduce((a, b) => a + b) / + audioFeatures.length; + + // Define stricter thresholds for moods + const double happyValenceThreshold = 0.6; + const double angryValenceThreshold = 0.3; + const double sadValenceThreshold = 0.2; + const double surprisedValenceThreshold = 0.8; + + const double highThreshold = 0.5; + const double lowThreshold = 0.5; + const double veryLowThreshold = 0.2; + + // Determine mood based on average values + if (avgValence > happyValenceThreshold && avgEnergy > highThreshold && + avgDanceability > highThreshold && avgAcousticness < lowThreshold) { + return 'Happy'; + } else + if (avgValence < angryValenceThreshold && avgEnergy > highThreshold && + avgDanceability > highThreshold && avgAcousticness < lowThreshold) { + return 'Angry'; + } else if (avgValence < sadValenceThreshold && avgEnergy < lowThreshold && + avgDanceability < lowThreshold && avgAcousticness > highThreshold) { + return 'Sad'; + } else + if (avgValence > surprisedValenceThreshold && avgEnergy > highThreshold && + avgDanceability > highThreshold && avgAcousticness < lowThreshold) { + return 'Surprised'; + } else { + // Check for neutral only if the playlist is perfectly balanced + bool isPerfectlyBalanced = + (avgValence > (sadValenceThreshold - 0.00005) && + avgValence < (happyValenceThreshold + 0.00005)) && + (avgEnergy > (lowThreshold - 0.00005) && + avgEnergy < (highThreshold + 0.00005)) && + (avgDanceability > (lowThreshold - 0.00005) && + avgDanceability < (highThreshold + 0.00005)) && + (avgAcousticness > (veryLowThreshold - 0.00005) && + avgAcousticness < (highThreshold + 0.00005)); + + + if (isPerfectlyBalanced) { + return 'Neutral'; + } else { + // Classify based on closest matching mood if not perfectly balanced + return _classifyMood( + avgValence, avgEnergy, avgDanceability, avgAcousticness); + } + } + } + +// Helper function to classify based on closest mood + String _classifyMood(double valence, double energy, double danceability, + double acousticness) { + if (valence > 0.7 && energy > 0.7 && danceability > 0.7 && + acousticness < 0.5) { + return 'Happy'; + } else if (valence < 0.3 && energy > 0.7 && danceability > 0.7 && + acousticness < 0.5) { + return 'Angry'; + } else if (valence < 0.3 && energy < 0.3 && danceability < 0.3 && + acousticness > 0.7) { + return 'Sad'; + } else if (valence > 0.8 && energy > 0.8 && danceability > 0.8 && + acousticness < 0.5) { + return 'Surprised'; + } else if (valence > 0.5 && energy > 0.5 && danceability < 0.5 && + acousticness < 0.5) { + return 'Angry'; // Example additional mood + } else if (valence < 0.5 && energy > 0.5 && danceability < 0.5 && + acousticness < 0.5) { + return 'Angry'; // Example additional mood + } else if (valence < 0.5 && energy < 0.5 && danceability > 0.5 && + acousticness < 0.5) { + return 'Sad'; // Example additional mood + } else if (valence > 0.5 && energy < 0.5 && danceability < 0.5 && + acousticness < 0.5) { + return 'Sad'; // Example additional mood + } else { + return 'Sad'; // For any cases not covered by the above conditions + } + } + + + Future>> fetchUserTopArtistsAndTracks() async { + if (_accessToken == null) { + print('Access token is not available'); + return {}; + } + + final String topArtistsEndpoint = 'https://api.spotify.com/v1/me/top/artists'; + + try { + final topArtistsResponse = await http.get( + Uri.parse('$topArtistsEndpoint?limit=10'), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (topArtistsResponse.statusCode == 200) { + // Parse the top artists + final Map artistsData = jsonDecode(topArtistsResponse.body); + + final List artists = artistsData['items']; + + // If no genre is selected, pick a random artist + final Random random = Random(); + final int randomIndex = random.nextInt(artists.length); + final Map randomArtist = artists[randomIndex]; + + // If user does not listen to that Genre + bool flag = false; + + // Store the chosen artist data + Map chosenArtist = {}; + + // Handle genre selection logic + if (selectedGenres.isNotEmpty) { + for (Map artist in artists) { + if (artist['genres'].contains(selectedGenres.toLowerCase())) { + chosenArtist = artist; + flag = true; // Will be true if a genre match is found + break; + } + } + } + + // If no genre was selected or no match was found, pick the random artist + if (!flag) { + chosenArtist = randomArtist; + } + + // Determine artistId based on selected genre or chosen artist + String artistId; + if (selectedGenres.isNotEmpty && !flag) { + // Use the base seed for specific genres if no matching artist is found + switch (selectedGenres.toLowerCase()) { + case "alternative": + artistId = "3AA28KZvwAUcZuOKwyblJQ"; + break; + case "classical": + artistId = "4NJhFmfw43RLBLjQvxDuRS"; + break; + case "country": + artistId = "40ZNYROS4zLfyyBSs2PGe2"; + break; + case "hip hop": + artistId = "3TVXtAsR1Inumwj472S9r4"; + break; + case "jazz": + artistId = "19eLuQmk9aCobbVDHc6eek"; + break; + case "pop": + artistId = "6jJ0s89eD6GaHleKKya26X"; + break; + case "r&b": + artistId = "23zg3TcAtWQy7J6upgbUnj"; + break; + case "reggae": + artistId = "2QsynagSdAqZj3U9HgDzjD"; + break; + case "rock": + artistId = "36QJpDe2go2KgaRleHCDTp"; + break; + default: + artistId = randomArtist['id']; + } + } else { + // Use the chosen artist's ID + artistId = chosenArtist['id']; + } + + addArtist(chosenArtist['name']); + + print("Artist ID:"); + print(artistId); + + // Fetch the artist's top tracks + final String topTracksEndpoint = 'https://api.spotify.com/v1/artists/$artistId/top-tracks'; + + print("After Endpoint"); + + final topTracksResponse = await http.get( + Uri.parse(topTracksEndpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (topTracksResponse.statusCode == 200) { + // Parse the top tracks + final Map tracksData = jsonDecode(topTracksResponse.body); + final List trackIds = []; + for (var track in tracksData['tracks']) { + trackIds.add(track['id']); + } + + // Return the combined object + return { + 'artistId': [artistId], // Wrapping artistId in a List to maintain consistency + 'genres': [selectedGenres], + 'topTracks': trackIds, + }; + } else { + print('Failed to fetch top tracks for artist'); + print(topTracksResponse.statusCode); + return {}; + } + } else { + print('Failed to fetch top artists'); + return {}; + } + } catch (e) { + print('Error occurred: $e'); + return {}; + } + } + + void addArtist( String artist) async{ + realtimeArtists.add(artist); + + + } + + //Filter the Top Artists Tracks based on the mood + Future> moodOfTrackIDs({ + required List tracks, + required String mood, + }) async{ + List moodCheckedTracks = []; + + try{ + + for(String track in tracks){ + String trackID = track; + final String audioFeaturesEndpoint = "https://api.spotify.com/v1/audio-features/$trackID"; + + final audioFeaturesResponse = await http.get( + Uri.parse(audioFeaturesEndpoint), + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (audioFeaturesResponse.statusCode == 200) { + final Map tracksData = jsonDecode(audioFeaturesResponse.body); + + if(tracksData['valence'] < 0.4 && mood.toLowerCase() == "sad"){ + moodCheckedTracks.add(trackID); + } + + if(tracksData['valence'] > 0.6 && mood.toLowerCase() == "happy"){ + moodCheckedTracks.add(trackID); + } + + if(mood.toLowerCase() == "angry" && tracksData['energy'] > 0.6){ + moodCheckedTracks.add(trackID); + } + + else if(mood.toLowerCase() == "neutral" && tracksData['valence'] > 0.4 && tracksData['valence'] < 0.6){ + moodCheckedTracks.add(trackID); + } + } + else{ + break; + } + } + }catch(e){ + print('Error Occurred $e'); + } + + return moodCheckedTracks; + } + + // Non-RealTime Picture Upload Playlist Recommendations + Future> getSpotifyRecommendations({ + required Map> topArtistsAndTracks, + required double valence, + required double energy, + required String mood, + }) async { + if (_accessToken == null) { + print('Access token is not available'); + return []; + } + + final String recommendationsEndpoint = 'https://api.spotify.com/v1/recommendations'; + + // Prepare seed artists, tracks, and genres + final List seedArtists = topArtistsAndTracks['artistId'] ?? []; + final List seedTracks = topArtistsAndTracks['topTracks'] ?? []; + final List genres = topArtistsAndTracks['genres'] ?? []; + + if (seedArtists.isEmpty || genres.isEmpty || seedTracks.isEmpty) { + print('Insufficient data to fetch recommendations'); + return []; + } + + // Shuffle and select necessary items + genres.shuffle(); + seedTracks.shuffle(); + + // Take only 1 artist, 2 genres, and 2 tracks + final List seedArtistsLimited = seedArtists.take(1).toList(); + final List seedGenresLimited = genres.take(2).toList(); + final List seedTracksLimited = seedTracks.take(2).toList(); + + if(selectedGenres == ''){ + selectedGenres = seedGenresLimited.first; + } + + // Construct the query parameters + final Map queryParams = { + 'limit': '50', + 'market':'ZA', + 'seed_artists': seedArtistsLimited.first, // We only have one artist + 'seed_genres': selectedGenres.toLowerCase(), + 'seed_tracks': seedTracksLimited.join(','), + }; + + //Setting Min/Max for Moods + if(mood.toLowerCase() == "happy"){ + Map queryParamsHappy = { + 'min_valence':valence.toString(), + }; + queryParams.addAll(queryParamsHappy); + } + if(mood.toLowerCase() == "sad"){ + Map queryParamsSad = { + 'max_valence': valence.toString(), + }; + queryParams.addAll(queryParamsSad); + } + if(mood.toLowerCase() == "angry"){ + Map queryParamsAngry = { + 'min_energy':energy.toString(), + }; + queryParams.addAll(queryParamsAngry); + } + if(mood.toLowerCase() == "neutral"){ + double minV = valence - 0.1; + double maxV = valence + 0.1; + + + // double minE = energy - 0.1; + // double maxE = energy + 0.1; + + + Map queryParamsNeutral = { + 'min_valence':minV.toString(), + 'max_valence': maxV.toString(), + }; + queryParams.addAll(queryParamsNeutral); + } + + print("Query parameters for fetching recommendations:"); + print(queryParams); + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + final List recommendedTrackIds = []; + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + return recommendedTrackIds; + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + return []; + } + } catch (e) { + print('Error: $e'); + return []; + } + } + + void setSelectedGenres(List genres){ + print('Set the Selected Genres'); + print(genres); + selectedGenres = genres.first; + } + + //RealTime Recommendations + Future> realTimeGetSpotifyRecommendations({ + required List moods, + required Map> topArtistsTracksGenres, + }) async{ + Map> topArtistsAndTracks = await fetchUserTopArtistsAndTracks(); + print(topArtistsAndTracks.toString()); + print(moods.toString()); + final List seedArtists = topArtistsAndTracks['artistId'] ?? []; + final List seedTracks = topArtistsAndTracks['topTracks'] ?? []; + final List genres = topArtistsAndTracks['genres'] ?? []; + + final List seedArtistsLimited = seedArtists.take(1).toList(); + final List seedGenresLimited = genres.take(2).toList(); + final List seedTracksLimited = seedTracks.take(2).toList(); + + final String recommendationsEndpoint = 'https://api.spotify.com/v1/recommendations'; + // final Map recommendationsData = {}; + final List recommendedTrackIds = []; + + if(selectedGenres == ''){ + selectedGenres = seedGenresLimited.first; + } + + String tempMood = moods.removeLast(); + //Generate songs by the mood + for(String m in moods){ + //If Sad + if(m.toLowerCase() == "sad"){ + //Query Parameters for a Sad Playlist + final Map queryParams = { + 'limit': '15', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'max_valence':"0.4", + }; + //Uri + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + print(uri.toString()); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + //Adding the Tracks to + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + // return []; + } + } catch (e) { + print('Error: $e'); + // return []; + } + } + if(m.toLowerCase() == "angry"){ + final Map queryParams = { + 'limit': '15', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':'0.6', + 'min_energy':'0.6', + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + if(m.toLowerCase() == "happy"){ + final Map queryParams = { + 'limit': '15', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':"0.6", + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + if(m.toLowerCase() == "neutral"){ + final Map queryParams = { + 'limit': '15', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':"0.4", + 'max_valence':"0.6", + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + + // FOR AUDIO + + if(tempMood.toLowerCase() == "sad"){ + //Query Parameters for a Sad Playlist + final Map queryParams = { + 'limit': '5', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'max_valence':"0.4", + }; + //Uri + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + print(uri.toString()); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + //Adding the Tracks to + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + // return []; + } + } catch (e) { + print('Error: $e'); + // return []; + } + } + if(tempMood.toLowerCase() == "angry"){ + final Map queryParams = { + 'limit': '5', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':'0.6', + 'min_energy':'0.6', + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + if(tempMood.toLowerCase() == "happy"){ + final Map queryParams = { + 'limit': '5', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':"0.6", + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + if(tempMood.toLowerCase() == "neutral"){ + final Map queryParams = { + 'limit': '5', + 'seed_artists': seedArtistsLimited.first, + 'seed_genres': selectedGenres, + 'seed_tracks': seedTracksLimited.first, + 'min_valence':"0.4", + 'max_valence':"0.6", + }; + + final Uri uri = Uri.parse(recommendationsEndpoint).replace(queryParameters: queryParams); + + try { + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $_accessToken'}, + ); + + if (response.statusCode == 200) { + final Map recommendationsData = jsonDecode(response.body); + + for (var track in recommendationsData['tracks']) { + recommendedTrackIds.add(track['id']); + } + + } else { + print(response.reasonPhrase); + print('Failed to fetch recommendations'); + } + } catch (e) { + print('Error: $e'); + } + } + } + return recommendedTrackIds; + } + + // Convert mood to a valence value + double moodToValence(String mood) { + switch (mood.toLowerCase()) { + case 'happy': + return 0.8; + case 'sad': + return 0.2; + case 'angry': + return 1.0; + case 'neutral': + return 0.5; + + + default: + return 0.5; // Neutral + } + } + + Map songParameters(String mood/*, String genres*/){ + double valence = 0.5; + double energy = 0.5; + + if(mood.toLowerCase() == "happy"){ + //All Min's + valence = 0.6; + energy = 0.6; + + } + if(mood.toLowerCase() == "sad"){ + //All Max's + valence = 0.4; + energy = 0.4; + } + if(mood.toLowerCase() == "angry"){ + //All Minimums + valence = 0.3; + energy = 0.6; + } + if(mood.toLowerCase() == "Neutral"){ + valence = 0.5; + energy = 0.5; + } + + return { + 'valence': valence, + 'energy': energy, + }; + + } + + // Function to create and populate playlist with recommendations + Future createAndPopulatePlaylistWithRecommendations( + String playlistName, + String mood + ) async { + if (_accessToken == null) { + print('Access token is not available'); + return; + } + + //Commented this out to Reduce Calls to fetchUserTopArtistAndTracks() + // Fetch top artists and tracks for seeds + + // final seeds = await fetchUserTopArtistsAndTracks(); + // + // if (seeds.isEmpty) { + // print('No seeds available for recommendations'); + // return; + // } + + // Generate recommendations based on mood + Map> topArtistsAndTracks = await fetchUserTopArtistsAndTracks(); + if(topArtistsAndTracks.isEmpty){ + print('No seeds available for recommendations'); + return; + } + + + Map params = songParameters(mood); + double? valence = params['valence']; + double? energy = params['energy']; + + List recommendedTracks = await getSpotifyRecommendations( + topArtistsAndTracks: topArtistsAndTracks, + valence: valence!, + energy: energy!, + mood: mood, + ); + + + // Create and populate the playlist + await createAndPopulatePlaylist(playlistName, mood, recommendedTracks); + } + + Future realTimeCreateAndPopulatePlaylistWithRecommendations( + String playlistName, + List moods, + ) async { + if (_accessToken == null) { + print('Access token is not available'); + return; + } + + // Make a copy of moods to avoid potential mutation issues + List moodsCopy = List.from(moods); + + // Fetch top artists and tracks for seeds + await fetchUserTopArtistsAndTracks().then((topArtistsAndTracks) async { + if (topArtistsAndTracks.isEmpty) { + print('No seeds available for recommendations'); + return; + } + + // Generate recommendations based on mood + await realTimeGetSpotifyRecommendations( + moods: moodsCopy, // Pass the copy instead + topArtistsTracksGenres: topArtistsAndTracks, + ).then((recommendedTracks) async { + if (recommendedTracks.isEmpty) { + print('No recommendations found.'); + return; + } + + // Create and populate the playlist + print(recommendedTracks.toString()); + await createAndPopulatePlaylist(playlistName, "mixed", recommendedTracks).then((_) { + print('Playlist created and populated successfully.'); + }).catchError((error) { + print('Error creating or populating playlist: $error'); + }); + }).catchError((error) { + print('Error fetching recommendations: $error'); + }); + }).catchError((error) { + print('Error fetching top artists and tracks: $error'); + }); + } + + + + // Function to create and populate playlist + Future createAndPopulatePlaylist( + String playlistName, + String mood, + List trackUris + ) async { + if (_accessToken == null) { + print('Access token is not available'); + return; + } + + if (currentUser == null) { + print('User details not available'); + return; + } + + final String userId = currentUser!.id; + final String createPlaylistEndpoint = 'https://api.spotify.com/v1/users/$userId/playlists'; + final String userName = currentUser!.displayName; + final String artistsNames = realtimeArtists.join(" , "); + final Map requestBody = { + 'name': '$userName - $mood', + 'description': 'a $mood playlist made and curated by MoodMix based off the Artist : $artistsNames ', + 'public': true, + }; + + try { + final createPlaylistResponse = await http.post( + Uri.parse(createPlaylistEndpoint), + headers: { + 'Authorization': 'Bearer $_accessToken', + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + + if (createPlaylistResponse.statusCode == 201) { + print('Playlist created successfully'); + realtimeArtists.clear(); + final Map playlistDetails = jsonDecode(createPlaylistResponse.body); + final String playlistId = playlistDetails['id']; + + String now() { + return DateFormat('yyyy-MM-dd').format(DateTime.now()); + } + + Map playlistData = { + 'playlistId': playlistId, + 'mood': mood, + 'userId': userId, + 'dateCreated': now(), + }; + await instance.insertPlaylist(playlistData); + + + + + await addTracksToPlaylist(playlistId, trackUris); + } else { + print('Failed to create playlist: ${createPlaylistResponse.statusCode}'); + print('Response body: ${createPlaylistResponse.body}'); + } + } catch (e) { + print('Error: $e'); + } + } + + // Function to add tracks to a playlist + Future addTracksToPlaylist(String playlistId, List trackUris) async { + if (_accessToken == null) { + print('Access token is not available'); + return; + } + + final String addTracksEndpoint = 'https://api.spotify.com/v1/playlists/$playlistId/tracks'; + + List finalTrackIds = trackUris.map((id) => 'spotify:track:$id').toList(); + + final Map requestBody = { + 'uris': finalTrackIds, + }; + + try { + final addTracksResponse = await http.post( + Uri.parse(addTracksEndpoint), + headers: { + 'Authorization': 'Bearer $_accessToken', + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + + if (addTracksResponse.statusCode == 201 || addTracksResponse.statusCode == 200) { + print('Tracks added successfully to playlist'); + } else { + print('Failed to add tracks: ${addTracksResponse.statusCode}'); + print('Response body: ${addTracksResponse.body}'); + } + } catch (e) { + print('Error: $e'); + } + } +} \ No newline at end of file diff --git a/frontend/test/user_playlist_test.dart b/frontend/test/user_playlist_test.dart new file mode 100644 index 0000000..d379c2b --- /dev/null +++ b/frontend/test/user_playlist_test.dart @@ -0,0 +1,67 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:frontend/pages/user_profile.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'model/auth_service_test.mocks.dart'; +import 'spotify_auth_non_static.dart'; +import 'package:provider/provider.dart'; // Assuming you use Provider for AuthService + +import 'user_profile_test.mocks.dart'; // Generated mock class for SpotifyAuth + +@GenerateMocks([SpotifyAuthNonStatic, AuthService]) +void main() { + late MockAuthService mockAuthService; + late MockSpotifyAuthNonStatic mockSpotifyAuth; + + setUp(() { + mockAuthService = MockAuthService(); + mockSpotifyAuth = MockSpotifyAuthNonStatic(); + }); + + group('User Profile Tests', () { + test('should authenticate user using SpotifyAuthNonStatic', () async { + // Arrange + when(mockSpotifyAuth.authenticate()).thenAnswer((_) async => 'user_token'); + + // Act + final token = await mockSpotifyAuth.authenticate(); + + // Assert + expect(token, equals('user_token')); + verify(mockSpotifyAuth.authenticate()).called(1); + }); + + test('should fetch Spotify user playlists', () async { + // Arrange + when(mockSpotifyAuth.fetchUserPlaylists('user123')) + .thenAnswer((_) async => [ + {'id': 'playlist1', 'name': 'My Playlist'}, + {'id': 'playlist2', 'name': 'Another Playlist'}, + ]); + + // Act + final playlists = await mockSpotifyAuth.fetchUserPlaylists('user123'); + + // Assert + expect(playlists?.length, 2); + expect(playlists?[0]['name'], 'My Playlist'); + verify(mockSpotifyAuth.fetchUserPlaylists('user123')).called(1); + }); + + test('should get current Firebase user from AuthService', () async { + // Arrange + final mockFirebaseUser = MockUser(); + when(mockAuthService.getCurrentUser()).thenAnswer((_) async => mockFirebaseUser); + + // Act + final user = await mockAuthService.getCurrentUser(); + + // Assert + expect(user, isA()); + verify(mockAuthService.getCurrentUser()).called(1); + }); + }); +} \ No newline at end of file diff --git a/frontend/test/user_playlist_test.mocks.dart b/frontend/test/user_playlist_test.mocks.dart new file mode 100644 index 0000000..1f0b1f3 --- /dev/null +++ b/frontend/test/user_playlist_test.mocks.dart @@ -0,0 +1,398 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/user_playlist_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:firebase_auth/firebase_auth.dart' as _i6; +import 'package:frontend/auth/auth_service.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +import 'spotify_auth_non_static.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [SpotifyAuthNonStatic]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSpotifyAuthNonStatic extends _i1.Mock + implements _i2.SpotifyAuthNonStatic { + MockSpotifyAuthNonStatic() { + _i1.throwOnMissingStub(this); + } + + @override + set currentUser(_i3.SpotifyUser? _currentUser) => super.noSuchMethod( + Invocation.setter( + #currentUser, + _currentUser, + ), + returnValueForMissingStub: null, + ); + + @override + String get selectedGenres => (super.noSuchMethod( + Invocation.getter(#selectedGenres), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#selectedGenres), + ), + ) as String); + + @override + set selectedGenres(String? _selectedGenres) => super.noSuchMethod( + Invocation.setter( + #selectedGenres, + _selectedGenres, + ), + returnValueForMissingStub: null, + ); + + @override + List get realtimeArtists => (super.noSuchMethod( + Invocation.getter(#realtimeArtists), + returnValue: [], + ) as List); + + @override + set realtimeArtists(List? _realtimeArtists) => super.noSuchMethod( + Invocation.setter( + #realtimeArtists, + _realtimeArtists, + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future authenticate() => (super.noSuchMethod( + Invocation.method( + #authenticate, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + void initialize(dynamic Function(String)? onSuccessCallback) => + super.noSuchMethod( + Invocation.method( + #initialize, + [onSuccessCallback], + ), + returnValueForMissingStub: null, + ); + + @override + void setAccessToken(String? a) => super.noSuchMethod( + Invocation.method( + #setAccessToken, + [a], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future?> fetchUserDetails() => (super.noSuchMethod( + Invocation.method( + #fetchUserDetails, + [], + ), + returnValue: _i5.Future?>.value(), + ) as _i5.Future?>); + + @override + _i5.Future>?> fetchUserPlaylists(String? userId) => + (super.noSuchMethod( + Invocation.method( + #fetchUserPlaylists, + [userId], + ), + returnValue: _i5.Future>?>.value(), + ) as _i5.Future>?>); + + @override + _i5.Future?> fetchPlaylistTracks(String? playlistId) => + (super.noSuchMethod( + Invocation.method( + #fetchPlaylistTracks, + [playlistId], + ), + returnValue: _i5.Future?>.value(), + ) as _i5.Future?>); + + @override + _i5.Future>?> fetchTrackAudioFeatures( + List? trackIds) => + (super.noSuchMethod( + Invocation.method( + #fetchTrackAudioFeatures, + [trackIds], + ), + returnValue: _i5.Future>?>.value(), + ) as _i5.Future>?>); + + @override + _i5.Future calculateAggregateMood(String? playlistId) => + (super.noSuchMethod( + Invocation.method( + #calculateAggregateMood, + [playlistId], + ), + returnValue: _i5.Future.value(_i4.dummyValue( + this, + Invocation.method( + #calculateAggregateMood, + [playlistId], + ), + )), + ) as _i5.Future); + + @override + _i5.Future>> fetchUserTopArtistsAndTracks() => + (super.noSuchMethod( + Invocation.method( + #fetchUserTopArtistsAndTracks, + [], + ), + returnValue: _i5.Future>>.value( + >{}), + ) as _i5.Future>>); + + @override + void addArtist(String? artist) => super.noSuchMethod( + Invocation.method( + #addArtist, + [artist], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future> moodOfTrackIDs({ + required List? tracks, + required String? mood, + }) => + (super.noSuchMethod( + Invocation.method( + #moodOfTrackIDs, + [], + { + #tracks: tracks, + #mood: mood, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + _i5.Future> getSpotifyRecommendations({ + required Map>? topArtistsAndTracks, + required double? valence, + required double? energy, + required String? mood, + }) => + (super.noSuchMethod( + Invocation.method( + #getSpotifyRecommendations, + [], + { + #topArtistsAndTracks: topArtistsAndTracks, + #valence: valence, + #energy: energy, + #mood: mood, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + void setSelectedGenres(List? genres) => super.noSuchMethod( + Invocation.method( + #setSelectedGenres, + [genres], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future> realTimeGetSpotifyRecommendations({ + required List? moods, + required Map>? topArtistsTracksGenres, + }) => + (super.noSuchMethod( + Invocation.method( + #realTimeGetSpotifyRecommendations, + [], + { + #moods: moods, + #topArtistsTracksGenres: topArtistsTracksGenres, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + double moodToValence(String? mood) => (super.noSuchMethod( + Invocation.method( + #moodToValence, + [mood], + ), + returnValue: 0.0, + ) as double); + + @override + Map songParameters(String? mood) => (super.noSuchMethod( + Invocation.method( + #songParameters, + [mood], + ), + returnValue: {}, + ) as Map); + + @override + _i5.Future createAndPopulatePlaylistWithRecommendations( + String? playlistName, + String? mood, + ) => + (super.noSuchMethod( + Invocation.method( + #createAndPopulatePlaylistWithRecommendations, + [ + playlistName, + mood, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future realTimeCreateAndPopulatePlaylistWithRecommendations( + String? playlistName, + List? moods, + ) => + (super.noSuchMethod( + Invocation.method( + #realTimeCreateAndPopulatePlaylistWithRecommendations, + [ + playlistName, + moods, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future createAndPopulatePlaylist( + String? playlistName, + String? mood, + List? trackUris, + ) => + (super.noSuchMethod( + Invocation.method( + #createAndPopulatePlaylist, + [ + playlistName, + mood, + trackUris, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future addTracksToPlaylist( + String? playlistId, + List? trackUris, + ) => + (super.noSuchMethod( + Invocation.method( + #addTracksToPlaylist, + [ + playlistId, + trackUris, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [AuthService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthService extends _i1.Mock implements _i3.AuthService { + MockAuthService() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future registration({ + required String? email, + required String? password, + required String? username, + }) => + (super.noSuchMethod( + Invocation.method( + #registration, + [], + { + #email: email, + #password: password, + #username: username, + }, + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future login({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future sendPasswordResetEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [email], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i6.User?> getCurrentUser() => (super.noSuchMethod( + Invocation.method( + #getCurrentUser, + [], + ), + returnValue: _i5.Future<_i6.User?>.value(), + ) as _i5.Future<_i6.User?>); +} diff --git a/frontend/test/user_profile_test.dart b/frontend/test/user_profile_test.dart new file mode 100644 index 0000000..3f6d990 --- /dev/null +++ b/frontend/test/user_profile_test.dart @@ -0,0 +1,80 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/auth/auth_service.dart'; +import 'package:frontend/pages/user_profile.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'model/auth_service_test.mocks.dart'; +import 'spotify_auth_non_static.dart'; +import 'package:provider/provider.dart'; // Assuming you use Provider for AuthService + +import 'user_profile_test.mocks.dart'; // Generated mock class for SpotifyAuth + +@GenerateMocks([SpotifyAuthNonStatic, AuthService]) +void main() { + late MockAuthService mockAuthService; + late MockSpotifyAuthNonStatic mockSpotifyAuth; + + setUp(() { + mockAuthService = MockAuthService(); + mockSpotifyAuth = MockSpotifyAuthNonStatic(); + }); + + group('User Profile Tests', () { + test('should authenticate user using SpotifyAuthNonStatic', () async { + // Arrange + when(mockSpotifyAuth.authenticate()).thenAnswer((_) async => 'user_token'); + + // Act + final token = await mockSpotifyAuth.authenticate(); + + // Assert + expect(token, equals('user_token')); + verify(mockSpotifyAuth.authenticate()).called(1); + }); + + test('should fetch Spotify user playlists', () async { + // Arrange + when(mockSpotifyAuth.fetchUserPlaylists('user123')) + .thenAnswer((_) async => [ + {'id': 'playlist1', 'name': 'My Playlist'}, + {'id': 'playlist2', 'name': 'Another Playlist'}, + ]); + + // Act + final playlists = await mockSpotifyAuth.fetchUserPlaylists('user123'); + + // Assert + expect(playlists?.length, 2); + expect(playlists?[0]['name'], 'My Playlist'); + verify(mockSpotifyAuth.fetchUserPlaylists('user123')).called(1); + }); + + test('should get current Firebase user from AuthService', () async { + // Arrange + final mockFirebaseUser = MockUser(); + when(mockAuthService.getCurrentUser()).thenAnswer((_) async => mockFirebaseUser); + + // Act + final user = await mockAuthService.getCurrentUser(); + + // Assert + expect(user, isA()); + verify(mockAuthService.getCurrentUser()).called(1); + }); + + test('should send password reset email', () async { + // Arrange + when(mockAuthService.sendPasswordResetEmail('test@example.com')) + .thenAnswer((_) async => 'email_sent'); + + // Act + final result = await mockAuthService.sendPasswordResetEmail('test@example.com'); + + // Assert + expect(result, 'email_sent'); + verify(mockAuthService.sendPasswordResetEmail('test@example.com')).called(1); + }); + }); +} \ No newline at end of file diff --git a/frontend/test/user_profile_test.mocks.dart b/frontend/test/user_profile_test.mocks.dart new file mode 100644 index 0000000..0af41b6 --- /dev/null +++ b/frontend/test/user_profile_test.mocks.dart @@ -0,0 +1,398 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in frontend/test/user_profile_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:firebase_auth/firebase_auth.dart' as _i6; +import 'package:frontend/auth/auth_service.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +import 'spotify_auth_non_static.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [SpotifyAuthNonStatic]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSpotifyAuthNonStatic extends _i1.Mock + implements _i2.SpotifyAuthNonStatic { + MockSpotifyAuthNonStatic() { + _i1.throwOnMissingStub(this); + } + + @override + set currentUser(_i3.SpotifyUser? _currentUser) => super.noSuchMethod( + Invocation.setter( + #currentUser, + _currentUser, + ), + returnValueForMissingStub: null, + ); + + @override + String get selectedGenres => (super.noSuchMethod( + Invocation.getter(#selectedGenres), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#selectedGenres), + ), + ) as String); + + @override + set selectedGenres(String? _selectedGenres) => super.noSuchMethod( + Invocation.setter( + #selectedGenres, + _selectedGenres, + ), + returnValueForMissingStub: null, + ); + + @override + List get realtimeArtists => (super.noSuchMethod( + Invocation.getter(#realtimeArtists), + returnValue: [], + ) as List); + + @override + set realtimeArtists(List? _realtimeArtists) => super.noSuchMethod( + Invocation.setter( + #realtimeArtists, + _realtimeArtists, + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future authenticate() => (super.noSuchMethod( + Invocation.method( + #authenticate, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + void initialize(dynamic Function(String)? onSuccessCallback) => + super.noSuchMethod( + Invocation.method( + #initialize, + [onSuccessCallback], + ), + returnValueForMissingStub: null, + ); + + @override + void setAccessToken(String? a) => super.noSuchMethod( + Invocation.method( + #setAccessToken, + [a], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future?> fetchUserDetails() => (super.noSuchMethod( + Invocation.method( + #fetchUserDetails, + [], + ), + returnValue: _i5.Future?>.value(), + ) as _i5.Future?>); + + @override + _i5.Future>?> fetchUserPlaylists(String? userId) => + (super.noSuchMethod( + Invocation.method( + #fetchUserPlaylists, + [userId], + ), + returnValue: _i5.Future>?>.value(), + ) as _i5.Future>?>); + + @override + _i5.Future?> fetchPlaylistTracks(String? playlistId) => + (super.noSuchMethod( + Invocation.method( + #fetchPlaylistTracks, + [playlistId], + ), + returnValue: _i5.Future?>.value(), + ) as _i5.Future?>); + + @override + _i5.Future>?> fetchTrackAudioFeatures( + List? trackIds) => + (super.noSuchMethod( + Invocation.method( + #fetchTrackAudioFeatures, + [trackIds], + ), + returnValue: _i5.Future>?>.value(), + ) as _i5.Future>?>); + + @override + _i5.Future calculateAggregateMood(String? playlistId) => + (super.noSuchMethod( + Invocation.method( + #calculateAggregateMood, + [playlistId], + ), + returnValue: _i5.Future.value(_i4.dummyValue( + this, + Invocation.method( + #calculateAggregateMood, + [playlistId], + ), + )), + ) as _i5.Future); + + @override + _i5.Future>> fetchUserTopArtistsAndTracks() => + (super.noSuchMethod( + Invocation.method( + #fetchUserTopArtistsAndTracks, + [], + ), + returnValue: _i5.Future>>.value( + >{}), + ) as _i5.Future>>); + + @override + void addArtist(String? artist) => super.noSuchMethod( + Invocation.method( + #addArtist, + [artist], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future> moodOfTrackIDs({ + required List? tracks, + required String? mood, + }) => + (super.noSuchMethod( + Invocation.method( + #moodOfTrackIDs, + [], + { + #tracks: tracks, + #mood: mood, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + _i5.Future> getSpotifyRecommendations({ + required Map>? topArtistsAndTracks, + required double? valence, + required double? energy, + required String? mood, + }) => + (super.noSuchMethod( + Invocation.method( + #getSpotifyRecommendations, + [], + { + #topArtistsAndTracks: topArtistsAndTracks, + #valence: valence, + #energy: energy, + #mood: mood, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + void setSelectedGenres(List? genres) => super.noSuchMethod( + Invocation.method( + #setSelectedGenres, + [genres], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future> realTimeGetSpotifyRecommendations({ + required List? moods, + required Map>? topArtistsTracksGenres, + }) => + (super.noSuchMethod( + Invocation.method( + #realTimeGetSpotifyRecommendations, + [], + { + #moods: moods, + #topArtistsTracksGenres: topArtistsTracksGenres, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + double moodToValence(String? mood) => (super.noSuchMethod( + Invocation.method( + #moodToValence, + [mood], + ), + returnValue: 0.0, + ) as double); + + @override + Map songParameters(String? mood) => (super.noSuchMethod( + Invocation.method( + #songParameters, + [mood], + ), + returnValue: {}, + ) as Map); + + @override + _i5.Future createAndPopulatePlaylistWithRecommendations( + String? playlistName, + String? mood, + ) => + (super.noSuchMethod( + Invocation.method( + #createAndPopulatePlaylistWithRecommendations, + [ + playlistName, + mood, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future realTimeCreateAndPopulatePlaylistWithRecommendations( + String? playlistName, + List? moods, + ) => + (super.noSuchMethod( + Invocation.method( + #realTimeCreateAndPopulatePlaylistWithRecommendations, + [ + playlistName, + moods, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future createAndPopulatePlaylist( + String? playlistName, + String? mood, + List? trackUris, + ) => + (super.noSuchMethod( + Invocation.method( + #createAndPopulatePlaylist, + [ + playlistName, + mood, + trackUris, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future addTracksToPlaylist( + String? playlistId, + List? trackUris, + ) => + (super.noSuchMethod( + Invocation.method( + #addTracksToPlaylist, + [ + playlistId, + trackUris, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [AuthService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthService extends _i1.Mock implements _i3.AuthService { + MockAuthService() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future registration({ + required String? email, + required String? password, + required String? username, + }) => + (super.noSuchMethod( + Invocation.method( + #registration, + [], + { + #email: email, + #password: password, + #username: username, + }, + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future login({ + required String? email, + required String? password, + }) => + (super.noSuchMethod( + Invocation.method( + #login, + [], + { + #email: email, + #password: password, + }, + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future sendPasswordResetEmail(String? email) => + (super.noSuchMethod( + Invocation.method( + #sendPasswordResetEmail, + [email], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i6.User?> getCurrentUser() => (super.noSuchMethod( + Invocation.method( + #getCurrentUser, + [], + ), + returnValue: _i5.Future<_i6.User?>.value(), + ) as _i5.Future<_i6.User?>); +} diff --git a/frontend/test/welcome_test.dart b/frontend/test/welcome_test.dart deleted file mode 100644 index 766a6e3..0000000 --- a/frontend/test/welcome_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:frontend/pages/welcome.dart'; // Import your Welcome widget - -void main() { - testWidgets('Welcome page UI test', (WidgetTester tester) async { - // Build the Welcome widget - await tester.pumpWidget(MaterialApp(home: Welcome())); - - // Expect to find the 'Sign Up' button - expect(find.byKey(Key('signupButton')), findsOneWidget); - - // Expect to find the 'Log In' button - expect(find.byKey(Key('loginButton')), findsOneWidget); - - // Expect to find the 'Terms and Conditions' text - expect(find.text('Terms and Conditions'), findsOneWidget); - }); -} diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index d7a3b70..d87425a 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowToFrontPluginRegisterWithRegistrar( diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index 8db8af9..ae20bc1 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_auth firebase_core flutter_secure_storage_windows + permission_handler_windows url_launcher_windows window_to_front )