@_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,122 @@
|
|
|
1
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
2
|
+
show
|
|
3
|
+
AppVersionGetBundlesArgs,
|
|
4
|
+
Bundle,
|
|
5
|
+
BundlePatchArtifact,
|
|
6
|
+
FingerprintGetBundlesArgs,
|
|
7
|
+
Platform,
|
|
8
|
+
UpdateStatus,
|
|
9
|
+
nilUuid;
|
|
10
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
11
|
+
show DatabasePlugin;
|
|
12
|
+
import 'package:test/test.dart';
|
|
13
|
+
|
|
14
|
+
import 'mock_d1_client.dart' show Store, newWorkerPlugin;
|
|
15
|
+
|
|
16
|
+
Bundle _makeBundle({
|
|
17
|
+
required String id,
|
|
18
|
+
String channel = 'production',
|
|
19
|
+
bool enabled = true,
|
|
20
|
+
String? fingerprintHash,
|
|
21
|
+
String? targetAppVersion,
|
|
22
|
+
String? message,
|
|
23
|
+
List<BundlePatchArtifact>? patches,
|
|
24
|
+
}) =>
|
|
25
|
+
Bundle(
|
|
26
|
+
id: id,
|
|
27
|
+
channel: channel,
|
|
28
|
+
enabled: enabled,
|
|
29
|
+
shouldForceUpdate: false,
|
|
30
|
+
fileHash: 'h-$id',
|
|
31
|
+
platform: Platform.android,
|
|
32
|
+
targetAppVersion: targetAppVersion,
|
|
33
|
+
storageUri: 's3://$id',
|
|
34
|
+
fingerprintHash: fingerprintHash,
|
|
35
|
+
message: message,
|
|
36
|
+
patches: patches,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
void main() {
|
|
40
|
+
group('cloudflare d1WorkerDatabase', () {
|
|
41
|
+
late Store store;
|
|
42
|
+
late DatabasePlugin plugin;
|
|
43
|
+
|
|
44
|
+
setUp(() {
|
|
45
|
+
store = Store();
|
|
46
|
+
plugin = newWorkerPlugin(store);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('getChannels returns distinct channels', () async {
|
|
50
|
+
await plugin.appendBundle(_makeBundle(id: 'a', channel: 'production'));
|
|
51
|
+
await plugin.appendBundle(_makeBundle(id: 'b', channel: 'staging'));
|
|
52
|
+
await plugin.appendBundle(_makeBundle(id: 'c', channel: 'production'));
|
|
53
|
+
await plugin.commitBundle();
|
|
54
|
+
|
|
55
|
+
final channels = await plugin.getChannels();
|
|
56
|
+
expect(channels, hasLength(2));
|
|
57
|
+
expect(channels, containsAll(['production', 'staging']));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('getBundleById returns null for missing, bundle for existing',
|
|
61
|
+
() async {
|
|
62
|
+
await plugin.appendBundle(_makeBundle(id: 'b1'));
|
|
63
|
+
await plugin.commitBundle();
|
|
64
|
+
|
|
65
|
+
expect(await plugin.getBundleById('missing'), isNull);
|
|
66
|
+
final got = await plugin.getBundleById('b1');
|
|
67
|
+
expect(got, isNotNull);
|
|
68
|
+
expect(got!.id, 'b1');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('commitBundle append -> update -> delete reflects in store',
|
|
72
|
+
() async {
|
|
73
|
+
await plugin.appendBundle(_makeBundle(id: 'b1', message: 'original'));
|
|
74
|
+
await plugin.commitBundle();
|
|
75
|
+
expect((await plugin.getBundleById('b1'))!.message, 'original');
|
|
76
|
+
|
|
77
|
+
var got = await plugin.getBundleById('b1');
|
|
78
|
+
await plugin.updateBundle(got!.id, {'message': 'updated'});
|
|
79
|
+
await plugin.commitBundle();
|
|
80
|
+
expect((await plugin.getBundleById('b1'))!.message, 'updated');
|
|
81
|
+
|
|
82
|
+
got = await plugin.getBundleById('b1');
|
|
83
|
+
await plugin.deleteBundle(got!);
|
|
84
|
+
await plugin.commitBundle();
|
|
85
|
+
expect(await plugin.getBundleById('b1'), isNull);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('getUpdateInfo (appVersion) returns UpdateInfo for compatible bundle',
|
|
89
|
+
() async {
|
|
90
|
+
await plugin.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
|
|
91
|
+
await plugin.commitBundle();
|
|
92
|
+
|
|
93
|
+
final info = await plugin.getUpdateInfo(
|
|
94
|
+
AppVersionGetBundlesArgs(
|
|
95
|
+
platform: Platform.android,
|
|
96
|
+
bundleId: nilUuid,
|
|
97
|
+
appVersion: '1.0.0',
|
|
98
|
+
channel: 'production',
|
|
99
|
+
minBundleId: nilUuid,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
expect(info, isNotNull);
|
|
103
|
+
expect(info!.id, 'b1');
|
|
104
|
+
expect(info.status, UpdateStatus.update);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('getUpdateInfo (fingerprint) returns matching bundle', () async {
|
|
108
|
+
await plugin.appendBundle(_makeBundle(id: 'fp', fingerprintHash: 'fp1'));
|
|
109
|
+
await plugin.commitBundle();
|
|
110
|
+
|
|
111
|
+
final info = await plugin.getUpdateInfo(
|
|
112
|
+
FingerprintGetBundlesArgs(
|
|
113
|
+
platform: Platform.android,
|
|
114
|
+
bundleId: nilUuid,
|
|
115
|
+
fingerprintHash: 'fp1',
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
expect(info, isNotNull);
|
|
119
|
+
expect(info!.id, 'fp');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import 'dart:convert' show jsonEncode;
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
4
|
+
show Bundle, BundleMetadata, BundlePatchArtifact, Platform;
|
|
5
|
+
import 'package:flutter_patcher_cloudflare/flutter_patcher_cloudflare.dart'
|
|
6
|
+
show
|
|
7
|
+
buildBundlePatchId,
|
|
8
|
+
bundleToPatchRows,
|
|
9
|
+
parseMetadata,
|
|
10
|
+
transformRowToBundle;
|
|
11
|
+
import 'package:test/test.dart';
|
|
12
|
+
|
|
13
|
+
void main() {
|
|
14
|
+
group('d1 bundle mapper', () {
|
|
15
|
+
test('transformRowToBundle rebuilds derived patch fields', () {
|
|
16
|
+
final row = {
|
|
17
|
+
'id': 'b1',
|
|
18
|
+
'channel': 'production',
|
|
19
|
+
'enabled': 1,
|
|
20
|
+
'should_force_update': 0,
|
|
21
|
+
'file_hash': 'h1',
|
|
22
|
+
'git_commit_hash': null,
|
|
23
|
+
'message': 'msg',
|
|
24
|
+
'platform': 'android',
|
|
25
|
+
'target_app_version': '1.0.0',
|
|
26
|
+
'storage_uri': 's3://b1',
|
|
27
|
+
'fingerprint_hash': null,
|
|
28
|
+
'metadata': jsonEncode({'app_version': '1.2.3'}),
|
|
29
|
+
'manifest_storage_uri': null,
|
|
30
|
+
'manifest_file_hash': null,
|
|
31
|
+
'asset_base_storage_uri': null,
|
|
32
|
+
'rollout_cohort_count': 100,
|
|
33
|
+
'target_cohorts': jsonEncode(['c1', 'c2']),
|
|
34
|
+
};
|
|
35
|
+
final patchRows = [
|
|
36
|
+
{
|
|
37
|
+
'id': 'b1:base1',
|
|
38
|
+
'bundle_id': 'b1',
|
|
39
|
+
'base_bundle_id': 'base1',
|
|
40
|
+
'base_file_hash': 'bf1',
|
|
41
|
+
'patch_file_hash': 'pf1',
|
|
42
|
+
'patch_storage_uri': 's3://p1',
|
|
43
|
+
'order_index': 0,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
final bundle = transformRowToBundle(row, patchRows);
|
|
48
|
+
|
|
49
|
+
expect(bundle.id, 'b1');
|
|
50
|
+
expect(bundle.enabled, isTrue);
|
|
51
|
+
expect(bundle.shouldForceUpdate, isFalse);
|
|
52
|
+
expect(bundle.platform, Platform.android);
|
|
53
|
+
expect(bundle.targetAppVersion, '1.0.0');
|
|
54
|
+
expect(bundle.targetCohorts, ['c1', 'c2']);
|
|
55
|
+
expect(bundle.patches, hasLength(1));
|
|
56
|
+
expect(bundle.patches!.first.baseBundleId, 'base1');
|
|
57
|
+
expect(bundle.patchFileHashLegacy, 'pf1');
|
|
58
|
+
expect(bundle.patchStorageUri, 's3://p1');
|
|
59
|
+
expect(bundle.rolloutCohortCount, 100);
|
|
60
|
+
expect(bundle.metadata, isA<BundleMetadata>());
|
|
61
|
+
expect(bundle.metadata!.appVersion, '1.2.3');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('bundleToPatchRows builds compound ids in order', () {
|
|
65
|
+
final bundle = Bundle(
|
|
66
|
+
id: 'b1',
|
|
67
|
+
channel: 'production',
|
|
68
|
+
enabled: true,
|
|
69
|
+
shouldForceUpdate: false,
|
|
70
|
+
fileHash: 'h1',
|
|
71
|
+
platform: Platform.android,
|
|
72
|
+
targetAppVersion: '1.0.0',
|
|
73
|
+
storageUri: 's3://b1',
|
|
74
|
+
patches: [
|
|
75
|
+
BundlePatchArtifact(
|
|
76
|
+
baseBundleId: 'base1',
|
|
77
|
+
baseFileHash: 'bf1',
|
|
78
|
+
patchFileHash: 'pf1',
|
|
79
|
+
patchStorageUri: 's3://p1',
|
|
80
|
+
),
|
|
81
|
+
BundlePatchArtifact(
|
|
82
|
+
baseBundleId: 'base2',
|
|
83
|
+
baseFileHash: 'bf2',
|
|
84
|
+
patchFileHash: 'pf2',
|
|
85
|
+
patchStorageUri: 's3://p2',
|
|
86
|
+
),
|
|
87
|
+
],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
final rows = bundleToPatchRows(bundle);
|
|
91
|
+
|
|
92
|
+
expect(rows, hasLength(2));
|
|
93
|
+
expect(rows[0]['id'], buildBundlePatchId('b1', 'base1'));
|
|
94
|
+
expect(rows[0]['order_index'], 0);
|
|
95
|
+
expect(rows[1]['id'], buildBundlePatchId('b1', 'base2'));
|
|
96
|
+
expect(rows[1]['order_index'], 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('parseMetadata handles string, object and null', () {
|
|
100
|
+
expect(parseMetadata(null), isNull);
|
|
101
|
+
expect(parseMetadata(jsonEncode({'a': 1})), {'a': 1});
|
|
102
|
+
expect(parseMetadata({'a': 1}), {'a': 1});
|
|
103
|
+
expect(parseMetadata('not-json'), isNull);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart'
|
|
2
|
+
show
|
|
3
|
+
AppVersionGetBundlesArgs,
|
|
4
|
+
Bundle,
|
|
5
|
+
BundlePatchArtifact,
|
|
6
|
+
FingerprintGetBundlesArgs,
|
|
7
|
+
Platform,
|
|
8
|
+
UpdateStatus,
|
|
9
|
+
nilUuid;
|
|
10
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
11
|
+
show
|
|
12
|
+
DatabaseBundleQueryOptions,
|
|
13
|
+
DatabaseBundleQueryOrder,
|
|
14
|
+
DatabaseBundleQueryWhere,
|
|
15
|
+
DatabasePlugin;
|
|
16
|
+
import 'package:test/test.dart';
|
|
17
|
+
|
|
18
|
+
import 'mock_d1_client.dart' show Store, newPlugin;
|
|
19
|
+
|
|
20
|
+
Bundle _makeBundle({
|
|
21
|
+
required String id,
|
|
22
|
+
String channel = 'production',
|
|
23
|
+
bool enabled = true,
|
|
24
|
+
String? fingerprintHash,
|
|
25
|
+
String? targetAppVersion,
|
|
26
|
+
String? message,
|
|
27
|
+
List<BundlePatchArtifact>? patches,
|
|
28
|
+
}) =>
|
|
29
|
+
Bundle(
|
|
30
|
+
id: id,
|
|
31
|
+
channel: channel,
|
|
32
|
+
enabled: enabled,
|
|
33
|
+
shouldForceUpdate: false,
|
|
34
|
+
fileHash: 'h-$id',
|
|
35
|
+
platform: Platform.android,
|
|
36
|
+
targetAppVersion: targetAppVersion,
|
|
37
|
+
storageUri: 's3://$id',
|
|
38
|
+
fingerprintHash: fingerprintHash,
|
|
39
|
+
message: message,
|
|
40
|
+
patches: patches,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
void main() {
|
|
44
|
+
group('cloudflare d1Database', () {
|
|
45
|
+
late Store store;
|
|
46
|
+
late DatabasePlugin plugin;
|
|
47
|
+
|
|
48
|
+
setUp(() {
|
|
49
|
+
store = Store();
|
|
50
|
+
plugin = newPlugin(store);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('getChannels returns distinct channels', () async {
|
|
54
|
+
await plugin.appendBundle(_makeBundle(id: 'a', channel: 'production'));
|
|
55
|
+
await plugin.appendBundle(_makeBundle(id: 'b', channel: 'staging'));
|
|
56
|
+
await plugin.appendBundle(_makeBundle(id: 'c', channel: 'production'));
|
|
57
|
+
await plugin.commitBundle();
|
|
58
|
+
|
|
59
|
+
final channels = await plugin.getChannels();
|
|
60
|
+
expect(channels, hasLength(2));
|
|
61
|
+
expect(channels, containsAll(['production', 'staging']));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('getBundles returns bundles and pagination; filters by channel',
|
|
65
|
+
() async {
|
|
66
|
+
for (final id in ['b1', 'b2', 'b3']) {
|
|
67
|
+
await plugin.appendBundle(_makeBundle(id: id));
|
|
68
|
+
}
|
|
69
|
+
await plugin.commitBundle();
|
|
70
|
+
|
|
71
|
+
final res = await plugin.getBundles(
|
|
72
|
+
DatabaseBundleQueryOptions(
|
|
73
|
+
where: const DatabaseBundleQueryWhere(),
|
|
74
|
+
limit: 2,
|
|
75
|
+
offset: 0,
|
|
76
|
+
orderBy: const DatabaseBundleQueryOrder(direction: 'desc'),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
expect(res.data, hasLength(2));
|
|
80
|
+
expect(res.pagination.total, 3);
|
|
81
|
+
expect(res.pagination.hasNextPage, isTrue);
|
|
82
|
+
|
|
83
|
+
final staging = await plugin.getBundles(
|
|
84
|
+
DatabaseBundleQueryOptions(
|
|
85
|
+
where: const DatabaseBundleQueryWhere(channel: 'staging'),
|
|
86
|
+
limit: 10,
|
|
87
|
+
offset: 0,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
expect(staging.data, isEmpty);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('getBundleById returns null for missing, bundle for existing',
|
|
94
|
+
() async {
|
|
95
|
+
await plugin.appendBundle(_makeBundle(id: 'b1'));
|
|
96
|
+
await plugin.commitBundle();
|
|
97
|
+
|
|
98
|
+
expect(await plugin.getBundleById('missing'), isNull);
|
|
99
|
+
final got = await plugin.getBundleById('b1');
|
|
100
|
+
expect(got, isNotNull);
|
|
101
|
+
expect(got!.id, 'b1');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('commitBundle append -> update -> delete reflects in store',
|
|
105
|
+
() async {
|
|
106
|
+
await plugin.appendBundle(_makeBundle(id: 'b1', message: 'original'));
|
|
107
|
+
await plugin.commitBundle();
|
|
108
|
+
expect((await plugin.getBundleById('b1'))!.message, 'original');
|
|
109
|
+
|
|
110
|
+
var got = await plugin.getBundleById('b1');
|
|
111
|
+
await plugin.updateBundle(got!.id, {'message': 'updated'});
|
|
112
|
+
await plugin.commitBundle();
|
|
113
|
+
expect((await plugin.getBundleById('b1'))!.message, 'updated');
|
|
114
|
+
|
|
115
|
+
got = await plugin.getBundleById('b1');
|
|
116
|
+
await plugin.deleteBundle(got!);
|
|
117
|
+
await plugin.commitBundle();
|
|
118
|
+
expect(await plugin.getBundleById('b1'), isNull);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('getUpdateInfo (appVersion) returns UpdateInfo for compatible bundle',
|
|
122
|
+
() async {
|
|
123
|
+
await plugin.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
|
|
124
|
+
await plugin.commitBundle();
|
|
125
|
+
|
|
126
|
+
final info = await plugin.getUpdateInfo(
|
|
127
|
+
AppVersionGetBundlesArgs(
|
|
128
|
+
platform: Platform.android,
|
|
129
|
+
bundleId: nilUuid,
|
|
130
|
+
appVersion: '1.0.0',
|
|
131
|
+
channel: 'production',
|
|
132
|
+
minBundleId: nilUuid,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(info, isNotNull);
|
|
137
|
+
expect(info!.id, 'b1');
|
|
138
|
+
expect(info.status, UpdateStatus.update);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('getUpdateInfo (appVersion) returns null when no compatible bundle',
|
|
142
|
+
() async {
|
|
143
|
+
await plugin.appendBundle(_makeBundle(id: 'b1', targetAppVersion: '1.0.0'));
|
|
144
|
+
await plugin.commitBundle();
|
|
145
|
+
|
|
146
|
+
final info = await plugin.getUpdateInfo(
|
|
147
|
+
AppVersionGetBundlesArgs(
|
|
148
|
+
platform: Platform.android,
|
|
149
|
+
bundleId: nilUuid,
|
|
150
|
+
appVersion: '9.9.9',
|
|
151
|
+
channel: 'production',
|
|
152
|
+
minBundleId: nilUuid,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(info, isNull);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('getUpdateInfo (fingerprint) returns matching bundle', () async {
|
|
160
|
+
await plugin.appendBundle(_makeBundle(id: 'fp', fingerprintHash: 'fp1'));
|
|
161
|
+
await plugin.commitBundle();
|
|
162
|
+
|
|
163
|
+
final info = await plugin.getUpdateInfo(
|
|
164
|
+
FingerprintGetBundlesArgs(
|
|
165
|
+
platform: Platform.android,
|
|
166
|
+
bundleId: nilUuid,
|
|
167
|
+
fingerprintHash: 'fp1',
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(info, isNotNull);
|
|
172
|
+
expect(info!.id, 'fp');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('getUpdateInfo (fingerprint) returns null on mismatch', () async {
|
|
176
|
+
await plugin.appendBundle(_makeBundle(id: 'fp', fingerprintHash: 'fp1'));
|
|
177
|
+
await plugin.commitBundle();
|
|
178
|
+
|
|
179
|
+
final info = await plugin.getUpdateInfo(
|
|
180
|
+
FingerprintGetBundlesArgs(
|
|
181
|
+
platform: Platform.android,
|
|
182
|
+
bundleId: nilUuid,
|
|
183
|
+
fingerprintHash: 'nope',
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(info, isNull);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import 'dart:convert' show jsonDecode;
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_patcher_cloudflare/flutter_patcher_cloudflare.dart'
|
|
4
|
+
show
|
|
5
|
+
CloudflareWorkerDatabaseConfig,
|
|
6
|
+
D1ClientLike,
|
|
7
|
+
D1DatabaseConfig,
|
|
8
|
+
cloudflareWorkerDatabase,
|
|
9
|
+
d1Database;
|
|
10
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart'
|
|
11
|
+
show DatabasePlugin;
|
|
12
|
+
|
|
13
|
+
/// In-memory store backing [MockD1Client], mirroring the D1 tables.
|
|
14
|
+
class Store {
|
|
15
|
+
final List<Map<String, dynamic>> bundleRows = [];
|
|
16
|
+
final List<Map<String, dynamic>> patchRows = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Mock D1 client that emulates the SQL issued by `_D1DatabasePlugin`.
|
|
20
|
+
class MockD1Client implements D1ClientLike {
|
|
21
|
+
MockD1Client(this.store);
|
|
22
|
+
|
|
23
|
+
final Store store;
|
|
24
|
+
|
|
25
|
+
bool _asBool(dynamic v) => v is bool ? v : (v as num? ?? 0) != 0;
|
|
26
|
+
|
|
27
|
+
List<String> _asList(dynamic v) {
|
|
28
|
+
final decoded = jsonDecode(v as String) as List;
|
|
29
|
+
return decoded.cast<String>();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bool _match(Map<String, dynamic> row, String where, List<dynamic> params) {
|
|
33
|
+
var p = 0;
|
|
34
|
+
if (where.contains('channel = ?')) {
|
|
35
|
+
if (row['channel'] != params[p++]) return false;
|
|
36
|
+
}
|
|
37
|
+
if (where.contains('platform = ?')) {
|
|
38
|
+
if (row['platform'] != params[p++]) return false;
|
|
39
|
+
}
|
|
40
|
+
if (where.contains('enabled = ?')) {
|
|
41
|
+
if ((_asBool(row['enabled']) ? 1 : 0) != params[p++]) return false;
|
|
42
|
+
}
|
|
43
|
+
if (where.contains('id IN (SELECT value FROM json_each(?))')) {
|
|
44
|
+
if (!_asList(params[p++]).contains(row['id'])) return false;
|
|
45
|
+
}
|
|
46
|
+
if (where.contains('id = ?')) {
|
|
47
|
+
if (row['id'] != params[p++]) return false;
|
|
48
|
+
}
|
|
49
|
+
if (where.contains('id > ?')) {
|
|
50
|
+
if ((row['id'] as String).compareTo(params[p++] as String) <= 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (where.contains('id >= ?')) {
|
|
55
|
+
if ((row['id'] as String).compareTo(params[p++] as String) < 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (where.contains('id < ?')) {
|
|
60
|
+
if ((row['id'] as String).compareTo(params[p++] as String) >= 0) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (where.contains('id <= ?')) {
|
|
65
|
+
if ((row['id'] as String).compareTo(params[p++] as String) > 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (where.contains('target_app_version IS NOT NULL') &&
|
|
70
|
+
!where.contains('target_app_version =')) {
|
|
71
|
+
if (row['target_app_version'] == null) return false;
|
|
72
|
+
}
|
|
73
|
+
if (where.contains('target_app_version IS NULL')) {
|
|
74
|
+
if (row['target_app_version'] != null) return false;
|
|
75
|
+
}
|
|
76
|
+
if (where.contains('target_app_version = ?')) {
|
|
77
|
+
if (row['target_app_version'] != params[p++]) return false;
|
|
78
|
+
}
|
|
79
|
+
if (where.contains('target_app_version IN (SELECT value FROM json_each(?))')) {
|
|
80
|
+
if (!_asList(params[p++]).contains(row['target_app_version'])) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (where.contains('fingerprint_hash IS NULL')) {
|
|
85
|
+
if (row['fingerprint_hash'] != null) return false;
|
|
86
|
+
}
|
|
87
|
+
if (where.contains('fingerprint_hash = ?')) {
|
|
88
|
+
if (row['fingerprint_hash'] != params[p++]) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
List<Map<String, dynamic>> _selectBundles(String sql, List<dynamic> params) {
|
|
94
|
+
final fromIdx = sql.indexOf('FROM bundles') + 'FROM bundles'.length;
|
|
95
|
+
var endIdx = sql.length;
|
|
96
|
+
final orderIdx = sql.indexOf('ORDER BY id');
|
|
97
|
+
if (orderIdx != -1) endIdx = orderIdx;
|
|
98
|
+
final whereClause = sql.substring(fromIdx, endIdx);
|
|
99
|
+
final whereParamCount = '?'.allMatches(whereClause).length;
|
|
100
|
+
final whereParams = params.sublist(0, whereParamCount);
|
|
101
|
+
|
|
102
|
+
final filtered = store.bundleRows
|
|
103
|
+
.where((row) => _match(row, whereClause, whereParams))
|
|
104
|
+
.toList();
|
|
105
|
+
|
|
106
|
+
final descending = !sql.contains('ORDER BY id ASC');
|
|
107
|
+
filtered.sort(
|
|
108
|
+
(a, b) => descending
|
|
109
|
+
? (b['id'] as String).compareTo(a['id'] as String)
|
|
110
|
+
: (a['id'] as String).compareTo(b['id'] as String),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (sql.contains('LIMIT ?')) {
|
|
114
|
+
final limit = params[whereParamCount] as int;
|
|
115
|
+
final offset = params[whereParamCount + 1] as int;
|
|
116
|
+
final start = offset.clamp(0, filtered.length);
|
|
117
|
+
final end = (offset + limit).clamp(0, filtered.length);
|
|
118
|
+
return filtered.sublist(start, end);
|
|
119
|
+
}
|
|
120
|
+
return filtered;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
Future<List<Map<String, dynamic>>> query(
|
|
125
|
+
String sql, [
|
|
126
|
+
List<dynamic> params = const [],
|
|
127
|
+
]) async {
|
|
128
|
+
if (sql.contains('SELECT COUNT(*)')) {
|
|
129
|
+
final fromIdx = sql.indexOf('FROM bundles') + 'FROM bundles'.length;
|
|
130
|
+
final whereClause = sql.substring(fromIdx);
|
|
131
|
+
final total = store.bundleRows
|
|
132
|
+
.where((row) => _match(row, whereClause, params))
|
|
133
|
+
.length;
|
|
134
|
+
return [
|
|
135
|
+
{'total': total},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (sql.contains('SELECT channel FROM bundles')) {
|
|
140
|
+
final channels = {
|
|
141
|
+
for (final row in store.bundleRows) row['channel'] as String,
|
|
142
|
+
};
|
|
143
|
+
return [for (final c in channels) {'channel': c}];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (sql.contains('SELECT target_app_version FROM bundles')) {
|
|
147
|
+
final channel = params[0] as String;
|
|
148
|
+
final platform = params[1] as String;
|
|
149
|
+
final minId = params[2] as String;
|
|
150
|
+
return [
|
|
151
|
+
for (final row in store.bundleRows)
|
|
152
|
+
if (row['channel'] == channel &&
|
|
153
|
+
row['platform'] == platform &&
|
|
154
|
+
_asBool(row['enabled']) &&
|
|
155
|
+
(row['id'] as String).compareTo(minId) >= 0 &&
|
|
156
|
+
row['target_app_version'] != null)
|
|
157
|
+
{'target_app_version': row['target_app_version'] as String},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (sql.contains('SELECT * FROM bundle_patches')) {
|
|
162
|
+
final ids = _asList(params[0]);
|
|
163
|
+
final rows = store.patchRows
|
|
164
|
+
.where((row) => ids.contains(row['bundle_id']))
|
|
165
|
+
.toList();
|
|
166
|
+
rows.sort(
|
|
167
|
+
(a, b) => (a['order_index'] as num).compareTo(b['order_index'] as num) +
|
|
168
|
+
(a['base_bundle_id'] as String)
|
|
169
|
+
.compareTo(b['base_bundle_id'] as String),
|
|
170
|
+
);
|
|
171
|
+
return rows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (sql.contains('SELECT * FROM bundles')) {
|
|
175
|
+
return _selectBundles(sql, params);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (sql.contains('INSERT OR REPLACE INTO bundles')) {
|
|
179
|
+
final row = <String, dynamic>{
|
|
180
|
+
'id': params[0],
|
|
181
|
+
'channel': params[1],
|
|
182
|
+
'enabled': params[2],
|
|
183
|
+
'should_force_update': params[3],
|
|
184
|
+
'file_hash': params[4],
|
|
185
|
+
'git_commit_hash': params[5],
|
|
186
|
+
'message': params[6],
|
|
187
|
+
'platform': params[7],
|
|
188
|
+
'target_app_version': params[8],
|
|
189
|
+
'storage_uri': params[9],
|
|
190
|
+
'fingerprint_hash': params[10],
|
|
191
|
+
'metadata': params[11],
|
|
192
|
+
'manifest_storage_uri': params[12],
|
|
193
|
+
'manifest_file_hash': params[13],
|
|
194
|
+
'asset_base_storage_uri': params[14],
|
|
195
|
+
'rollout_cohort_count': params[15],
|
|
196
|
+
'target_cohorts': params[16],
|
|
197
|
+
};
|
|
198
|
+
store.bundleRows
|
|
199
|
+
.removeWhere((r) => r['id'] == row['id']);
|
|
200
|
+
store.bundleRows.add(row);
|
|
201
|
+
return const [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (sql.contains('INSERT OR REPLACE INTO bundle_patches')) {
|
|
205
|
+
final row = <String, dynamic>{
|
|
206
|
+
'id': params[0],
|
|
207
|
+
'bundle_id': params[1],
|
|
208
|
+
'base_bundle_id': params[2],
|
|
209
|
+
'base_file_hash': params[3],
|
|
210
|
+
'patch_file_hash': params[4],
|
|
211
|
+
'patch_storage_uri': params[5],
|
|
212
|
+
'order_index': params[6],
|
|
213
|
+
};
|
|
214
|
+
store.patchRows.removeWhere((r) => r['id'] == row['id']);
|
|
215
|
+
store.patchRows.add(row);
|
|
216
|
+
return const [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (sql.contains('DELETE FROM bundles')) {
|
|
220
|
+
final id = params[0];
|
|
221
|
+
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
222
|
+
store.patchRows
|
|
223
|
+
.removeWhere((r) => r['bundle_id'] == id || r['base_bundle_id'] == id);
|
|
224
|
+
return const [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (sql.contains('DELETE FROM bundle_patches')) {
|
|
228
|
+
final id = params[0];
|
|
229
|
+
if (sql.contains('base_bundle_id')) {
|
|
230
|
+
store.patchRows.removeWhere((r) => r['base_bundle_id'] == id);
|
|
231
|
+
} else {
|
|
232
|
+
store.patchRows.removeWhere((r) => r['bundle_id'] == id);
|
|
233
|
+
}
|
|
234
|
+
return const [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return const [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Build a [D1DatabaseConfig] wired to an in-memory [Store].
|
|
242
|
+
D1DatabaseConfig mockConfig(Store store) => D1DatabaseConfig(
|
|
243
|
+
databaseId: 'test-db',
|
|
244
|
+
accountId: 'test-account',
|
|
245
|
+
cloudflareApiToken: 'test-token',
|
|
246
|
+
clientFactory: (_) => MockD1Client(store),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
/// Construct the plugin with an in-memory store.
|
|
250
|
+
DatabasePlugin newPlugin(Store store) => d1Database(mockConfig(store))();
|
|
251
|
+
|
|
252
|
+
/// Construct the Workers D1 variant with an in-memory store.
|
|
253
|
+
DatabasePlugin newWorkerPlugin(Store store) =>
|
|
254
|
+
cloudflareWorkerDatabase(
|
|
255
|
+
CloudflareWorkerDatabaseConfig(getDb: () => MockD1Client(store)),
|
|
256
|
+
)();
|