forked from dart-lang/pub-dev
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Admin action to fix lingering ModeratedPackage entities. (dart-lang#7359
- Loading branch information
Showing
3 changed files
with
125 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
app/lib/admin/actions/merge_moderated_package_into_existing.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'package:pub_dev/admin/actions/actions.dart'; | ||
import 'package:pub_dev/package/backend.dart'; | ||
import 'package:pub_dev/package/models.dart'; | ||
import 'package:pub_dev/shared/datastore.dart'; | ||
|
||
final mergeModeratedPackageIntoExisting = AdminAction( | ||
name: 'merge-moderated-package-into-existing', | ||
summary: | ||
'Removes a ModeratedPackage tombstone and merges it into an existing Package entity.', | ||
description: ''' | ||
This action will remove a package moderation tombstone (ModeratedPackage) | ||
entity from the Datastore, and merges its deleted version list into its | ||
already existing Package entity. | ||
The removal will unblock new uploads to the package. | ||
WARNING: Ownership information stored on the ModeratedPackage (e.g. publisher | ||
or uploaders) will be lost. | ||
NOTE: The published versions will be rolled into Package.deletedVersions. | ||
Fails if that package has no ModeratedPackage tombstone. | ||
Fails if that package has no existing Package entity. | ||
''', | ||
options: { | ||
'package': | ||
'The name of ModeratedPackage to merge into its existing Package.', | ||
}, | ||
invoke: (args) async { | ||
final packageName = args['package'] as String; | ||
|
||
await withRetryTransaction(dbService, (tx) async { | ||
// check ModeratedPackage existence | ||
final mpKey = | ||
dbService.emptyKey.append(ModeratedPackage, id: packageName); | ||
final mp = await tx.lookupOrNull<ModeratedPackage>(mpKey); | ||
InvalidInputException.check( | ||
mp != null, 'ModeratedPackage does not exists.'); | ||
|
||
// check Package existence | ||
final pKey = dbService.emptyKey.append(Package, id: packageName); | ||
final p = await tx.lookupOrNull<Package>(pKey); | ||
InvalidInputException.check(p != null, 'Package does not exists.'); | ||
|
||
// update deleted version list | ||
final versionList = await tx.query<PackageVersion>(pKey).run().toList(); | ||
final existingVersions = versionList.map((v) => v.version!).toSet(); | ||
final deletedVersions = (mp!.versions ?? <String>[]) | ||
.where((v) => !existingVersions.contains(v)) | ||
.toList(); | ||
if (deletedVersions.isNotEmpty) { | ||
p!.deletedVersions = <String>{ | ||
...?p.deletedVersions, | ||
...deletedVersions, | ||
}.toList(); | ||
} | ||
|
||
tx.insert(p!); | ||
tx.delete(mpKey); | ||
}); | ||
await purgePackageCache(packageName); | ||
|
||
return { | ||
'package': packageName, | ||
'merged': true, | ||
}; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,16 @@ | |
import 'dart:convert'; | ||
|
||
import 'package:_pub_shared/data/admin_api.dart'; | ||
import 'package:clock/clock.dart'; | ||
import 'package:pub_dev/account/backend.dart'; | ||
import 'package:pub_dev/package/backend.dart'; | ||
import 'package:pub_dev/package/models.dart'; | ||
import 'package:pub_dev/publisher/backend.dart'; | ||
import 'package:pub_dev/shared/datastore.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
import '../package/backend_test_utils.dart'; | ||
import '../shared/handlers_test_utils.dart'; | ||
import '../shared/test_models.dart'; | ||
import '../shared/test_services.dart'; | ||
|
||
|
@@ -107,4 +112,51 @@ void main() { | |
final emails = await accountBackend.getEmailsOfUserIds(neon.uploaders!); | ||
expect(emails, {'[email protected]'}); | ||
}); | ||
|
||
testWithProfile('merge existing moderated package into existing', | ||
fn: () async { | ||
final originalVersionList = await packageBackend.listVersions('oxygen'); | ||
|
||
// inject "bad" ModeratedPackage tombstone | ||
await dbService.commit(inserts: [ | ||
ModeratedPackage() | ||
..parentKey = dbService.emptyKey | ||
..id = 'oxygen' | ||
..name = 'oxygen' | ||
..moderated = clock.now().toUtc() | ||
..versions = [ | ||
originalVersionList.versions.first.version, // existing | ||
'8.99.100', // non-existing | ||
] | ||
..uploaders = [], | ||
]); | ||
|
||
// verify that new upload is blocked | ||
final pubspecContent = generatePubspecYaml('oxygen', '9.0.0'); | ||
final bytes = await packageArchiveBytes(pubspecContent: pubspecContent); | ||
final rs1 = createPubApiClient(authToken: adminClientToken) | ||
.uploadPackageBytes(bytes); | ||
await expectApiException( | ||
rs1, | ||
status: 400, | ||
code: 'PackageRejected', | ||
message: 'Package name oxygen is reserved', | ||
); | ||
|
||
// merge tombstone | ||
final api = createPubApiClient(authToken: siteAdminToken); | ||
final result = await api.adminInvokeAction( | ||
'merge-moderated-package-into-existing', | ||
AdminInvokeActionArguments(arguments: {'package': 'oxygen'}), | ||
); | ||
expect(result.output, { | ||
'package': 'oxygen', | ||
'merged': true, | ||
}); | ||
|
||
// verify that upload is unblocked | ||
final rs2 = await createPubApiClient(authToken: adminClientToken) | ||
.uploadPackageBytes(bytes); | ||
expect(rs2.success.message, contains('Successfully uploaded')); | ||
}); | ||
} |