From 70b89d40ec18a709b34c6b12406e636492d46d49 Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 5 Jun 2026 11:30:35 -0600 Subject: [PATCH] feat(shorebird_code_push_protocol): add patch-adoption API surface (#3809) --- .../lib/model_helpers.dart | 17 +++ .../lib/shorebird_code_push_protocol.dart | 5 + .../get_patch_adoption_response.dart | 109 ++++++++++++++++++ .../models/get_patch_adoption_parameter3.dart | 38 ++++++ .../lib/src/models/patch_adoption_entry.dart | 92 +++++++++++++++ .../lib/src/models/patch_adoption_point.dart | 86 ++++++++++++++ .../lib/src/models/patch_adoption_range.dart | 63 ++++++++++ .../get_patch_adoption_response_test.dart | 49 ++++++++ .../get_patch_adoption_parameter3_test.dart | 43 +++++++ .../models/patch_adoption_entry_test.dart | 37 ++++++ .../models/patch_adoption_point_test.dart | 30 +++++ .../models/patch_adoption_range_test.dart | 28 +++++ 12 files changed, 597 insertions(+) create mode 100644 packages/shorebird_code_push_protocol/lib/src/messages/get_patch_adoption/get_patch_adoption_response.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/get_patch_adoption_parameter3.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_entry.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_point.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_range.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/messages/get_patch_adoption/get_patch_adoption_response_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/get_patch_adoption_parameter3_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_entry_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_point_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_range_test.dart diff --git a/packages/shorebird_code_push_protocol/lib/model_helpers.dart b/packages/shorebird_code_push_protocol/lib/model_helpers.dart index c70775f3..2757edc4 100644 --- a/packages/shorebird_code_push_protocol/lib/model_helpers.dart +++ b/packages/shorebird_code_push_protocol/lib/model_helpers.dart @@ -1,5 +1,22 @@ import 'package:collection/collection.dart'; +/// Parse a nullable string as a DateTime. +DateTime? maybeParseDateTime(String? value) { + if (value == null) { + return null; + } + return DateTime.parse(value); +} + +/// Parse a nullable RFC 3339 full-date string (`YYYY-MM-DD`) as a DateTime. +/// Time and timezone components are zero. +DateTime? maybeParseDate(String? value) { + if (value == null) { + return null; + } + return DateTime.parse(value); +} + /// Runs [build] to construct a `fromJson`-parsed value of type [T], /// converting any `TypeError` (e.g. an unexpected null or a cast /// failure on a required field) into a `FormatException` that names diff --git a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart index 63364d83..791722bf 100644 --- a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart +++ b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart @@ -29,6 +29,7 @@ export 'package:shorebird_code_push_protocol/src/messages/get_gcp_upload_speed_t export 'package:shorebird_code_push_protocol/src/messages/get_organization_apps/get_organization_apps_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_organization_users/get_organization_users_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_organizations_response.dart'; +export 'package:shorebird_code_push_protocol/src/messages/get_patch_adoption/get_patch_adoption_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_release/get_release_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_release_artifacts/get_release_artifacts_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_release_patches/get_release_patches_response.dart'; @@ -44,12 +45,16 @@ export 'package:shorebird_code_push_protocol/src/models/app.dart'; export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart'; export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart'; export 'package:shorebird_code_push_protocol/src/models/channel.dart'; +export 'package:shorebird_code_push_protocol/src/models/get_patch_adoption_parameter3.dart'; export 'package:shorebird_code_push_protocol/src/models/latest_release.dart'; export 'package:shorebird_code_push_protocol/src/models/organization.dart'; export 'package:shorebird_code_push_protocol/src/models/organization_membership.dart'; export 'package:shorebird_code_push_protocol/src/models/organization_type.dart'; export 'package:shorebird_code_push_protocol/src/models/organization_user.dart'; export 'package:shorebird_code_push_protocol/src/models/patch.dart'; +export 'package:shorebird_code_push_protocol/src/models/patch_adoption_entry.dart'; +export 'package:shorebird_code_push_protocol/src/models/patch_adoption_point.dart'; +export 'package:shorebird_code_push_protocol/src/models/patch_adoption_range.dart'; export 'package:shorebird_code_push_protocol/src/models/patch_artifact.dart'; export 'package:shorebird_code_push_protocol/src/models/patch_check_metadata.dart'; export 'package:shorebird_code_push_protocol/src/models/pending_release.dart'; diff --git a/packages/shorebird_code_push_protocol/lib/src/messages/get_patch_adoption/get_patch_adoption_response.dart b/packages/shorebird_code_push_protocol/lib/src/messages/get_patch_adoption/get_patch_adoption_response.dart new file mode 100644 index 00000000..ee21c3ad --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/messages/get_patch_adoption/get_patch_adoption_response.dart @@ -0,0 +1,109 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; +import 'package:shorebird_code_push_protocol/src/models/patch_adoption_entry.dart'; +import 'package:shorebird_code_push_protocol/src/models/patch_adoption_range.dart'; + +/// {@template get_patch_adoption_response} +/// The response body for GET /apps/{appId}/metrics/patch-adoption. Covers +/// exactly one release. +/// {@endtemplate} +@immutable +class GetPatchAdoptionResponse { + /// {@macro get_patch_adoption_response} + const GetPatchAdoptionResponse({ + required this.releaseVersion, + required this.isLatest, + required this.granularity, + required this.range, + required this.asOf, + required this.patches, + }); + + /// Converts a `Map` to a [GetPatchAdoptionResponse]. + factory GetPatchAdoptionResponse.fromJson(Map json) { + return parseFromJson( + 'GetPatchAdoptionResponse', + json, + () => GetPatchAdoptionResponse( + releaseVersion: json['release_version'] as String, + isLatest: json['is_latest'] as bool, + granularity: checkedKey(json, 'granularity') as String?, + range: PatchAdoptionRange.fromJson( + json['range'] as Map, + ), + asOf: DateTime.parse(json['as_of'] as String), + patches: (json['patches'] as List) + .map( + (e) => PatchAdoptionEntry.fromJson(e as Map), + ) + .toList(), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static GetPatchAdoptionResponse? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return GetPatchAdoptionResponse.fromJson(json); + } + + /// The release version this response is for. + final String releaseVersion; + + /// True when the release was resolved via the "latest release by + /// creation date" default (no `release_version` was supplied). + final bool isLatest; + + /// The bucket resolution (`hour`, `day`, `week`, or `month`), or null + /// when each patch carries a single full-window value. + final String? granularity; + + /// The effective (post-clamp) window the response covers. + final PatchAdoptionRange range; + + /// Server's UTC timestamp at the moment the response was + /// constructed. Not a freshness indicator for the underlying + /// data, which is refreshed by an hourly scheduled query and + /// may lag by up to ~1 hour. + final DateTime asOf; + + /// One entry per patch of the release. + final List patches; + + /// Converts a [GetPatchAdoptionResponse] to a `Map`. + Map toJson() { + return { + 'release_version': releaseVersion, + 'is_latest': isLatest, + 'granularity': granularity, + 'range': range.toJson(), + 'as_of': asOf.toIso8601String(), + 'patches': patches.map((e) => e.toJson()).toList(), + }; + } + + @override + int get hashCode => Object.hashAll([ + releaseVersion, + isLatest, + granularity, + range, + asOf, + listHash(patches), + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is GetPatchAdoptionResponse && + releaseVersion == other.releaseVersion && + isLatest == other.isLatest && + granularity == other.granularity && + range == other.range && + asOf == other.asOf && + listsEqual(patches, other.patches); + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/get_patch_adoption_parameter3.dart b/packages/shorebird_code_push_protocol/lib/src/models/get_patch_adoption_parameter3.dart new file mode 100644 index 00000000..119c9d0d --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/get_patch_adoption_parameter3.dart @@ -0,0 +1,38 @@ +enum GetPatchAdoptionParameter3 { + hour._('hour'), + day._('day'), + week._('week'), + month._('month'); + + const GetPatchAdoptionParameter3._(this.value); + + /// Creates a GetPatchAdoptionParameter3 from a json value. + factory GetPatchAdoptionParameter3.fromJson(String json) { + return GetPatchAdoptionParameter3.values.firstWhere( + (value) => value.value == json, + orElse: () => throw FormatException( + 'Unknown GetPatchAdoptionParameter3 value: $json', + ), + ); + } + + /// Convenience to create a nullable type from a nullable json value. + /// Useful when parsing optional fields. + static GetPatchAdoptionParameter3? maybeFromJson(String? json) { + if (json == null) { + return null; + } + return GetPatchAdoptionParameter3.fromJson(json); + } + + /// The value of the enum. This is the exact value + /// from the OpenAPI spec and will be used for network transport. + final String value; + + /// Converts the enum to its json value. + String toJson() => value; + + /// Returns the string form of the enum. + @override + String toString() => value; +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_entry.dart b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_entry.dart new file mode 100644 index 00000000..7d295dbd --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_entry.dart @@ -0,0 +1,92 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; +import 'package:shorebird_code_push_protocol/src/models/patch_adoption_point.dart'; +import 'package:shorebird_code_push_protocol/src/models/release_platform.dart'; + +/// {@template patch_adoption_entry} +/// Cumulative adoption for one patch of the release. Values are +/// cumulative — "patch `>= patch_number`" — so they are monotonic +/// non-increasing in `patch_number`. +/// {@endtemplate} +@immutable +class PatchAdoptionEntry { + /// {@macro patch_adoption_entry} + const PatchAdoptionEntry({ + required this.patchNumber, + required this.targetPlatforms, + required this.isRolledBack, + required this.series, + }); + + /// Converts a `Map` to a [PatchAdoptionEntry]. + factory PatchAdoptionEntry.fromJson(Map json) { + return parseFromJson( + 'PatchAdoptionEntry', + json, + () => PatchAdoptionEntry( + patchNumber: json['patch_number'] as int, + targetPlatforms: (json['target_platforms'] as List) + .map((e) => ReleasePlatform.fromJson(e as String)) + .toList(), + isRolledBack: json['is_rolled_back'] as bool, + series: (json['series'] as List) + .map( + (e) => PatchAdoptionPoint.fromJson(e as Map), + ) + .toList(), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static PatchAdoptionEntry? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return PatchAdoptionEntry.fromJson(json); + } + + /// The patch number these cumulative values are anchored at. + final int patchNumber; + + /// The platform(s) the patch was built for (from its artifacts). The + /// denominator counts only devices on these platforms. + final List targetPlatforms; + + /// Whether the patch has been rolled back. + final bool isRolledBack; + + /// The adoption series. Exactly one point (`period: null`) when no + /// granularity was requested; otherwise one point per bucket, ordered + /// by `period` ascending. + final List series; + + /// Converts a [PatchAdoptionEntry] to a `Map`. + Map toJson() { + return { + 'patch_number': patchNumber, + 'target_platforms': targetPlatforms.map((e) => e.toJson()).toList(), + 'is_rolled_back': isRolledBack, + 'series': series.map((e) => e.toJson()).toList(), + }; + } + + @override + int get hashCode => Object.hashAll([ + patchNumber, + listHash(targetPlatforms), + isRolledBack, + listHash(series), + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PatchAdoptionEntry && + patchNumber == other.patchNumber && + listsEqual(targetPlatforms, other.targetPlatforms) && + isRolledBack == other.isRolledBack && + listsEqual(series, other.series); + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_point.dart b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_point.dart new file mode 100644 index 00000000..772a0ab7 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_point.dart @@ -0,0 +1,86 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template patch_adoption_point} +/// One point in a patch's adoption series: the cumulative distinct +/// devices on `patch >= patch_number` (`devices`) over the patch's target +/// (`target`), and their ratio (`adoption_pct`), for one bucket. +/// {@endtemplate} +@immutable +class PatchAdoptionPoint { + /// {@macro patch_adoption_point} + const PatchAdoptionPoint({ + required this.period, + required this.devices, + required this.target, + required this.adoptionPct, + }); + + /// Converts a `Map` to a [PatchAdoptionPoint]. + factory PatchAdoptionPoint.fromJson(Map json) { + return parseFromJson( + 'PatchAdoptionPoint', + json, + () => PatchAdoptionPoint( + period: maybeParseDateTime(checkedKey(json, 'period') as String?), + devices: json['devices'] as int, + target: json['target'] as int, + adoptionPct: (json['adoption_pct'] as num).toDouble(), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static PatchAdoptionPoint? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return PatchAdoptionPoint.fromJson(json); + } + + /// The bucket start (UTC), or null when the response is a single + /// full-window value (no granularity requested). + final DateTime? period; + + /// Distinct devices running patch `>= patch_number` on the patch's + /// target platform(s) within this bucket (an HLL count). + final int devices; + + /// Distinct devices on the release whose platform the patch targets — + /// the patch's reachable denominator — within this bucket. + final int target; + + /// `devices / target`, in [0.0, 1.0] (0 when `target` is 0). Server + /// is the source of truth; clients should render this value rather + /// than recomputing it. + final double adoptionPct; + + /// Converts a [PatchAdoptionPoint] to a `Map`. + Map toJson() { + return { + 'period': period?.toIso8601String(), + 'devices': devices, + 'target': target, + 'adoption_pct': adoptionPct, + }; + } + + @override + int get hashCode => Object.hashAll([ + period, + devices, + target, + adoptionPct, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PatchAdoptionPoint && + period == other.period && + devices == other.devices && + target == other.target && + adoptionPct == other.adoptionPct; + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_range.dart b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_range.dart new file mode 100644 index 00000000..a934cd84 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/patch_adoption_range.dart @@ -0,0 +1,63 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template patch_adoption_range} +/// The effective (post-clamp) window the response covers. +/// {@endtemplate} +@immutable +class PatchAdoptionRange { + /// {@macro patch_adoption_range} + const PatchAdoptionRange({ + required this.start, + required this.end, + }); + + /// Converts a `Map` to a [PatchAdoptionRange]. + factory PatchAdoptionRange.fromJson(Map json) { + return parseFromJson( + 'PatchAdoptionRange', + json, + () => PatchAdoptionRange( + start: DateTime.parse(json['start'] as String), + end: DateTime.parse(json['end'] as String), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static PatchAdoptionRange? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return PatchAdoptionRange.fromJson(json); + } + + /// Window start (UTC, inclusive). + final DateTime start; + + /// Window end (UTC, exclusive). + final DateTime end; + + /// Converts a [PatchAdoptionRange] to a `Map`. + Map toJson() { + return { + 'start': start.toIso8601String(), + 'end': end.toIso8601String(), + }; + } + + @override + int get hashCode => Object.hashAll([ + start, + end, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PatchAdoptionRange && + start == other.start && + end == other.end; + } +} diff --git a/packages/shorebird_code_push_protocol/test/generated/messages/get_patch_adoption/get_patch_adoption_response_test.dart b/packages/shorebird_code_push_protocol/test/generated/messages/get_patch_adoption/get_patch_adoption_response_test.dart new file mode 100644 index 00000000..2c0671f2 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/messages/get_patch_adoption/get_patch_adoption_response_test.dart @@ -0,0 +1,49 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('GetPatchAdoptionResponse', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = GetPatchAdoptionResponse( + releaseVersion: 'example', + isLatest: false, + granularity: 'example', + range: PatchAdoptionRange( + start: DateTime.utc(2024), + end: DateTime.utc(2024), + ), + asOf: DateTime.utc(2024), + patches: [ + PatchAdoptionEntry( + patchNumber: 0, + targetPlatforms: [ReleasePlatform.values.first], + isRolledBack: false, + series: [ + PatchAdoptionPoint( + period: DateTime.utc(2024), + devices: 0, + target: 0, + adoptionPct: 0, + ), + ], + ), + ], + ); + final parsed = GetPatchAdoptionResponse.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(GetPatchAdoptionResponse.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => GetPatchAdoptionResponse.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/get_patch_adoption_parameter3_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/get_patch_adoption_parameter3_test.dart new file mode 100644 index 00000000..851fc9f9 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/get_patch_adoption_parameter3_test.dart @@ -0,0 +1,43 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('GetPatchAdoptionParameter3', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = GetPatchAdoptionParameter3.values.first; + final parsed = GetPatchAdoptionParameter3.maybeFromJson( + instance.toJson(), + )!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(GetPatchAdoptionParameter3.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => + GetPatchAdoptionParameter3.maybeFromJson('__invalid_enum_value__'), + throwsFormatException, + ); + }); + + test('toString matches toJson for every value', () { + for (final value in GetPatchAdoptionParameter3.values) { + expect(value.toString(), equals(value.toJson())); + } + }); + + test('fromJson round-trips every value', () { + for (final value in GetPatchAdoptionParameter3.values) { + expect( + GetPatchAdoptionParameter3.fromJson(value.toJson()), + equals(value), + ); + } + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_entry_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_entry_test.dart new file mode 100644 index 00000000..b8a59797 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_entry_test.dart @@ -0,0 +1,37 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('PatchAdoptionEntry', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = PatchAdoptionEntry( + patchNumber: 0, + targetPlatforms: [ReleasePlatform.values.first], + isRolledBack: false, + series: [ + PatchAdoptionPoint( + period: DateTime.utc(2024), + devices: 0, + target: 0, + adoptionPct: 0, + ), + ], + ); + final parsed = PatchAdoptionEntry.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(PatchAdoptionEntry.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => PatchAdoptionEntry.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_point_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_point_test.dart new file mode 100644 index 00000000..b8e57dc1 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_point_test.dart @@ -0,0 +1,30 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('PatchAdoptionPoint', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = PatchAdoptionPoint( + period: DateTime.utc(2024), + devices: 0, + target: 0, + adoptionPct: 0, + ); + final parsed = PatchAdoptionPoint.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(PatchAdoptionPoint.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => PatchAdoptionPoint.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_range_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_range_test.dart new file mode 100644 index 00000000..2ddb6ea3 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/patch_adoption_range_test.dart @@ -0,0 +1,28 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('PatchAdoptionRange', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = PatchAdoptionRange( + start: DateTime.utc(2024), + end: DateTime.utc(2024), + ); + final parsed = PatchAdoptionRange.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(PatchAdoptionRange.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => PatchAdoptionRange.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +}