Skip to content

Commit

Permalink
Integration verification test with script-callback based tool. (#7811)
Browse files Browse the repository at this point in the history
  • Loading branch information
isoos committed Jun 14, 2024
1 parent e4447b9 commit bb8f83a
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 98 deletions.
129 changes: 129 additions & 0 deletions pkg/pub_integration/bin/fake_user_callback.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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 'dart:async';
import 'dart:convert';

import 'package:args/command_runner.dart';
import 'package:pub_integration/src/fake_credentials.dart';
import 'package:pub_integration/src/fake_pub_server_process.dart';
import 'package:pub_integration/src/fake_test_context_provider.dart';
import 'package:pub_integration/src/pub_puppeteer_helpers.dart';
import 'package:pub_integration/src/test_browser.dart';

Future<void> main(List<String> args) async {
final runner = CommandRunner<String>(
'fake_user_callback',
'Provides integration test details for fake server user authentication.',
)
..addCommand(_ApiAccessTokenCommand())
..addCommand(_ClientCredentialsJsonCommand())
..addCommand(_BrowserCookiesCommand())
..addCommand(_LastEmailCommand());
final rs = await runner.run(args);
print(rs);
}

class _ApiAccessTokenCommand extends Command<String> {
@override
String get name => 'api-access-token';

@override
String get description => 'Provides API access token';

_ApiAccessTokenCommand() {
argParser
..addOption('pub-hosted-url')
..addOption('email');
}

@override
Future<String> run() async {
final pubHostedUrl = argResults!['pub-hosted-url'] as String;
final email = argResults!['email'] as String;
return await createFakeGcpToken(pubHostedUrl: pubHostedUrl, email: email);
}
}

class _ClientCredentialsJsonCommand extends Command<String> {
@override
String get name => 'client-credentials-json';

@override
String get description =>
'Provides pub client credentials.json file content.';

_ClientCredentialsJsonCommand() {
argParser.addOption('email');
}

@override
Future<String> run() async {
final email = argResults!['email'] as String;
final map = fakeCredentialsMap(email: email);
return json.encode(map);
}
}

class _BrowserCookiesCommand extends Command<String> {
@override
String get name => 'browser-cookies';

@override
String get description =>
'Provides browser cookies for the signed-in user session';

_BrowserCookiesCommand() {
argParser
..addOption('pub-hosted-url')
..addOption('email')
..addOption('scopes');
}

@override
Future<String> run() async {
final pubHostedUrl = argResults!['pub-hosted-url'] as String;
final email = argResults!['email'] as String;
final scopes = (argResults!['scopes'] as String?)?.split(',');
final testBrowser = TestBrowser(
origin: pubHostedUrl,
);
try {
await testBrowser.startBrowser();
final session = await testBrowser.createSession();
return await session.withPage(fn: (page) async {
await page.fakeAuthSignIn(email: email, scopes: scopes);
await page.gotoOrigin('/my-liked-packages');
final cookies = await page.cookies();
return json.encode(cookies.map((e) => e.toJson()).toList());
});
} finally {
await testBrowser.close();
}
}
}

class _LastEmailCommand extends Command<String> {
@override
String get name => 'last-email';

@override
String get description => 'Provides the last email body for the user';

_LastEmailCommand() {
argParser
..addOption('email-output-dir')
..addOption('email');
}

@override
Future<String> run() async {
final emailOutputDir = argResults!['email-output-dir'] as String;
final email = argResults!['email'] as String;

final reader = FakeEmailReaderFromOutputDirectory(emailOutputDir);
final map = await reader.readLatestEmail(recipient: email);
return map['bodyText'] as String;
}
}
91 changes: 13 additions & 78 deletions pkg/pub_integration/bin/verify_prod_pub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:http/http.dart' as http;
import 'package:http/retry.dart' as http_retry;
import 'package:pub_integration/pub_integration.dart';
import 'package:pub_integration/src/test_browser.dart';
Expand Down Expand Up @@ -34,13 +33,9 @@ extension on ArgParser {
mandatory: true,
);
addOption(
'${prefix}client-access-token-callback',
help: 'The command to execute to get pub client access token for $name',
mandatory: true,
);
addOption(
'${prefix}client-refresh-token-callback',
help: 'The command to execute to get pub client refresh token for $name',
'${prefix}client-credentials-json-callback',
help:
'The command to execute to get pub client credentials.json content for $name',
mandatory: true,
);
addOption(
Expand All @@ -50,8 +45,8 @@ extension on ArgParser {
mandatory: true,
);
addOption(
'${prefix}gmail-access-token-callback',
help: 'The command to execute to get Gmail access token for $name',
'${prefix}last-email-callback',
help: 'The command to execute to read last email for $name',
mandatory: true,
);
}
Expand All @@ -60,6 +55,7 @@ extension on ArgParser {
Future<void> main(List<String> arguments) async {
final argv = _argParser.parse(arguments);
final pubHostedUrl = argv['pub-hosted-url'] as String;
final expectLiveSite = !pubHostedUrl.startsWith('http://localhost:');

final browser = TestBrowser(origin: pubHostedUrl);
try {
Expand All @@ -70,7 +66,7 @@ Future<void> main(List<String> arguments) async {
browser, pubHostedUrl, _argsWithPrefix(argv, 'user-a-')),
invitedUser: await _initializeUser(
browser, pubHostedUrl, _argsWithPrefix(argv, 'user-b-')),
expectLiveSite: true,
expectLiveSite: expectLiveSite,
);
} finally {
await browser.close();
Expand All @@ -96,11 +92,8 @@ Future<TestUser> _initializeUser(
) async {
final email = map['email']!;
final apiAccessToken = await _callback(map['api-access-token-callback']!);
final clientAccessToken =
await _callback(map['client-access-token-callback']!);
final clientRefreshToken =
await _callback(map['client-refresh-token-callback']!);
final gmailAccessToken = await _callback(map['gmail-access-token-callback']!);
final credentialsJsonContent =
await _callback(map['client-credentials-json-callback']!);
final cookiesString = await _callback(map['browser-cookies-callback']!);
List<CookieParam>? cookies;
if (cookiesString.trim().isNotEmpty) {
Expand Down Expand Up @@ -140,21 +133,14 @@ Future<TestUser> _initializeUser(
return await fn(page);
});
},
readLatestEmail: () async => _readLastEmail(email, gmailAccessToken),
createCredentials: () => {
'accessToken': clientAccessToken,
'refreshToken': clientRefreshToken,
'tokenEndpoint': 'https://accounts.google.com/o/oauth2/token',
'scopes': [
'openid',
'https://www.googleapis.com/auth/userinfo.email',
],
'expiration': 0,
},
readLatestEmail: () async => await _callback(map['last-email-callback']!),
createCredentials: () =>
json.decode(credentialsJsonContent) as Map<String, dynamic>,
);
}

Future<String> _callback(String command) async {
print('... $command');
final parts = command.split(' ');
final pr = await Process.run(
parts.first,
Expand All @@ -165,54 +151,3 @@ Future<String> _callback(String command) async {
}
return pr.stdout.toString().trim();
}

Future<String> _readLastEmail(String email, String token) async {
final client = http_retry.RetryClient(http.Client());
try {
final gmail = _GmailClient(client, email, token);
final messageIds = await gmail.listMessages();
return await gmail.getMessage(messageIds.first);
} finally {
client.close();
}
}

class _GmailClient {
final http.Client _client;
final String _email;
final String _token;

_GmailClient(this._client, this._email, this._token);

Future<List<String>> listMessages() async {
final u = Uri.parse(
'https://gmail.googleapis.com/gmail/v1/users/$_email/messages');
final res = await _client.get(u, headers: {
'Authorization': 'Bearer $_token',
});
if (res.statusCode != 200) {
throw Exception('Failed to list messages from gmail');
}
final ids = <String>[];
for (final m in json.decode(res.body)['messages'] as Iterable) {
ids.add(m['id'] as String);
}
return ids;
}

Future<String> getMessage(String id) async {
final u = Uri.parse(
'https://gmail.googleapis.com/gmail/v1/users/$_email/messages/$id');
final res = await _client.get(u, headers: {
'Authorization': 'Bearer $_token',
});
if (res.statusCode != 200) {
throw Exception('Failed to fetch message from gmail');
}

final data = json.decode(res.body) as Map;
final content = base64Decode(data['payload']['body']['data'] as String);

return utf8.decode(content);
}
}
2 changes: 1 addition & 1 deletion pkg/pub_integration/lib/script/publishing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class PublishingScript {
await dart.getDependencies(_dummyExampleDir.path);
await dart.run(_dummyExampleDir.path, 'bin/main.dart');

if (pubHostedUrl.startsWith('http://localhost:')) {
if (!expectLiveSite) {
// invite uploader
// TODO: use page.invitePackageAdmin instead
await adminUser.withBrowserPage((page) async {
Expand Down
6 changes: 4 additions & 2 deletions pkg/pub_integration/lib/src/fake_pub_server_process.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ class FakePubServerProcess {
}
}

late final fakeEmailReader = FakeEmailReaderFromOutputDirectory(
p.join(_tmpDir, 'fake-email-sender-output-dir'));
late final fakeEmailOutputPath =
p.join(_tmpDir, 'fake-email-sender-output-dir');
late final fakeEmailReader =
FakeEmailReaderFromOutputDirectory(fakeEmailOutputPath);
}

class FakeEmailReaderFromOutputDirectory {
Expand Down
53 changes: 36 additions & 17 deletions pkg/pub_integration/lib/src/fake_test_context_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,13 @@ class TestContextProvider {
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,
browserApi: browserApi,
serverApi: await createClientWithAudience(),
serverApi: await _createClientWithAudience(
pubHostedUrl: pubHostedUrl,
email: email,
),
createCredentials: () => fakeCredentialsMap(email: email),
readLatestEmail: () async {
final map = await _fakePubServerProcess.fakeEmailReader
Expand All @@ -112,6 +99,38 @@ class TestContextProvider {
}
}

Future<PubApiClient> _createClientWithAudience({
required String pubHostedUrl,
required String email,
String? audience,
}) async {
final token = await createFakeGcpToken(
pubHostedUrl: pubHostedUrl,
email: email,
audience: audience,
);
return PubApiClient(pubHostedUrl,
client: createHttpClientWithHeaders({
'authorization': 'Bearer $token',
}));
}

Future<String> createFakeGcpToken({
required String pubHostedUrl,
required String email,
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>;
return map['token'] as String;
}

/// Extracts the HTTP headers required for pub.dev API client (session cookies and CSRF token).
Future<PubApiClient> _apiClientHttpHeadersFromSignedInSession(Page page) async {
await page.gotoOrigin('/my-liked-packages');
Expand Down
Loading

0 comments on commit bb8f83a

Please sign in to comment.