@_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,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
|
+
}
|