Skip to content

Commit

Permalink
More integration test for report intake + moderation flow. (#7753)
Browse files Browse the repository at this point in the history
  • Loading branch information
isoos authored May 29, 2024
1 parent 06abfff commit 50579e2
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 16 deletions.
13 changes: 13 additions & 0 deletions app/lib/fake/server/fake_server_entrypoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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.');
}

Expand Down
5 changes: 4 additions & 1 deletion pkg/pub_integration/lib/src/fake_pub_server_process.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ class FakeEmailReaderFromOutputDirectory {
}) async {
final emails = await readAllEmails();
return emails.lastWhere((map) {
final recipients = (map['recipients'] as List).cast<String>();
final recipients = {
...(map['recipients'] as List? ?? <String>[]).cast<String>(),
...(map['ccRecipients'] as List? ?? <String>[]).cast<String>(),
};
return recipients.contains(recipient);
});
}
Expand Down
28 changes: 24 additions & 4 deletions pkg/pub_integration/lib/src/fake_test_context_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -55,7 +56,8 @@ class TestContextProvider {
final session = await _testBrowser.createSession();
return TestUser(
email: '',
api: PubApiClient(pubHostedUrl),
browserApi: PubApiClient(pubHostedUrl),
serverApi: PubApiClient(pubHostedUrl),
withBrowserPage: <T>(Future<T> Function(Page) fn) async {
return await session.withPage<T>(fn: fn);
},
Expand All @@ -68,15 +70,33 @@ class TestContextProvider {
required String email,
List<String>? 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<PubApiClient> 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<String, dynamic>;
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
Expand Down
6 changes: 3 additions & 3 deletions pkg/pub_integration/lib/src/scenarios/like_package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
13 changes: 9 additions & 4 deletions pkg/pub_integration/lib/src/test_scenario.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down
134 changes: 130 additions & 4 deletions pkg/pub_integration/test/report_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand All @@ -40,18 +41,26 @@ void main() {
},
}));

final user = await fakeTestScenario.createAnonymousTestUser();
final anonReporter = await fakeTestScenario.createAnonymousTestUser();
final reporter =
await fakeTestScenario.createTestUser(email: '[email protected]');
final pkgAdminUser =
await fakeTestScenario.createTestUser(email: '[email protected]');
final adminUser =
await fakeTestScenario.createTestUser(email: '[email protected]');
final supportUser =
await fakeTestScenario.createTestUser(email: '[email protected]');

// visit report page and file a report
await user.withBrowserPage(
await anonReporter.withBrowserPage(
(page) async {
// enable experimental flag
await page.gotoOrigin('/experimental?report=1');
await Future.delayed(Duration(seconds: 1));

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);
Expand All @@ -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': '[email protected]',
'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': '[email protected]',
'to': '[email protected]',
'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': '[email protected]',
'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));
}

0 comments on commit 50579e2

Please sign in to comment.