From f2f596dc70756402fd27edea69cfd399dd39b183 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 20 May 2026 07:53:14 +0300 Subject: Guard dateTimeFromJson against malformed date strings (c9) Wrap DateTime.parse in try/catch so a malformed or non-ISO-8601 date string from the server degrades to null instead of throwing a FormatException that crashes model deserialization across Media, MediaSet, User, PodcastFeed, PodcastEpisode, PlaybackHint, Share, and Note. Add player-android/test/json_helpers_test.dart with 11 tests covering null, empty, non-string, valid ISO, and malformed inputs (regression guard for the original crash). Co-Authored-By: Claude Opus 4.7 --- player-android/test/json_helpers_test.dart | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 player-android/test/json_helpers_test.dart (limited to 'player-android/test') 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'); + }); + }); +} -- cgit v1.2.3