Skip to content

Commit 372b6ec

Browse files
committed
Typing for ExportedApi and minimal test
1 parent ba053e6 commit 372b6ec

File tree

2 files changed

+103
-6
lines changed

2 files changed

+103
-6
lines changed

app/lib/package/api_export/exported_api.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'dart:async';
22
import 'dart:convert';
33
import 'dart:io';
44

5+
import 'package:_pub_shared/data/advisories_api.dart';
6+
import 'package:_pub_shared/data/package_api.dart';
57
import 'package:clock/clock.dart';
68
import 'package:gcloud/storage.dart';
79
import 'package:logging/logging.dart';
@@ -43,7 +45,8 @@ final class ExportedApi {
4345
ExportedPackage._(this, packageName);
4446

4547
/// Interface for writing `/api/package-name-completion-data`
46-
ExportedJsonFile get packageNameCompletionData => ExportedJsonFile._(
48+
ExportedJsonFile<Map<String, Object?>> get packageNameCompletionData =>
49+
ExportedJsonFile<Map<String, Object?>>._(
4750
this,
4851
'/api/package-name-completion-data',
4952
Duration(hours: 8),
@@ -178,7 +181,7 @@ final class ExportedPackage {
178181

179182
ExportedPackage._(this._owner, this._package);
180183

181-
ExportedJsonFile _suffix(String suffix) => ExportedJsonFile._(
184+
ExportedJsonFile<T> _suffix<T>(String suffix) => ExportedJsonFile<T>._(
182185
_owner,
183186
'/api/packages/$_package$suffix',
184187
Duration(minutes: 10),
@@ -187,10 +190,11 @@ final class ExportedPackage {
187190
/// Interface for writing `/api/packages/<package>`.
188191
///
189192
/// Which contains version listing information.
190-
ExportedJsonFile get versions => _suffix('');
193+
ExportedJsonFile<PackageData> get versions => _suffix<PackageData>('');
191194

192195
/// Interface for writing `/api/packages/<package>/advisories`.
193-
ExportedJsonFile get advisories => _suffix('/advisories');
196+
ExportedJsonFile<ListAdvisoriesResponse> get advisories =>
197+
_suffix<ListAdvisoriesResponse>('/advisories');
194198

195199
/// Interace for writing `/api/archives/<package>-<version>.tar.gz`.
196200
ExportedBlob tarball(String version) => ExportedBlob._(
@@ -239,7 +243,7 @@ sealed class ExportedObject {
239243
/// * `Content-Type`,
240244
/// * `Content-Encoding`, and,
241245
/// * `Cache-Control`.
242-
final class ExportedJsonFile extends ExportedObject {
246+
final class ExportedJsonFile<T> extends ExportedObject {
243247
static final _jsonGzip = json.fuse(utf8).fuse(gzip);
244248
final Duration _maxAge;
245249

@@ -256,7 +260,7 @@ final class ExportedJsonFile extends ExportedObject {
256260
);
257261

258262
/// Write [data] as gzipped JSON in UTF-8 format.
259-
Future<void> write(Map<String, Object?> data) async {
263+
Future<void> write(T data) async {
260264
final gzipped = _jsonGzip.encode(data);
261265
await Future.wait(_owner._prefixes.map((prefix) async {
262266
await _owner._pool.withResource(() async {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) 2023, 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 'dart:convert';
6+
import 'dart:io';
7+
import 'dart:typed_data';
8+
9+
import 'package:_pub_shared/data/package_api.dart';
10+
import 'package:gcloud/storage.dart';
11+
import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
12+
import 'package:pub_dev/package/api_export/exported_api.dart';
13+
import 'package:pub_dev/shared/storage.dart';
14+
import 'package:pub_dev/shared/utils.dart';
15+
import 'package:test/test.dart';
16+
import '../../shared/test_services.dart';
17+
18+
void main() {
19+
testWithFakeTime('ExportedApi', (fakeTime) async {
20+
await storageService.createBucket('exported-api');
21+
final bucket = storageService.bucket('exported-api');
22+
23+
/// Read bytes from bucket
24+
Future<Uint8List?> readBytes(String path) async {
25+
try {
26+
return await bucket.readAsBytes(path);
27+
} on DetailedApiRequestError catch (e) {
28+
if (e.status == 404) return null;
29+
rethrow;
30+
}
31+
}
32+
33+
/// Read gzipped JSON from bucket
34+
Future<Object?> readGzippedJson(String path) async {
35+
final bytes = await readBytes(path);
36+
if (bytes == null) {
37+
return null;
38+
}
39+
return utf8JsonDecoder.convert(gzip.decode(bytes));
40+
}
41+
42+
final exportedApi = ExportedApi(storageService, bucket);
43+
44+
// Test that deletion works when bucket is empty
45+
await exportedApi.package('retry').delete();
46+
47+
// Test that GC works when bucket is empty
48+
await exportedApi.garbageCollect({});
49+
50+
final retryPkgData1 = PackageData(
51+
name: 'retry',
52+
latest: VersionInfo(
53+
version: '1.2.3',
54+
retracted: false,
55+
pubspec: {},
56+
archiveUrl: '-',
57+
archiveSha256: '-',
58+
published: DateTime.now(),
59+
),
60+
versions: [],
61+
);
62+
63+
await exportedApi.package('retry').versions.write(retryPkgData1);
64+
65+
expect(
66+
await readGzippedJson('latest/api/packages/retry'),
67+
json.decode(json.encode(retryPkgData1.toJson())),
68+
);
69+
70+
// Check that GC after 10 mins won't delete a package we don't recognize
71+
fakeTime.elapseSync(minutes: 10);
72+
await exportedApi.garbageCollect({});
73+
expect(
74+
await readGzippedJson('latest/api/packages/retry'),
75+
isNotNull,
76+
);
77+
78+
// Check that GC after 2 days won't delete a package we know
79+
fakeTime.elapseSync(days: 2);
80+
await exportedApi.garbageCollect({'retry'});
81+
expect(
82+
await readGzippedJson('latest/api/packages/retry'),
83+
isNotNull,
84+
);
85+
86+
// Check retry after 2 days will delete a package we don't know.
87+
await exportedApi.garbageCollect({});
88+
expect(
89+
await readGzippedJson('latest/api/packages/retry'),
90+
isNull,
91+
);
92+
});
93+
}

0 commit comments

Comments
 (0)