@_nazmiforreal/flutter-ota 0.1.2 → 0.1.3
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 +6 -5
- 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/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,297 @@
|
|
|
1
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_postgres/flutter_patcher_postgres.dart';
|
|
4
|
+
|
|
5
|
+
/// In-memory store backing [MockPostgresClient]. Mirrors the `bundles` and
|
|
6
|
+
/// `bundle_patches` tables (snake_case row maps).
|
|
7
|
+
class Store {
|
|
8
|
+
Store({List<Bundle>? seed}) {
|
|
9
|
+
if (seed != null) {
|
|
10
|
+
for (final b in seed) {
|
|
11
|
+
bundleRows.add(bundleToRowValues(b));
|
|
12
|
+
bundlePatchRows.addAll(bundleToPatchRows(b));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
final List<Map<String, dynamic>> bundleRows = [];
|
|
18
|
+
final List<Map<String, dynamic>> bundlePatchRows = [];
|
|
19
|
+
|
|
20
|
+
Map<String, dynamic>? bundleRowById(String id) => bundleRows
|
|
21
|
+
.where((row) => row['id'] == id)
|
|
22
|
+
.cast<Map<String, dynamic>?>()
|
|
23
|
+
.firstOrNull;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Mock [PostgresClientLike] that emulates the queries the plugin issues
|
|
27
|
+
/// against an in-memory [Store]. Faithful enough to exercise the plugin's
|
|
28
|
+
/// SQL building, row mapping, transactions, and RPC calls without a live DB.
|
|
29
|
+
class MockPostgresClient implements PostgresClientLike {
|
|
30
|
+
MockPostgresClient(this.store);
|
|
31
|
+
|
|
32
|
+
final Store store;
|
|
33
|
+
final List<String> executedSqls = [];
|
|
34
|
+
final List<Map<String, dynamic>> executedParams = [];
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
Future<List<Map<String, dynamic>>> execute(
|
|
38
|
+
String sql,
|
|
39
|
+
Map<String, dynamic> parameters,
|
|
40
|
+
) async {
|
|
41
|
+
executedSqls.add(sql);
|
|
42
|
+
executedParams.add(parameters);
|
|
43
|
+
return _handle(sql, parameters);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) async {
|
|
48
|
+
return fn(_TxClient(this));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
Future<void> close() async {}
|
|
53
|
+
|
|
54
|
+
List<Map<String, dynamic>> _handle(
|
|
55
|
+
String sql,
|
|
56
|
+
Map<String, dynamic> params,
|
|
57
|
+
) {
|
|
58
|
+
if (sql.contains('get_target_app_version_list')) {
|
|
59
|
+
return _targetAppVersionList(params);
|
|
60
|
+
}
|
|
61
|
+
if (sql.contains('get_update_info_by_app_version')) {
|
|
62
|
+
return _updateInfoByAppVersion(params);
|
|
63
|
+
}
|
|
64
|
+
if (sql.contains('get_update_info_by_fingerprint_hash')) {
|
|
65
|
+
return _updateInfoByFingerprint(params);
|
|
66
|
+
}
|
|
67
|
+
if (sql.contains('GROUP BY channel')) return _channels();
|
|
68
|
+
if (sql.contains('COUNT(*)')) return _count(sql, params);
|
|
69
|
+
if (sql.contains('FROM bundle_patches') && sql.contains('ANY')) {
|
|
70
|
+
return _patchesForIds(params);
|
|
71
|
+
}
|
|
72
|
+
if (sql.contains('INSERT INTO bundle_patches')) {
|
|
73
|
+
_upsertPatch(params);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
if (sql.contains('INSERT INTO bundles')) {
|
|
77
|
+
_upsertBundle(params);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
if (sql.contains('DELETE FROM bundle_patches')) {
|
|
81
|
+
_deletePatches(sql, params);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
if (sql.contains('DELETE FROM bundles')) {
|
|
85
|
+
return _deleteBundle(sql, params);
|
|
86
|
+
}
|
|
87
|
+
if (sql.contains('SELECT * FROM bundles')) {
|
|
88
|
+
return _selectBundles(sql, params);
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dynamic _paramAfter(String sql, String token, Map<String, dynamic> params) {
|
|
94
|
+
final idx = sql.indexOf(token);
|
|
95
|
+
if (idx < 0) return null;
|
|
96
|
+
final rest = sql.substring(idx + token.length).trim();
|
|
97
|
+
final match = RegExp(r'@(\w+)').firstMatch(rest);
|
|
98
|
+
if (match == null) return null;
|
|
99
|
+
final name = match.group(1)!;
|
|
100
|
+
return params['@$name'] ?? params[name];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
List<Map<String, dynamic>> _filterBundles(
|
|
104
|
+
String sql,
|
|
105
|
+
Map<String, dynamic> params,
|
|
106
|
+
) {
|
|
107
|
+
return store.bundleRows.where((row) {
|
|
108
|
+
final channel = _paramAfter(sql, 'channel =', params);
|
|
109
|
+
if (channel != null && row['channel'] != channel) return false;
|
|
110
|
+
final platform = _paramAfter(sql, 'platform =', params);
|
|
111
|
+
if (platform != null && row['platform'] != platform) return false;
|
|
112
|
+
final enabled = _paramAfter(sql, 'enabled =', params);
|
|
113
|
+
if (enabled != null && row['enabled'] != enabled) return false;
|
|
114
|
+
final target = _paramAfter(sql, 'target_app_version =', params);
|
|
115
|
+
if (target != null && row['target_app_version'] != target) return false;
|
|
116
|
+
final idEq = _paramAfter(sql, 'id =', params);
|
|
117
|
+
if (idEq != null && row['id'] != idEq) return false;
|
|
118
|
+
return true;
|
|
119
|
+
}).toList();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
List<Map<String, dynamic>> _selectBundles(
|
|
123
|
+
String sql,
|
|
124
|
+
Map<String, dynamic> params,
|
|
125
|
+
) {
|
|
126
|
+
var rows = _filterBundles(sql, params);
|
|
127
|
+
|
|
128
|
+
if (sql.contains('ORDER BY id asc')) {
|
|
129
|
+
rows = rows.toList()..sort((a, b) => (a['id'] as String).compareTo(b['id'] as String));
|
|
130
|
+
} else {
|
|
131
|
+
rows = rows.toList()
|
|
132
|
+
..sort((a, b) => (b['id'] as String).compareTo(a['id'] as String));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
final limit = params['@limit'] as int?;
|
|
136
|
+
final offset = params['@offset'] as int? ?? 0;
|
|
137
|
+
if (limit != null && limit > 0) {
|
|
138
|
+
final end = (offset + limit).clamp(0, rows.length);
|
|
139
|
+
rows = rows.sublist(offset.clamp(0, rows.length), end);
|
|
140
|
+
} else if (offset > 0) {
|
|
141
|
+
rows = rows.sublist(offset.clamp(0, rows.length));
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
List<Map<String, dynamic>> _count(String sql, Map<String, dynamic> params) {
|
|
147
|
+
return [
|
|
148
|
+
{'total': _filterBundles(sql, params).length},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
List<Map<String, dynamic>> _channels() {
|
|
153
|
+
final channels = store.bundleRows.map((r) => r['channel'] as String).toSet().toList();
|
|
154
|
+
return channels.map((c) => {'channel': c}).toList();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
List<Map<String, dynamic>> _patchesForIds(Map<String, dynamic> params) {
|
|
158
|
+
final ids = (params['bundle_ids'] as List).cast<String>();
|
|
159
|
+
return store.bundlePatchRows.where((p) => ids.contains(p['bundle_id'])).toList();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
void _upsertBundle(Map<String, dynamic> params) {
|
|
163
|
+
final row = <String, dynamic>{};
|
|
164
|
+
params.forEach((k, v) => row[k] = v);
|
|
165
|
+
final existing = store.bundleRows.indexWhere((r) => r['id'] == row['id']);
|
|
166
|
+
if (existing >= 0) {
|
|
167
|
+
store.bundleRows[existing] = row;
|
|
168
|
+
} else {
|
|
169
|
+
store.bundleRows.add(row);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void _upsertPatch(Map<String, dynamic> params) {
|
|
174
|
+
final row = <String, dynamic>{};
|
|
175
|
+
params.forEach((k, v) => row[k] = v);
|
|
176
|
+
final existing = store.bundlePatchRows.indexWhere((r) => r['id'] == row['id']);
|
|
177
|
+
if (existing >= 0) {
|
|
178
|
+
store.bundlePatchRows[existing] = row;
|
|
179
|
+
} else {
|
|
180
|
+
store.bundlePatchRows.add(row);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
void _deletePatches(String sql, Map<String, dynamic> params) {
|
|
185
|
+
final id = params['@p0'] ?? params['p0'];
|
|
186
|
+
if (sql.contains('base_bundle_id')) {
|
|
187
|
+
store.bundlePatchRows.removeWhere((p) => p['base_bundle_id'] == id);
|
|
188
|
+
} else {
|
|
189
|
+
store.bundlePatchRows.removeWhere((p) => p['bundle_id'] == id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
List<Map<String, dynamic>> _deleteBundle(
|
|
194
|
+
String sql,
|
|
195
|
+
Map<String, dynamic> params,
|
|
196
|
+
) {
|
|
197
|
+
final id = params['@p0'] ?? params['p0'];
|
|
198
|
+
final existed = store.bundleRows.any((r) => r['id'] == id);
|
|
199
|
+
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
200
|
+
store.bundlePatchRows.removeWhere((p) => p['bundle_id'] == id);
|
|
201
|
+
return existed ? [{'id': id}] : [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
List<Map<String, dynamic>> _targetAppVersionList(Map<String, dynamic> params) {
|
|
205
|
+
final platform = params['app_platform'];
|
|
206
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
207
|
+
final versions = store.bundleRows
|
|
208
|
+
.where((r) => r['platform'] == platform && (r['id'] as String).compareTo(min) >= 0)
|
|
209
|
+
.map((r) => r['target_app_version'])
|
|
210
|
+
.where((v) => v != null)
|
|
211
|
+
.toSet()
|
|
212
|
+
.cast<String>()
|
|
213
|
+
.toList();
|
|
214
|
+
return versions.map((v) => {'target_app_version': v}).toList();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Map<String, dynamic>? _newestMatch(
|
|
218
|
+
bool Function(Map<String, dynamic> row) predicate,
|
|
219
|
+
) {
|
|
220
|
+
final matches = store.bundleRows.where(predicate).toList()
|
|
221
|
+
..sort((a, b) => (b['id'] as String).compareTo(a['id'] as String));
|
|
222
|
+
return matches.isEmpty ? null : matches.first;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
List<Map<String, dynamic>> _updateInfoByAppVersion(Map<String, dynamic> params) {
|
|
226
|
+
final platform = params['app_platform'];
|
|
227
|
+
final channel = params['target_channel'];
|
|
228
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
229
|
+
final list = (params['target_app_version_list'] as List?)?.cast<String>() ?? [];
|
|
230
|
+
final row = _newestMatch((r) =>
|
|
231
|
+
r['platform'] == platform &&
|
|
232
|
+
r['enabled'] == true &&
|
|
233
|
+
r['channel'] == channel &&
|
|
234
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
235
|
+
list.contains(r['target_app_version']));
|
|
236
|
+
if (row == null) return [];
|
|
237
|
+
return [_infoRow(row)];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
List<Map<String, dynamic>> _updateInfoByFingerprint(Map<String, dynamic> params) {
|
|
241
|
+
final platform = params['app_platform'];
|
|
242
|
+
final channel = params['target_channel'];
|
|
243
|
+
final min = params['min_bundle_id'] as String? ?? '';
|
|
244
|
+
final fp = params['target_fingerprint_hash'];
|
|
245
|
+
final row = _newestMatch((r) =>
|
|
246
|
+
r['platform'] == platform &&
|
|
247
|
+
r['enabled'] == true &&
|
|
248
|
+
r['channel'] == channel &&
|
|
249
|
+
(r['id'] as String).compareTo(min) >= 0 &&
|
|
250
|
+
r['fingerprint_hash'] == fp);
|
|
251
|
+
if (row == null) return [];
|
|
252
|
+
return [_infoRow(row)];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Map<String, dynamic> _infoRow(Map<String, dynamic> row) => {
|
|
256
|
+
'id': row['id'],
|
|
257
|
+
'should_force_update': row['should_force_update'],
|
|
258
|
+
'message': row['message'],
|
|
259
|
+
'status': 'UPDATE',
|
|
260
|
+
'storage_uri': row['storage_uri'],
|
|
261
|
+
'file_hash': row['file_hash'],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
class _TxClient implements PostgresClientLike {
|
|
266
|
+
_TxClient(this._parent);
|
|
267
|
+
|
|
268
|
+
final MockPostgresClient _parent;
|
|
269
|
+
|
|
270
|
+
@override
|
|
271
|
+
Future<List<Map<String, dynamic>>> execute(
|
|
272
|
+
String sql,
|
|
273
|
+
Map<String, dynamic> parameters,
|
|
274
|
+
) =>
|
|
275
|
+
_parent.execute(sql, parameters);
|
|
276
|
+
|
|
277
|
+
@override
|
|
278
|
+
Future<T> runTx<T>(Future<T> Function(PostgresClientLike tx) fn) =>
|
|
279
|
+
throw UnsupportedError('Nested transactions are not supported');
|
|
280
|
+
|
|
281
|
+
@override
|
|
282
|
+
Future<void> close() async {}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Build a [PostgresConfig] wired to [MockPostgresClient].
|
|
286
|
+
PostgresConfig mockPostgresConfig(Store store) => PostgresConfig(
|
|
287
|
+
host: 'mock',
|
|
288
|
+
database: 'mock',
|
|
289
|
+
clientFactory: (_) => MockPostgresClient(store),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
/// Create a [DatabasePlugin] backed by the mock store.
|
|
293
|
+
DatabasePlugin newPlugin({Store? store}) {
|
|
294
|
+
final s = store ?? Store();
|
|
295
|
+
final plugin = postgresDatabase(mockPostgresConfig(s))();
|
|
296
|
+
return plugin;
|
|
297
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
2
|
+
show Bundle, BundleMetadata, BundlePatchArtifact, Platform;
|
|
3
|
+
import 'package:flutter_patcher_postgres/flutter_patcher_postgres.dart';
|
|
4
|
+
import 'package:test/test.dart';
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
group('postgres bundle mapper', () {
|
|
8
|
+
test('bundleToRowValues emits snake_case columns', () {
|
|
9
|
+
final bundle = Bundle(
|
|
10
|
+
id: '018f0000-0000-7000-8000-000000000001',
|
|
11
|
+
platform: Platform.android,
|
|
12
|
+
shouldForceUpdate: true,
|
|
13
|
+
enabled: true,
|
|
14
|
+
fileHash: 'hash-1',
|
|
15
|
+
gitCommitHash: 'abc',
|
|
16
|
+
message: 'hello',
|
|
17
|
+
channel: 'production',
|
|
18
|
+
storageUri: 'file:///b1',
|
|
19
|
+
targetAppVersion: '1.2.3',
|
|
20
|
+
fingerprintHash: null,
|
|
21
|
+
metadata: const BundleMetadata(appVersion: '1.2.3'),
|
|
22
|
+
manifestStorageUri: 'file:///m1',
|
|
23
|
+
manifestFileHash: 'mh1',
|
|
24
|
+
assetBaseStorageUri: 'file:///a1',
|
|
25
|
+
patches: const [
|
|
26
|
+
BundlePatchArtifact(
|
|
27
|
+
baseBundleId: '018f0000-0000-7000-8000-000000000000',
|
|
28
|
+
baseFileHash: 'base-hash',
|
|
29
|
+
patchFileHash: 'patch-hash',
|
|
30
|
+
patchStorageUri: 'file:///p1',
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
patchBaseBundleId: '018f0000-0000-7000-8000-000000000000',
|
|
34
|
+
patchBaseFileHash: 'base-hash',
|
|
35
|
+
patchFileHash: 'patch-hash',
|
|
36
|
+
patchStorageUri: 'file:///p1',
|
|
37
|
+
rolloutCohortCount: 1000,
|
|
38
|
+
targetCohorts: const ['team-a'],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
final row = bundleToRowValues(bundle);
|
|
42
|
+
expect(row['id'], bundle.id);
|
|
43
|
+
expect(row['should_force_update'], true);
|
|
44
|
+
expect(row['file_hash'], 'hash-1');
|
|
45
|
+
expect(row['target_app_version'], '1.2.3');
|
|
46
|
+
expect(row['platform'], 'android');
|
|
47
|
+
expect(row['manifest_storage_uri'], 'file:///m1');
|
|
48
|
+
expect(row['rollout_cohort_count'], 1000);
|
|
49
|
+
expect(row['target_cohorts'], const ['team-a']);
|
|
50
|
+
expect(row['metadata'], isA<Map>());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('mapRowToBundle rebuilds derived patch fields', () {
|
|
54
|
+
final bundle = Bundle(
|
|
55
|
+
id: '018f0000-0000-7000-8000-000000000001',
|
|
56
|
+
platform: Platform.android,
|
|
57
|
+
shouldForceUpdate: false,
|
|
58
|
+
enabled: true,
|
|
59
|
+
fileHash: 'hash-1',
|
|
60
|
+
channel: 'production',
|
|
61
|
+
storageUri: 'file:///b1',
|
|
62
|
+
targetAppVersion: '1.2.3',
|
|
63
|
+
message: null,
|
|
64
|
+
fingerprintHash: null,
|
|
65
|
+
metadata: const BundleMetadata(appVersion: '1.2.3'),
|
|
66
|
+
manifestStorageUri: null,
|
|
67
|
+
manifestFileHash: null,
|
|
68
|
+
assetBaseStorageUri: null,
|
|
69
|
+
patches: const [
|
|
70
|
+
BundlePatchArtifact(
|
|
71
|
+
baseBundleId: '018f0000-0000-7000-8000-000000000000',
|
|
72
|
+
baseFileHash: 'base-hash',
|
|
73
|
+
patchFileHash: 'patch-hash',
|
|
74
|
+
patchStorageUri: 'file:///p1',
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
patchBaseBundleId: null,
|
|
78
|
+
patchBaseFileHash: null,
|
|
79
|
+
patchFileHash: null,
|
|
80
|
+
patchStorageUri: null,
|
|
81
|
+
rolloutCohortCount: 1000,
|
|
82
|
+
targetCohorts: null,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
final row = bundleToRowValues(bundle);
|
|
86
|
+
final patchRows = bundleToPatchRows(bundle);
|
|
87
|
+
final back = mapRowToBundle(
|
|
88
|
+
PostgresBundleRow.fromJson(row),
|
|
89
|
+
[PostgresBundlePatchRow.fromJson(patchRows.first)],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(back.id, bundle.id);
|
|
93
|
+
expect(back.fileHash, 'hash-1');
|
|
94
|
+
expect(back.patches, hasLength(1));
|
|
95
|
+
expect(back.patches!.first.patchFileHash, 'patch-hash');
|
|
96
|
+
expect(back.patchStorageUri, 'file:///p1');
|
|
97
|
+
expect(back.rolloutCohortCount, 1000);
|
|
98
|
+
expect(back.metadata?.appVersion, '1.2.3');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('bundleToPatchRows builds compound ids', () {
|
|
102
|
+
final bundle = Bundle(
|
|
103
|
+
id: 'bid',
|
|
104
|
+
platform: Platform.android,
|
|
105
|
+
shouldForceUpdate: false,
|
|
106
|
+
enabled: true,
|
|
107
|
+
fileHash: 'h',
|
|
108
|
+
channel: 'production',
|
|
109
|
+
storageUri: 's',
|
|
110
|
+
message: null,
|
|
111
|
+
fingerprintHash: null,
|
|
112
|
+
metadata: null,
|
|
113
|
+
manifestStorageUri: null,
|
|
114
|
+
manifestFileHash: null,
|
|
115
|
+
assetBaseStorageUri: null,
|
|
116
|
+
patches: const [
|
|
117
|
+
BundlePatchArtifact(
|
|
118
|
+
baseBundleId: 'base',
|
|
119
|
+
baseFileHash: 'bh',
|
|
120
|
+
patchFileHash: 'ph',
|
|
121
|
+
patchStorageUri: 'ps',
|
|
122
|
+
),
|
|
123
|
+
],
|
|
124
|
+
patchBaseBundleId: null,
|
|
125
|
+
patchBaseFileHash: null,
|
|
126
|
+
patchFileHash: null,
|
|
127
|
+
patchStorageUri: null,
|
|
128
|
+
rolloutCohortCount: 1000,
|
|
129
|
+
targetCohorts: null,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
final rows = bundleToPatchRows(bundle);
|
|
133
|
+
expect(rows, hasLength(1));
|
|
134
|
+
expect(rows.first['id'], 'bid:base');
|
|
135
|
+
expect(rows.first['bundle_id'], 'bid');
|
|
136
|
+
expect(rows.first['base_bundle_id'], 'base');
|
|
137
|
+
expect(rows.first['order_index'], 0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
2
|
+
show
|
|
3
|
+
AppVersionGetBundlesArgs,
|
|
4
|
+
Bundle,
|
|
5
|
+
FingerprintGetBundlesArgs,
|
|
6
|
+
Platform,
|
|
7
|
+
UpdateInfo,
|
|
8
|
+
UpdateStatus,
|
|
9
|
+
nilUuid;
|
|
10
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
11
|
+
show
|
|
12
|
+
DatabaseBundleQueryOptions,
|
|
13
|
+
DatabaseBundleQueryWhere,
|
|
14
|
+
Paginated;
|
|
15
|
+
import 'package:test/test.dart';
|
|
16
|
+
|
|
17
|
+
import 'mock_postgres_client.dart';
|
|
18
|
+
|
|
19
|
+
Bundle _bundle(String id,
|
|
20
|
+
{String channel = 'production',
|
|
21
|
+
String? fingerprintHash}) =>
|
|
22
|
+
Bundle(
|
|
23
|
+
id: id,
|
|
24
|
+
platform: Platform.android,
|
|
25
|
+
shouldForceUpdate: false,
|
|
26
|
+
enabled: true,
|
|
27
|
+
fileHash: 'file-hash-$id',
|
|
28
|
+
channel: channel,
|
|
29
|
+
storageUri: 'file:///bundle-$id',
|
|
30
|
+
targetAppVersion: '1.0.0',
|
|
31
|
+
message: null,
|
|
32
|
+
fingerprintHash: fingerprintHash,
|
|
33
|
+
metadata: null,
|
|
34
|
+
manifestStorageUri: null,
|
|
35
|
+
manifestFileHash: null,
|
|
36
|
+
assetBaseStorageUri: null,
|
|
37
|
+
patches: const [],
|
|
38
|
+
patchBaseBundleId: null,
|
|
39
|
+
patchBaseFileHash: null,
|
|
40
|
+
patchFileHash: null,
|
|
41
|
+
patchStorageUri: null,
|
|
42
|
+
rolloutCohortCount: 1000,
|
|
43
|
+
targetCohorts: null,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
void main() {
|
|
47
|
+
group('postgresDatabase', () {
|
|
48
|
+
test('getChannels returns distinct channels', () async {
|
|
49
|
+
final plugin = newPlugin();
|
|
50
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
51
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000002',
|
|
52
|
+
channel: 'staging'));
|
|
53
|
+
await plugin.commitBundle();
|
|
54
|
+
|
|
55
|
+
final channels = await plugin.getChannels();
|
|
56
|
+
expect(channels, containsAll(['production', 'staging']));
|
|
57
|
+
expect(channels.length, 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('getBundles returns bundles and pagination', () async {
|
|
61
|
+
final plugin = newPlugin();
|
|
62
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
63
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000002'));
|
|
64
|
+
await plugin.commitBundle();
|
|
65
|
+
|
|
66
|
+
final result = await plugin.getBundles(
|
|
67
|
+
const DatabaseBundleQueryOptions(limit: 10),
|
|
68
|
+
);
|
|
69
|
+
expect(result, isA<Paginated<List<Bundle>>>());
|
|
70
|
+
expect(result.data, hasLength(2));
|
|
71
|
+
expect(result.pagination.total, 2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('getBundles filters by channel', () async {
|
|
75
|
+
final plugin = newPlugin();
|
|
76
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
77
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000002',
|
|
78
|
+
channel: 'staging'));
|
|
79
|
+
await plugin.commitBundle();
|
|
80
|
+
|
|
81
|
+
final result = await plugin.getBundles(
|
|
82
|
+
DatabaseBundleQueryOptions(
|
|
83
|
+
where: const DatabaseBundleQueryWhere(channel: 'staging'),
|
|
84
|
+
limit: 10,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
expect(result.data, hasLength(1));
|
|
88
|
+
expect(result.data.first.channel, 'staging');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('getBundleById returns null for missing, bundle for existing',
|
|
92
|
+
() async {
|
|
93
|
+
final plugin = newPlugin();
|
|
94
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
95
|
+
await plugin.commitBundle();
|
|
96
|
+
|
|
97
|
+
expect(await plugin.getBundleById('does-not-exist'), isNull);
|
|
98
|
+
final got = await plugin.getBundleById('018f0000-0000-7000-8000-000000000001');
|
|
99
|
+
expect(got, isNotNull);
|
|
100
|
+
expect(got!.id, '018f0000-0000-7000-8000-000000000001');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('commitBundle append -> update -> delete reflects in store', () async {
|
|
104
|
+
final plugin = newPlugin();
|
|
105
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
106
|
+
await plugin.commitBundle();
|
|
107
|
+
|
|
108
|
+
var got = await plugin.getBundleById('018f0000-0000-7000-8000-000000000001');
|
|
109
|
+
expect(got!.enabled, true);
|
|
110
|
+
|
|
111
|
+
await plugin.updateBundle(got.id, {'enabled': false});
|
|
112
|
+
await plugin.commitBundle();
|
|
113
|
+
got = await plugin.getBundleById('018f0000-0000-7000-8000-000000000001');
|
|
114
|
+
expect(got!.enabled, false);
|
|
115
|
+
|
|
116
|
+
await plugin.deleteBundle(got);
|
|
117
|
+
await plugin.commitBundle();
|
|
118
|
+
expect(
|
|
119
|
+
await plugin.getBundleById('018f0000-0000-7000-8000-000000000001'),
|
|
120
|
+
isNull,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('getUpdateInfo via RPC returns UpdateInfo for compatible bundle',
|
|
125
|
+
() async {
|
|
126
|
+
final plugin = newPlugin();
|
|
127
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
128
|
+
await plugin.commitBundle();
|
|
129
|
+
|
|
130
|
+
final info = await plugin.getUpdateInfo(
|
|
131
|
+
AppVersionGetBundlesArgs(
|
|
132
|
+
appVersion: '1.0.0',
|
|
133
|
+
bundleId: nilUuid,
|
|
134
|
+
channel: 'production',
|
|
135
|
+
minBundleId: nilUuid,
|
|
136
|
+
platform: Platform.android,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
expect(info, isA<UpdateInfo>());
|
|
140
|
+
expect(info!.status, UpdateStatus.update);
|
|
141
|
+
expect(info.storageUri, 'file:///bundle-018f0000-0000-7000-8000-000000000001');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('getUpdateInfo via fingerprint returns matching bundle', () async {
|
|
145
|
+
final plugin = newPlugin();
|
|
146
|
+
final fpBundle = _bundle('018f0000-0000-7000-8000-000000000001',
|
|
147
|
+
fingerprintHash: 'fp-abc');
|
|
148
|
+
await plugin.appendBundle(fpBundle);
|
|
149
|
+
await plugin.commitBundle();
|
|
150
|
+
|
|
151
|
+
final info = await plugin.getUpdateInfo(
|
|
152
|
+
FingerprintGetBundlesArgs(
|
|
153
|
+
fingerprintHash: 'fp-abc',
|
|
154
|
+
bundleId: nilUuid,
|
|
155
|
+
channel: 'production',
|
|
156
|
+
minBundleId: nilUuid,
|
|
157
|
+
platform: Platform.android,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
expect(info, isA<UpdateInfo>());
|
|
161
|
+
expect(info!.fileHash, 'file-hash-018f0000-0000-7000-8000-000000000001');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('getUpdateInfo returns null when no compatible bundle', () async {
|
|
165
|
+
final plugin = newPlugin();
|
|
166
|
+
await plugin.appendBundle(_bundle('018f0000-0000-7000-8000-000000000001'));
|
|
167
|
+
await plugin.commitBundle();
|
|
168
|
+
|
|
169
|
+
final info = await plugin.getUpdateInfo(
|
|
170
|
+
AppVersionGetBundlesArgs(
|
|
171
|
+
appVersion: '9.9.9',
|
|
172
|
+
bundleId: nilUuid,
|
|
173
|
+
channel: 'production',
|
|
174
|
+
minBundleId: nilUuid,
|
|
175
|
+
platform: Platform.android,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
expect(info, isNull);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|