feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features

Version: 1.1.0

Major changes:
- Implemented complete Flutter app structure with all core features
- Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features
- Integrated Supabase backend with authentication and data repositories
- Added offline support with Hive caching and local storage
- Implemented comprehensive routing with go_router
- Added location services with Google Maps integration
- Implemented notifications and home widget support
- Added voice recording capabilities and AI chat features
- Created comprehensive test suite and documentation
- Added Android and iOS platform configurations
- Implemented achievements system and social features
- Added calendar integration and bucket list functionality

This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
Tomas Dvorak
2026-01-04 14:33:54 +01:00
parent 1a29315672
commit 37ffb93923
210 changed files with 29417 additions and 477 deletions
@@ -0,0 +1,258 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lifetimer/data/models/activity_model.dart';
void main() {
group('Activity Model', () {
group('Constructor and Properties', () {
test('should create Activity with required fields', () {
final now = DateTime.now();
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
expect(activity.id, equals('activity-1'));
expect(activity.userId, equals('user-1'));
expect(activity.type, equals('goal_created'));
expect(activity.payload, isNull);
});
test('should create Activity with payload', () {
final now = DateTime.now();
final payload = {'goal_id': 'goal-1', 'title': 'Test Goal'};
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
payload: payload,
createdAt: now,
);
expect(activity.id, equals('activity-1'));
expect(activity.userId, equals('user-1'));
expect(activity.type, equals('goal_completed'));
expect(activity.payload, equals(payload));
});
});
group('copyWith', () {
test('should create copy with updated fields', () {
final now = DateTime.now();
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
final updatedActivity = activity.copyWith(
type: 'goal_completed',
payload: const {'goal_id': 'goal-1'},
);
expect(updatedActivity.id, equals(activity.id));
expect(updatedActivity.userId, equals(activity.userId));
expect(updatedActivity.type, equals('goal_completed'));
expect(updatedActivity.payload, equals({'goal_id': 'goal-1'}));
});
test('should preserve original when no fields provided', () {
final now = DateTime.now();
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
final copiedActivity = activity.copyWith();
expect(copiedActivity.id, equals(activity.id));
expect(copiedActivity.type, equals(activity.type));
expect(copiedActivity.payload, equals(activity.payload));
});
});
group('toJson and fromJson', () {
test('should serialize to JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final payload = {'goal_id': 'goal-1', 'title': 'Test Goal'};
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
payload: payload,
createdAt: now,
);
final json = activity.toJson();
expect(json['id'], equals('activity-1'));
expect(json['user_id'], equals('user-1'));
expect(json['type'], equals('goal_completed'));
expect(json['payload'], equals(payload));
expect(json['created_at'], equals(now.toIso8601String()));
});
test('should deserialize from JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final payload = {'goal_id': 'goal-1', 'title': 'Test Goal'};
final json = {
'id': 'activity-1',
'user_id': 'user-1',
'type': 'goal_completed',
'payload': payload,
'created_at': now.toIso8601String(),
};
final activity = Activity.fromJson(json);
expect(activity.id, equals('activity-1'));
expect(activity.userId, equals('user-1'));
expect(activity.type, equals('goal_completed'));
expect(activity.payload, equals(payload));
});
test('should handle null payload in JSON', () {
final now = DateTime(2024, 1, 1);
final json = {
'id': 'activity-1',
'user_id': 'user-1',
'type': 'countdown_started',
'payload': null,
'created_at': now.toIso8601String(),
};
final activity = Activity.fromJson(json);
expect(activity.payload, isNull);
});
test('should roundtrip through JSON', () {
final payload = {'goal_id': 'goal-1', 'progress': 100};
final activity = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
payload: payload,
createdAt: DateTime(2024, 1, 1),
);
final json = activity.toJson();
final deserializedActivity = Activity.fromJson(json);
expect(deserializedActivity.id, equals(activity.id));
expect(deserializedActivity.userId, equals(activity.userId));
expect(deserializedActivity.type, equals(activity.type));
expect(deserializedActivity.payload, equals(activity.payload));
});
});
group('Equatable', () {
test('should be equal when all properties match', () {
final now = DateTime.now();
final activity1 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
final activity2 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
expect(activity1, equals(activity2));
expect(activity1.hashCode, equals(activity2.hashCode));
});
test('should not be equal when properties differ', () {
final now = DateTime.now();
final activity1 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
final activity2 = Activity(
id: 'activity-2',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
expect(activity1, isNot(equals(activity2)));
});
test('should not be equal when type differs', () {
final now = DateTime.now();
final activity1 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_created',
createdAt: now,
);
final activity2 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
createdAt: now,
);
expect(activity1, isNot(equals(activity2)));
});
test('should not be equal when payload differs', () {
final now = DateTime.now();
final activity1 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
payload: const {'goal_id': 'goal-1'},
createdAt: now,
);
final activity2 = Activity(
id: 'activity-1',
userId: 'user-1',
type: 'goal_completed',
payload: const {'goal_id': 'goal-2'},
createdAt: now,
);
expect(activity1, isNot(equals(activity2)));
});
});
group('Activity Types', () {
test('should support various activity types', () {
const types = [
'goal_created',
'goal_completed',
'countdown_started',
'countdown_finished',
'milestone_reached',
'profile_updated',
];
for (final type in types) {
final activity = Activity(
id: 'activity-$type',
userId: 'user-1',
type: type,
createdAt: DateTime.now(),
);
expect(activity.type, equals(type));
}
});
});
});
}
@@ -0,0 +1,363 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lifetimer/data/models/goal_model.dart';
void main() {
group('Goal Model', () {
group('Constructor and Properties', () {
test('should create Goal with required fields', () {
final now = DateTime.now();
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
expect(goal.id, equals('goal-1'));
expect(goal.ownerId, equals('user-1'));
expect(goal.title, equals('Test Goal'));
expect(goal.description, isNull);
expect(goal.progress, equals(0));
expect(goal.locationLat, isNull);
expect(goal.locationLng, isNull);
expect(goal.locationName, isNull);
expect(goal.imageUrl, isNull);
expect(goal.completed, isFalse);
});
test('should create Goal with all fields', () {
final now = DateTime.now();
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
description: 'Test description',
progress: 50,
locationLat: 40.7128,
locationLng: -74.0060,
locationName: 'New York',
imageUrl: 'https://example.com/image.jpg',
completed: false,
createdAt: now,
updatedAt: now,
);
expect(goal.id, equals('goal-1'));
expect(goal.ownerId, equals('user-1'));
expect(goal.title, equals('Test Goal'));
expect(goal.description, equals('Test description'));
expect(goal.progress, equals(50));
expect(goal.locationLat, equals(40.7128));
expect(goal.locationLng, equals(-74.0060));
expect(goal.locationName, equals('New York'));
expect(goal.imageUrl, equals('https://example.com/image.jpg'));
expect(goal.completed, isFalse);
});
});
group('Computed Properties', () {
test('hasLocation should return false when location is null', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasLocation, isFalse);
});
test('hasLocation should return false when only lat is set', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
locationLat: 40.7128,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasLocation, isFalse);
});
test('hasLocation should return false when only lng is set', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
locationLng: -74.0060,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasLocation, isFalse);
});
test('hasLocation should return true when both lat and lng are set', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
locationLat: 40.7128,
locationLng: -74.0060,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasLocation, isTrue);
});
test('hasImage should return false when imageUrl is null', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasImage, isFalse);
});
test('hasImage should return false when imageUrl is empty', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
imageUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasImage, isFalse);
});
test('hasImage should return true when imageUrl is set', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
imageUrl: 'https://example.com/image.jpg',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(goal.hasImage, isTrue);
});
});
group('copyWith', () {
test('should create copy with updated fields', () {
final now = DateTime.now();
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
final updatedGoal = goal.copyWith(
title: 'Updated Goal',
progress: 75,
completed: true,
);
expect(updatedGoal.id, equals(goal.id));
expect(updatedGoal.ownerId, equals(goal.ownerId));
expect(updatedGoal.title, equals('Updated Goal'));
expect(updatedGoal.progress, equals(75));
expect(updatedGoal.completed, isTrue);
});
test('should preserve original when no fields provided', () {
final now = DateTime.now();
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
final copiedGoal = goal.copyWith();
expect(copiedGoal.id, equals(goal.id));
expect(copiedGoal.title, equals(goal.title));
expect(copiedGoal.progress, equals(goal.progress));
});
});
group('toJson and fromJson', () {
test('should serialize to JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
description: 'Test description',
progress: 50,
locationLat: 40.7128,
locationLng: -74.0060,
locationName: 'New York',
imageUrl: 'https://example.com/image.jpg',
completed: false,
createdAt: now,
updatedAt: now,
);
final json = goal.toJson();
expect(json['id'], equals('goal-1'));
expect(json['owner_id'], equals('user-1'));
expect(json['title'], equals('Test Goal'));
expect(json['description'], equals('Test description'));
expect(json['progress'], equals(50));
expect(json['location_lat'], equals(40.7128));
expect(json['location_lng'], equals(-74.0060));
expect(json['location_name'], equals('New York'));
expect(json['image_url'], equals('https://example.com/image.jpg'));
expect(json['completed'], isFalse);
expect(json['created_at'], equals(now.toIso8601String()));
expect(json['updated_at'], equals(now.toIso8601String()));
});
test('should deserialize from JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final json = {
'id': 'goal-1',
'owner_id': 'user-1',
'title': 'Test Goal',
'description': 'Test description',
'progress': 50,
'location_lat': 40.7128,
'location_lng': -74.0060,
'location_name': 'New York',
'image_url': 'https://example.com/image.jpg',
'completed': false,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
};
final goal = Goal.fromJson(json);
expect(goal.id, equals('goal-1'));
expect(goal.ownerId, equals('user-1'));
expect(goal.title, equals('Test Goal'));
expect(goal.description, equals('Test description'));
expect(goal.progress, equals(50));
expect(goal.locationLat, equals(40.7128));
expect(goal.locationLng, equals(-74.0060));
expect(goal.locationName, equals('New York'));
expect(goal.imageUrl, equals('https://example.com/image.jpg'));
expect(goal.completed, isFalse);
});
test('should handle null optional fields in JSON', () {
final now = DateTime(2024, 1, 1);
final json = {
'id': 'goal-1',
'owner_id': 'user-1',
'title': 'Test Goal',
'description': null,
'progress': null,
'location_lat': null,
'location_lng': null,
'location_name': null,
'image_url': null,
'completed': null,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
};
final goal = Goal.fromJson(json);
expect(goal.description, isNull);
expect(goal.progress, equals(0));
expect(goal.locationLat, isNull);
expect(goal.locationLng, isNull);
expect(goal.locationName, isNull);
expect(goal.imageUrl, isNull);
expect(goal.completed, isFalse);
});
test('should roundtrip through JSON', () {
final goal = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
description: 'Test description',
progress: 50,
locationLat: 40.7128,
locationLng: -74.0060,
locationName: 'New York',
imageUrl: 'https://example.com/image.jpg',
completed: false,
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
);
final json = goal.toJson();
final deserializedGoal = Goal.fromJson(json);
expect(deserializedGoal.id, equals(goal.id));
expect(deserializedGoal.ownerId, equals(goal.ownerId));
expect(deserializedGoal.title, equals(goal.title));
expect(deserializedGoal.description, equals(goal.description));
expect(deserializedGoal.progress, equals(goal.progress));
expect(deserializedGoal.locationLat, equals(goal.locationLat));
expect(deserializedGoal.locationLng, equals(goal.locationLng));
expect(deserializedGoal.locationName, equals(goal.locationName));
expect(deserializedGoal.imageUrl, equals(goal.imageUrl));
expect(deserializedGoal.completed, equals(goal.completed));
});
});
group('Equatable', () {
test('should be equal when all properties match', () {
final now = DateTime.now();
final goal1 = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
final goal2 = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
expect(goal1, equals(goal2));
expect(goal1.hashCode, equals(goal2.hashCode));
});
test('should not be equal when properties differ', () {
final now = DateTime.now();
final goal1 = Goal(
id: 'goal-1',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
final goal2 = Goal(
id: 'goal-2',
ownerId: 'user-1',
title: 'Test Goal',
createdAt: now,
updatedAt: now,
);
expect(goal1, isNot(equals(goal2)));
});
});
});
}
@@ -0,0 +1,241 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lifetimer/data/models/goal_step_model.dart';
void main() {
group('GoalStep Model', () {
group('Constructor and Properties', () {
test('should create GoalStep with required fields', () {
final now = DateTime.now();
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
expect(step.id, equals('step-1'));
expect(step.goalId, equals('goal-1'));
expect(step.title, equals('Test Step'));
expect(step.isDone, isFalse);
expect(step.orderIndex, equals(0));
});
test('should create GoalStep with isDone true', () {
final now = DateTime.now();
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Completed Step',
isDone: true,
orderIndex: 1,
createdAt: now,
);
expect(step.id, equals('step-1'));
expect(step.goalId, equals('goal-1'));
expect(step.title, equals('Completed Step'));
expect(step.isDone, isTrue);
expect(step.orderIndex, equals(1));
});
});
group('copyWith', () {
test('should create copy with updated fields', () {
final now = DateTime.now();
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final updatedStep = step.copyWith(
title: 'Updated Step',
isDone: true,
orderIndex: 1,
);
expect(updatedStep.id, equals(step.id));
expect(updatedStep.goalId, equals(step.goalId));
expect(updatedStep.title, equals('Updated Step'));
expect(updatedStep.isDone, isTrue);
expect(updatedStep.orderIndex, equals(1));
});
test('should preserve original when no fields provided', () {
final now = DateTime.now();
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final copiedStep = step.copyWith();
expect(copiedStep.id, equals(step.id));
expect(copiedStep.title, equals(step.title));
expect(copiedStep.isDone, equals(step.isDone));
expect(copiedStep.orderIndex, equals(step.orderIndex));
});
});
group('toJson and fromJson', () {
test('should serialize to JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final json = step.toJson();
expect(json['id'], equals('step-1'));
expect(json['goal_id'], equals('goal-1'));
expect(json['title'], equals('Test Step'));
expect(json['is_done'], isFalse);
expect(json['order_index'], equals(0));
expect(json['created_at'], equals(now.toIso8601String()));
});
test('should deserialize from JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final json = {
'id': 'step-1',
'goal_id': 'goal-1',
'title': 'Test Step',
'is_done': false,
'order_index': 0,
'created_at': now.toIso8601String(),
};
final step = GoalStep.fromJson(json);
expect(step.id, equals('step-1'));
expect(step.goalId, equals('goal-1'));
expect(step.title, equals('Test Step'));
expect(step.isDone, isFalse);
expect(step.orderIndex, equals(0));
});
test('should handle null optional fields in JSON', () {
final now = DateTime(2024, 1, 1);
final json = {
'id': 'step-1',
'goal_id': 'goal-1',
'title': 'Test Step',
'is_done': null,
'order_index': null,
'created_at': now.toIso8601String(),
};
final step = GoalStep.fromJson(json);
expect(step.isDone, isFalse);
expect(step.orderIndex, equals(0));
});
test('should roundtrip through JSON', () {
final step = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: true,
orderIndex: 2,
createdAt: DateTime(2024, 1, 1),
);
final json = step.toJson();
final deserializedStep = GoalStep.fromJson(json);
expect(deserializedStep.id, equals(step.id));
expect(deserializedStep.goalId, equals(step.goalId));
expect(deserializedStep.title, equals(step.title));
expect(deserializedStep.isDone, equals(step.isDone));
expect(deserializedStep.orderIndex, equals(step.orderIndex));
});
});
group('Equatable', () {
test('should be equal when all properties match', () {
final now = DateTime.now();
final step1 = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final step2 = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
expect(step1, equals(step2));
expect(step1.hashCode, equals(step2.hashCode));
});
test('should not be equal when properties differ', () {
final now = DateTime.now();
final step1 = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final step2 = GoalStep(
id: 'step-2',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
expect(step1, isNot(equals(step2)));
});
test('should not be equal when isDone differs', () {
final now = DateTime.now();
final step1 = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: false,
orderIndex: 0,
createdAt: now,
);
final step2 = GoalStep(
id: 'step-1',
goalId: 'goal-1',
title: 'Test Step',
isDone: true,
orderIndex: 0,
createdAt: now,
);
expect(step1, isNot(equals(step2)));
});
});
});
}
@@ -0,0 +1,341 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lifetimer/data/models/user_model.dart';
void main() {
group('User Model', () {
group('Constructor and Properties', () {
test('should create User with required fields', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
);
expect(user.id, equals('user-1'));
expect(user.username, equals('testuser'));
expect(user.email, equals('test@example.com'));
expect(user.avatarUrl, isNull);
expect(user.bio, isNull);
expect(user.isPublicProfile, isFalse);
expect(user.countdownStartDate, isNull);
expect(user.countdownEndDate, isNull);
});
test('should create User with all fields', () {
final now = DateTime.now();
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.jpg',
bio: 'Test bio',
isPublicProfile: true,
countdownStartDate: now,
countdownEndDate: now.add(const Duration(days: 1356)),
createdAt: now,
updatedAt: now,
);
expect(user.id, equals('user-1'));
expect(user.username, equals('testuser'));
expect(user.email, equals('test@example.com'));
expect(user.avatarUrl, equals('https://example.com/avatar.jpg'));
expect(user.bio, equals('Test bio'));
expect(user.isPublicProfile, isTrue);
expect(user.countdownStartDate, equals(now));
expect(user.countdownEndDate, equals(now.add(const Duration(days: 1356))));
});
});
group('Computed Properties', () {
test('hasCountdownStarted should return false when countdownStartDate is null', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(user.hasCountdownStarted, isFalse);
});
test('hasCountdownStarted should return true when countdownStartDate is set', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
countdownStartDate: DateTime.now(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(user.hasCountdownStarted, isTrue);
});
test('isCountdownActive should return false when countdown not started', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(user.isCountdownActive, isFalse);
});
test('isCountdownActive should return true when countdown is active', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
countdownStartDate: DateTime.now().subtract(const Duration(days: 10)),
countdownEndDate: DateTime.now().add(const Duration(days: 1346)),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(user.isCountdownActive, isTrue);
});
test('isCountdownActive should return false when countdown has ended', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
countdownStartDate: DateTime(2023, 1, 1),
countdownEndDate: DateTime(2023, 12, 31),
createdAt: DateTime(2023, 1, 1),
updatedAt: DateTime(2023, 12, 31),
);
expect(user.isCountdownActive, isFalse);
});
test('daysRemaining should return null when countdown not active', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(user.daysRemaining, isNull);
});
test('daysRemaining should return correct days when countdown is active', () {
final endDate = DateTime.now().add(const Duration(days: 100));
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
countdownStartDate: DateTime.now(),
countdownEndDate: endDate,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final daysRemaining = user.daysRemaining;
expect(daysRemaining, isNotNull);
expect(daysRemaining, greaterThan(0));
expect(daysRemaining, lessThanOrEqualTo(100));
});
});
group('copyWith', () {
test('should create copy with updated fields', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final updatedUser = user.copyWith(
username: 'newuser',
bio: 'New bio',
);
expect(updatedUser.id, equals(user.id));
expect(updatedUser.username, equals('newuser'));
expect(updatedUser.email, equals(user.email));
expect(updatedUser.bio, equals('New bio'));
});
test('should preserve original when no fields provided', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final copiedUser = user.copyWith();
expect(copiedUser.id, equals(user.id));
expect(copiedUser.username, equals(user.username));
expect(copiedUser.email, equals(user.email));
});
});
group('toJson and fromJson', () {
test('should serialize to JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.jpg',
bio: 'Test bio',
isPublicProfile: true,
countdownStartDate: now,
countdownEndDate: now.add(const Duration(days: 1356)),
createdAt: now,
updatedAt: now,
);
final json = user.toJson();
expect(json['id'], equals('user-1'));
expect(json['username'], equals('testuser'));
expect(json['email'], equals('test@example.com'));
expect(json['avatar_url'], equals('https://example.com/avatar.jpg'));
expect(json['bio'], equals('Test bio'));
expect(json['is_public_profile'], isTrue);
expect(json['countdown_start_date'], equals(now.toIso8601String()));
expect(json['countdown_end_date'], equals(now.add(const Duration(days: 1356)).toIso8601String()));
expect(json['created_at'], equals(now.toIso8601String()));
expect(json['updated_at'], equals(now.toIso8601String()));
});
test('should deserialize from JSON correctly', () {
final now = DateTime(2024, 1, 1, 12, 0, 0);
final json = {
'id': 'user-1',
'username': 'testuser',
'email': 'test@example.com',
'avatar_url': 'https://example.com/avatar.jpg',
'bio': 'Test bio',
'is_public_profile': true,
'countdown_start_date': now.toIso8601String(),
'countdown_end_date': now.add(const Duration(days: 1356)).toIso8601String(),
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
};
final user = User.fromJson(json);
expect(user.id, equals('user-1'));
expect(user.username, equals('testuser'));
expect(user.email, equals('test@example.com'));
expect(user.avatarUrl, equals('https://example.com/avatar.jpg'));
expect(user.bio, equals('Test bio'));
expect(user.isPublicProfile, isTrue);
expect(user.countdownStartDate, equals(now));
expect(user.countdownEndDate, equals(now.add(const Duration(days: 1356))));
});
test('should handle null optional fields in JSON', () {
final now = DateTime(2024, 1, 1);
final json = {
'id': 'user-1',
'username': 'testuser',
'email': 'test@example.com',
'avatar_url': null,
'bio': null,
'is_public_profile': null,
'countdown_start_date': null,
'countdown_end_date': null,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
};
final user = User.fromJson(json);
expect(user.avatarUrl, isNull);
expect(user.bio, isNull);
expect(user.isPublicProfile, isFalse);
expect(user.countdownStartDate, isNull);
expect(user.countdownEndDate, isNull);
});
test('should roundtrip through JSON', () {
final user = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.jpg',
bio: 'Test bio',
isPublicProfile: true,
countdownStartDate: DateTime(2024, 1, 1),
countdownEndDate: DateTime(2024, 1, 1).add(const Duration(days: 1356)),
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
);
final json = user.toJson();
final deserializedUser = User.fromJson(json);
expect(deserializedUser.id, equals(user.id));
expect(deserializedUser.username, equals(user.username));
expect(deserializedUser.email, equals(user.email));
expect(deserializedUser.avatarUrl, equals(user.avatarUrl));
expect(deserializedUser.bio, equals(user.bio));
expect(deserializedUser.isPublicProfile, equals(user.isPublicProfile));
expect(deserializedUser.countdownStartDate, equals(user.countdownStartDate));
expect(deserializedUser.countdownEndDate, equals(user.countdownEndDate));
});
});
group('Equatable', () {
test('should be equal when all properties match', () {
final now = DateTime.now();
final user1 = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: now,
updatedAt: now,
);
final user2 = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: now,
updatedAt: now,
);
expect(user1, equals(user2));
expect(user1.hashCode, equals(user2.hashCode));
});
test('should not be equal when properties differ', () {
final now = DateTime.now();
final user1 = User(
id: 'user-1',
username: 'testuser',
email: 'test@example.com',
createdAt: now,
updatedAt: now,
);
final user2 = User(
id: 'user-2',
username: 'testuser',
email: 'test@example.com',
createdAt: now,
updatedAt: now,
);
expect(user1, isNot(equals(user2)));
});
});
});
}