@_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.
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +7 -6
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +46 -49
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +44 -23
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +27 -17
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +11 -6
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +39 -22
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -13
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +117 -36
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +15 -9
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +14 -8
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +37 -12
- package/dart-src/packages/cli-tools/lib/src/config.dart +13 -3
- package/dart-src/packages/cli-tools/lib/src/operations.dart +45 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
- package/dart-src/packages/cli-tools/pubspec.lock +638 -0
- package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
- package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
- package/dart-src/packages/core/test/rollout_test.dart +128 -0
- package/dart-src/packages/core/test/semver_test.dart +52 -0
- package/dart-src/packages/core/test/semver_vectors.json +1 -0
- package/dart-src/packages/core/test/types_test.dart +129 -0
- package/dart-src/packages/core/test/uuid_test.dart +38 -0
- package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
- package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
- package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
- package/package.json +1 -1
- 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
|
+
);
|