@_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,442 @@
1
+ import 'dart:io';
2
+ import 'dart:typed_data';
3
+
4
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
+ import 'package:flutter_patcher_supabase/src/supabase_client_adapter.dart';
6
+ import 'package:path/path.dart' as p;
7
+ import 'package:test/test.dart';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Minimal in-memory Supabase mock (PostgREST + storage surfaces).
11
+ // ---------------------------------------------------------------------------
12
+
13
+ class MockResponse implements SupabaseResponseLike {
14
+ const MockResponse({this.data, this.error, this.count});
15
+ @override
16
+ final Object? data;
17
+ @override
18
+ final Object? error;
19
+ @override
20
+ final int? count;
21
+ }
22
+
23
+ class MockRpcResponse implements SupabaseRpcResponse {
24
+ const MockRpcResponse({this.data, this.error});
25
+ @override
26
+ final Object? data;
27
+ @override
28
+ final Object? error;
29
+ }
30
+
31
+ class MockUploadResult implements SupabaseUploadResult {
32
+ const MockUploadResult({this.data, this.error});
33
+ @override
34
+ final Object? data;
35
+ @override
36
+ final Object? error;
37
+ }
38
+
39
+ class MockSignedUrlResult implements SupabaseSignedUrlResult {
40
+ const MockSignedUrlResult({this.signedUrl, this.error});
41
+ @override
42
+ final String? signedUrl;
43
+ @override
44
+ final Object? error;
45
+ }
46
+
47
+ class MockSignedUrlListResult implements SupabaseSignedUrlListResult {
48
+ const MockSignedUrlListResult({this.data, this.error});
49
+ @override
50
+ final List<SupabaseSignedUrlResult>? data;
51
+ @override
52
+ final Object? error;
53
+ }
54
+
55
+ class MockDownloadResult implements SupabaseDownloadResult {
56
+ const MockDownloadResult({this.data, this.error, this.message});
57
+ @override
58
+ final List<int>? data;
59
+ @override
60
+ final Object? error;
61
+ @override
62
+ final String? message;
63
+ }
64
+
65
+ class MockRemoveResult implements SupabaseRemoveResult {
66
+ const MockRemoveResult({this.data, this.error, this.message});
67
+ @override
68
+ final Object? data;
69
+ @override
70
+ final Object? error;
71
+ @override
72
+ final String? message;
73
+ }
74
+
75
+ class MockExistsResult implements SupabaseExistsResult {
76
+ const MockExistsResult({this.data, this.error, this.message});
77
+ @override
78
+ final bool? data;
79
+ @override
80
+ final Object? error;
81
+ @override
82
+ final String? message;
83
+ }
84
+
85
+ class MockStorageObject implements SupabaseStorageObject {
86
+ MockStorageObject(this.key, this.size);
87
+ @override
88
+ final String key;
89
+ @override
90
+ final int size;
91
+ @override
92
+ @override
93
+ final String? lastModifiedAt = null;
94
+ }
95
+
96
+ class MockListResult implements SupabaseListResult {
97
+ const MockListResult({this.data, this.error});
98
+ @override
99
+ final List<SupabaseStorageObject>? data;
100
+ @override
101
+ final Object? error;
102
+ }
103
+
104
+ class MockBuilder
105
+ implements SupabaseQueryBuilderLike, SupabaseFilterBuilderLike {
106
+ MockBuilder(this._client, this._table);
107
+
108
+ final MockSupabaseClient _client;
109
+ final String _table;
110
+
111
+ String _op = 'select';
112
+ final Map<String, Object?> _eq = {};
113
+ final Map<String, List<Object?>> _in = {};
114
+ bool _single = false;
115
+ bool _head = false;
116
+ bool _count = false;
117
+ String _order = 'id';
118
+ bool _ascending = false;
119
+
120
+ @override
121
+ SupabaseFilterBuilderLike select(String columns,
122
+ {bool? count, bool? head}) =>
123
+ _asSelect(count: count, head: head);
124
+
125
+ MockBuilder _asSelect({bool? count, bool? head}) {
126
+ _op = 'select';
127
+ _count = count ?? false;
128
+ _head = head ?? false;
129
+ return this;
130
+ }
131
+
132
+ @override
133
+ SupabaseFilterBuilderLike delete() {
134
+ _op = 'delete';
135
+ return this;
136
+ }
137
+
138
+ Map<String, Object?> _toRow(dynamic raw) =>
139
+ raw is Map ? raw.cast<String, Object?>() : (raw as dynamic).toJson();
140
+
141
+ @override
142
+ Future<dynamic> upsert(dynamic values, {String? onConflict}) {
143
+ final rows = (values is List) ? values.cast<Object?>() : [values];
144
+ for (final raw in rows) {
145
+ final row = _toRow(raw);
146
+ if (_table == 'bundles') {
147
+ _client._bundles[row['id'] as String] = row;
148
+ } else if (_table == 'bundle_patches') {
149
+ _client._patches[row['id'] as String] = row;
150
+ }
151
+ }
152
+ return Future.value(const MockResponse(data: {}, error: null));
153
+ }
154
+
155
+ @override
156
+ SupabaseFilterBuilderLike eq(String column, Object? value) {
157
+ _eq[column] = value;
158
+ return this;
159
+ }
160
+
161
+ @override
162
+ SupabaseFilterBuilderLike in_(String column, List<Object?> values) {
163
+ _in[column] = values;
164
+ return this;
165
+ }
166
+
167
+ @override
168
+ SupabaseFilterBuilderLike gt(String column, Object? value) => this;
169
+ @override
170
+ SupabaseFilterBuilderLike gte(String column, Object? value) => this;
171
+ @override
172
+ SupabaseFilterBuilderLike lt(String column, Object? value) => this;
173
+ @override
174
+ SupabaseFilterBuilderLike lte(String column, Object? value) => this;
175
+ @override
176
+ SupabaseFilterBuilderLike isFilter(String column, Object? value) => this;
177
+ @override
178
+ SupabaseFilterBuilderLike not(String column, String operator, Object? value) =>
179
+ this;
180
+ @override
181
+ SupabaseFilterBuilderLike order(String column, {bool? ascending}) {
182
+ _order = column;
183
+ _ascending = ascending ?? false;
184
+ return this;
185
+ }
186
+ @override
187
+ SupabaseFilterBuilderLike limit(int value) => this;
188
+ @override
189
+ SupabaseFilterBuilderLike range(int from, int to) => this;
190
+ @override
191
+ SupabaseFilterBuilderLike single() {
192
+ _single = true;
193
+ return this;
194
+ }
195
+
196
+ @override
197
+ Future<SupabaseResponseLike> execute() async {
198
+ if (_op == 'delete') {
199
+ if (_table == 'bundles' && _eq['id'] != null) {
200
+ _client._bundles.remove(_eq['id']);
201
+ } else if (_table == 'bundle_patches' && _eq['bundle_id'] != null) {
202
+ _client._patches.removeWhere(
203
+ (k, v) => v['bundle_id'] == _eq['bundle_id'],
204
+ );
205
+ }
206
+ return const MockResponse(data: {}, error: null);
207
+ }
208
+ // select
209
+ if (_head && _count) {
210
+ final total =
211
+ _table == 'bundles' ? _client._bundles.length : _client._patches.length;
212
+ return MockResponse(data: const [], count: total, error: null);
213
+ }
214
+ if (_table == 'bundle_patches') {
215
+ var rows = _client._patches.values.toList();
216
+ if (_in['bundle_id'] != null) {
217
+ rows = rows
218
+ .where((r) => _in['bundle_id']!.contains(r['bundle_id']))
219
+ .toList();
220
+ }
221
+ return MockResponse(data: rows, error: null);
222
+ }
223
+ var rows = _client._bundles.values.toList();
224
+ if (_eq['id'] != null) {
225
+ rows = rows.where((r) => r['id'] == _eq['id']).toList();
226
+ }
227
+ if (_table == 'bundles' && _order == 'id') {
228
+ rows.sort((a, b) => (_ascending ? 1 : -1) *
229
+ (a['id'] as String).compareTo(b['id'] as String));
230
+ }
231
+ if (_single) {
232
+ return MockResponse(data: rows.isEmpty ? null : rows.first, error: null);
233
+ }
234
+ return MockResponse(data: rows, error: null);
235
+ }
236
+ }
237
+
238
+ class MockBucket implements SupabaseStorageBucketLike {
239
+ MockBucket(this._client, this._bucket);
240
+ final MockSupabaseClient _client;
241
+ final String _bucket;
242
+
243
+ @override
244
+ Future<SupabaseSignedUrlResult> createSignedUrl(String path, int expiresIn) async =>
245
+ MockSignedUrlResult(signedUrl: 'https://signed/$_bucket/$path', error: null);
246
+
247
+ @override
248
+ Future<SupabaseSignedUrlListResult> createSignedUrls(
249
+ List<String> paths, int expiresIn) async =>
250
+ MockSignedUrlListResult(
251
+ data: paths
252
+ .map((p) =>
253
+ MockSignedUrlResult(signedUrl: 'https://signed/$_bucket/$p'))
254
+ .toList(),
255
+ error: null);
256
+
257
+ @override
258
+ Future<SupabaseUploadResult> upload(String path, List<int> fileBytes,
259
+ {String? contentType, String? cacheControl}) async {
260
+ _client._storage[path] = Uint8List.fromList(fileBytes);
261
+ return MockUploadResult(data: {'Key': path}, error: null);
262
+ }
263
+
264
+ @override
265
+ Future<SupabaseRemoveResult> remove(List<String> paths) async {
266
+ for (final p in paths) {
267
+ _client._storage.remove(p);
268
+ }
269
+ return MockRemoveResult(data: paths, error: null);
270
+ }
271
+
272
+ @override
273
+ Future<SupabaseExistsResult> exists(String path) async =>
274
+ MockExistsResult(data: _client._storage.containsKey(path), error: null);
275
+
276
+ @override
277
+ Future<SupabaseDownloadResult> download(String path) async =>
278
+ MockDownloadResult(data: _client._storage[path], error: null);
279
+
280
+ @override
281
+ Future<SupabaseListResult> list([String? prefix]) async {
282
+ final data = _client._storage.keys
283
+ .where((k) => prefix == null || k.startsWith(prefix))
284
+ .map((k) => MockStorageObject(k, _client._storage[k]!.length))
285
+ .toList();
286
+ return MockListResult(data: data, error: null);
287
+ }
288
+ }
289
+
290
+ class MockStorageClient implements SupabaseStorageClientLike {
291
+ MockStorageClient(this._client);
292
+ final MockSupabaseClient _client;
293
+ @override
294
+ SupabaseStorageBucketLike from(String bucket) =>
295
+ MockBucket(_client, bucket);
296
+ }
297
+
298
+ class MockSupabaseClient implements SupabaseClientLike {
299
+ final Map<String, Map<String, Object?>> _bundles = {};
300
+ final Map<String, Map<String, Object?>> _patches = {};
301
+ final Map<String, List<int>> _storage = {};
302
+
303
+ @override
304
+ SupabaseQueryBuilderLike from(String table) => MockBuilder(this, table);
305
+
306
+ @override
307
+ SupabaseStorageClientLike get storage => MockStorageClient(this);
308
+
309
+ @override
310
+ Future<SupabaseRpcResponse> rpc(String functionName,
311
+ {Map<String, dynamic>? params}) async {
312
+ if (functionName == 'get_channels') {
313
+ return const MockRpcResponse(
314
+ data: [
315
+ {'channel': 'production'},
316
+ {'channel': 'staging'},
317
+ ],
318
+ error: null,
319
+ );
320
+ }
321
+ return const MockRpcResponse(data: [], error: null);
322
+ }
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Tests
327
+ // ---------------------------------------------------------------------------
328
+
329
+ FlutterPatcherConfig _config() => FlutterPatcherConfig(
330
+ provider: 'supabase',
331
+ supabase: SupabaseConfigJson(
332
+ url: 'https://demo.supabase.co',
333
+ serviceRoleKey: 'service-role-key',
334
+ bucket: 'bundles',
335
+ ),
336
+ channel: 'production',
337
+ platform: 'android',
338
+ source: './dist',
339
+ );
340
+
341
+ Backend _backend(MockSupabaseClient mock) =>
342
+ resolveBackend(_config(), supabaseClientFactory: (url, key) => mock);
343
+
344
+ void main() {
345
+ group('flutter_patcher_cli', () {
346
+ late MockSupabaseClient mock;
347
+ late Backend backend;
348
+
349
+ setUp(() {
350
+ mock = MockSupabaseClient();
351
+ backend = _backend(mock);
352
+ });
353
+
354
+ test('deploy uploads a bundle and registers it', () async {
355
+ final dir = await Directory.systemTemp.createTemp('src');
356
+ try {
357
+ await File(p.join(dir.path, 'main.dart')).writeAsString('print(1);');
358
+ final bundle = await deployBundle(
359
+ backend,
360
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
361
+ );
362
+ expect(bundle.enabled, isTrue);
363
+ expect(mock._bundles.containsKey(bundle.id), isTrue);
364
+ expect(mock._storage.isNotEmpty, isTrue);
365
+ expect(bundle.storageUri, contains('supabase-storage://'));
366
+ } finally {
367
+ await dir.delete(recursive: true);
368
+ }
369
+ });
370
+
371
+ test('bundle list returns deployed bundle', () async {
372
+ final dir = await Directory.systemTemp.createTemp('src');
373
+ try {
374
+ await File(p.join(dir.path, 'a.txt')).writeAsString('x');
375
+ final deployed = await deployBundle(
376
+ backend,
377
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
378
+ );
379
+ final res = await listBundles(backend, ListOptions(channel: 'production'));
380
+ expect(res.data.map((b) => b.id), contains(deployed.id));
381
+ } finally {
382
+ await dir.delete(recursive: true);
383
+ }
384
+ });
385
+
386
+ test('channel list returns seeded channels', () async {
387
+ final channels = await listChannels(backend);
388
+ expect(channels, containsAll(['production', 'staging']));
389
+ });
390
+
391
+ test('bundle delete removes the bundle', () async {
392
+ final dir = await Directory.systemTemp.createTemp('src');
393
+ try {
394
+ await File(p.join(dir.path, 'a.txt')).writeAsString('x');
395
+ final deployed = await deployBundle(
396
+ backend,
397
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
398
+ );
399
+ await deleteBundle(backend, deployed.id);
400
+ expect(mock._bundles.containsKey(deployed.id), isFalse);
401
+ } finally {
402
+ await dir.delete(recursive: true);
403
+ }
404
+ });
405
+
406
+ test('rollback disables the latest enabled bundle on a channel', () async {
407
+ final dir = await Directory.systemTemp.createTemp('src');
408
+ try {
409
+ await File(p.join(dir.path, 'a.txt')).writeAsString('x');
410
+ final first = await deployBundle(
411
+ backend,
412
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
413
+ );
414
+ final second = await deployBundle(
415
+ backend,
416
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
417
+ );
418
+ final disabled = await rollbackChannel(backend, 'production');
419
+ expect(disabled, equals(second.id));
420
+ expect(mock._bundles[first.id]!['enabled'], isTrue);
421
+ expect(mock._bundles[second.id]!['enabled'], isFalse);
422
+ } finally {
423
+ await dir.delete(recursive: true);
424
+ }
425
+ });
426
+
427
+ test('promote assigns a bundle to a channel', () async {
428
+ final dir = await Directory.systemTemp.createTemp('src');
429
+ try {
430
+ await File(p.join(dir.path, 'a.txt')).writeAsString('x');
431
+ final b = await deployBundle(
432
+ backend,
433
+ DeployOptions(source: dir.path, channel: 'production', platform: 'android'),
434
+ );
435
+ await promoteBundle(backend, b.id, 'staging');
436
+ expect(mock._bundles[b.id]!['channel'], equals('staging'));
437
+ } finally {
438
+ await dir.delete(recursive: true);
439
+ }
440
+ });
441
+ });
442
+ }
@@ -0,0 +1,83 @@
1
+ import 'dart:convert' show utf8;
2
+
3
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
4
+ show StorageObject;
5
+
6
+ import 'package:flutter_patcher_aws/flutter_patcher_aws.dart'
7
+ show AwsS3ClientLike, AwsS3StorageConfig;
8
+
9
+ /// In-memory store backing [MockAwsS3Client].
10
+ class Store {
11
+ final Map<String, List<int>> objects = {};
12
+ }
13
+
14
+ /// Mock AWS S3 client that stores objects in memory.
15
+ class MockAwsS3Client implements AwsS3ClientLike {
16
+ MockAwsS3Client(this.store, {this.bucketName = 'bucket'});
17
+
18
+ final Store store;
19
+ final String bucketName;
20
+
21
+ @override
22
+ Future<void> deleteObject(String key) async => store.objects.remove(key);
23
+
24
+ @override
25
+ Future<void> deleteObjects(List<String> keys) async {
26
+ for (final key in keys) {
27
+ store.objects.remove(key);
28
+ }
29
+ }
30
+
31
+ @override
32
+ Future<bool> headObject(String key) async =>
33
+ store.objects.containsKey(key);
34
+
35
+ @override
36
+ Future<List<int>> getObjectAsBytes(String key) async {
37
+ final bytes = store.objects[key];
38
+ if (bytes == null) throw Exception('NoSuchKey: $key');
39
+ return bytes;
40
+ }
41
+
42
+ @override
43
+ Future<String> getObjectAsString(String key) async =>
44
+ utf8.decode(await getObjectAsBytes(key));
45
+
46
+ @override
47
+ Future<String> getPresignedUrl(String key, {int expiresIn = 3600}) async =>
48
+ 'https://$bucketName.s3.us-east-1.amazonaws.com/$bucketName/$key'
49
+ '?X-Amz-Signature=mock&X-Amz-Expires=$expiresIn';
50
+
51
+ @override
52
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
53
+ final p = prefix ?? '';
54
+ return store.objects.keys
55
+ .where((k) => k.startsWith(p))
56
+ .map(
57
+ (k) => StorageObject(
58
+ key: k,
59
+ storageUri: 's3://$bucketName/$k',
60
+ size: store.objects[k]!.length,
61
+ ),
62
+ )
63
+ .toList();
64
+ }
65
+
66
+ @override
67
+ Future<void> putObject(
68
+ String key,
69
+ List<int> body,
70
+ String contentType,
71
+ ) async {
72
+ store.objects[key] = body;
73
+ }
74
+ }
75
+
76
+ /// Build an [AwsS3StorageConfig] wired to an in-memory [Store].
77
+ AwsS3StorageConfig mockConfig(Store store) => AwsS3StorageConfig(
78
+ bucketName: 'test-bucket',
79
+ region: 'us-east-1',
80
+ accessKeyId: 'test-access-key',
81
+ secretAccessKey: 'test-secret',
82
+ clientFactory: (_) => MockAwsS3Client(store),
83
+ );