[Compass App] User name and profile picture (#2435)

This PR completes the home screen, adding the username and the profile
picture.

![Screenshot from 2024-09-11
14-52-32](https://github.com/user-attachments/assets/197f9932-ae86-4277-92ac-8fd413b52010)
![Screenshot from 2024-09-11
14-52-23](https://github.com/user-attachments/assets/915b8c54-ac85-40d9-adb8-3bf0521b78f5)

This feature follows the basic structure:

- Added repository, both local and remote.
- Added API call + API model.
- Added Domain model (reduced version only containing name and profile
picture).
- Modified the ViewModel to obtain the user and expose it to the Widget.
- Updated Widget to display the username and profile picture.
- Added `/user` endpoint to server project.
- Updated widget tests, as well as integration tests.

The Compass App is basically completed with it. Maybe the next step is
to merge it to the `main` branch? And then we can do a full review,
remove TODOs and setup CI jobs.

## Pre-launch Checklist

- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I read the [Contributors Guide].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-devrel
channel on [Discord].

<!-- Links -->
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md
[CLA]: https://cla.developers.google.com/
[Discord]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md
[Contributors Guide]:
https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
pull/2437/head
Miguel Beltran 2 months ago committed by GitHub
parent 93b86b86f3
commit b00ce6c7b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@ -44,6 +44,9 @@ void main() {
expect(find.byType(HomeScreen), findsOneWidget); expect(find.byType(HomeScreen), findsOneWidget);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Should show user name
expect(find.text('Sofie\'s Trips'), findsOneWidget);
// Tap on booking (Alaska is created by default) // Tap on booking (Alaska is created by default)
await tester.tap(find.text('Alaska, North America')); await tester.tap(find.text('Alaska, North America'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

@ -84,6 +84,9 @@ void main() {
expect(find.byType(HomeScreen), findsOneWidget); expect(find.byType(HomeScreen), findsOneWidget);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Should show user name
expect(find.text('Sofie\'s Trips'), findsOneWidget);
// Tap on booking (Alaska is created by default) // Tap on booking (Alaska is created by default)
await tester.tap(find.text('Alaska, North America')); await tester.tap(find.text('Alaska, North America'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

@ -7,6 +7,9 @@ import '../data/repositories/auth/auth_repository_remote.dart';
import '../data/repositories/booking/booking_repository.dart'; import '../data/repositories/booking/booking_repository.dart';
import '../data/repositories/booking/booking_repository_local.dart'; import '../data/repositories/booking/booking_repository_local.dart';
import '../data/repositories/booking/booking_repository_remote.dart'; import '../data/repositories/booking/booking_repository_remote.dart';
import '../data/repositories/user/user_repository.dart';
import '../data/repositories/user/user_repository_local.dart';
import '../data/repositories/user/user_repository_remote.dart';
import '../data/services/api/auth_api_client.dart'; import '../data/services/api/auth_api_client.dart';
import '../data/services/local/local_data_service.dart'; import '../data/services/local/local_data_service.dart';
import '../data/services/shared_preferences_service.dart'; import '../data/services/shared_preferences_service.dart';
@ -84,6 +87,11 @@ List<SingleChildWidget> get providersRemote {
apiClient: context.read(), apiClient: context.read(),
) as BookingRepository, ) as BookingRepository,
), ),
Provider(
create: (context) => UserRepositoryRemote(
apiClient: context.read(),
) as UserRepository,
),
..._sharedProviders, ..._sharedProviders,
]; ];
} }
@ -122,6 +130,11 @@ List<SingleChildWidget> get providersLocal {
Provider.value( Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository, value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
), ),
Provider(
create: (context) => UserRepositoryLocal(
localDataService: context.read(),
) as UserRepository,
),
..._sharedProviders, ..._sharedProviders,
]; ];
} }

@ -0,0 +1,8 @@
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
/// Data source for user related data
abstract class UserRepository {
/// Get current user
Future<Result<User>> getUser();
}

@ -0,0 +1,17 @@
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'user_repository.dart';
class UserRepositoryLocal implements UserRepository {
UserRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
final LocalDataService _localDataService;
@override
Future<Result<User>> getUser() async {
return Result.ok(_localDataService.getUser());
}
}

@ -0,0 +1,35 @@
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import '../../services/api/model/user/user_api_model.dart';
import 'user_repository.dart';
class UserRepositoryRemote implements UserRepository {
UserRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
User? _cachedData;
@override
Future<Result<User>> getUser() async {
if (_cachedData != null) {
return Future.value(Result.ok(_cachedData!));
}
final result = await _apiClient.getUser();
switch (result) {
case Ok<UserApiModel>():
final user = User(
name: result.value.name,
picture: result.value.picture,
);
_cachedData = user;
return Result.ok(user);
case Error<UserApiModel>():
return Result.error(result.error);
}
}
}

@ -6,6 +6,7 @@ import '../../../domain/models/continent/continent.dart';
import '../../../domain/models/destination/destination.dart'; import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart'; import '../../../utils/result.dart';
import 'model/booking/booking_api_model.dart'; import 'model/booking/booking_api_model.dart';
import 'model/user/user_api_model.dart';
/// Adds the `Authentication` header to a header configuration. /// Adds the `Authentication` header to a header configuration.
typedef AuthHeaderProvider = String? Function(); typedef AuthHeaderProvider = String? Function();
@ -154,4 +155,24 @@ class ApiClient {
client.close(); client.close();
} }
} }
Future<Result<UserApiModel>> getUser() async {
final client = HttpClient();
try {
final request = await client.get('localhost', 8080, '/user');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final user = UserApiModel.fromJson(jsonDecode(stringData));
return Result.ok(user);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
} }

@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_api_model.freezed.dart';
part 'user_api_model.g.dart';
@freezed
abstract class UserApiModel with _$UserApiModel {
const factory UserApiModel({
/// The user's ID.
required String id,
/// The user's name.
required String name,
/// The user's email.
required String email,
/// The user's picture URL.
required String picture,
}) = _UserApiModel;
factory UserApiModel.fromJson(Map<String, Object?> json) =>
_$UserApiModelFromJson(json);
}

@ -0,0 +1,241 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_api_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
UserApiModel _$UserApiModelFromJson(Map<String, dynamic> json) {
return _UserApiModel.fromJson(json);
}
/// @nodoc
mixin _$UserApiModel {
/// The user's ID.
String get id => throw _privateConstructorUsedError;
/// The user's name.
String get name => throw _privateConstructorUsedError;
/// The user's email.
String get email => throw _privateConstructorUsedError;
/// The user's picture URL.
String get picture => throw _privateConstructorUsedError;
/// Serializes this UserApiModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserApiModelCopyWith<UserApiModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserApiModelCopyWith<$Res> {
factory $UserApiModelCopyWith(
UserApiModel value, $Res Function(UserApiModel) then) =
_$UserApiModelCopyWithImpl<$Res, UserApiModel>;
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class _$UserApiModelCopyWithImpl<$Res, $Val extends UserApiModel>
implements $UserApiModelCopyWith<$Res> {
_$UserApiModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserApiModelImplCopyWith<$Res>
implements $UserApiModelCopyWith<$Res> {
factory _$$UserApiModelImplCopyWith(
_$UserApiModelImpl value, $Res Function(_$UserApiModelImpl) then) =
__$$UserApiModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class __$$UserApiModelImplCopyWithImpl<$Res>
extends _$UserApiModelCopyWithImpl<$Res, _$UserApiModelImpl>
implements _$$UserApiModelImplCopyWith<$Res> {
__$$UserApiModelImplCopyWithImpl(
_$UserApiModelImpl _value, $Res Function(_$UserApiModelImpl) _then)
: super(_value, _then);
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_$UserApiModelImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserApiModelImpl implements _UserApiModel {
const _$UserApiModelImpl(
{required this.id,
required this.name,
required this.email,
required this.picture});
factory _$UserApiModelImpl.fromJson(Map<String, dynamic> json) =>
_$$UserApiModelImplFromJson(json);
/// The user's ID.
@override
final String id;
/// The user's name.
@override
final String name;
/// The user's email.
@override
final String email;
/// The user's picture URL.
@override
final String picture;
@override
String toString() {
return 'UserApiModel(id: $id, name: $name, email: $email, picture: $picture)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserApiModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.picture, picture) || other.picture == picture));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, name, email, picture);
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith =>
__$$UserApiModelImplCopyWithImpl<_$UserApiModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserApiModelImplToJson(
this,
);
}
}
abstract class _UserApiModel implements UserApiModel {
const factory _UserApiModel(
{required final String id,
required final String name,
required final String email,
required final String picture}) = _$UserApiModelImpl;
factory _UserApiModel.fromJson(Map<String, dynamic> json) =
_$UserApiModelImpl.fromJson;
/// The user's ID.
@override
String get id;
/// The user's name.
@override
String get name;
/// The user's email.
@override
String get email;
/// The user's picture URL.
@override
String get picture;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_api_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserApiModelImpl _$$UserApiModelImplFromJson(Map<String, dynamic> json) =>
_$UserApiModelImpl(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
picture: json['picture'] as String,
);
Map<String, dynamic> _$$UserApiModelImplToJson(_$UserApiModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'email': instance.email,
'picture': instance.picture,
};

@ -6,6 +6,7 @@ import '../../../config/assets.dart';
import '../../../domain/models/activity/activity.dart'; import '../../../domain/models/activity/activity.dart';
import '../../../domain/models/continent/continent.dart'; import '../../../domain/models/continent/continent.dart';
import '../../../domain/models/destination/destination.dart'; import '../../../domain/models/destination/destination.dart';
import '../../../domain/models/user/user.dart';
class LocalDataService { class LocalDataService {
List<Continent> getContinents() { List<Continent> getContinents() {
@ -55,4 +56,12 @@ class LocalDataService {
final localData = await rootBundle.loadString(asset); final localData = await rootBundle.loadString(asset);
return (jsonDecode(localData) as List).cast<Map<String, dynamic>>(); return (jsonDecode(localData) as List).cast<Map<String, dynamic>>();
} }
User getUser() {
return const User(
name: 'Sofie',
// For demo purposes we use a local asset
picture: 'assets/user.jpg',
);
}
} }

@ -23,10 +23,10 @@ mixin _$BookingSummary {
/// Booking id /// Booking id
int get id => throw _privateConstructorUsedError; int get id => throw _privateConstructorUsedError;
/// Destination name to be displayed. /// Name to be displayed
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
/// Start date of the booking. /// Start date of the booking
DateTime get startDate => throw _privateConstructorUsedError; DateTime get startDate => throw _privateConstructorUsedError;
/// End date of the booking /// End date of the booking
@ -158,11 +158,11 @@ class _$BookingSummaryImpl implements _BookingSummary {
@override @override
final int id; final int id;
/// Destination name to be displayed. /// Name to be displayed
@override @override
final String name; final String name;
/// Start date of the booking. /// Start date of the booking
@override @override
final DateTime startDate; final DateTime startDate;
@ -222,11 +222,11 @@ abstract class _BookingSummary implements BookingSummary {
@override @override
int get id; int get id;
/// Destination name to be displayed. /// Name to be displayed
@override @override
String get name; String get name;
/// Start date of the booking. /// Start date of the booking
@override @override
DateTime get startDate; DateTime get startDate;

@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class User with _$User {
const factory User({
/// The user's name.
required String name,
/// The user's picture URL.
required String picture,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

@ -0,0 +1,185 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
User _$UserFromJson(Map<String, dynamic> json) {
return _User.fromJson(json);
}
/// @nodoc
mixin _$User {
/// The user's name.
String get name => throw _privateConstructorUsedError;
/// The user's picture URL.
String get picture => throw _privateConstructorUsedError;
/// Serializes this User to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserCopyWith<User> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserCopyWith<$Res> {
factory $UserCopyWith(User value, $Res Function(User) then) =
_$UserCopyWithImpl<$Res, User>;
@useResult
$Res call({String name, String picture});
}
/// @nodoc
class _$UserCopyWithImpl<$Res, $Val extends User>
implements $UserCopyWith<$Res> {
_$UserCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? picture = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> {
factory _$$UserImplCopyWith(
_$UserImpl value, $Res Function(_$UserImpl) then) =
__$$UserImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String picture});
}
/// @nodoc
class __$$UserImplCopyWithImpl<$Res>
extends _$UserCopyWithImpl<$Res, _$UserImpl>
implements _$$UserImplCopyWith<$Res> {
__$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then)
: super(_value, _then);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? picture = null,
}) {
return _then(_$UserImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserImpl implements _User {
const _$UserImpl({required this.name, required this.picture});
factory _$UserImpl.fromJson(Map<String, dynamic> json) =>
_$$UserImplFromJson(json);
/// The user's name.
@override
final String name;
/// The user's picture URL.
@override
final String picture;
@override
String toString() {
return 'User(name: $name, picture: $picture)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.picture, picture) || other.picture == picture));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, name, picture);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
__$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserImplToJson(
this,
);
}
}
abstract class _User implements User {
const factory _User(
{required final String name, required final String picture}) = _$UserImpl;
factory _User.fromJson(Map<String, dynamic> json) = _$UserImpl.fromJson;
/// The user's name.
@override
String get name;
/// The user's picture URL.
@override
String get picture;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
throw _privateConstructorUsedError;
}

@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
name: json['name'] as String,
picture: json['picture'] as String,
);
Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
<String, dynamic>{
'name': instance.name,
'picture': instance.picture,
};

@ -45,6 +45,7 @@ GoRouter router(
builder: (context, state) { builder: (context, state) {
final viewModel = HomeViewModel( final viewModel = HomeViewModel(
bookingRepository: context.read(), bookingRepository: context.read(),
userRepository: context.read(),
); );
return HomeScreen(viewModel: viewModel); return HomeScreen(viewModel: viewModel);
}, },

@ -19,6 +19,7 @@ class AppLocalization {
'errorWhileLoadingBooking': 'Error while loading booking', 'errorWhileLoadingBooking': 'Error while loading booking',
'errorWhileLoadingContinents': 'Error while loading continents', 'errorWhileLoadingContinents': 'Error while loading continents',
'errorWhileLoadingDestinations': 'Error while loading destinations', 'errorWhileLoadingDestinations': 'Error while loading destinations',
'errorWhileLoadingHome': 'Error while loading home',
'errorWhileLogin': 'Error while trying to login', 'errorWhileLogin': 'Error while trying to login',
'errorWhileLogout': 'Error while trying to logout', 'errorWhileLogout': 'Error while trying to logout',
'errorWhileSavingActivities': 'Error while saving activities', 'errorWhileSavingActivities': 'Error while saving activities',
@ -26,12 +27,12 @@ class AppLocalization {
'errorWhileSharing': 'Error while sharing booking', 'errorWhileSharing': 'Error while sharing booking',
'evening': 'Evening', 'evening': 'Evening',
'login': 'Login', 'login': 'Login',
'nameTrips': '{name}\'s Trips',
'search': 'Search', 'search': 'Search',
'searchDestination': 'Search destination', 'searchDestination': 'Search destination',
'selected': '{1} selected', 'selected': '{1} selected',
'shareTrip': 'Share Trip', 'shareTrip': 'Share Trip',
'tryAgain': 'Try again', 'tryAgain': 'Try again',
'yourBookings': 'Your bookings:',
'yourChosenActivities': 'Your chosen activities', 'yourChosenActivities': 'Your chosen activities',
'when': 'When', 'when': 'When',
}; };
@ -87,7 +88,9 @@ class AppLocalization {
String get bookNewTrip => _get('bookNewTrip'); String get bookNewTrip => _get('bookNewTrip');
String get yourBookings => _get('yourBookings'); String get errorWhileLoadingHome => _get('errorWhileLoadingHome');
String nameTrips(String name) => _get('nameTrips').replaceAll('{name}', name);
String selected(int value) => String selected(int value) =>
_get('selected').replaceAll('{1}', value.toString()); _get('selected').replaceAll('{1}', value.toString());

@ -32,6 +32,8 @@ sealed class Dimens {
> 600 => dimensDesktop, > 600 => dimensDesktop,
_ => dimensMobile, _ => dimensMobile,
}; };
abstract final double profilePictureSize;
} }
/// Mobile dimensions /// Mobile dimensions
@ -41,6 +43,9 @@ class DimensMobile extends Dimens {
@override @override
double paddingScreenVertical = Dimens.paddingVertical; double paddingScreenVertical = Dimens.paddingVertical;
@override
double get profilePictureSize => 64.0;
} }
/// Desktop/Web dimensions /// Desktop/Web dimensions
@ -50,4 +55,7 @@ class DimensDesktop extends Dimens {
@override @override
double paddingScreenVertical = 64.0; double paddingScreenVertical = 64.0;
@override
double get profilePictureSize => 128.0;
} }

@ -4,25 +4,33 @@ import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../../../data/repositories/booking/booking_repository.dart'; import '../../../data/repositories/booking/booking_repository.dart';
import '../../../data/repositories/user/user_repository.dart';
import '../../../domain/models/booking/booking_summary.dart'; import '../../../domain/models/booking/booking_summary.dart';
import '../../../domain/models/user/user.dart';
import '../../../utils/command.dart'; import '../../../utils/command.dart';
import '../../../utils/result.dart'; import '../../../utils/result.dart';
class HomeViewModel extends ChangeNotifier { class HomeViewModel extends ChangeNotifier {
HomeViewModel({ HomeViewModel({
required BookingRepository bookingRepository, required BookingRepository bookingRepository,
}) : _bookingRepository = bookingRepository { required UserRepository userRepository,
}) : _bookingRepository = bookingRepository,
_userRepository = userRepository {
load = Command0(_load)..execute(); load = Command0(_load)..execute();
} }
final BookingRepository _bookingRepository; final BookingRepository _bookingRepository;
final UserRepository _userRepository;
final _log = Logger('HomeViewModel'); final _log = Logger('HomeViewModel');
List<BookingSummary> _bookings = []; List<BookingSummary> _bookings = [];
User? _user;
late Command0 load; late Command0 load;
List<BookingSummary> get bookings => _bookings; List<BookingSummary> get bookings => _bookings;
User? get user => _user;
Future<Result> _load() async { Future<Result> _load() async {
try { try {
final result = await _bookingRepository.getBookingsList(); final result = await _bookingRepository.getBookingsList();
@ -32,8 +40,19 @@ class HomeViewModel extends ChangeNotifier {
_log.fine('Loaded bookings'); _log.fine('Loaded bookings');
case Error<List<BookingSummary>>(): case Error<List<BookingSummary>>():
_log.warning('Failed to load bookings', result.error); _log.warning('Failed to load bookings', result.error);
return result;
}
final userResult = await _userRepository.getUser();
switch (userResult) {
case Ok<User>():
_user = userResult.value;
_log.fine('Loaded user');
case Error<User>():
_log.warning('Failed to load user', userResult.error);
} }
return result;
return userResult;
} finally { } finally {
notifyListeners(); notifyListeners();
} }

@ -1,15 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../../domain/models/booking/booking_summary.dart'; import '../../../domain/models/booking/booking_summary.dart';
import '../../../routing/routes.dart'; import '../../../routing/routes.dart';
import '../../auth/logout/view_models/logout_viewmodel.dart';
import '../../auth/logout/widgets/logout_button.dart';
import '../../core/localization/applocalization.dart'; import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart'; import '../../core/themes/dimens.dart';
import '../../core/ui/date_format_start_end.dart'; import '../../core/ui/date_format_start_end.dart';
import '../../core/ui/error_indicator.dart';
import '../view_models/home_viewmodel.dart'; import '../view_models/home_viewmodel.dart';
import 'home_title.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
const HomeScreen({ const HomeScreen({
@ -34,51 +33,58 @@ class HomeScreen extends StatelessWidget {
top: true, top: true,
bottom: true, bottom: true,
child: ListenableBuilder( child: ListenableBuilder(
listenable: viewModel, listenable: viewModel.load,
builder: (context, _) { builder: (context, child) {
return CustomScrollView( if (viewModel.load.running) {
slivers: [ return const Center(
SliverToBoxAdapter( child: CircularProgressIndicator(),
child: Padding( );
padding: EdgeInsets.symmetric( }
vertical: Dimens.of(context).paddingScreenVertical,
horizontal: Dimens.of(context).paddingScreenHorizontal, if (viewModel.load.error) {
), return ErrorIndicator(
child: Row( title: AppLocalization.of(context).errorWhileLoadingHome,
mainAxisAlignment: MainAxisAlignment.spaceBetween, label: AppLocalization.of(context).tryAgain,
children: [ onPressed: viewModel.load.execute,
Text( );
AppLocalization.of(context).yourBookings, }
style: Theme.of(context).textTheme.headlineMedium,
), return child!;
LogoutButton( },
viewModel: LogoutViewModel( child: ListenableBuilder(
authRepository: context.read(), listenable: viewModel,
itineraryConfigRepository: context.read(), builder: (context, _) {
), return CustomScrollView(
), slivers: [
], SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.of(context).paddingScreenVertical,
horizontal: Dimens.of(context).paddingScreenHorizontal,
),
child: HomeHeader(viewModel: viewModel),
), ),
), ),
), SliverList.builder(
SliverList.builder( itemCount: viewModel.bookings.length,
itemCount: viewModel.bookings.length, itemBuilder: (_, index) => _Booking(
itemBuilder: (_, index) => _Booking( key: ValueKey(index),
key: ValueKey(index), booking: viewModel.bookings[index],
booking: viewModel.bookings[index], onTap: () => context.push(
onTap: () => context.push( Routes.bookingWithId(viewModel.bookings[index].id)),
Routes.bookingWithId(viewModel.bookings[index].id)), ),
), )
) ],
], );
); },
}, ),
), ),
), ),
); );
} }
} }
class _Booking extends StatelessWidget { class _Booking extends StatelessWidget {
const _Booking({ const _Booking({
super.key, super.key,

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../../auth/logout/view_models/logout_viewmodel.dart';
import '../../auth/logout/widgets/logout_button.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../view_models/home_viewmodel.dart';
class HomeHeader extends StatelessWidget {
const HomeHeader({
super.key,
required this.viewModel,
});
final HomeViewModel viewModel;
@override
Widget build(BuildContext context) {
final user = viewModel.user;
if (user == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipOval(
child: Image.asset(
user.picture,
width: Dimens.of(context).profilePictureSize,
height: Dimens.of(context).profilePictureSize,
),
),
LogoutButton(
viewModel: LogoutViewModel(
authRepository: context.read(),
itineraryConfigRepository: context.read(),
),
),
],
),
const SizedBox(height: Dimens.paddingVertical),
_Title(
text: AppLocalization.of(context).nameTrips(user.name),
),
],
);
}
}
class _Title extends StatelessWidget {
const _Title({
required this.text,
});
final String text;
@override
Widget build(BuildContext context) {
return ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: (bounds) => RadialGradient(
center: Alignment.bottomLeft,
radius: 2,
colors: [
Colors.purple.shade700,
Colors.purple.shade400,
],
).createShader(
Rect.fromLTWH(0, 0, bounds.width, bounds.height),
),
child: Text(
text,
style: GoogleFonts.rubik(
textStyle: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}

@ -42,3 +42,4 @@ flutter:
- assets/activities.json - assets/activities.json
- assets/destinations.json - assets/destinations.json
- assets/logo.svg - assets/logo.svg
- assets/user.jpg

@ -12,6 +12,7 @@ import '../../../../testing/app.dart';
import '../../../../testing/fakes/repositories/fake_auth_repository.dart'; import '../../../../testing/fakes/repositories/fake_auth_repository.dart';
import '../../../../testing/fakes/repositories/fake_booking_repository.dart'; import '../../../../testing/fakes/repositories/fake_booking_repository.dart';
import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart'; import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
import '../../../../testing/fakes/repositories/fake_user_repository.dart';
import '../../../../testing/mocks.dart'; import '../../../../testing/mocks.dart';
import '../../../../testing/models/booking.dart'; import '../../../../testing/models/booking.dart';
@ -23,6 +24,7 @@ void main() {
setUp(() { setUp(() {
viewModel = HomeViewModel( viewModel = HomeViewModel(
bookingRepository: FakeBookingRepository()..createBooking(kBooking), bookingRepository: FakeBookingRepository()..createBooking(kBooking),
userRepository: FakeUserRepository(),
); );
goRouter = MockGoRouter(); goRouter = MockGoRouter();
when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null)); when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null));
@ -49,6 +51,13 @@ void main() {
expect(find.byType(HomeScreen), findsOneWidget); expect(find.byType(HomeScreen), findsOneWidget);
}); });
testWidgets('should show user name', (tester) async {
await loadWidget(tester);
await tester.pumpAndSettle();
expect(find.text('NAME\'s Trips'), findsOneWidget);
});
testWidgets('should navigate to search', (tester) async { testWidgets('should navigate to search', (tester) async {
await loadWidget(tester); await loadWidget(tester);
await tester.pumpAndSettle(); await tester.pumpAndSettle();

@ -0,0 +1,12 @@
import 'package:compass_app/data/repositories/user/user_repository.dart';
import 'package:compass_app/domain/models/user/user.dart';
import 'package:compass_app/utils/result.dart';
import '../../models/user.dart';
class FakeUserRepository implements UserRepository {
@override
Future<Result<User>> getUser() async {
return Result.ok(user);
}
}

@ -1,5 +1,6 @@
import 'package:compass_app/data/services/api/api_client.dart'; import 'package:compass_app/data/services/api/api_client.dart';
import 'package:compass_app/data/services/api/model/booking/booking_api_model.dart'; import 'package:compass_app/data/services/api/model/booking/booking_api_model.dart';
import 'package:compass_app/data/services/api/model/user/user_api_model.dart';
import 'package:compass_app/domain/models/activity/activity.dart'; import 'package:compass_app/domain/models/activity/activity.dart';
import 'package:compass_app/domain/models/continent/continent.dart'; import 'package:compass_app/domain/models/continent/continent.dart';
import 'package:compass_app/domain/models/destination/destination.dart'; import 'package:compass_app/domain/models/destination/destination.dart';
@ -7,6 +8,7 @@ import 'package:compass_app/utils/result.dart';
import '../../models/activity.dart'; import '../../models/activity.dart';
import '../../models/booking.dart'; import '../../models/booking.dart';
import '../../models/user.dart';
class FakeApiClient implements ApiClient { class FakeApiClient implements ApiClient {
// Should not increase when using cached data // Should not increase when using cached data
@ -100,4 +102,9 @@ class FakeApiClient implements ApiClient {
bookings.add(bookingWithId); bookings.add(bookingWithId);
return Result.ok(bookingWithId); return Result.ok(bookingWithId);
} }
@override
Future<Result<UserApiModel>> getUser() async {
return Result.ok(userApiModel);
}
} }

@ -0,0 +1,14 @@
import 'package:compass_app/data/services/api/model/user/user_api_model.dart';
import 'package:compass_app/domain/models/user/user.dart';
const userApiModel = UserApiModel(
id: 'ID',
name: 'NAME',
email: 'EMAIL',
picture: 'assets/user.jpg',
);
const user = User(
name: 'NAME',
picture: 'assets/user.jpg',
);

@ -5,6 +5,7 @@ import 'package:compass_server/routes/booking.dart';
import 'package:compass_server/routes/continent.dart'; import 'package:compass_server/routes/continent.dart';
import 'package:compass_server/routes/destination.dart'; import 'package:compass_server/routes/destination.dart';
import 'package:compass_server/routes/login.dart'; import 'package:compass_server/routes/login.dart';
import 'package:compass_server/routes/user.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart'; import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
@ -14,6 +15,7 @@ final _router = Router()
..get('/continent', continentHandler) ..get('/continent', continentHandler)
..mount('/destination', DestinationApi().router.call) ..mount('/destination', DestinationApi().router.call)
..mount('/booking', BookingApi().router.call) ..mount('/booking', BookingApi().router.call)
..mount('/user', UserApi().router.call)
..mount('/login', LoginApi().router.call); ..mount('/login', LoginApi().router.call);
void main(List<String> args) async { void main(List<String> args) async {

@ -1,3 +1,5 @@
import '../model/user/user.dart';
class Constants { class Constants {
/// Email for the hardcoded login. /// Email for the hardcoded login.
static const email = 'email@example.com'; static const email = 'email@example.com';
@ -11,4 +13,18 @@ class Constants {
/// User id to be returned on successful login. /// User id to be returned on successful login.
static const userId = '123'; static const userId = '123';
/// User name for the hardcoded user.
static const name = 'Sofie';
/// For demo purposes we use a local asset.
static const picture = 'assets/user.jpg';
/// Hardcoded user.
static const user = User(
id: Constants.userId,
name: Constants.name,
email: Constants.email,
picture: Constants.picture,
);
} }

@ -29,7 +29,7 @@ mixin _$Booking {
/// End date of the trip /// End date of the trip
DateTime get endDate => throw _privateConstructorUsedError; DateTime get endDate => throw _privateConstructorUsedError;
/// Booking display name /// Booking name
/// Should be "Destination, Continent" /// Should be "Destination, Continent"
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
@ -205,7 +205,7 @@ class _$BookingImpl implements _Booking {
@override @override
final DateTime endDate; final DateTime endDate;
/// Booking display name /// Booking name
/// Should be "Destination, Continent" /// Should be "Destination, Continent"
@override @override
final String name; final String name;
@ -290,7 +290,7 @@ abstract class _Booking implements Booking {
@override @override
DateTime get endDate; DateTime get endDate;
/// Booking display name /// Booking name
/// Should be "Destination, Continent" /// Should be "Destination, Continent"
@override @override
String get name; String get name;

@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class User with _$User {
const factory User({
/// The user's ID.
required String id,
/// The user's name.
required String name,
/// The user's email.
required String email,
/// The user's picture URL.
required String picture,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

@ -0,0 +1,236 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
User _$UserFromJson(Map<String, dynamic> json) {
return _User.fromJson(json);
}
/// @nodoc
mixin _$User {
/// The user's ID.
String get id => throw _privateConstructorUsedError;
/// The user's name.
String get name => throw _privateConstructorUsedError;
/// The user's email.
String get email => throw _privateConstructorUsedError;
/// The user's picture URL.
String get picture => throw _privateConstructorUsedError;
/// Serializes this User to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserCopyWith<User> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserCopyWith<$Res> {
factory $UserCopyWith(User value, $Res Function(User) then) =
_$UserCopyWithImpl<$Res, User>;
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class _$UserCopyWithImpl<$Res, $Val extends User>
implements $UserCopyWith<$Res> {
_$UserCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> {
factory _$$UserImplCopyWith(
_$UserImpl value, $Res Function(_$UserImpl) then) =
__$$UserImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class __$$UserImplCopyWithImpl<$Res>
extends _$UserCopyWithImpl<$Res, _$UserImpl>
implements _$$UserImplCopyWith<$Res> {
__$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then)
: super(_value, _then);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_$UserImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserImpl implements _User {
const _$UserImpl(
{required this.id,
required this.name,
required this.email,
required this.picture});
factory _$UserImpl.fromJson(Map<String, dynamic> json) =>
_$$UserImplFromJson(json);
/// The user's ID.
@override
final String id;
/// The user's name.
@override
final String name;
/// The user's email.
@override
final String email;
/// The user's picture URL.
@override
final String picture;
@override
String toString() {
return 'User(id: $id, name: $name, email: $email, picture: $picture)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.picture, picture) || other.picture == picture));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, name, email, picture);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
__$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserImplToJson(
this,
);
}
}
abstract class _User implements User {
const factory _User(
{required final String id,
required final String name,
required final String email,
required final String picture}) = _$UserImpl;
factory _User.fromJson(Map<String, dynamic> json) = _$UserImpl.fromJson;
/// The user's ID.
@override
String get id;
/// The user's name.
@override
String get name;
/// The user's email.
@override
String get email;
/// The user's picture URL.
@override
String get picture;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
throw _privateConstructorUsedError;
}

@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
picture: json['picture'] as String,
);
Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'email': instance.email,
'picture': instance.picture,
};

@ -0,0 +1,23 @@
import 'dart:convert';
import 'package:compass_server/config/constants.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
/// Implements a simple user API.
///
/// This API only returns a hardcoded user for demonstration purposes.
class UserApi {
Router get router {
final router = Router();
router.get('/', (Request request) async {
return Response.ok(
json.encode(Constants.user),
headers: {'Content-Type': 'application/json'},
);
});
return router;
}
}

@ -9,6 +9,7 @@ import 'package:compass_server/model/continent/continent.dart';
import 'package:compass_server/model/destination/destination.dart'; import 'package:compass_server/model/destination/destination.dart';
import 'package:compass_server/model/login_request/login_request.dart'; import 'package:compass_server/model/login_request/login_request.dart';
import 'package:compass_server/model/login_response/login_response.dart'; import 'package:compass_server/model/login_response/login_response.dart';
import 'package:compass_server/model/user/user.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -128,6 +129,19 @@ void main() {
expect(booking.id, 1); expect(booking.id, 1);
}); });
test('Get user', () async {
final response = await get(
Uri.parse('$host/user'),
headers: headers,
);
expect(response.statusCode, 200);
final user = User.fromJson(jsonDecode(response.body));
// Should get the hardcoded user
expect(user, Constants.user);
});
test('404', () async { test('404', () async {
final response = await get( final response = await get(
Uri.parse('$host/foobar'), Uri.parse('$host/foobar'),

Loading…
Cancel
Save