[Compass App] Swipe to delete bookings (#2440)

This PR adds the swipe-to-delete functionality on the home screen.

To properly handle this, I had to change how booking IDs work (otherwise
the `Dimissable` widget would not like it). Now, IDs are generated
sequentially, either by the local Bookings repository or by the server.


https://github.com/user-attachments/assets/523cb786-f2ab-4c57-b241-23901eee76b1
(it's the same video I shared yesterday)

## 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/2444/head
Miguel Beltran 2 weeks ago committed by GitHub
parent 799ce7f548
commit 678e8a44a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,4 +11,7 @@ abstract class BookingRepository {
/// Creates a new [Booking].
Future<Result<void>> createBooking(Booking booking);
/// Delete booking
Future<Result<void>> delete(int id);
}

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import '../../../domain/models/booking/booking.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../utils/result.dart';
@ -13,37 +14,47 @@ class BookingRepositoryLocal implements BookingRepository {
required LocalDataService localDataService,
}) : _localDataService = localDataService;
// Only create default booking once
bool _isInitialized = false;
// Used to generate IDs for bookings
int _sequentialId = 0;
final _bookings = List<Booking>.empty(growable: true);
final LocalDataService _localDataService;
@override
Future<Result<void>> createBooking(Booking booking) async {
_bookings.add(booking);
// Bookings created come without id, we need to assign one
final bookingWithId = booking.copyWith(id: _sequentialId++);
_bookings.add(bookingWithId);
return Result.ok(null);
}
@override
Future<Result<Booking>> getBooking(int id) async {
await _createDefaultBooking();
if (id >= _bookings.length || id < 0) {
return Result.error(Exception('Invalid id: $id'));
final booking = _bookings.firstWhereOrNull((booking) => booking.id == id);
if (booking == null) {
return Result.error(Exception('Booking not found'));
}
return Result.ok(_bookings[id]);
return Result.ok(booking);
}
@override
Future<Result<List<BookingSummary>>> getBookingsList() async {
await _createDefaultBooking();
// Initialize the repository with a default booking
if (!_isInitialized) {
await _createDefaultBooking();
_isInitialized = true;
}
return Result.ok(_createSummaries());
}
List<BookingSummary> _createSummaries() {
return _bookings
.mapIndexed(
(index, booking) => BookingSummary(
id: index,
.map(
(booking) => BookingSummary(
id: booking.id!,
name:
'${booking.destination.name}, ${booking.destination.continent}',
startDate: booking.startDate,
@ -64,6 +75,7 @@ class BookingRepositoryLocal implements BookingRepository {
_bookings.add(
Booking(
id: _sequentialId++,
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 2, 1),
destination: destination,
@ -72,4 +84,10 @@ class BookingRepositoryLocal implements BookingRepository {
);
}
}
@override
Future<Result<void>> delete(int id) async {
_bookings.removeWhere((booking) => booking.id == id);
return Result.ok(null);
}
}

@ -68,6 +68,7 @@ class BookingRepositoryRemote implements BookingRepository {
return Result.ok(
Booking(
id: booking.id,
startDate: booking.startDate,
endDate: booking.endDate,
destination: destination,
@ -101,4 +102,13 @@ class BookingRepositoryRemote implements BookingRepository {
return Result.error(e);
}
}
@override
Future<Result<void>> delete(int id) async {
try {
return _apiClient.deleteBooking(id);
} on Exception catch (e) {
return Result.error(e);
}
}
}

@ -184,4 +184,23 @@ class ApiClient {
client.close();
}
}
Future<Result<void>> deleteBooking(int id) async {
final client = _clientFactory();
try {
final request = await client.delete(_host, _port, '/booking/$id');
await _authHeader(request.headers);
final response = await request.close();
// Response 204 "No Content", delete was successful
if (response.statusCode == 204) {
return Result.ok(null);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
}

@ -9,6 +9,10 @@ part 'booking.g.dart';
@freezed
class Booking with _$Booking {
const factory Booking({
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
int? id,
/// Start date of the trip
required DateTime startDate,

@ -20,6 +20,10 @@ Booking _$BookingFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$Booking {
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
int? get id => throw _privateConstructorUsedError;
/// Start date of the trip
DateTime get startDate => throw _privateConstructorUsedError;
@ -47,7 +51,8 @@ abstract class $BookingCopyWith<$Res> {
_$BookingCopyWithImpl<$Res, Booking>;
@useResult
$Res call(
{DateTime startDate,
{int? id,
DateTime startDate,
DateTime endDate,
Destination destination,
List<Activity> activity});
@ -70,12 +75,17 @@ class _$BookingCopyWithImpl<$Res, $Val extends Booking>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? destination = null,
Object? activity = null,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
@ -114,7 +124,8 @@ abstract class _$$BookingImplCopyWith<$Res> implements $BookingCopyWith<$Res> {
@override
@useResult
$Res call(
{DateTime startDate,
{int? id,
DateTime startDate,
DateTime endDate,
Destination destination,
List<Activity> activity});
@ -136,12 +147,17 @@ class __$$BookingImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? destination = null,
Object? activity = null,
}) {
return _then(_$BookingImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
@ -166,7 +182,8 @@ class __$$BookingImplCopyWithImpl<$Res>
@JsonSerializable()
class _$BookingImpl implements _Booking {
const _$BookingImpl(
{required this.startDate,
{this.id,
required this.startDate,
required this.endDate,
required this.destination,
required final List<Activity> activity})
@ -175,6 +192,11 @@ class _$BookingImpl implements _Booking {
factory _$BookingImpl.fromJson(Map<String, dynamic> json) =>
_$$BookingImplFromJson(json);
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
@override
final int? id;
/// Start date of the trip
@override
final DateTime startDate;
@ -200,7 +222,7 @@ class _$BookingImpl implements _Booking {
@override
String toString() {
return 'Booking(startDate: $startDate, endDate: $endDate, destination: $destination, activity: $activity)';
return 'Booking(id: $id, startDate: $startDate, endDate: $endDate, destination: $destination, activity: $activity)';
}
@override
@ -208,6 +230,7 @@ class _$BookingImpl implements _Booking {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BookingImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate) &&
@ -218,8 +241,8 @@ class _$BookingImpl implements _Booking {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, startDate, endDate, destination,
const DeepCollectionEquality().hash(_activity));
int get hashCode => Object.hash(runtimeType, id, startDate, endDate,
destination, const DeepCollectionEquality().hash(_activity));
/// Create a copy of Booking
/// with the given fields replaced by the non-null parameter values.
@ -239,13 +262,19 @@ class _$BookingImpl implements _Booking {
abstract class _Booking implements Booking {
const factory _Booking(
{required final DateTime startDate,
{final int? id,
required final DateTime startDate,
required final DateTime endDate,
required final Destination destination,
required final List<Activity> activity}) = _$BookingImpl;
factory _Booking.fromJson(Map<String, dynamic> json) = _$BookingImpl.fromJson;
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
@override
int? get id;
/// Start date of the trip
@override
DateTime get startDate;

@ -8,6 +8,7 @@ part of 'booking.dart';
_$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
_$BookingImpl(
id: (json['id'] as num?)?.toInt(),
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
destination:
@ -19,6 +20,7 @@ _$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) =>
<String, dynamic>{
'id': instance.id,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate.toIso8601String(),
'destination': instance.destination,

@ -11,10 +11,12 @@ class AppLocalization {
static const _strings = <String, String>{
'activities': 'Activities',
'addDates': 'Add Dates',
'bookingDeleted': 'Booking deleted',
'bookNewTrip': 'Book New Trip',
'close': 'Close',
'confirm': 'Confirm',
'daytime': 'Daytime',
'errorWhileDeletingBooking': 'Error while deleting booking',
'errorWhileLoadingActivities': 'Error while loading activities',
'errorWhileLoadingBooking': 'Error while loading booking',
'errorWhileLoadingContinents': 'Error while loading continents',
@ -90,6 +92,10 @@ class AppLocalization {
String get errorWhileLoadingHome => _get('errorWhileLoadingHome');
String get bookingDeleted => _get('bookingDeleted');
String get errorWhileDeletingBooking => _get('errorWhileDeletingBooking');
String nameTrips(String name) => _get('nameTrips').replaceAll('{name}', name);
String selected(int value) =>

@ -17,6 +17,7 @@ class HomeViewModel extends ChangeNotifier {
}) : _bookingRepository = bookingRepository,
_userRepository = userRepository {
load = Command0(_load)..execute();
deleteBooking = Command1(_deleteBooking);
}
final BookingRepository _bookingRepository;
@ -26,6 +27,7 @@ class HomeViewModel extends ChangeNotifier {
User? _user;
late Command0 load;
late Command1<void, int> deleteBooking;
List<BookingSummary> get bookings => _bookings;
@ -57,4 +59,33 @@ class HomeViewModel extends ChangeNotifier {
notifyListeners();
}
}
Future<Result<void>> _deleteBooking(int id) async {
try {
final resultDelete = await _bookingRepository.delete(id);
switch (resultDelete) {
case Ok<void>():
_log.fine('Deleted booking $id');
case Error<void>():
_log.warning('Failed to delete booking $id', resultDelete.error);
return resultDelete;
}
// After deleting the booking, we need to reload the bookings list.
// BookingRepository is the source of truth for bookings.
final resultLoadBookings = await _bookingRepository.getBookingsList();
switch (resultLoadBookings) {
case Ok<List<BookingSummary>>():
_bookings = resultLoadBookings.value;
_log.fine('Loaded bookings');
case Error<List<BookingSummary>>():
_log.warning('Failed to load bookings', resultLoadBookings.error);
return resultLoadBookings;
}
return resultLoadBookings;
} finally {
notifyListeners();
}
}
}

@ -10,7 +10,7 @@ import '../../core/ui/error_indicator.dart';
import '../view_models/home_viewmodel.dart';
import 'home_title.dart';
class HomeScreen extends StatelessWidget {
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.viewModel,
@ -18,6 +18,30 @@ class HomeScreen extends StatelessWidget {
final HomeViewModel viewModel;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
widget.viewModel.deleteBooking.addListener(_onResult);
}
@override
void didUpdateWidget(covariant HomeScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.deleteBooking.removeListener(_onResult);
widget.viewModel.deleteBooking.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.deleteBooking.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -33,26 +57,26 @@ class HomeScreen extends StatelessWidget {
top: true,
bottom: true,
child: ListenableBuilder(
listenable: viewModel.load,
listenable: widget.viewModel.load,
builder: (context, child) {
if (viewModel.load.running) {
if (widget.viewModel.load.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (viewModel.load.error) {
if (widget.viewModel.load.error) {
return ErrorIndicator(
title: AppLocalization.of(context).errorWhileLoadingHome,
label: AppLocalization.of(context).tryAgain,
onPressed: viewModel.load.execute,
onPressed: widget.viewModel.load.execute,
);
}
return child!;
},
child: ListenableBuilder(
listenable: viewModel,
listenable: widget.viewModel,
builder: (context, _) {
return CustomScrollView(
slivers: [
@ -62,16 +86,20 @@ class HomeScreen extends StatelessWidget {
vertical: Dimens.of(context).paddingScreenVertical,
horizontal: Dimens.of(context).paddingScreenHorizontal,
),
child: HomeHeader(viewModel: viewModel),
child: HomeHeader(viewModel: widget.viewModel),
),
),
SliverList.builder(
itemCount: viewModel.bookings.length,
itemCount: widget.viewModel.bookings.length,
itemBuilder: (_, index) => _Booking(
key: ValueKey(index),
booking: viewModel.bookings[index],
onTap: () => context.push(
Routes.bookingWithId(viewModel.bookings[index].id)),
key: ValueKey(widget.viewModel.bookings[index].id),
booking: widget.viewModel.bookings[index],
onTap: () => context.push(Routes.bookingWithId(
widget.viewModel.bookings[index].id)),
onDismissed: (_) =>
widget.viewModel.deleteBooking.execute(
widget.viewModel.bookings[index].id,
),
),
)
],
@ -82,6 +110,26 @@ class HomeScreen extends StatelessWidget {
),
);
}
void _onResult() {
if (widget.viewModel.deleteBooking.completed) {
widget.viewModel.deleteBooking.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).bookingDeleted),
),
);
}
if (widget.viewModel.deleteBooking.error) {
widget.viewModel.deleteBooking.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileDeletingBooking),
),
);
}
}
}
class _Booking extends StatelessWidget {
@ -89,37 +137,44 @@ class _Booking extends StatelessWidget {
super.key,
required this.booking,
required this.onTap,
required this.onDismissed,
});
final BookingSummary booking;
final GestureTapCallback onTap;
final DismissDirectionCallback onDismissed;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: Dimens.of(context).paddingScreenHorizontal,
vertical: Dimens.paddingVertical,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.name,
style: Theme.of(context).textTheme.titleLarge,
),
Text(
dateFormatStartEnd(
DateTimeRange(
start: booking.startDate,
end: booking.endDate,
return Dismissible(
key: ValueKey(booking.id),
direction: DismissDirection.endToStart,
onDismissed: onDismissed,
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: Dimens.of(context).paddingScreenHorizontal,
vertical: Dimens.paddingVertical,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.name,
style: Theme.of(context).textTheme.titleLarge,
),
Text(
dateFormatStartEnd(
DateTimeRange(
start: booking.startDate,
end: booking.endDate,
),
),
style: Theme.of(context).textTheme.bodyLarge,
),
style: Theme.of(context).textTheme.bodyLarge,
),
],
],
),
),
),
);

@ -21,7 +21,7 @@ void main() {
test('should get booking', () async {
final result = await bookingRepository.getBooking(0);
final booking = result.asOk.value;
expect(booking, kBooking);
expect(booking, kBooking.copyWith(id: 0));
});
test('should create booking', () async {
@ -36,5 +36,21 @@ void main() {
final list = result.asOk.value;
expect(list, [kBookingSummary]);
});
test('should delete booking', () async {
// Ensure no bookings exist
expect(fakeApiClient.bookings, isEmpty);
// Add a booking
var result = await bookingRepository.createBooking(kBooking);
expect(result, isA<Ok<void>>());
// Delete the booking
result = await bookingRepository.delete(0);
expect(result, isA<Ok<void>>());
// Check if the booking was deleted from the server
expect(fakeApiClient.bookings, isEmpty);
});
});
}

@ -1,5 +1,6 @@
import 'package:compass_app/data/services/api/api_client.dart';
import 'package:compass_app/domain/models/continent/continent.dart';
import 'package:compass_app/utils/result.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../testing/mocks.dart';
@ -68,5 +69,11 @@ void main() {
final result = await apiClient.postBooking(kBookingApiModel);
expect(result.asOk.value, kBookingApiModel);
});
test('should delete booking', () async {
mockHttpClient.mockDelete('/booking/0');
final result = await apiClient.deleteBooking(0);
expect(result, isA<Ok<void>>());
});
});
}

@ -20,10 +20,12 @@ void main() {
group('HomeScreen tests', () {
late HomeViewModel viewModel;
late MockGoRouter goRouter;
late FakeBookingRepository bookingRepository;
setUp(() {
bookingRepository = FakeBookingRepository()..createBooking(kBooking);
viewModel = HomeViewModel(
bookingRepository: FakeBookingRepository()..createBooking(kBooking),
bookingRepository: bookingRepository,
userRepository: FakeUserRepository(),
);
goRouter = MockGoRouter();
@ -81,5 +83,20 @@ void main() {
// Should navigate to results screen
verify(() => goRouter.push(Routes.bookingWithId(0))).called(1);
});
testWidgets('should delete booking', (tester) async {
await loadWidget(tester);
await tester.pumpAndSettle();
// Swipe on booking (created from kBooking)
await tester.drag(find.text('name1, Europe'), const Offset(-1000, 0));
await tester.pumpAndSettle();
// Existing booking should be gone
expect(find.text('name1, Europe'), findsNothing);
// Booking should be deleted from repository
expect(bookingRepository.bookings, isEmpty);
});
});
}

@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:compass_app/data/repositories/booking/booking_repository.dart';
import 'package:compass_app/domain/models/booking/booking.dart';
import 'package:compass_app/domain/models/booking/booking_summary.dart';
@ -6,10 +5,12 @@ import 'package:compass_app/utils/result.dart';
class FakeBookingRepository implements BookingRepository {
List<Booking> bookings = List.empty(growable: true);
int sequentialId = 0;
@override
Future<Result<void>> createBooking(Booking booking) async {
bookings.add(booking);
final bookingWithId = booking.copyWith(id: sequentialId++);
bookings.add(bookingWithId);
return Result.ok(null);
}
@ -25,9 +26,9 @@ class FakeBookingRepository implements BookingRepository {
List<BookingSummary> _createSummaries() {
return bookings
.mapIndexed(
(index, booking) => BookingSummary(
id: index,
.map(
(booking) => BookingSummary(
id: booking.id!,
name:
'${booking.destination.name}, ${booking.destination.continent}',
startDate: booking.startDate,
@ -36,4 +37,10 @@ class FakeBookingRepository implements BookingRepository {
)
.toList();
}
@override
Future<Result<void>> delete(int id) async {
bookings.removeWhere((booking) => booking.id == id);
return Result.ok(null);
}
}

@ -107,4 +107,10 @@ class FakeApiClient implements ApiClient {
Future<Result<UserApiModel>> getUser() async {
return Result.ok(userApiModel);
}
@override
Future<Result<void>> deleteBooking(int id) async {
bookings.removeWhere((booking) => booking.id == id);
return Result.ok(null);
}
}

@ -40,4 +40,15 @@ extension HttpMethodMocks on MockHttpClient {
return Future.value(request);
});
}
void mockDelete(String path) {
when(() => delete(any(), any(), path)).thenAnswer((invocation) {
final request = MockHttpClientRequest();
final response = MockHttpClientResponse();
when(() => request.close()).thenAnswer((_) => Future.value(response));
when(() => request.headers).thenReturn(MockHttpHeaders());
when(() => response.statusCode).thenReturn(204);
return Future.value(request);
});
}
}

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
@ -22,10 +23,9 @@ class BookingApi {
.where((activity) => activity.destinationRef == destination.ref)
.map((activity) => activity.ref)
.toList();
_bookings.insert(
0,
_bookings.add(
Booking(
id: 0,
id: _sequentialId++,
name: '${destination.name}, ${destination.continent}',
startDate: DateTime(2024, 7, 20),
endDate: DateTime(2024, 8, 15),
@ -39,6 +39,9 @@ class BookingApi {
// To keep things simple, the id is also the index in the list.
final List<Booking> _bookings = List.empty(growable: true);
// Used to generate IDs for bookings
int _sequentialId = 0;
Router get router {
final router = Router();
@ -52,12 +55,16 @@ class BookingApi {
// Get a booking by id
router.get('/<id>', (Request request, String id) {
final index = int.parse(id);
if (index < 0 || index >= _bookings.length) {
final bookingId = int.parse(id);
final booking =
_bookings.firstWhereOrNull((booking) => booking.id == bookingId);
if (booking == null) {
return Response.notFound('Invalid id');
}
return Response.ok(
json.encode(_bookings[index]),
json.encode(booking),
headers: {'Content-Type': 'application/json'},
);
});
@ -74,8 +81,7 @@ class BookingApi {
}
// Add ID to new booking
final id = _bookings.length;
final bookingWithId = booking.copyWith(id: id);
final bookingWithId = booking.copyWith(id: _sequentialId++);
// Store booking
_bookings.add(bookingWithId);
@ -88,6 +94,19 @@ class BookingApi {
);
});
// Delete booking
router.delete('/<id>', (Request request, String id) async {
final bookingId = int.parse(id);
final booking =
_bookings.firstWhereOrNull((booking) => booking.id == bookingId);
if (booking == null) {
return Response.notFound('Invalid id');
}
_bookings.remove(booking);
// 240: no content
return Response(204);
});
return router;
}
}

@ -12,6 +12,7 @@ dependencies:
shelf_router: ^1.1.0
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0
collection: ^1.19.0
dev_dependencies:
http: ^1.1.0

@ -128,6 +128,42 @@ void main() {
expect(booking.id, 1);
});
test('Delete a booking', () async {
// First create a booking
final response = await post(
Uri.parse('$host/booking'),
headers: headers,
body: jsonEncode(
Booking(
name: "DESTINATION, CONTINENT",
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 2, 2),
destinationRef: 'REF',
activitiesRef: ['ACT1', 'ACT2'],
),
),
);
expect(response.statusCode, 201);
final booking = Booking.fromJson(jsonDecode(response.body));
// Then delete it
final deleteResponse = await delete(
Uri.parse('$host/booking/${booking.id}'),
headers: headers,
);
expect(deleteResponse.statusCode, 204);
});
test('Delete a booking is not found', () async {
final response = await delete(
Uri.parse('$host/booking/42'),
headers: headers,
);
expect(response.statusCode, 404);
});
test('Get user', () async {
final response = await get(
Uri.parse('$host/user'),

Loading…
Cancel
Save