From bc4b6a70b8a619c70e65cb9cdaa7902f8542bee2 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 24 May 2024 18:55:10 +0200 Subject: [PATCH] More integration test for report intake + moderation flow. --- .../fake/server/fake_server_entrypoint.dart | 13 ++ .../lib/src/fake_pub_server_process.dart | 5 +- .../lib/src/fake_test_context_provider.dart | 28 +++- .../lib/src/scenarios/like_package.dart | 6 +- .../lib/src/test_scenario.dart | 13 +- pkg/pub_integration/test/report_test.dart | 134 +++++++++++++++++- 6 files changed, 183 insertions(+), 16 deletions(-) diff --git a/app/lib/fake/server/fake_server_entrypoint.dart b/app/lib/fake/server/fake_server_entrypoint.dart index 8cd83e4948..88b75d6ef9 100644 --- a/app/lib/fake/server/fake_server_entrypoint.dart +++ b/app/lib/fake/server/fake_server_entrypoint.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:http/http.dart'; +import 'package:pub_dev/fake/backend/fake_auth_provider.dart'; import 'package:pub_dev/fake/backend/fake_pub_worker.dart'; import 'package:pub_dev/fake/server/fake_analyzer_service.dart'; import 'package:pub_dev/fake/server/fake_default_service.dart'; @@ -14,6 +15,7 @@ import 'package:pub_dev/fake/server/fake_storage_server.dart'; import 'package:pub_dev/fake/server/local_server_state.dart'; import 'package:pub_dev/frontend/static_files.dart'; import 'package:pub_dev/shared/configuration.dart'; +import 'package:pub_dev/shared/handlers.dart'; import 'package:pub_dev/shared/logging.dart'; import 'package:pub_dev/task/cloudcompute/fakecloudcompute.dart'; import 'package:pub_dev/tool/test_profile/import_source.dart'; @@ -124,6 +126,17 @@ class FakeServerCommand extends Command { if (rq.requestedUri.path == '/fake-update-search') { return await _updateUpstream(searchPort); } + if (rq.requestedUri.path == '/fake-gcp-token') { + final email = rq.requestedUri.queryParameters['email']; + final audience = rq.requestedUri.queryParameters['audience']; + return jsonResponse({ + // ignore: invalid_use_of_visible_for_testing_member + 'token': createFakeServiceAccountToken( + email: email!, + audience: audience, + ), + }); + } return shelf.Response.notFound('Not Found.'); } diff --git a/pkg/pub_integration/lib/src/fake_pub_server_process.dart b/pkg/pub_integration/lib/src/fake_pub_server_process.dart index 25687a717b..ea387b9293 100644 --- a/pkg/pub_integration/lib/src/fake_pub_server_process.dart +++ b/pkg/pub_integration/lib/src/fake_pub_server_process.dart @@ -212,7 +212,10 @@ class FakeEmailReaderFromOutputDirectory { }) async { final emails = await readAllEmails(); return emails.lastWhere((map) { - final recipients = (map['recipients'] as List).cast(); + final recipients = { + ...(map['recipients'] as List? ?? []).cast(), + ...(map['ccRecipients'] as List? ?? []).cast(), + }; return recipients.contains(recipient); }); } diff --git a/pkg/pub_integration/lib/src/fake_test_context_provider.dart b/pkg/pub_integration/lib/src/fake_test_context_provider.dart index 133b024b65..52d5f1b59d 100644 --- a/pkg/pub_integration/lib/src/fake_test_context_provider.dart +++ b/pkg/pub_integration/lib/src/fake_test_context_provider.dart @@ -2,6 +2,7 @@ // 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 'dart:convert'; import 'dart:io'; import 'dart:isolate'; @@ -55,7 +56,8 @@ class TestContextProvider { final session = await _testBrowser.createSession(); return TestUser( email: '', - api: PubApiClient(pubHostedUrl), + browserApi: PubApiClient(pubHostedUrl), + serverApi: PubApiClient(pubHostedUrl), withBrowserPage: (Future Function(Page) fn) async { return await session.withPage(fn: fn); }, @@ -68,15 +70,33 @@ class TestContextProvider { required String email, List? scopes, }) async { - late PubApiClient api; + late PubApiClient browserApi; final session = await _testBrowser.createSession(); await session.withPage(fn: (page) async { await page.fakeAuthSignIn(email: email, scopes: scopes); - api = await _apiClientHttpHeadersFromSignedInSession(page); + browserApi = await _apiClientHttpHeadersFromSignedInSession(page); }); + + Future createClientWithAudience({String? audience}) async { + final rs = await http.get(Uri.parse(pubHostedUrl).replace( + path: '/fake-gcp-token', + queryParameters: { + 'email': email, + if (audience != null) 'audience': audience, + }, + )); + final map = json.decode(rs.body) as Map; + final token = map['token'] as String; + return PubApiClient(pubHostedUrl, + client: createHttpClientWithHeaders({ + 'authorization': 'Bearer $token', + })); + } + return TestUser( email: email, - api: api, + browserApi: browserApi, + serverApi: await createClientWithAudience(), createCredentials: () => fakeCredentialsMap(email: email), readLatestEmail: () async { final map = await _fakePubServerProcess.fakeEmailReader diff --git a/pkg/pub_integration/lib/src/scenarios/like_package.dart b/pkg/pub_integration/lib/src/scenarios/like_package.dart index 7da7aede93..a896eb585c 100644 --- a/pkg/pub_integration/lib/src/scenarios/like_package.dart +++ b/pkg/pub_integration/lib/src/scenarios/like_package.dart @@ -6,13 +6,13 @@ import 'package:pub_integration/src/test_scenario.dart'; final likePackageScenario = TestScenario('like-package', (ctx) async { // Clean up by unliking initially, this should always be safe. - await ctx.userA.api.unlikePackage(ctx.testPackage); + await ctx.userA.browserApi.unlikePackage(ctx.testPackage); // Try to like the package - await ctx.userA.api.likePackage(ctx.testPackage); + await ctx.userA.browserApi.likePackage(ctx.testPackage); // TODO: grep html in the browser to check if the liked state is updated! // Try to unlike the package - await ctx.userA.api.unlikePackage(ctx.testPackage); + await ctx.userA.browserApi.unlikePackage(ctx.testPackage); }); diff --git a/pkg/pub_integration/lib/src/test_scenario.dart b/pkg/pub_integration/lib/src/test_scenario.dart index a4c8d6197c..8538217e6f 100644 --- a/pkg/pub_integration/lib/src/test_scenario.dart +++ b/pkg/pub_integration/lib/src/test_scenario.dart @@ -93,9 +93,13 @@ final class TestUser { /// The email of the given test user. final String email; - /// An API client for access the API authenticated with a session associated - /// with this user. - final PubApiClient api; + /// A browser-based API client for access the API authenticated with a + /// session associated with this user. + final PubApiClient browserApi; + + /// A command-line based API client for accessing the API authenticated + /// via an auth token (for pub site audience). + final PubApiClient serverApi; /// Executes callback `fn` with the browser page where this test user is /// signed-in to their account. @@ -112,7 +116,8 @@ final class TestUser { TestUser({ required this.email, - required this.api, + required this.browserApi, + required this.serverApi, required this.withBrowserPage, required this.readLatestEmail, required this.createCredentials, diff --git a/pkg/pub_integration/test/report_test.dart b/pkg/pub_integration/test/report_test.dart index 47404d60a6..1707ba1807 100644 --- a/pkg/pub_integration/test/report_test.dart +++ b/pkg/pub_integration/test/report_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:_pub_shared/data/admin_api.dart'; import 'package:http/http.dart' as http; import 'package:pub_integration/src/fake_test_context_provider.dart'; import 'package:pub_integration/src/pub_puppeteer_helpers.dart'; @@ -30,7 +31,7 @@ void main() { Uri.parse('${fakeTestScenario.pubHostedUrl}/fake-test-profile'), body: json.encode({ 'testProfile': { - 'defaultUser': 'admin@pub.dev', + 'defaultUser': 'user@pub.dev', 'packages': [ { 'name': 'oxygen', @@ -40,10 +41,18 @@ void main() { }, })); - final user = await fakeTestScenario.createAnonymousTestUser(); + final anonReporter = await fakeTestScenario.createAnonymousTestUser(); + final reporter = + await fakeTestScenario.createTestUser(email: 'reporter@pub.dev'); + final pkgAdminUser = + await fakeTestScenario.createTestUser(email: 'user@pub.dev'); + final adminUser = + await fakeTestScenario.createTestUser(email: 'admin@pub.dev'); + final supportUser = + await fakeTestScenario.createTestUser(email: 'support@pub.dev'); // visit report page and file a report - await user.withBrowserPage( + await anonReporter.withBrowserPage( (page) async { // enable experimental flag await page.gotoOrigin('/experimental?report=1'); @@ -51,7 +60,7 @@ void main() { await page.gotoOrigin('/report?subject=package:oxygen'); await page.waitAndClick('.report-page-direct-report'); - await page.waitFocusAndType('#report-email', 'user@pub.dev'); + await page.waitFocusAndType('#report-email', 'reporter@pub.dev'); await page.waitFocusAndType( '#report-message', 'Huston, we have a problem.'); await page.waitAndClick('#report-submit', waitForOneResponse: true); @@ -60,6 +69,123 @@ void main() { await page.waitAndClickOnDialogOk(); }, ); + + // verify emails + final reportEmail1 = await reporter.readLatestEmail(); + final reportEmail2 = await supportUser.readLatestEmail(); + expect(reportEmail1, contains('package:oxygen')); + expect(reportEmail2, contains('package:oxygen')); + + // verify moderation case + final caseId = reportEmail2.split('\n')[1]; + final caseData = await adminUser.serverApi.adminInvokeAction( + 'moderation-case-info', + AdminInvokeActionArguments( + arguments: { + 'case': caseId, + }, + ), + ); + expect(caseData.output, { + 'caseId': caseId, + 'reporterEmail': 'reporter@pub.dev', + 'kind': 'notification', + 'opened': isNotEmpty, + 'source': 'external-notification', + 'status': 'pending', + 'subject': 'package:oxygen', + 'url': null, + 'actionLog': {'entries': []} + }); + + // moderate package + final moderateRs = await adminUser.serverApi.adminInvokeAction( + 'moderate-package', + AdminInvokeActionArguments( + arguments: { + 'case': caseId, + 'package': 'oxygen', + 'state': 'true', + }, + ), + ); + expect( + moderateRs.output, + { + 'package': 'oxygen', + 'before': {'isModerated': false, 'moderatedAt': null}, + 'after': {'isModerated': true, 'moderatedAt': isNotEmpty}, + }, + ); + + // package page is not accessible + await anonReporter.withBrowserPage((page) async { + await page.gotoOrigin('/packages/oxygen'); + final content = await page.content; + expect(content, contains('has been moderated')); + }); + + final appealPageUrl = + Uri.parse('https://pub.dev/report').replace(queryParameters: { + 'appeal': caseId, + 'subject': 'package:oxygen', + }).toString(); + + // TODO: close case + + // sending email to reporter + await adminUser.serverApi.adminInvokeAction( + 'send-email', + AdminInvokeActionArguments( + arguments: { + 'from': 'support@pub.dev', + 'to': 'reporter@pub.dev', + 'subject': 'Resolution on your report - $caseId', + 'body': 'Dear reporter,\n\n' + 'We have closed the case with the following resolution: ...\n\n' + 'If you want to appeal this decision, you may use the following URL:\n' + '$appealPageUrl\n\n' + 'Best regards,\n pub.dev admins' + }, + ), + ); + final reporterConclusionMail = await reporter.readLatestEmail(); + expect(reporterConclusionMail, contains(appealPageUrl)); + + // sending email to moderated admins + await adminUser.serverApi.adminInvokeAction( + 'send-email', + AdminInvokeActionArguments( + arguments: { + 'from': 'support@pub.dev', + 'to': 'package:oxygen', + 'subject': 'You have been moderated', + 'body': 'Appeal on $appealPageUrl', + }, + ), + ); + final packageAmindConclusionMail = await pkgAdminUser.readLatestEmail(); + expect(packageAmindConclusionMail, contains(appealPageUrl)); + + // admin appeals + await pkgAdminUser.withBrowserPage((page) async { + // enable experimental flag + await page.gotoOrigin('/experimental?report=1'); + await Future.delayed(Duration(seconds: 1)); + + await page + .gotoOrigin(appealPageUrl.replaceAll('https://pub.dev/', '/')); + // TODO: these should be working after the case gets closed + // await page.waitFocusAndType( + // '#report-message', 'Huston, I have a different idea.'); + // await page.waitAndClick('#report-submit', waitForOneResponse: true); + // expect(await page.content, + // contains('The appeal was submitted successfully.')); + // await page.waitAndClickOnDialogOk(); + }); + + // TODO: extract new case id from email + // TODO: admin appeal is rejected (email + closing case) }); }, timeout: Timeout.factor(testTimeoutFactor)); }