From 6a39cee4f1207c5484c54d739873d6ab2c76925a Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 15 May 2026 17:34:08 -0600 Subject: [PATCH] feat(shorebird_code_push_protocol): add release analysis API surface (#3744) --- cspell.config.yaml | 1 + .../lib/shorebird_code_push_protocol.dart | 3 + .../patch_check/patch_check_request.dart | 9 +- .../lib/src/models/app_metadata.dart | 74 +++++++++- .../lib/src/models/latest_release.dart | 130 ++++++++++++++++++ .../lib/src/models/pending_release.dart | 74 ++++++++++ .../lib/src/models/release_analysis.dart | 93 +++++++++++++ .../generated/models/latest_release_test.dart | 39 ++++++ .../models/pending_release_test.dart | 29 ++++ .../models/release_analysis_test.dart | 31 +++++ .../test/src/models/app_metadata_test.dart | 43 ++++++ .../test/src/models/latest_release_test.dart | 70 ++++++++++ .../test/src/models/pending_release_test.dart | 18 +++ .../src/models/release_analysis_test.dart | 20 +++ 14 files changed, 627 insertions(+), 7 deletions(-) create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/latest_release.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/pending_release.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/release_analysis.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/latest_release_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/pending_release_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/release_analysis_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/src/models/latest_release_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/src/models/pending_release_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/src/models/release_analysis_test.dart diff --git a/cspell.config.yaml b/cspell.config.yaml index 4554be33..f4a4b572 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -142,6 +142,7 @@ words: - timebase # Mach timebase (mach_timebase_info) - udevadm # From .github/workflows/e2e.yaml - udid # Unique Device Identifier + - unanalyzed - unawaited - unmockable - unobfuscated 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 4a6570a4..5c7b7c3a 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 @@ -43,6 +43,7 @@ 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/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'; @@ -50,9 +51,11 @@ 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_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'; export 'package:shorebird_code_push_protocol/src/models/private_user.dart'; export 'package:shorebird_code_push_protocol/src/models/public_user.dart'; export 'package:shorebird_code_push_protocol/src/models/release.dart'; +export 'package:shorebird_code_push_protocol/src/models/release_analysis.dart'; export 'package:shorebird_code_push_protocol/src/models/release_artifact.dart'; export 'package:shorebird_code_push_protocol/src/models/release_patch.dart'; export 'package:shorebird_code_push_protocol/src/models/release_platform.dart'; diff --git a/packages/shorebird_code_push_protocol/lib/src/messages/patch_check/patch_check_request.dart b/packages/shorebird_code_push_protocol/lib/src/messages/patch_check/patch_check_request.dart index 393ced9c..7ed22f28 100644 --- a/packages/shorebird_code_push_protocol/lib/src/messages/patch_check/patch_check_request.dart +++ b/packages/shorebird_code_push_protocol/lib/src/messages/patch_check/patch_check_request.dart @@ -75,12 +75,9 @@ class PatchCheckRequest { /// unique per app. Optional for backward compatibility. final String? clientId; - /// The number of the patch currently running on the device, if any. - /// - /// Supersedes [patchNumber] for newer updater clients. Unlike - /// [patchNumber], this does not affect the server's response; - /// [patchNumber] is retained for compatibility with legacy clients that - /// rely on the server's short-circuit path. + /// The patch number currently running on the device, if any. + /// Supersedes `patch_number` for newer clients; unlike + /// `patch_number`, this does not affect the server's response. final int? currentPatchNumber; /// Converts a [PatchCheckRequest] to a `Map`. diff --git a/packages/shorebird_code_push_protocol/lib/src/models/app_metadata.dart b/packages/shorebird_code_push_protocol/lib/src/models/app_metadata.dart index 63e8c7ec..55305e94 100644 --- a/packages/shorebird_code_push_protocol/lib/src/models/app_metadata.dart +++ b/packages/shorebird_code_push_protocol/lib/src/models/app_metadata.dart @@ -1,5 +1,8 @@ import 'package:meta/meta.dart'; import 'package:shorebird_code_push_protocol/model_helpers.dart'; +import 'package:shorebird_code_push_protocol/src/models/latest_release.dart'; +import 'package:shorebird_code_push_protocol/src/models/pending_release.dart'; +import 'package:shorebird_code_push_protocol/src/models/release_platform.dart'; /// {@template app_metadata} /// A single app which contains zero or more releases. @@ -14,6 +17,10 @@ class AppMetadata { required this.updatedAt, this.latestReleaseVersion, this.latestPatchNumber, + this.platforms, + this.latestReleases, + this.pendingReleases, + this.iconUrl, }); /// Converts a `Map` to an [AppMetadata]. @@ -28,6 +35,23 @@ class AppMetadata { latestPatchNumber: json['latest_patch_number'] as int?, createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), + platforms: (json['platforms'] as List?) + ?.map((e) => ReleasePlatform.fromJson(e as String)) + .toList(), + latestReleases: (json['latest_releases'] as Map?)?.map( + (key, value) => MapEntry( + ReleasePlatform.fromJson(key), + LatestRelease.fromJson(value as Map), + ), + ), + pendingReleases: (json['pending_releases'] as Map?) + ?.map( + (key, value) => MapEntry( + ReleasePlatform.fromJson(key), + PendingRelease.fromJson(value as Map), + ), + ), + iconUrl: json['icon_url'] as String?, ), ); } @@ -59,6 +83,38 @@ class AppMetadata { /// The date and time the app was last updated. final DateTime updatedAt; + /// Every platform the app has shipped to (i.e. has at least one + /// release artifact for). Independent of `latest_releases`: + /// an app can list a platform here even if no release on that + /// platform has been analyzed yet. + final List? platforms; + + /// The latest analyzed release per platform. A platform is + /// omitted when no release for that platform has been analyzed + /// yet. When the latest release for a platform is not yet + /// analyzed but a previous one is, the previous release is + /// returned here and `pending_releases.{platform}` identifies + /// the unanalyzed newer release. + final Map? latestReleases; + + /// The newest unanalyzed release per platform, whenever one + /// exists. A platform is omitted when its most recent release + /// has already been analyzed. Independent of `latest_releases`: + /// both can be present (a newer release than the analyzed one + /// is being processed) or only `pending_releases` can be + /// present (no release on the platform has been analyzed yet). + final Map? pendingReleases; + + /// Server-emitted URL for the app's launcher icon, sourced from + /// the most recent analyzed iOS release (or Android, when iOS + /// has no analyzed release with an icon). Requires the same + /// auth as the rest of the apps API. The URL embeds the picked + /// release's id as a `v` query parameter so it can be cached + /// indefinitely; when a different release becomes the icon + /// source the URL changes and clients re-fetch. Omitted when + /// no analyzed release has an icon. + final String? iconUrl; + /// Converts an [AppMetadata] to a `Map`. Map toJson() { return { @@ -68,6 +124,14 @@ class AppMetadata { 'latest_patch_number': latestPatchNumber, 'created_at': createdAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(), + 'platforms': platforms?.map((e) => e.toJson()).toList(), + 'latest_releases': latestReleases?.map( + (key, value) => MapEntry(key.toJson(), value.toJson()), + ), + 'pending_releases': pendingReleases?.map( + (key, value) => MapEntry(key.toJson(), value.toJson()), + ), + 'icon_url': iconUrl, }; } @@ -79,6 +143,10 @@ class AppMetadata { latestPatchNumber, createdAt, updatedAt, + listHash(platforms), + mapHash(latestReleases), + mapHash(pendingReleases), + iconUrl, ]); @override @@ -90,6 +158,10 @@ class AppMetadata { latestReleaseVersion == other.latestReleaseVersion && latestPatchNumber == other.latestPatchNumber && createdAt == other.createdAt && - updatedAt == other.updatedAt; + updatedAt == other.updatedAt && + listsEqual(platforms, other.platforms) && + mapsEqual(latestReleases, other.latestReleases) && + mapsEqual(pendingReleases, other.pendingReleases) && + iconUrl == other.iconUrl; } } diff --git a/packages/shorebird_code_push_protocol/lib/src/models/latest_release.dart b/packages/shorebird_code_push_protocol/lib/src/models/latest_release.dart new file mode 100644 index 00000000..d84e389b --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/latest_release.dart @@ -0,0 +1,130 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; +import 'package:shorebird_code_push_protocol/src/models/release_analysis.dart'; +import 'package:shorebird_code_push_protocol/src/models/release_status.dart'; + +/// {@template latest_release} +/// Per-platform projection of an analyzed release as surfaced by +/// `AppMetadata.latest_releases`. Each entry corresponds to the +/// most recent analyzed release on the keying platform, so +/// `analysis` is always populated and `status` is the single +/// per-platform status (rather than the cross-platform +/// `platform_statuses` map carried by `Release`). +/// {@endtemplate} +@immutable +class LatestRelease { + /// {@macro latest_release} + const LatestRelease({ + required this.id, + required this.version, + required this.flutterRevision, + required this.createdAt, + required this.updatedAt, + required this.status, + required this.analysis, + this.flutterVersion, + this.notes, + }); + + /// Converts a `Map` to a [LatestRelease]. + factory LatestRelease.fromJson(Map json) { + return parseFromJson( + 'LatestRelease', + json, + () => LatestRelease( + id: json['id'] as int, + version: json['version'] as String, + flutterRevision: json['flutter_revision'] as String, + flutterVersion: json['flutter_version'] as String?, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + status: ReleaseStatus.fromJson(json['status'] as String), + notes: json['notes'] as String?, + analysis: ReleaseAnalysis.fromJson( + json['analysis'] as Map, + ), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static LatestRelease? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return LatestRelease.fromJson(json); + } + + /// The ID of the release. + final int id; + + /// The version of the release. + final String version; + + /// The Flutter revision used to create the release. + final String flutterRevision; + + /// The Flutter version used to create the release. Optional + /// because it was added later; older releases do not have it. + final String? flutterVersion; + + /// The date and time the release was created. + final DateTime createdAt; + + /// The date and time the release was last updated. + final DateTime updatedAt; + + /// The status of a release. + final ReleaseStatus status; + + /// Freeform notes associated with the release, if any. + final String? notes; + + /// Analyzer-extracted metadata for a release artifact on a single + /// platform. + final ReleaseAnalysis analysis; + + /// Converts a [LatestRelease] to a `Map`. + Map toJson() { + return { + 'id': id, + 'version': version, + 'flutter_revision': flutterRevision, + 'flutter_version': flutterVersion, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'status': status.toJson(), + 'notes': notes, + 'analysis': analysis.toJson(), + }; + } + + @override + int get hashCode => Object.hashAll([ + id, + version, + flutterRevision, + flutterVersion, + createdAt, + updatedAt, + status, + notes, + analysis, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is LatestRelease && + id == other.id && + version == other.version && + flutterRevision == other.flutterRevision && + flutterVersion == other.flutterVersion && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + status == other.status && + notes == other.notes && + analysis == other.analysis; + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/pending_release.dart b/packages/shorebird_code_push_protocol/lib/src/models/pending_release.dart new file mode 100644 index 00000000..22a9c6e1 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/pending_release.dart @@ -0,0 +1,74 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template pending_release} +/// A newer release that has been created but not yet analyzed. +/// Surfaced alongside the most recent analyzed release so clients +/// can show an "analyzing…" indicator without losing the stable +/// icon and metadata. +/// {@endtemplate} +@immutable +class PendingRelease { + /// {@macro pending_release} + const PendingRelease({ + required this.id, + required this.version, + required this.createdAt, + }); + + /// Converts a `Map` to a [PendingRelease]. + factory PendingRelease.fromJson(Map json) { + return parseFromJson( + 'PendingRelease', + json, + () => PendingRelease( + id: json['id'] as int, + version: json['version'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static PendingRelease? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return PendingRelease.fromJson(json); + } + + /// The ID of the pending release. + final int id; + + /// The version of the pending release. + final String version; + + /// The date and time the pending release was created. + final DateTime createdAt; + + /// Converts a [PendingRelease] to a `Map`. + Map toJson() { + return { + 'id': id, + 'version': version, + 'created_at': createdAt.toIso8601String(), + }; + } + + @override + int get hashCode => Object.hashAll([ + id, + version, + createdAt, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PendingRelease && + id == other.id && + version == other.version && + createdAt == other.createdAt; + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/release_analysis.dart b/packages/shorebird_code_push_protocol/lib/src/models/release_analysis.dart new file mode 100644 index 00000000..196d93e3 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/release_analysis.dart @@ -0,0 +1,93 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template release_analysis} +/// Analyzer-extracted metadata for a release artifact on a single +/// platform. +/// {@endtemplate} +@immutable +class ReleaseAnalysis { + /// {@macro release_analysis} + const ReleaseAnalysis({ + required this.displayName, + required this.packageName, + required this.minSdkVersion, + required this.targetSdkVersion, + required this.architectures, + }); + + /// Converts a `Map` to a [ReleaseAnalysis]. + factory ReleaseAnalysis.fromJson(Map json) { + return parseFromJson( + 'ReleaseAnalysis', + json, + () => ReleaseAnalysis( + displayName: json['display_name'] as String, + packageName: json['package_name'] as String, + minSdkVersion: json['min_sdk_version'] as String, + targetSdkVersion: json['target_sdk_version'] as String, + architectures: (json['architectures'] as List).cast(), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static ReleaseAnalysis? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return ReleaseAnalysis.fromJson(json); + } + + /// The user-visible application name extracted from the artifact + /// (e.g. AndroidManifest `application:label`). May differ from + /// the user-curated `App.display_name`. + final String displayName; + + /// The application package name (e.g. `com.example.app`). + final String packageName; + + /// The minimum SDK level required to install the artifact + /// (Android API level for android, iOS deployment target for ios). + final String minSdkVersion; + + /// The SDK level the artifact targets (Android targetSdk for + /// android, iOS SDK for ios). + final String targetSdkVersion; + + /// CPU architectures present in the artifact + /// (e.g. `["arm64-v8a", "armeabi-v7a"]`). + final List architectures; + + /// Converts a [ReleaseAnalysis] to a `Map`. + Map toJson() { + return { + 'display_name': displayName, + 'package_name': packageName, + 'min_sdk_version': minSdkVersion, + 'target_sdk_version': targetSdkVersion, + 'architectures': architectures, + }; + } + + @override + int get hashCode => Object.hashAll([ + displayName, + packageName, + minSdkVersion, + targetSdkVersion, + listHash(architectures), + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ReleaseAnalysis && + displayName == other.displayName && + packageName == other.packageName && + minSdkVersion == other.minSdkVersion && + targetSdkVersion == other.targetSdkVersion && + listsEqual(architectures, other.architectures); + } +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/latest_release_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/latest_release_test.dart new file mode 100644 index 00000000..577d2973 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/latest_release_test.dart @@ -0,0 +1,39 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('LatestRelease', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = LatestRelease( + id: 0, + version: 'example', + flutterRevision: 'example', + createdAt: DateTime.utc(2024), + updatedAt: DateTime.utc(2024), + status: ReleaseStatus.values.first, + analysis: const ReleaseAnalysis( + displayName: 'example', + packageName: 'example', + minSdkVersion: 'example', + targetSdkVersion: 'example', + architectures: ['example'], + ), + ); + final parsed = LatestRelease.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(LatestRelease.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => LatestRelease.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/pending_release_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/pending_release_test.dart new file mode 100644 index 00000000..705d4edb --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/pending_release_test.dart @@ -0,0 +1,29 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('PendingRelease', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = PendingRelease( + id: 0, + version: 'example', + createdAt: DateTime.utc(2024), + ); + final parsed = PendingRelease.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(PendingRelease.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => PendingRelease.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/release_analysis_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/release_analysis_test.dart new file mode 100644 index 00000000..0ae3b86a --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/release_analysis_test.dart @@ -0,0 +1,31 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('ReleaseAnalysis', () { + test('round-trips via maybeFromJson/toJson', () { + const instance = ReleaseAnalysis( + displayName: 'example', + packageName: 'example', + minSdkVersion: 'example', + targetSdkVersion: 'example', + architectures: ['example'], + ); + final parsed = ReleaseAnalysis.maybeFromJson(instance.toJson())!; + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(ReleaseAnalysis.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => ReleaseAnalysis.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/src/models/app_metadata_test.dart b/packages/shorebird_code_push_protocol/test/src/models/app_metadata_test.dart index 333d75a0..3aaa5838 100644 --- a/packages/shorebird_code_push_protocol/test/src/models/app_metadata_test.dart +++ b/packages/shorebird_code_push_protocol/test/src/models/app_metadata_test.dart @@ -18,6 +18,49 @@ void main() { ); }); + test('can be (de)serialized with platforms, latestReleases, ' + 'pendingReleases, and iconUrl', () { + final appMetadata = AppMetadata( + appId: '30370f27-dbf1-4673-8b20-fb096e38dffa', + displayName: 'My App', + latestReleaseVersion: '1.0.0', + latestPatchNumber: 1, + createdAt: DateTime(2022), + updatedAt: DateTime(2023), + platforms: const [ReleasePlatform.android, ReleasePlatform.ios], + latestReleases: { + ReleasePlatform.android: LatestRelease( + id: 535, + version: '1.2.3+5', + flutterRevision: 'abc123', + flutterVersion: '3.27.0', + createdAt: DateTime(2023), + updatedAt: DateTime(2023), + status: ReleaseStatus.active, + analysis: const ReleaseAnalysis( + displayName: 'My App', + packageName: 'com.example.app', + minSdkVersion: '24', + targetSdkVersion: '34', + architectures: ['arm64-v8a'], + ), + ), + }, + pendingReleases: { + ReleasePlatform.ios: PendingRelease( + id: 700, + version: '1.2.4+6', + createdAt: DateTime(2024), + ), + }, + iconUrl: '/api/v1/app-icons/30370f27-dbf1-4673-8b20-fb096e38dffa?v=535', + ); + expect( + AppMetadata.fromJson(appMetadata.toJson()).toJson(), + equals(appMetadata.toJson()), + ); + }); + group('equality', () { test('should return true if all properties are equal', () { final appMetadata1 = AppMetadata( diff --git a/packages/shorebird_code_push_protocol/test/src/models/latest_release_test.dart b/packages/shorebird_code_push_protocol/test/src/models/latest_release_test.dart new file mode 100644 index 00000000..15c8d4fa --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/src/models/latest_release_test.dart @@ -0,0 +1,70 @@ +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group(LatestRelease, () { + test('can be (de)serialized', () { + final latest = LatestRelease( + id: 535, + version: '1.2.3+5', + flutterRevision: 'abc123', + flutterVersion: '3.27.0', + createdAt: DateTime(2023, 3), + updatedAt: DateTime(2023, 4), + status: ReleaseStatus.active, + notes: 'launch notes', + analysis: const ReleaseAnalysis( + displayName: 'My App', + packageName: 'com.example.app', + minSdkVersion: '24', + targetSdkVersion: '34', + architectures: ['arm64-v8a'], + ), + ); + expect( + LatestRelease.fromJson(latest.toJson()).toJson(), + equals(latest.toJson()), + ); + }); + + test('is equatable', () { + const analysis = ReleaseAnalysis( + displayName: 'My App', + packageName: 'com.example.app', + minSdkVersion: '24', + targetSdkVersion: '34', + architectures: ['arm64-v8a'], + ); + final latest1 = LatestRelease( + id: 535, + version: '1.2.3+5', + flutterRevision: 'abc123', + createdAt: DateTime(2023, 3), + updatedAt: DateTime(2023, 4), + status: ReleaseStatus.active, + analysis: analysis, + ); + final latest1Copy = LatestRelease( + id: 535, + version: '1.2.3+5', + flutterRevision: 'abc123', + createdAt: DateTime(2023, 3), + updatedAt: DateTime(2023, 4), + status: ReleaseStatus.active, + analysis: analysis, + ); + final latest2 = LatestRelease( + id: 600, + version: '1.2.4+6', + flutterRevision: 'abc123', + createdAt: DateTime(2023, 3), + updatedAt: DateTime(2023, 4), + status: ReleaseStatus.active, + analysis: analysis, + ); + + expect(latest1, equals(latest1Copy)); + expect(latest1, isNot(equals(latest2))); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/src/models/pending_release_test.dart b/packages/shorebird_code_push_protocol/test/src/models/pending_release_test.dart new file mode 100644 index 00000000..368352de --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/src/models/pending_release_test.dart @@ -0,0 +1,18 @@ +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group(PendingRelease, () { + test('can be (de)serialized', () { + final pending = PendingRelease( + id: 537, + version: '1.2.4+6', + createdAt: DateTime(2024), + ); + expect( + PendingRelease.fromJson(pending.toJson()).toJson(), + equals(pending.toJson()), + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/src/models/release_analysis_test.dart b/packages/shorebird_code_push_protocol/test/src/models/release_analysis_test.dart new file mode 100644 index 00000000..f332404d --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/src/models/release_analysis_test.dart @@ -0,0 +1,20 @@ +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group(ReleaseAnalysis, () { + test('can be (de)serialized', () { + const analysis = ReleaseAnalysis( + displayName: 'My App', + packageName: 'com.example.app', + minSdkVersion: '24', + targetSdkVersion: '34', + architectures: ['arm64-v8a', 'armeabi-v7a'], + ); + expect( + ReleaseAnalysis.fromJson(analysis.toJson()).toJson(), + equals(analysis.toJson()), + ); + }); + }); +}