@_nazmiforreal/flutter-ota 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  3. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +7 -6
  4. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +46 -49
  5. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +44 -23
  6. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +27 -17
  7. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +11 -6
  8. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +39 -22
  9. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -13
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +117 -36
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +15 -9
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +14 -8
  13. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +37 -12
  14. package/dart-src/packages/cli-tools/lib/src/config.dart +13 -3
  15. package/dart-src/packages/cli-tools/lib/src/operations.dart +45 -0
  16. package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
  17. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
  18. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  19. package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
  20. package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
  21. package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
  22. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
  23. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
  24. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
  25. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
  26. package/dart-src/packages/core/test/rollout_test.dart +128 -0
  27. package/dart-src/packages/core/test/semver_test.dart +52 -0
  28. package/dart-src/packages/core/test/semver_vectors.json +1 -0
  29. package/dart-src/packages/core/test/types_test.dart +129 -0
  30. package/dart-src/packages/core/test/uuid_test.dart +38 -0
  31. package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
  32. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
  33. package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
  34. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
  35. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
  36. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
  37. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
  38. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
  39. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
  40. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
  41. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
  42. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
  43. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
  44. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
  45. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
  46. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
  47. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
  48. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
  49. package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
  50. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
  51. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
  52. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
  53. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
  54. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
  55. package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
  56. package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
  57. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
  58. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
  59. package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
  60. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
  61. package/package.json +1 -1
  62. package/scripts/vendor.sh +29 -0
@@ -0,0 +1,654 @@
1
+ /// Faithful mock of the Supabase client surface, mirroring hot-updater's
2
+ /// `vi.mock("@supabase/supabase-js")` test seam.
3
+ ///
4
+ /// The mock keeps an in-memory set of `bundles` / `bundle_patches` rows and
5
+ /// implements the PostgREST query-builder + RPC contract used by the database
6
+ /// plugin, plus a configurable storage bucket used by the storage plugin.
7
+ library;
8
+
9
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
10
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
11
+
12
+ import 'package:flutter_patcher_supabase/src/supabase_bundle_mapper.dart';
13
+ import 'package:flutter_patcher_supabase/src/supabase_client_adapter.dart';
14
+ import 'package:flutter_patcher_supabase/src/types.dart';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // In-memory row store
18
+ // ---------------------------------------------------------------------------
19
+
20
+ class Store {
21
+ final Map<String, Map<String, dynamic>> bundleRows = {};
22
+ final Map<String, Map<String, dynamic>> bundlePatchRows = {};
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Query builder mock
27
+ // ---------------------------------------------------------------------------
28
+
29
+ sealed class _Filter {
30
+ const _Filter();
31
+ }
32
+
33
+ class _EqFilter extends _Filter {
34
+ final String column;
35
+ final Object? value;
36
+ _EqFilter(this.column, this.value);
37
+ }
38
+
39
+ class _CompareFilter extends _Filter {
40
+ final String type; // gt | gte | lt | lte
41
+ final String column;
42
+ final Object? value;
43
+ _CompareFilter(this.type, this.column, this.value);
44
+ }
45
+
46
+ class _InFilter extends _Filter {
47
+ final String column;
48
+ final List<Object?> values;
49
+ _InFilter(this.column, this.values);
50
+ }
51
+
52
+ class _IsFilter extends _Filter {
53
+ final String column;
54
+ final Object? value;
55
+ _IsFilter(this.column, this.value);
56
+ }
57
+
58
+ class _NotFilter extends _Filter {
59
+ final String column;
60
+ final String operator;
61
+ final Object? value;
62
+ _NotFilter(this.column, this.operator, this.value);
63
+ }
64
+
65
+ int _compareValues(Object? left, Object? right) {
66
+ if (left is String && right is String) return left.compareTo(right);
67
+ if (left is num && right is num) return left.compareTo(right);
68
+ if (left is bool && right is bool) {
69
+ return (left ? 1 : 0).compareTo(right ? 1 : 0);
70
+ }
71
+ return left.toString().compareTo(right.toString());
72
+ }
73
+
74
+ class _MockFilterBuilder implements SupabaseFilterBuilderLike {
75
+ _MockFilterBuilder(
76
+ this._store,
77
+ this._table,
78
+ this._mode, {
79
+ this.head = false,
80
+ });
81
+
82
+ final Store _store;
83
+ final String _table;
84
+ final String _mode;
85
+ final bool head;
86
+ final bool count = false;
87
+
88
+ final List<_Filter> _filters = [];
89
+ bool ascending = true;
90
+ int? limitValue;
91
+ int? rangeStart;
92
+ int? rangeEnd;
93
+ bool singleRow = false;
94
+
95
+ @override
96
+ SupabaseFilterBuilderLike eq(String column, Object? value) {
97
+ _filters.add(_EqFilter(column, value));
98
+ return this;
99
+ }
100
+
101
+ @override
102
+ SupabaseFilterBuilderLike gt(String column, Object? value) {
103
+ _filters.add(_CompareFilter('gt', column, value));
104
+ return this;
105
+ }
106
+
107
+ @override
108
+ SupabaseFilterBuilderLike gte(String column, Object? value) {
109
+ _filters.add(_CompareFilter('gte', column, value));
110
+ return this;
111
+ }
112
+
113
+ @override
114
+ SupabaseFilterBuilderLike lt(String column, Object? value) {
115
+ _filters.add(_CompareFilter('lt', column, value));
116
+ return this;
117
+ }
118
+
119
+ @override
120
+ SupabaseFilterBuilderLike lte(String column, Object? value) {
121
+ _filters.add(_CompareFilter('lte', column, value));
122
+ return this;
123
+ }
124
+
125
+ @override
126
+ SupabaseFilterBuilderLike in_(String column, List<Object?> values) {
127
+ _filters.add(_InFilter(column, values));
128
+ return this;
129
+ }
130
+
131
+ @override
132
+ SupabaseFilterBuilderLike isFilter(String column, Object? value) {
133
+ _filters.add(_IsFilter(column, value));
134
+ return this;
135
+ }
136
+
137
+ @override
138
+ SupabaseFilterBuilderLike not(String column, String operator, Object? value) {
139
+ _filters.add(_NotFilter(column, operator, value));
140
+ return this;
141
+ }
142
+
143
+ @override
144
+ SupabaseFilterBuilderLike order(String column, {bool? ascending}) {
145
+ this.ascending = ascending ?? true;
146
+ return this;
147
+ }
148
+
149
+ @override
150
+ SupabaseFilterBuilderLike limit(int value) {
151
+ limitValue = value;
152
+ return this;
153
+ }
154
+
155
+ @override
156
+ SupabaseFilterBuilderLike range(int from, int to) {
157
+ rangeStart = from;
158
+ rangeEnd = to;
159
+ return this;
160
+ }
161
+
162
+ @override
163
+ SupabaseFilterBuilderLike single() {
164
+ singleRow = true;
165
+ return this;
166
+ }
167
+
168
+ List<Map<String, dynamic>> _rowsForTable() =>
169
+ _table == 'bundles' ? _store.bundleRows.values.toList()
170
+ : _store.bundlePatchRows.values.toList();
171
+
172
+ List<Map<String, dynamic>> _getFilteredRows() {
173
+ var rows = _rowsForTable();
174
+ for (final filter in _filters) {
175
+ rows = rows.where((row) {
176
+ return switch (filter) {
177
+ _EqFilter(:final column, :final value) => row[column] == value,
178
+ _CompareFilter(:final type, :final column, :final value) =>
179
+ switch (type) {
180
+ 'gt' => _compareValues(row[column], value) > 0,
181
+ 'gte' => _compareValues(row[column], value) >= 0,
182
+ 'lt' => _compareValues(row[column], value) < 0,
183
+ 'lte' => _compareValues(row[column], value) <= 0,
184
+ _ => false,
185
+ },
186
+ _InFilter(:final column, :final values) =>
187
+ values.contains(row[column]),
188
+ _IsFilter(:final column, :final value) => row[column] == value,
189
+ _NotFilter(:final column, :final operator, :final value) =>
190
+ operator == 'is' ? row[column] != value : false,
191
+ };
192
+ }).toList();
193
+ }
194
+ return rows;
195
+ }
196
+
197
+ @override
198
+ Future<SupabaseResponseLike> execute() async {
199
+ if (_mode == 'delete') {
200
+ final filtered = _getFilteredRows();
201
+ for (final row in filtered) {
202
+ if (_table == 'bundles') {
203
+ _store.bundleRows.remove(row['id']);
204
+ } else {
205
+ _store.bundlePatchRows.remove(row['id']);
206
+ }
207
+ }
208
+ return _MockResponse(data: null, error: null);
209
+ }
210
+
211
+ var filtered = _getFilteredRows();
212
+ filtered = filtered.toList()
213
+ ..sort((a, b) =>
214
+ ascending ? a['id'].compareTo(b['id']) : b['id'].compareTo(a['id']));
215
+ final total = filtered.length;
216
+
217
+ if (singleRow) {
218
+ final data = filtered.isEmpty ? null : filtered.first;
219
+ return _MockResponse(
220
+ data: data,
221
+ error: data == null ? {'message': 'Row not found'} : null,
222
+ );
223
+ }
224
+
225
+ if (head || count) {
226
+ return _MockResponse(data: null, error: null, count: total);
227
+ }
228
+
229
+ if (rangeStart != null && rangeEnd != null) {
230
+ filtered = filtered.sublist(
231
+ rangeStart!,
232
+ (rangeEnd! + 1).clamp(0, filtered.length),
233
+ );
234
+ } else if (limitValue != null && limitValue! > 0) {
235
+ filtered = filtered.sublist(0, limitValue!.clamp(0, filtered.length));
236
+ }
237
+
238
+ return _MockResponse(data: filtered, error: null, count: total);
239
+ }
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Storage bucket mock
244
+ // ---------------------------------------------------------------------------
245
+
246
+ class _ExistsResult implements SupabaseExistsResult {
247
+ _ExistsResult({this.data, this.error});
248
+ @override
249
+ final bool? data;
250
+ @override
251
+ final Object? error;
252
+ @override
253
+ final String? message = null;
254
+ }
255
+
256
+ class _SignedUrlResult implements SupabaseSignedUrlResult {
257
+ _SignedUrlResult({this.signedUrl, this.error});
258
+ @override
259
+ final String? signedUrl;
260
+ @override
261
+ final Object? error;
262
+ }
263
+
264
+ class _SignedUrlListResult implements SupabaseSignedUrlListResult {
265
+ _SignedUrlListResult({this.data, this.error});
266
+ @override
267
+ final List<SupabaseSignedUrlResult>? data;
268
+ @override
269
+ final Object? error;
270
+ }
271
+
272
+ class _UploadResult implements SupabaseUploadResult {
273
+ _UploadResult({this.data, this.error});
274
+ @override
275
+ final Object? data;
276
+ @override
277
+ final Object? error;
278
+ }
279
+
280
+ class _RemoveResult implements SupabaseRemoveResult {
281
+ _RemoveResult({this.data, this.error, this.message});
282
+ @override
283
+ final Object? data;
284
+ @override
285
+ final Object? error;
286
+ @override
287
+ final String? message;
288
+ }
289
+
290
+ class _DownloadResult implements SupabaseDownloadResult {
291
+ _DownloadResult({this.data, this.error, this.message});
292
+ @override
293
+ final List<int>? data;
294
+ @override
295
+ final Object? error;
296
+ @override
297
+ final String? message;
298
+ }
299
+
300
+ class _ListResult implements SupabaseListResult {
301
+ _ListResult({this.data, this.error});
302
+ @override
303
+ final List<SupabaseStorageObject>? data;
304
+ @override
305
+ final Object? error;
306
+ }
307
+
308
+ /// Configurable in-memory storage bucket.
309
+ class FakeStorageBucket implements SupabaseStorageBucketLike {
310
+ FakeStorageBucket({
311
+ this.existsData,
312
+ this.existsError,
313
+ this.existsThrows,
314
+ this.signedUrlData,
315
+ this.signedUrlError,
316
+ this.uploadData,
317
+ this.uploadError,
318
+ this.removeError,
319
+ this.removeMessage,
320
+ this.downloadData,
321
+ this.downloadError,
322
+ this.downloadMessage,
323
+ this.listData,
324
+ this.listError,
325
+ this.createSignedUrlBase = 'https://example.supabase.co',
326
+ });
327
+
328
+ bool? existsData;
329
+ Object? existsError;
330
+ Object? existsThrows;
331
+ String? signedUrlData;
332
+ Object? signedUrlError;
333
+ Object? uploadData;
334
+ Object? uploadError;
335
+ Object? removeError;
336
+ String? removeMessage;
337
+ List<int>? downloadData;
338
+ Object? downloadError;
339
+ String? downloadMessage;
340
+ List<SupabaseStorageObject>? listData;
341
+ Object? listError;
342
+ final String createSignedUrlBase;
343
+
344
+ final List<String> existsCalls = [];
345
+ final List<String> createSignedUrlCalls = [];
346
+ final List<List<String>> createSignedUrlsCalls = [];
347
+ final List<String> uploadCalls = [];
348
+ final List<List<String>> removeCalls = [];
349
+ final List<String> downloadCalls = [];
350
+ final List<String> listCalls = [];
351
+
352
+ @override
353
+ Future<SupabaseExistsResult> exists(String path) {
354
+ existsCalls.add(path);
355
+ if (existsThrows != null) {
356
+ return Future.error(existsThrows!);
357
+ }
358
+ return Future.value(
359
+ _ExistsResult(data: existsData, error: existsError),
360
+ );
361
+ }
362
+
363
+ @override
364
+ Future<SupabaseSignedUrlResult> createSignedUrl(String path, int expiresIn) {
365
+ createSignedUrlCalls.add(path);
366
+ return Future.value(
367
+ _SignedUrlResult(
368
+ signedUrl: signedUrlData ?? '$createSignedUrlBase/signed-url',
369
+ error: signedUrlError,
370
+ ),
371
+ );
372
+ }
373
+
374
+ @override
375
+ Future<SupabaseSignedUrlListResult> createSignedUrls(
376
+ List<String> paths,
377
+ int expiresIn,
378
+ ) {
379
+ createSignedUrlsCalls.add(List.of(paths));
380
+ return Future.value(
381
+ _SignedUrlListResult(
382
+ data: paths
383
+ .map((p) => _SignedUrlResult(
384
+ signedUrl: '$createSignedUrlBase/$expiresIn/$p',
385
+ error: signedUrlError,
386
+ ))
387
+ .toList(),
388
+ error: signedUrlError,
389
+ ),
390
+ );
391
+ }
392
+
393
+ @override
394
+ Future<SupabaseUploadResult> upload(
395
+ String path,
396
+ List<int> fileBytes, {
397
+ String? contentType,
398
+ String? cacheControl,
399
+ }) {
400
+ uploadCalls.add(path);
401
+ return Future.value(
402
+ _UploadResult(
403
+ data: uploadData ?? {'fullPath': path},
404
+ error: uploadError,
405
+ ),
406
+ );
407
+ }
408
+
409
+ @override
410
+ Future<SupabaseRemoveResult> remove(List<String> paths) {
411
+ removeCalls.add(List.of(paths));
412
+ return Future.value(
413
+ _RemoveResult(data: [], error: removeError, message: removeMessage),
414
+ );
415
+ }
416
+
417
+ @override
418
+ Future<SupabaseDownloadResult> download(String path) {
419
+ downloadCalls.add(path);
420
+ return Future.value(
421
+ _DownloadResult(
422
+ data: downloadData,
423
+ error: downloadError,
424
+ message: downloadMessage,
425
+ ),
426
+ );
427
+ }
428
+
429
+ @override
430
+ Future<SupabaseListResult> list([String? prefix]) {
431
+ listCalls.add(prefix ?? '');
432
+ return Future.value(_ListResult(data: listData, error: listError));
433
+ }
434
+ }
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Top-level mock client
438
+ // ---------------------------------------------------------------------------
439
+
440
+ class _MockStorageClient implements SupabaseStorageClientLike {
441
+ _MockStorageClient(this.bucket);
442
+ final SupabaseStorageBucketLike bucket;
443
+
444
+ @override
445
+ SupabaseStorageBucketLike from(String bucketName) => bucket;
446
+ }
447
+
448
+ class _MockQueryBuilder implements SupabaseQueryBuilderLike {
449
+ _MockQueryBuilder(this._store, this._table);
450
+ final Store _store;
451
+ final String _table;
452
+
453
+ @override
454
+ SupabaseFilterBuilderLike select(
455
+ String columns, {
456
+ bool? count,
457
+ bool? head,
458
+ }) =>
459
+ _MockFilterBuilder(_store, _table, 'select', head: head ?? false);
460
+
461
+ @override
462
+ SupabaseFilterBuilderLike delete() =>
463
+ _MockFilterBuilder(_store, _table, 'delete');
464
+
465
+ @override
466
+ Future<dynamic> upsert(dynamic values, {String? onConflict}) async {
467
+ final list = values is List ? values : [values];
468
+ for (final value in list) {
469
+ final Map<String, dynamic> map;
470
+ if (value is SupabaseBundleRow) {
471
+ map = value.toJson();
472
+ } else if (value is SupabaseBundlePatchRow) {
473
+ map = value.toJson();
474
+ } else {
475
+ map = Map<String, dynamic>.from(value as Map);
476
+ }
477
+ if (_table == 'bundles') {
478
+ _store.bundleRows[map['id'] as String] = map;
479
+ } else {
480
+ _store.bundlePatchRows[map['id'] as String] = map;
481
+ }
482
+ }
483
+ return _MockResponse(data: null, error: null);
484
+ }
485
+ }
486
+
487
+ class _MockResponse implements SupabaseResponseLike {
488
+ _MockResponse({this.data, this.error, this.count});
489
+ @override
490
+ final Object? data;
491
+ @override
492
+ final Object? error;
493
+ @override
494
+ final int? count;
495
+ }
496
+
497
+ class _MockRpcResponse implements SupabaseRpcResponse {
498
+ _MockRpcResponse({this.data, this.error});
499
+ @override
500
+ final Object? data;
501
+ @override
502
+ final Object? error;
503
+ }
504
+
505
+ /// Build an in-memory mock Supabase client backed by [store] and [bucket].
506
+ SupabaseClientLike createMockSupabaseClient({
507
+ required Store store,
508
+ required FakeStorageBucket bucket,
509
+ }) {
510
+ final storageClient = _MockStorageClient(bucket);
511
+
512
+ Platform platformFrom(String value) =>
513
+ value == 'ios' ? Platform.ios : Platform.android;
514
+
515
+ Bundle rowToBundle(Map<String, dynamic> row) => mapRowToBundle(
516
+ SupabaseBundleRow.fromJson(row),
517
+ );
518
+
519
+ return _MockClient(store, storageClient, platformFrom, rowToBundle);
520
+ }
521
+
522
+ class _MockClient implements SupabaseClientLike {
523
+ _MockClient(
524
+ this._store,
525
+ this._storage,
526
+ this._platformFrom,
527
+ this._rowToBundle,
528
+ );
529
+
530
+ final Store _store;
531
+ final SupabaseStorageClientLike _storage;
532
+ final Platform Function(String) _platformFrom;
533
+ final Bundle Function(Map<String, dynamic>) _rowToBundle;
534
+
535
+ @override
536
+ SupabaseQueryBuilderLike from(String table) =>
537
+ _MockQueryBuilder(_store, table);
538
+
539
+ @override
540
+ SupabaseStorageClientLike get storage => _storage;
541
+
542
+ @override
543
+ Future<SupabaseRpcResponse> rpc(
544
+ String functionName, {
545
+ Map<String, dynamic>? params,
546
+ }) async {
547
+ if (functionName == 'get_channels') {
548
+ final channels = _store.bundleRows.values
549
+ .map((r) => r['channel'] as String)
550
+ .toSet()
551
+ .map((c) => {'channel': c})
552
+ .toList();
553
+ return _MockRpcResponse(data: channels, error: null);
554
+ }
555
+
556
+ if (functionName == 'get_target_app_version_list') {
557
+ final platform = params!['app_platform'] as String;
558
+ final minBundleId = params['min_bundle_id'] as String;
559
+ final list = _store.bundleRows.values
560
+ .where((r) =>
561
+ r['platform'] == platform &&
562
+ (r['id'] as String).compareTo(minBundleId) >= 0 &&
563
+ r['target_app_version'] != null)
564
+ .map((r) => r['target_app_version'] as String)
565
+ .toSet()
566
+ .map((v) => {'target_app_version': v})
567
+ .toList();
568
+ return _MockRpcResponse(data: list, error: null);
569
+ }
570
+
571
+ if (functionName == 'get_update_info_by_app_version') {
572
+ final platform = params!['app_platform'] as String;
573
+ final appVersion = params['app_version'] as String;
574
+ final bundleId = params['bundle_id'] as String;
575
+ final minBundleId = params['min_bundle_id'] as String;
576
+ final channel = params['target_channel'] as String;
577
+ final targetAppVersionList =
578
+ List<String>.from(params['target_app_version_list'] as List? ?? []);
579
+ final cohort = params['cohort'] as String?;
580
+
581
+ final bundles = _store.bundleRows.values
582
+ .where((r) =>
583
+ r['enabled'] == true &&
584
+ r['platform'] == platform &&
585
+ r['channel'] == channel &&
586
+ (r['id'] as String).compareTo(minBundleId) >= 0 &&
587
+ (targetAppVersionList.contains(r['target_app_version'])))
588
+ .map(_rowToBundle)
589
+ .toList();
590
+
591
+ final info = await getUpdateInfo(
592
+ bundles,
593
+ AppVersionGetBundlesArgs(
594
+ appVersion: appVersion,
595
+ bundleId: bundleId,
596
+ channel: channel,
597
+ minBundleId: minBundleId,
598
+ platform: _platformFrom(platform),
599
+ cohort: cohort,
600
+ ),
601
+ );
602
+ return _MockRpcResponse(
603
+ data: info == null ? [] : [_toUpdateInfoRow(info)],
604
+ error: null,
605
+ );
606
+ }
607
+
608
+ if (functionName == 'get_update_info_by_fingerprint_hash') {
609
+ final platform = params!['app_platform'] as String;
610
+ final bundleId = params['bundle_id'] as String;
611
+ final minBundleId = params['min_bundle_id'] as String;
612
+ final channel = params['target_channel'] as String;
613
+ final fingerprintHash = params['target_fingerprint_hash'] as String;
614
+ final cohort = params['cohort'] as String?;
615
+
616
+ final bundles = _store.bundleRows.values
617
+ .where((r) =>
618
+ r['enabled'] == true &&
619
+ r['platform'] == platform &&
620
+ r['channel'] == channel &&
621
+ (r['id'] as String).compareTo(minBundleId) >= 0 &&
622
+ r['fingerprint_hash'] == fingerprintHash)
623
+ .map(_rowToBundle)
624
+ .toList();
625
+
626
+ final info = await getUpdateInfo(
627
+ bundles,
628
+ FingerprintGetBundlesArgs(
629
+ fingerprintHash: fingerprintHash,
630
+ bundleId: bundleId,
631
+ channel: channel,
632
+ minBundleId: minBundleId,
633
+ platform: _platformFrom(platform),
634
+ cohort: cohort,
635
+ ),
636
+ );
637
+ return _MockRpcResponse(
638
+ data: info == null ? [] : [_toUpdateInfoRow(info)],
639
+ error: null,
640
+ );
641
+ }
642
+
643
+ return _MockRpcResponse(data: null, error: 'Unsupported RPC: $functionName');
644
+ }
645
+ }
646
+
647
+ Map<String, dynamic> _toUpdateInfoRow(UpdateInfo info) => {
648
+ 'id': info.id,
649
+ 'should_force_update': info.shouldForceUpdate,
650
+ 'message': info.message,
651
+ 'status': info.status == UpdateStatus.rollback ? 'ROLLBACK' : 'UPDATE',
652
+ 'storage_uri': info.storageUri,
653
+ 'file_hash': info.fileHash,
654
+ };