summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--player-android/lib/models/json_helpers.dart23
-rw-r--r--player-android/test/json_helpers_test.dart82
2 files changed, 103 insertions, 2 deletions
diff --git a/player-android/lib/models/json_helpers.dart b/player-android/lib/models/json_helpers.dart
index c966476..b6d3f7c 100644
--- a/player-android/lib/models/json_helpers.dart
+++ b/player-android/lib/models/json_helpers.dart
@@ -1,4 +1,23 @@
-DateTime? dateTimeFromJson(Object? value) =>
- value is String && value.isNotEmpty ? DateTime.parse(value) : null;
+// JSON conversion helpers shared by all model classes.
+//
+// `dateTimeFromJson` is defensive against malformed server responses: it
+// accepts any dynamic JSON value, rejects null/non-string/empty input up
+// front, and catches `FormatException` from `DateTime.parse` so that an
+// unexpected date string degrades to null instead of crashing the entire
+// model deserialization. This protects every consumer (Media, MediaSet,
+// User, PodcastFeed, PodcastEpisode, PlaybackHint, Share, Note, ...) from
+// a single bad field propagating an exception up the JSON decode stack.
+DateTime? dateTimeFromJson(Object? value) {
+ if (value is! String || value.isEmpty) return null;
+ try {
+ return DateTime.parse(value);
+ } on FormatException catch (e) {
+ // Defensive: server returned an unexpected date string. Log and
+ // degrade to null rather than crashing model deserialization.
+ // ignore: avoid_print
+ print('dateTimeFromJson: failed to parse "$value": $e');
+ return null;
+ }
+}
String? dateTimeToJson(DateTime? value) => value?.toIso8601String();
diff --git a/player-android/test/json_helpers_test.dart b/player-android/test/json_helpers_test.dart
new file mode 100644
index 0000000..a0596e2
--- /dev/null
+++ b/player-android/test/json_helpers_test.dart
@@ -0,0 +1,82 @@
+// Unit tests for json_helpers.dart.
+//
+// `dateTimeFromJson` is the shared deserialization helper used by every
+// model that has a nullable DateTime field. These tests pin down its
+// defensive contract: only well-formed ISO-8601 strings produce a
+// DateTime; everything else (null, non-string, empty string, malformed
+// string) silently degrades to null without throwing.
+//
+// The malformed-string case is the regression guard for the original
+// crash bug: prior to the fix, `DateTime.parse('not a date')` propagated
+// a FormatException out of fromJson() and crashed the app.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:player_android/models/json_helpers.dart';
+
+void main() {
+ group('dateTimeFromJson', () {
+ test('returns null for null input', () {
+ expect(dateTimeFromJson(null), isNull);
+ });
+
+ test('returns null for empty string', () {
+ expect(dateTimeFromJson(''), isNull);
+ });
+
+ test('returns null for non-string input (int)', () {
+ expect(dateTimeFromJson(12345), isNull);
+ });
+
+ test('returns null for non-string input (bool)', () {
+ expect(dateTimeFromJson(true), isNull);
+ });
+
+ test('parses a valid ISO-8601 UTC string', () {
+ final dt = dateTimeFromJson('2026-05-20T12:34:56.000Z');
+ expect(dt, isNotNull);
+ expect(dt!.toUtc().year, 2026);
+ expect(dt.toUtc().month, 5);
+ expect(dt.toUtc().day, 20);
+ expect(dt.toUtc().hour, 12);
+ expect(dt.toUtc().minute, 34);
+ expect(dt.toUtc().second, 56);
+ });
+
+ test('parses a valid ISO-8601 date-only string', () {
+ final dt = dateTimeFromJson('2026-01-15');
+ expect(dt, isNotNull);
+ expect(dt!.year, 2026);
+ expect(dt.month, 1);
+ expect(dt.day, 15);
+ });
+
+ test('returns null for malformed string instead of throwing', () {
+ // Regression guard: this used to throw FormatException and crash
+ // the entire model deserialization.
+ expect(() => dateTimeFromJson('not a date'), returnsNormally);
+ expect(dateTimeFromJson('not a date'), isNull);
+ });
+
+ test('returns null for ISO-like string with non-numeric junk', () {
+ // DateTime.parse is surprisingly tolerant of out-of-range numeric
+ // components (it rolls them over), so we use a clearly non-numeric
+ // value to force a FormatException.
+ expect(dateTimeFromJson('2026-XX-YYTzz:zz:zzZ'), isNull);
+ });
+
+ test('returns null for garbage tokens', () {
+ expect(dateTimeFromJson('@@@'), isNull);
+ });
+ });
+
+ group('dateTimeToJson', () {
+ test('returns null for null input', () {
+ expect(dateTimeToJson(null), isNull);
+ });
+
+ test('serializes DateTime to ISO-8601 string', () {
+ final dt = DateTime.utc(2026, 5, 20, 12, 34, 56);
+ expect(dateTimeToJson(dt), '2026-05-20T12:34:56.000Z');
+ });
+ });
+}