@_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,294 @@
|
|
|
1
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart';
|
|
2
|
+
import 'package:flutter_patcher_plugin_core/src/bundle_unit_of_work.dart';
|
|
3
|
+
import 'package:flutter_patcher_plugin_core/src/bundle_unit_of_work_store.dart';
|
|
4
|
+
import 'package:flutter_patcher_plugin_core/src/create_database_plugin.dart';
|
|
5
|
+
import 'package:flutter_patcher_plugin_core/src/types.dart';
|
|
6
|
+
import 'package:test/test.dart';
|
|
7
|
+
|
|
8
|
+
class _MockDatabasePlugin extends AbstractDatabasePlugin {
|
|
9
|
+
_MockDatabasePlugin({required this.id, required this.bundles});
|
|
10
|
+
|
|
11
|
+
final String id;
|
|
12
|
+
final List<Bundle> bundles;
|
|
13
|
+
|
|
14
|
+
final List<BundleChange> committedChanges = [];
|
|
15
|
+
|
|
16
|
+
String get name => 'mock-$id';
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
Future<Bundle?> getBundleById(String bundleId) async {
|
|
20
|
+
return bundles.where((b) => b.id == bundleId).firstOrNull;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
Future<Paginated<List<Bundle>>> getBundles(
|
|
25
|
+
DatabaseBundleQueryOptions options,
|
|
26
|
+
) async {
|
|
27
|
+
final where = options.where;
|
|
28
|
+
var filtered = bundles;
|
|
29
|
+
if (where?.channel != null) {
|
|
30
|
+
filtered = filtered.where((b) => b.channel == where!.channel).toList();
|
|
31
|
+
}
|
|
32
|
+
return Paginated(
|
|
33
|
+
data: filtered,
|
|
34
|
+
pagination: PaginationInfo(
|
|
35
|
+
total: filtered.length,
|
|
36
|
+
hasNextPage: false,
|
|
37
|
+
hasPreviousPage: false,
|
|
38
|
+
currentPage: 1,
|
|
39
|
+
totalPages: 1,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
Future<List<String>> getChannels() async => ['production'];
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
Future<void> commitBundle({required List<BundleChange> changedSets}) async {
|
|
49
|
+
committedChanges.addAll(changedSets);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async => null;
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
Future<void> onUnmount() async {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Bundle _bundle(String id, {String channel = 'production'}) => Bundle(
|
|
60
|
+
id: id,
|
|
61
|
+
platform: Platform.android,
|
|
62
|
+
shouldForceUpdate: false,
|
|
63
|
+
enabled: true,
|
|
64
|
+
fileHash: 'hash_$id',
|
|
65
|
+
storageUri: 's3://bucket/$id.zip',
|
|
66
|
+
channel: channel,
|
|
67
|
+
targetAppVersion: '1.0.0',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
void main() {
|
|
71
|
+
setUp(() {
|
|
72
|
+
clearUnitOfWorkStore();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
group('createDatabasePlugin', () {
|
|
76
|
+
late _MockDatabasePlugin mockImpl;
|
|
77
|
+
|
|
78
|
+
setUp(() {
|
|
79
|
+
mockImpl = _MockDatabasePlugin(
|
|
80
|
+
id: 'test',
|
|
81
|
+
bundles: [_bundle('b1'), _bundle('b2')],
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('returns a factory function', () {
|
|
86
|
+
final factory = createDatabasePlugin<String>(
|
|
87
|
+
name: 'testDb',
|
|
88
|
+
factory: (config) => _MockDatabasePlugin(
|
|
89
|
+
id: config, bundles: [_bundle('b1')],
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
final createDb = factory('test');
|
|
93
|
+
expect(createDb, isA<Function>());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('factory creates a DatabasePlugin', () {
|
|
97
|
+
final factory = createDatabasePlugin<String>(
|
|
98
|
+
name: 'testDb',
|
|
99
|
+
factory: (config) => _MockDatabasePlugin(
|
|
100
|
+
id: config, bundles: [_bundle('b1')],
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
final db = factory('test')();
|
|
104
|
+
expect(db, isA<DatabasePlugin>());
|
|
105
|
+
expect(db.name, 'testDb');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('getBundleById delegates to underlying impl', () async {
|
|
109
|
+
final factory = createDatabasePlugin<String>(
|
|
110
|
+
name: 'testDb',
|
|
111
|
+
factory: (config) => _MockDatabasePlugin(
|
|
112
|
+
id: config, bundles: [_bundle('b1'), _bundle('b2')],
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
final db = factory('test')();
|
|
116
|
+
final result = await db.getBundleById('b1');
|
|
117
|
+
expect(result, isNotNull);
|
|
118
|
+
expect(result!.id, 'b1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('getBundleById returns null for missing', () async {
|
|
122
|
+
final factory = createDatabasePlugin<String>(
|
|
123
|
+
name: 'testDb',
|
|
124
|
+
factory: (config) => _MockDatabasePlugin(
|
|
125
|
+
id: config, bundles: [_bundle('b1')],
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
final db = factory('test')();
|
|
129
|
+
final result = await db.getBundleById('missing');
|
|
130
|
+
expect(result, isNull);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('getBundles returns paginated results', () async {
|
|
134
|
+
final factory = createDatabasePlugin<String>(
|
|
135
|
+
name: 'testDb',
|
|
136
|
+
factory: (config) => _MockDatabasePlugin(
|
|
137
|
+
id: config, bundles: [_bundle('b1'), _bundle('b2')],
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
final db = factory('test')();
|
|
141
|
+
final result = await db.getBundles(
|
|
142
|
+
const DatabaseBundleQueryOptions(),
|
|
143
|
+
);
|
|
144
|
+
expect(result.data.length, 2);
|
|
145
|
+
expect(result.pagination.total, 2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('getChannels delegates correctly', () async {
|
|
149
|
+
final factory = createDatabasePlugin<String>(
|
|
150
|
+
name: 'testDb',
|
|
151
|
+
factory: (config) => _MockDatabasePlugin(
|
|
152
|
+
id: config, bundles: [_bundle('b1')],
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
final db = factory('test')();
|
|
156
|
+
final channels = await db.getChannels();
|
|
157
|
+
expect(channels, ['production']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('appendBundle + commitBundle sends insert', () async {
|
|
161
|
+
final factory = createDatabasePlugin<String>(
|
|
162
|
+
name: 'testDb',
|
|
163
|
+
factory: (config) => mockImpl,
|
|
164
|
+
);
|
|
165
|
+
final db = factory('test')();
|
|
166
|
+
|
|
167
|
+
await db.appendBundle(_bundle('new'));
|
|
168
|
+
await db.commitBundle();
|
|
169
|
+
|
|
170
|
+
expect(mockImpl.committedChanges.length, 1);
|
|
171
|
+
expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.insert);
|
|
172
|
+
expect(mockImpl.committedChanges.first.data.id, 'new');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('updateBundle + commitBundle sends update', () async {
|
|
176
|
+
final factory = createDatabasePlugin<String>(
|
|
177
|
+
name: 'testDb',
|
|
178
|
+
factory: (config) => mockImpl,
|
|
179
|
+
);
|
|
180
|
+
final db = factory('test')();
|
|
181
|
+
|
|
182
|
+
await db.updateBundle('b1', {'enabled': false});
|
|
183
|
+
await db.commitBundle();
|
|
184
|
+
|
|
185
|
+
expect(mockImpl.committedChanges.length, 1);
|
|
186
|
+
expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.update);
|
|
187
|
+
expect(mockImpl.committedChanges.first.data.enabled, false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('deleteBundle + commitBundle sends delete', () async {
|
|
191
|
+
final factory = createDatabasePlugin<String>(
|
|
192
|
+
name: 'testDb',
|
|
193
|
+
factory: (config) => mockImpl,
|
|
194
|
+
);
|
|
195
|
+
final db = factory('test')();
|
|
196
|
+
|
|
197
|
+
await db.deleteBundle(_bundle('b1'));
|
|
198
|
+
await db.commitBundle();
|
|
199
|
+
|
|
200
|
+
expect(mockImpl.committedChanges.length, 1);
|
|
201
|
+
expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.delete);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('commitBundle with no changes sends empty', () async {
|
|
205
|
+
final factory = createDatabasePlugin<String>(
|
|
206
|
+
name: 'testDb',
|
|
207
|
+
factory: (config) => mockImpl,
|
|
208
|
+
);
|
|
209
|
+
final db = factory('test')();
|
|
210
|
+
|
|
211
|
+
await db.commitBundle();
|
|
212
|
+
expect(mockImpl.committedChanges, isEmpty);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('each factory call creates isolated UoW', () async {
|
|
216
|
+
final factory = createDatabasePlugin<String>(
|
|
217
|
+
name: 'testDb',
|
|
218
|
+
factory: (config) => mockImpl,
|
|
219
|
+
);
|
|
220
|
+
final db1 = factory('test')();
|
|
221
|
+
final db2 = factory('test')();
|
|
222
|
+
|
|
223
|
+
await db1.appendBundle(_bundle('only1'));
|
|
224
|
+
// db2 should not have db1's changes
|
|
225
|
+
await db2.commitBundle();
|
|
226
|
+
expect(mockImpl.committedChanges, isEmpty);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('hook onDatabaseUpdated fires after commit', () async {
|
|
230
|
+
var hookCalled = false;
|
|
231
|
+
final factory = createDatabasePlugin<String>(
|
|
232
|
+
name: 'testDb',
|
|
233
|
+
factory: (config) => mockImpl,
|
|
234
|
+
);
|
|
235
|
+
final db = factory('test', DatabasePluginHooks(
|
|
236
|
+
onDatabaseUpdated: () async { hookCalled = true; },
|
|
237
|
+
))();
|
|
238
|
+
|
|
239
|
+
await db.appendBundle(_bundle('x'));
|
|
240
|
+
await db.commitBundle();
|
|
241
|
+
expect(hookCalled, isTrue);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('getUpdateInfo delegates to underlying impl', () async {
|
|
245
|
+
final factory = createDatabasePlugin<String>(
|
|
246
|
+
name: 'testDb',
|
|
247
|
+
factory: (config) => mockImpl,
|
|
248
|
+
);
|
|
249
|
+
final db = factory('test')();
|
|
250
|
+
final result = await db.getUpdateInfo(
|
|
251
|
+
FingerprintGetBundlesArgs(
|
|
252
|
+
platform: Platform.android,
|
|
253
|
+
bundleId: 'b1',
|
|
254
|
+
fingerprintHash: 'abc',
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
expect(result, isNull);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
group('mergeBundleUpdate', () {
|
|
262
|
+
test('replaces targetCohorts', () {
|
|
263
|
+
final base = _bundle('a');
|
|
264
|
+
final updated = mergeBundleUpdate(base, {
|
|
265
|
+
'targetCohorts': ['cohort1'],
|
|
266
|
+
});
|
|
267
|
+
expect(updated.targetCohorts, ['cohort1']);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('replaces patches', () {
|
|
271
|
+
final base = _bundle('a');
|
|
272
|
+
final updated = mergeBundleUpdate(base, {
|
|
273
|
+
'patches': [],
|
|
274
|
+
});
|
|
275
|
+
expect(updated.patches, isEmpty);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('updates scalar fields', () {
|
|
279
|
+
final base = _bundle('a');
|
|
280
|
+
final updated = mergeBundleUpdate(base, {
|
|
281
|
+
'enabled': false,
|
|
282
|
+
});
|
|
283
|
+
expect(updated.enabled, false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('preserves existing fields not in patch', () {
|
|
287
|
+
final base = _bundle('a');
|
|
288
|
+
final updated = mergeBundleUpdate(base, {
|
|
289
|
+
'enabled': false,
|
|
290
|
+
});
|
|
291
|
+
expect(updated.fileHash, 'hash_a');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import 'package:flutter_patcher_plugin_core/src/create_storage_plugin.dart';
|
|
2
|
+
import 'package:flutter_patcher_plugin_core/src/types.dart';
|
|
3
|
+
import 'package:test/test.dart';
|
|
4
|
+
|
|
5
|
+
class _MockNodeProfile implements NodeStorageProfile {
|
|
6
|
+
@override
|
|
7
|
+
Future<Map<String, String>> upload(String key, String filePath) async => {
|
|
8
|
+
'storageUri': 'mock://$key',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
Future<bool> exists(String storageUri) async => true;
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
Future<void> delete(String storageUri) async {}
|
|
16
|
+
|
|
17
|
+
@override
|
|
18
|
+
Future<void> downloadFile(String storageUri, String filePath) async {}
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
Future<List<StorageObject>> listObjects([String? prefix]) async => [];
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
Future<void> deleteObjects(List<String> keys) async {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class _MockRuntimeProfile implements RuntimeStorageProfile {
|
|
28
|
+
@override
|
|
29
|
+
Future<Map<String, String>> getDownloadUrl(String storageUri) async => {
|
|
30
|
+
'url': 'https://example.com/$storageUri',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
Future<String?> readText(String storageUri) async => 'content';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
group('createNodeStoragePlugin', () {
|
|
39
|
+
test('creates a storage plugin with node profile', () {
|
|
40
|
+
final factory = createNodeStoragePlugin<String>(
|
|
41
|
+
name: 'testStorage',
|
|
42
|
+
supportedProtocol: 'mock://',
|
|
43
|
+
factory: (config) => _MockNodeProfile(),
|
|
44
|
+
);
|
|
45
|
+
final plugin = factory('cfg');
|
|
46
|
+
expect(plugin.name, 'testStorage');
|
|
47
|
+
expect(plugin.supportedProtocol, 'mock://');
|
|
48
|
+
expect(plugin.profiles.hasNode, isTrue);
|
|
49
|
+
expect(plugin.profiles.hasRuntime, isFalse);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('factory is lazy — not called until profile method invoked', () async {
|
|
53
|
+
var called = false;
|
|
54
|
+
final factory = createNodeStoragePlugin<String>(
|
|
55
|
+
name: 'testStorage',
|
|
56
|
+
supportedProtocol: 'mock://',
|
|
57
|
+
factory: (config) {
|
|
58
|
+
called = true;
|
|
59
|
+
return _MockNodeProfile();
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
final plugin = factory('cfg');
|
|
63
|
+
expect(called, isFalse);
|
|
64
|
+
// Calling a method on the profile triggers the factory
|
|
65
|
+
await plugin.profiles.node!.exists('uri');
|
|
66
|
+
expect(called, isTrue);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('upload calls hook after completion', () async {
|
|
70
|
+
var hookCalled = false;
|
|
71
|
+
final factory = createNodeStoragePlugin<String>(
|
|
72
|
+
name: 'testStorage',
|
|
73
|
+
supportedProtocol: 'mock://',
|
|
74
|
+
factory: (config) => _MockNodeProfile(),
|
|
75
|
+
);
|
|
76
|
+
final plugin = factory('cfg', StoragePluginHooks(
|
|
77
|
+
onStorageUploaded: () async { hookCalled = true; },
|
|
78
|
+
));
|
|
79
|
+
await plugin.profiles.node!.upload('key', '/path');
|
|
80
|
+
expect(hookCalled, isTrue);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('without hook, upload works fine', () async {
|
|
84
|
+
final factory = createNodeStoragePlugin<String>(
|
|
85
|
+
name: 'testStorage',
|
|
86
|
+
supportedProtocol: 'mock://',
|
|
87
|
+
factory: (config) => _MockNodeProfile(),
|
|
88
|
+
);
|
|
89
|
+
final plugin = factory('cfg');
|
|
90
|
+
final result = await plugin.profiles.node!.upload('key', '/path');
|
|
91
|
+
expect(result['storageUri'], 'mock://key');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('multiple method calls use same cached underlying profile', () async {
|
|
95
|
+
var callCount = 0;
|
|
96
|
+
final factory = createNodeStoragePlugin<String>(
|
|
97
|
+
name: 'testStorage',
|
|
98
|
+
supportedProtocol: 'mock://',
|
|
99
|
+
factory: (config) {
|
|
100
|
+
callCount++;
|
|
101
|
+
return _MockNodeProfile();
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
final plugin = factory('cfg');
|
|
105
|
+
// First method call triggers factory
|
|
106
|
+
await plugin.profiles.node!.exists('uri');
|
|
107
|
+
expect(callCount, 1);
|
|
108
|
+
// Second method call reuses the same cached profile
|
|
109
|
+
await plugin.profiles.node!.exists('uri2');
|
|
110
|
+
expect(callCount, 1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
group('createRuntimeStoragePlugin', () {
|
|
115
|
+
test('creates a storage plugin with runtime profile', () {
|
|
116
|
+
final factory = createRuntimeStoragePlugin<String>(
|
|
117
|
+
name: 'testRuntime',
|
|
118
|
+
supportedProtocol: 'mock://',
|
|
119
|
+
factory: (config) => _MockRuntimeProfile(),
|
|
120
|
+
);
|
|
121
|
+
final plugin = factory('cfg');
|
|
122
|
+
expect(plugin.name, 'testRuntime');
|
|
123
|
+
expect(plugin.profiles.hasNode, isFalse);
|
|
124
|
+
expect(plugin.profiles.hasRuntime, isTrue);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('runtime profile delegates correctly', () async {
|
|
128
|
+
final factory = createRuntimeStoragePlugin<String>(
|
|
129
|
+
name: 'testRuntime',
|
|
130
|
+
supportedProtocol: 'mock://',
|
|
131
|
+
factory: (config) => _MockRuntimeProfile(),
|
|
132
|
+
);
|
|
133
|
+
final plugin = factory('cfg');
|
|
134
|
+
final url = await plugin.profiles.runtime!
|
|
135
|
+
.getDownloadUrl('mock://file.zip');
|
|
136
|
+
expect(url['url'], contains('file.zip'));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
group('createUniversalStoragePlugin', () {
|
|
141
|
+
test('creates a storage plugin with both profiles', () {
|
|
142
|
+
final factory = createUniversalStoragePlugin<String>(
|
|
143
|
+
name: 'testUniversal',
|
|
144
|
+
supportedProtocol: 'mock://',
|
|
145
|
+
factory: (config) => (
|
|
146
|
+
node: _MockNodeProfile(),
|
|
147
|
+
runtime: _MockRuntimeProfile(),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
final plugin = factory('cfg');
|
|
151
|
+
expect(plugin.name, 'testUniversal');
|
|
152
|
+
expect(plugin.profiles.hasNode, isTrue);
|
|
153
|
+
expect(plugin.profiles.hasRuntime, isTrue);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('both profiles work independently', () async {
|
|
157
|
+
final factory = createUniversalStoragePlugin<String>(
|
|
158
|
+
name: 'testUniversal',
|
|
159
|
+
supportedProtocol: 'mock://',
|
|
160
|
+
factory: (config) => (
|
|
161
|
+
node: _MockNodeProfile(),
|
|
162
|
+
runtime: _MockRuntimeProfile(),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
final plugin = factory('cfg');
|
|
166
|
+
|
|
167
|
+
final nodeResult = await plugin.profiles.node!.upload('k', '/p');
|
|
168
|
+
expect(nodeResult, isNotEmpty);
|
|
169
|
+
|
|
170
|
+
final runtimeUrl = await plugin.profiles.runtime!
|
|
171
|
+
.getDownloadUrl('mock://file');
|
|
172
|
+
expect(runtimeUrl, isNotEmpty);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('factory is lazy', () async {
|
|
176
|
+
var called = false;
|
|
177
|
+
final factory = createUniversalStoragePlugin<String>(
|
|
178
|
+
name: 'testUniversal',
|
|
179
|
+
supportedProtocol: 'mock://',
|
|
180
|
+
factory: (config) {
|
|
181
|
+
called = true;
|
|
182
|
+
return (node: _MockNodeProfile(), runtime: _MockRuntimeProfile());
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
final plugin = factory('cfg');
|
|
186
|
+
expect(called, isFalse);
|
|
187
|
+
// Calling a method on the node profile triggers the factory
|
|
188
|
+
await plugin.profiles.node!.exists('uri');
|
|
189
|
+
expect(called, isTrue);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import 'package:flutter_patcher_plugin_core/src/generate_min_bundle_id.dart';
|
|
2
|
+
import 'package:flutter_patcher_plugin_core/src/uuidv7.dart';
|
|
3
|
+
import 'package:test/test.dart';
|
|
4
|
+
|
|
5
|
+
void main() {
|
|
6
|
+
group('generateMinBundleId', () {
|
|
7
|
+
test('produces valid UUID v7 format', () {
|
|
8
|
+
final id = generateMinBundleId();
|
|
9
|
+
expect(id, matches(RegExp(
|
|
10
|
+
r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
|
11
|
+
)));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('has zeroed random bits', () {
|
|
15
|
+
final id = generateMinBundleId();
|
|
16
|
+
final parts = id.split('-');
|
|
17
|
+
// version+rand_a should be 7000 (version 7, rand_a=0)
|
|
18
|
+
expect(parts[2], '7000');
|
|
19
|
+
// variant+rand_b starts with 8, rest is 0
|
|
20
|
+
expect(parts[3], '8000');
|
|
21
|
+
// node is all zeros
|
|
22
|
+
expect(parts[4], '000000000000');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('timestamp is close to current time', () {
|
|
26
|
+
final before = DateTime.now().millisecondsSinceEpoch;
|
|
27
|
+
final id = generateMinBundleId();
|
|
28
|
+
final after = DateTime.now().millisecondsSinceEpoch;
|
|
29
|
+
final ts = extractTimestampFromUUIDv7(id);
|
|
30
|
+
expect(ts, greaterThanOrEqualTo(before));
|
|
31
|
+
expect(ts, lessThanOrEqualTo(after));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('sorts before a normal UUIDv7 at the same millisecond', () {
|
|
35
|
+
// Create a min bundle ID
|
|
36
|
+
final minId = generateMinBundleId();
|
|
37
|
+
// Create a normal UUIDv7 (with random bits)
|
|
38
|
+
final normalId = createUUIDv7();
|
|
39
|
+
// Both are at approximately the same time, but min should be <= normal
|
|
40
|
+
// when timestamps are close
|
|
41
|
+
final minTs = extractTimestampFromUUIDv7(minId);
|
|
42
|
+
final normalTs = extractTimestampFromUUIDv7(normalId);
|
|
43
|
+
// If timestamps are the same, min should sort first
|
|
44
|
+
if (minTs == normalTs) {
|
|
45
|
+
expect(minId.compareTo(normalId), lessThan(0));
|
|
46
|
+
}
|
|
47
|
+
// Otherwise just verify the format is valid
|
|
48
|
+
expect(minId.length, 36);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|