Skip to content

Commit 93d748f

Browse files
authored
Tests for admin action of API exporter sync. (#8278)
1 parent 05673e1 commit 93d748f

File tree

2 files changed

+228
-2
lines changed

2 files changed

+228
-2
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:_pub_shared/data/admin_api.dart';
6+
import 'package:gcloud/storage.dart';
7+
import 'package:pub_dev/shared/configuration.dart';
8+
import 'package:pub_dev/shared/storage.dart';
9+
import 'package:pub_dev/shared/versions.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../shared/test_models.dart';
13+
import '../shared/test_services.dart';
14+
15+
void main() {
16+
group('Exported API sync admin action', () {
17+
/// Invoke exported-api-sync action
18+
Future<void> syncExportedApi({
19+
List<String>? packages,
20+
bool forceWrite = false,
21+
}) async {
22+
final api = createPubApiClient(authToken: siteAdminToken);
23+
await api.adminInvokeAction(
24+
'exported-api-sync',
25+
AdminInvokeActionArguments(arguments: {
26+
'packages': packages?.join(' ') ?? 'ALL',
27+
if (forceWrite) 'force-write': 'true',
28+
}),
29+
);
30+
}
31+
32+
/// Return map of all objects in the Exported API bucket.
33+
///
34+
/// Returns a map from object name to JSON on the form:
35+
/// ```js
36+
/// {
37+
/// 'updated': '<date-time>',
38+
/// 'metadata': {
39+
/// 'contentType': '<contentType>',
40+
/// 'custom': {...},
41+
/// },
42+
/// 'length': <length>,
43+
/// 'bytes': [...],
44+
/// }
45+
/// ```
46+
Future<Map<String, dynamic>> listExportedApi() async {
47+
final data = <String, dynamic>{};
48+
final bucket =
49+
storageService.bucket(activeConfiguration.exportedApiBucketName!);
50+
await for (final entry in bucket.list(delimiter: '')) {
51+
if (!entry.isObject) continue;
52+
final info = await bucket.info(entry.name);
53+
final bytes = await bucket.readAsBytes(entry.name);
54+
data[entry.name] = {
55+
'updated': info.updated.toIso8601String(),
56+
'metadata': {
57+
'contentType': info.metadata.contentType,
58+
'custom': info.metadata.custom,
59+
},
60+
'length': bytes.length,
61+
'bytes': bytes.toList(),
62+
};
63+
}
64+
return data;
65+
}
66+
67+
testWithProfile('baseline checks', fn: () async {
68+
await syncExportedApi();
69+
final data = await listExportedApi();
70+
expect(data.keys, hasLength(greaterThan(20)));
71+
expect(data.keys, hasLength(lessThan(40)));
72+
73+
final oxygenFiles = data.keys.where((k) => k.contains('oxygen')).toSet();
74+
expect(oxygenFiles, hasLength(greaterThan(5)));
75+
expect(oxygenFiles, {
76+
'$runtimeVersion/api/archives/oxygen-1.0.0.tar.gz',
77+
'$runtimeVersion/api/archives/oxygen-1.2.0.tar.gz',
78+
'$runtimeVersion/api/archives/oxygen-2.0.0-dev.tar.gz',
79+
'$runtimeVersion/api/packages/oxygen',
80+
'$runtimeVersion/api/packages/oxygen/advisories',
81+
'latest/api/archives/oxygen-1.0.0.tar.gz',
82+
'latest/api/archives/oxygen-1.2.0.tar.gz',
83+
'latest/api/archives/oxygen-2.0.0-dev.tar.gz',
84+
'latest/api/packages/oxygen',
85+
'latest/api/packages/oxygen/advisories',
86+
});
87+
88+
final oxygenDataJson = data['latest/api/packages/oxygen'];
89+
expect(oxygenDataJson, {
90+
'updated': isNotEmpty,
91+
'metadata': {
92+
'contentType': 'application/json; charset="utf-8"',
93+
'custom': {
94+
'validated': isNotEmpty,
95+
},
96+
},
97+
'length': greaterThan(100),
98+
'bytes': isNotEmpty,
99+
});
100+
});
101+
102+
testWithProfile('deleted files + full sync', fn: () async {
103+
await syncExportedApi();
104+
final oldRoot = await listExportedApi();
105+
106+
for (final e in oldRoot.entries) {
107+
final path = e.key;
108+
final oldData = e.value as Map;
109+
110+
final bucket =
111+
storageService.bucket(activeConfiguration.exportedApiBucketName!);
112+
await bucket.delete(path);
113+
114+
await syncExportedApi();
115+
final newRoot = await listExportedApi();
116+
expect(newRoot.containsKey(path), true);
117+
final newData = newRoot[path] as Map;
118+
expect(oldData['bytes'], isNotEmpty);
119+
expect(oldData['bytes'], newData['bytes']);
120+
}
121+
});
122+
123+
testWithProfile('delete files + selective sync', fn: () async {
124+
await syncExportedApi();
125+
final oldRoot = await listExportedApi();
126+
127+
final oxygenFiles =
128+
oldRoot.keys.where((k) => k.contains('oxygen')).toList();
129+
expect(oxygenFiles, hasLength(greaterThan(5)));
130+
131+
for (final path in oxygenFiles) {
132+
final bucket =
133+
storageService.bucket(activeConfiguration.exportedApiBucketName!);
134+
await bucket.delete(path);
135+
136+
await syncExportedApi(packages: ['neon']);
137+
expect(await bucket.tryInfo(path), isNull);
138+
139+
await syncExportedApi(packages: ['oxygen']);
140+
expect(await bucket.tryInfo(path), isNotNull);
141+
}
142+
});
143+
144+
testWithProfile('modified files + selective sync', fn: () async {
145+
await syncExportedApi();
146+
final oldRoot = await listExportedApi();
147+
148+
final oxygenFiles =
149+
oldRoot.keys.where((k) => k.contains('oxygen')).toList();
150+
expect(oxygenFiles, hasLength(greaterThan(5)));
151+
152+
for (final path in oxygenFiles) {
153+
final oldData = oldRoot[path] as Map;
154+
final bucket =
155+
storageService.bucket(activeConfiguration.exportedApiBucketName!);
156+
await bucket.writeBytes(path, [1]);
157+
158+
await syncExportedApi(packages: ['neon']);
159+
expect((await bucket.info(path)).length, 1);
160+
161+
await syncExportedApi(packages: ['oxygen']);
162+
expect((await bucket.info(path)).length, greaterThan(1));
163+
164+
// also check file content
165+
final newRoot = await listExportedApi();
166+
final newData = newRoot[path] as Map;
167+
expect(newData['length'], greaterThan(1));
168+
expect(oldData['bytes'], newData['bytes']);
169+
}
170+
});
171+
172+
testWithProfile('modified metadata + selective sync', fn: () async {
173+
await syncExportedApi();
174+
final oldRoot = await listExportedApi();
175+
176+
final oxygenFiles =
177+
oldRoot.keys.where((k) => k.contains('oxygen')).toList();
178+
expect(oxygenFiles, hasLength(greaterThan(5)));
179+
180+
for (final path in oxygenFiles) {
181+
final oldData = oldRoot[path] as Map;
182+
final bucket =
183+
storageService.bucket(activeConfiguration.exportedApiBucketName!);
184+
await bucket.updateMetadata(
185+
path,
186+
ObjectMetadata(
187+
contentType: 'text/plain',
188+
custom: {'x': 'x'},
189+
),
190+
);
191+
192+
await syncExportedApi(packages: ['neon']);
193+
expect((await bucket.info(path)).metadata.custom, {'x': 'x'});
194+
195+
await syncExportedApi(packages: ['oxygen']);
196+
final newInfo = await bucket.info(path);
197+
expect(
198+
newInfo.metadata.contentType, oldData['metadata']['contentType']);
199+
expect(newInfo.metadata.custom, {'validated': isNotEmpty});
200+
201+
// also check file content
202+
final newRoot = await listExportedApi();
203+
final newData = newRoot[path] as Map;
204+
expect(newData['length'], greaterThan(1));
205+
expect(oldData['bytes'], newData['bytes']);
206+
}
207+
});
208+
});
209+
}

pkg/fake_gcloud/lib/mem_storage.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,23 @@ class _File implements BucketObjectEntry {
193193

194194
@override
195195
bool get isObject => true;
196+
197+
_File replace({ObjectMetadata? metadata}) {
198+
return _File(
199+
bucketName: bucketName,
200+
name: name,
201+
content: content,
202+
metadata: this.metadata.replace(
203+
acl: metadata!.acl,
204+
cacheControl: metadata.cacheControl,
205+
contentDisposition: metadata.contentDisposition,
206+
contentEncoding: metadata.contentEncoding,
207+
contentLanguage: metadata.contentLanguage,
208+
contentType: metadata.contentType,
209+
custom: metadata.custom,
210+
),
211+
);
212+
}
196213
}
197214

198215
class _Bucket implements Bucket {
@@ -353,8 +370,8 @@ class _Bucket implements Bucket {
353370
@override
354371
Future<void> updateMetadata(
355372
String objectName, ObjectMetadata metadata) async {
356-
_logger.severe(
357-
'UpdateMetadata: $objectName not yet implemented, call ignored.');
373+
_validateObjectName(objectName);
374+
_files[objectName] = _files[objectName]!.replace(metadata: metadata);
358375
}
359376
}
360377

0 commit comments

Comments
 (0)