@_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.
Files changed (61) 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 +6 -5
  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/operations.dart +45 -0
  15. package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
  16. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
  17. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  18. package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
  19. package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
  20. package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
  21. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
  22. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
  23. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
  24. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
  25. package/dart-src/packages/core/test/rollout_test.dart +128 -0
  26. package/dart-src/packages/core/test/semver_test.dart +52 -0
  27. package/dart-src/packages/core/test/semver_vectors.json +1 -0
  28. package/dart-src/packages/core/test/types_test.dart +129 -0
  29. package/dart-src/packages/core/test/uuid_test.dart +38 -0
  30. package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
  31. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
  32. package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
  33. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
  34. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
  35. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
  36. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
  37. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
  38. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
  39. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
  40. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
  41. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
  42. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
  43. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
  44. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
  45. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
  46. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
  47. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
  48. package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
  49. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
  50. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
  51. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
  52. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
  53. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
  54. package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
  55. package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
  56. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
  57. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
  58. package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
  59. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
  60. package/package.json +1 -1
  61. package/scripts/vendor.sh +29 -0
@@ -0,0 +1,207 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:flutter_patcher_aws/flutter_patcher_aws.dart';
4
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
+ import 'package:flutter_patcher_cloudflare/flutter_patcher_cloudflare.dart';
6
+ import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
7
+ import 'package:test/test.dart';
8
+
9
+ import 'mocks/mock_aws_s3_client.dart' as aws;
10
+ import 'mocks/mock_d1_client.dart' as d1;
11
+ import 'mocks/mock_postgres_client.dart' as pg;
12
+
13
+ // --- R2 (cloudflare storage) mock ------------------------------------------
14
+
15
+ class MockR2Client implements R2S3ClientLike {
16
+ MockR2Client(this.store);
17
+ final aws.Store store;
18
+
19
+ @override
20
+ Future<void> deleteObject(String key) async => store.objects.remove(key);
21
+ @override
22
+ Future<void> deleteObjects(List<String> keys) async =>
23
+ keys.forEach(store.objects.remove);
24
+ @override
25
+ Future<bool> headObject(String key) async => store.objects.containsKey(key);
26
+ @override
27
+ Future<List<int>> getObjectAsBytes(String key) async =>
28
+ store.objects[key] ?? (throw Exception('NoSuchKey: $key'));
29
+ @override
30
+ Future<String> getObjectAsString(String key) async =>
31
+ String.fromCharCodes(await getObjectAsBytes(key));
32
+ @override
33
+ Future<String> getSignedUrl(String key, {int expiresIn = 3600}) async =>
34
+ 'https://r2.mock/$key?sig=mock&expires=$expiresIn';
35
+ @override
36
+ Future<List<StorageObject>> listObjects([String? prefix]) async {
37
+ final p = prefix ?? '';
38
+ return store.objects.keys
39
+ .where((k) => k.startsWith(p))
40
+ .map((k) => StorageObject(
41
+ key: k,
42
+ storageUri: 'r2://$k',
43
+ size: store.objects[k]!.length,
44
+ ))
45
+ .toList();
46
+ }
47
+
48
+ @override
49
+ Future<void> putObject(String key, List<int> body, String contentType) async =>
50
+ store.objects[key] = body;
51
+ }
52
+
53
+ // --- CloudFront (aws db invalidation) mock ----------------------------------
54
+
55
+ class MockCloudFrontClient implements AwsCloudFrontClientLike {
56
+ @override
57
+ Future<void> createInvalidation(
58
+ String distributionId,
59
+ List<String> paths, {
60
+ bool shouldWait = false,
61
+ }) async {
62
+ // No-op for tests.
63
+ }
64
+ }
65
+
66
+ // --- helpers ----------------------------------------------------------------
67
+
68
+ Directory makeSource() {
69
+ final dir = Directory.systemTemp.createTempSync('fp-src-');
70
+ File('${dir.path}/app.so').writeAsStringSync('fake-libapp-bytes');
71
+ return dir;
72
+ }
73
+
74
+ FlutterPatcherConfig cfgFor(String provider) => FlutterPatcherConfig(
75
+ provider: provider,
76
+ supabase: const SupabaseConfigJson(url: 'https://x.supabase.co'),
77
+ postgres: const PostgresConfigJson(host: 'mock', database: 'mock'),
78
+ cloudflare: const CloudflareConfigJson(
79
+ accountId: 'acct',
80
+ d1DatabaseId: 'dbid',
81
+ apiToken: 'token',
82
+ r2Bucket: 'bundles',
83
+ r2AccessKeyId: 'rk',
84
+ r2SecretAccessKey: 'rs',
85
+ ),
86
+ aws: const AwsConfigJson(
87
+ bucket: 'b',
88
+ region: 'us-east-1',
89
+ accessKeyId: 'k',
90
+ secretAccessKey: 's',
91
+ ),
92
+ channel: 'production',
93
+ platform: 'android',
94
+ source: './dist',
95
+ );
96
+
97
+ Future<void> runBackendLifecycle(String name, Backend backend) async {
98
+ final source = makeSource();
99
+ try {
100
+ final bundle = await deployBundle(
101
+ backend,
102
+ DeployOptions(
103
+ source: source.path,
104
+ channel: 'production',
105
+ platform: 'android',
106
+ message: 'first',
107
+ targetAppVersion: '>=1.0.0',
108
+ ),
109
+ );
110
+ expect(bundle.id, isNotEmpty, reason: '$name: deployed id');
111
+ expect(bundle.storageUri, isNotEmpty, reason: '$name: storageUri set');
112
+
113
+ // The artifact bytes were actually uploaded to storage.
114
+ expect(await backend.storage.exists(bundle.storageUri), isTrue,
115
+ reason: '$name: storage has bundle');
116
+
117
+ // Node profile can download the bytes back.
118
+ final dl = File('${Directory.systemTemp.path}/fp-dl-${bundle.id}.zip');
119
+ await backend.storage.downloadFile(bundle.storageUri, dl.path);
120
+ expect(dl.existsSync() && await dl.length() > 0, isTrue,
121
+ reason: '$name: download succeeds');
122
+ await dl.delete();
123
+
124
+ // listBundles round-trips.
125
+ final listed = await listBundles(backend, const ListOptions());
126
+ final found = listed.data.where((b) => b.id == bundle.id).toList();
127
+ expect(found, hasLength(1), reason: '$name: bundle listed');
128
+
129
+ // getChannel resolves the live (enabled) bundle.
130
+ final live = await getChannel(backend, 'production');
131
+ expect(live?.id, bundle.id, reason: '$name: channel live');
132
+
133
+ // promote a second bundle, then rollback disables the latest.
134
+ final second = await deployBundle(
135
+ backend,
136
+ DeployOptions(
137
+ source: source.path,
138
+ channel: 'production',
139
+ platform: 'android',
140
+ message: 'second',
141
+ targetAppVersion: '>=1.0.0',
142
+ ),
143
+ );
144
+ await promoteBundle(backend, second.id, 'production');
145
+ final disabled = await rollbackChannel(backend, 'production');
146
+ expect(disabled, second.id, reason: '$name: rollback disabled latest');
147
+
148
+ // delete removes the bundle.
149
+ await deleteBundle(backend, bundle.id);
150
+ final afterDelete = await listBundles(
151
+ backend,
152
+ ListOptions(channel: 'production'),
153
+ );
154
+ expect(
155
+ afterDelete.data.where((b) => b.id == bundle.id).isEmpty,
156
+ isTrue,
157
+ reason: '$name: bundle deleted',
158
+ );
159
+ } finally {
160
+ source.deleteSync(recursive: true);
161
+ }
162
+ }
163
+
164
+ // --- tests ------------------------------------------------------------------
165
+
166
+ void main() {
167
+ group('cli backends', () {
168
+ test('postgres backend (postgresDatabase + postgresStorage)', () async {
169
+ final store = pg.Store();
170
+ final backend = resolveBackend(
171
+ cfgFor('postgres'),
172
+ postgresClientFactory: (_) => pg.MockPostgresClient(store),
173
+ );
174
+ await runBackendLifecycle('postgres', backend);
175
+ });
176
+
177
+ test('cloudflare backend (d1Database + r2Storage)', () async {
178
+ final dbStore = d1.Store();
179
+ final r2Store = aws.Store();
180
+ final backend = resolveBackend(
181
+ cfgFor('cloudflare'),
182
+ d1ClientFactory: (_) => d1.MockD1Client(dbStore),
183
+ r2ClientFactory: (_) => MockR2Client(r2Store),
184
+ );
185
+ await runBackendLifecycle('cloudflare', backend);
186
+ });
187
+
188
+ test('aws backend (s3Database + s3Storage)', () async {
189
+ final dbStore = aws.Store();
190
+ final storageStore = aws.Store();
191
+ final backend = resolveBackend(
192
+ cfgFor('aws'),
193
+ awsS3ClientFactory: (_) => aws.MockAwsS3Client(dbStore),
194
+ awsCloudFrontClientFactory: (_) => MockCloudFrontClient(),
195
+ awsStorageClientFactory: (_) => aws.MockAwsS3Client(storageStore),
196
+ );
197
+ await runBackendLifecycle('aws', backend);
198
+ });
199
+
200
+ test('resolveBackend rejects unknown provider', () {
201
+ expect(
202
+ () => resolveBackend(cfgFor('bogus')),
203
+ throwsA(isA<UnsupportedError>()),
204
+ );
205
+ });
206
+ });
207
+ }
@@ -0,0 +1,101 @@
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+ import 'dart:typed_data';
4
+
5
+ import 'package:archive/archive.dart';
6
+ import 'package:crypto/crypto.dart';
7
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
8
+ import 'package:test/test.dart';
9
+
10
+ // Builds an in-memory APK containing libapp.so for every supported ABI, then
11
+ // verifies `packPatch` emits a single device-ready patch.zip that includes all
12
+ // ABIs (so one bundle installs on any device).
13
+ Future<Uint8List> _buildFakeApk() async {
14
+ final apk = Archive();
15
+ for (final abi in const ['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
16
+ final content = utf8.encode('LIBAPP_$abi');
17
+ apk.addFile(ArchiveFile('lib/$abi/libapp.so', content.length, content));
18
+ }
19
+ final bytes = ZipEncoder().encode(apk);
20
+ return Uint8List.fromList(bytes);
21
+ }
22
+
23
+ void main() {
24
+ test('packPatch includes every ABI found in the APK', () async {
25
+ final apkBytes = await _buildFakeApk();
26
+ final dir = Directory.systemTemp.createTempSync('fp-pack-');
27
+ final apkFile = File('${dir.path}/app.apk')..writeAsBytesSync(apkBytes);
28
+ final outDir = '${dir.path}/dist';
29
+ try {
30
+ final result = await packPatch(
31
+ apkPath: apkFile.path,
32
+ version: '1.2.3',
33
+ targetVersionCode: 7,
34
+ out: outDir,
35
+ );
36
+
37
+ expect(result.abis, hasLength(3));
38
+ expect(result.abis, containsAll(['arm64-v8a', 'armeabi-v7a', 'x86_64']));
39
+
40
+ final patchBytes = File(result.payloadPath).readAsBytesSync();
41
+ final patch = ZipDecoder().decodeBytes(patchBytes);
42
+
43
+ final names = patch.files.map((f) => f.name).toList();
44
+ expect(names, contains('manifest.json'));
45
+ expect(names, contains('lib/arm64-v8a/libapp.so'));
46
+ expect(names, contains('lib/armeabi-v7a/libapp.so'));
47
+ expect(names, contains('lib/x86_64/libapp.so'));
48
+
49
+ final manifestFile =
50
+ patch.files.firstWhere((f) => f.name == 'manifest.json');
51
+ final manifest = Map<String, dynamic>.from(
52
+ jsonDecode(utf8.decode(manifestFile.content as List<int>))
53
+ as Map,
54
+ );
55
+ expect(manifest['schemaVersion'], 2);
56
+ expect(manifest['version'], '1.2.3');
57
+ expect(manifest['targetVersionCode'], 7);
58
+
59
+ final lib = Map<String, dynamic>.from(manifest['lib'] as Map);
60
+ expect(lib.keys, hasLength(3));
61
+ for (final abi in ['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
62
+ final entry = Map<String, dynamic>.from(lib[abi] as Map);
63
+ expect(entry['path'], 'lib/$abi/libapp.so');
64
+ final expectedMd5 = md5.convert(utf8.encode('LIBAPP_$abi')).toString();
65
+ expect(entry['md5'], expectedMd5);
66
+ }
67
+
68
+ // outer manifest records all abis too
69
+ final outer = Map<String, dynamic>.from(
70
+ jsonDecode(File(result.manifestPath).readAsStringSync()) as Map,
71
+ );
72
+ expect(outer['payload'], 'patch.zip');
73
+ expect(
74
+ (outer['abis'] as List).cast<String>(),
75
+ containsAll(['arm64-v8a', 'armeabi-v7a', 'x86_64']),
76
+ );
77
+ } finally {
78
+ dir.deleteSync(recursive: true);
79
+ }
80
+ });
81
+
82
+ test('packPatch throws PackException when libapp.so is missing', () async {
83
+ final apk = Archive()..addFile(ArchiveFile('classes.dex', 0, <int>[]));
84
+ final dir = Directory.systemTemp.createTempSync('fp-pack-bad-');
85
+ final apkFile = File('${dir.path}/bad.apk')
86
+ ..writeAsBytesSync(Uint8List.fromList(ZipEncoder().encode(apk)));
87
+ try {
88
+ expect(
89
+ () => packPatch(
90
+ apkPath: apkFile.path,
91
+ version: '1.0.0',
92
+ targetVersionCode: 1,
93
+ out: '${dir.path}/dist',
94
+ ),
95
+ throwsA(isA<PackException>()),
96
+ );
97
+ } finally {
98
+ dir.deleteSync(recursive: true);
99
+ }
100
+ });
101
+ }