blob: 851d74c27599b9dc90f224468fcfcb2f561f778b [file] [log] [blame]
// Copyright (c) 2019, 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 'dart:io';
import 'package:googleapis/firestore/v1.dart';
import 'package:sendgrid_mailer/sendgrid_mailer.dart';
import 'package:test/test.dart';
import 'package:github_label_notifier/firestore_helpers.dart';
import 'package:github_label_notifier/github_utils.dart';
import 'package:github_label_notifier/subscriptions_db.dart';
import '../bin/server.dart' as server_lib;
// Note: must match project-id used in test.sh
final projectId = 'github-label-notifier';
void createGithubLabelNotifier() {
server_lib.main();
}
Future<HttpServer> createSendgridMockServer(
List<SendgridRequest> requestLog) async {
// Start mock SendGrid server at the address/port specified in
// SENDGRID_MOCK_SERVER environment variable.
// This server will simply record headers and bodies of all requests
// it receives in the [sendgridRequests] variable.
final serverAddress = Platform.environment['SENDGRID_MOCK_SERVER'];
if (serverAddress == null) {
throw 'SENDGRID_MOCK_SERVER environment variable is not set';
}
final serverUri = Uri.parse('http://$serverAddress');
final server = await HttpServer.bind(serverUri.host, serverUri.port);
server.listen((request) async {
final body = await utf8.decoder.fuse(json.decoder).bind(request).single;
requestLog.add(SendgridRequest(
headers: request.headers, body: body as Map<String, dynamic>));
print(
'Sendgrid request received by mock server ${requestLog.last.toJson()}');
// Reply with 200 OK.
await (request.response
..statusCode = HttpStatus.ok
..reasonPhrase = 'OK'
..headers.contentType = ContentType.text
..write('OK'))
.close();
});
return server;
}
void main() async {
if (!Platform.environment.containsKey('FIRESTORE_EMULATOR_HOST')) {
throw 'This test must run in an emulated environment via test.sh script';
}
// Mock SendGrid server which simply records all requests it recieves.
late HttpServer sendgridMockServer;
final sendgridRequests = <SendgridRequest>[];
late HttpServer symbolizerServer;
final symbolizerCommands = <String>[];
late int notifierPort;
final client = HttpClient();
setUpAll(() async {
notifierPort = int.parse(Platform.environment['PORT'] ?? '8080');
// Populate firestore with mock data.
await ensureInitialized;
final firestore = firestoreApi;
final documentsApi = firestore.projects.databases.documents;
final documentList = await documentsApi
.list(documents, 'github-label-subscriptions', mask_fieldPaths: []);
final deletes = [
for (final document in documentList.documents ?? [])
Write(delete: document.name)
];
if (deletes.isNotEmpty) {
await documentsApi.batchWrite(
BatchWriteRequest(writes: deletes), documents);
}
await documentsApi.createDocument(
Document(
fields: taggedMap({
'email': 'first@example.com',
'subscriptions': [
'dart-lang/webhook-test:some-label',
'dart-lang/webhook-test:feature',
],
})),
documents,
labelCollection);
await documentsApi.createDocument(
Document(
fields: taggedMap({
'email': 'second@example.com',
'subscriptions': [
'dart-lang/webhook-test:bug',
'dart-lang/webhook-test:feature',
'dart-lang/webhook-test:special-label',
],
})),
documents,
labelCollection);
await documentsApi.patch(
Document(
fields: taggedMap({
'keywords': [
'jit',
'aot',
'third_party/dart',
'crash',
],
'label': 'special-label',
})),
'$keywordDatabase/dart-lang\$webhook-test');
sendgridMockServer = await createSendgridMockServer(sendgridRequests);
createGithubLabelNotifier();
});
setUp(() {
sendgridRequests.clear();
symbolizerCommands.clear();
});
tearDownAll(() async {
// Shutdown the mock SendGrid server.
await sendgridMockServer.close();
});
// Helper to send a mock GitHub event to the locally running instance of the
// webhook.
Future<HttpClientResponse> sendEvent({
String? delivery = '7e754400-fa9b-11e9-9421-bb996f50ac6b',
String? event = 'issues',
String? signature,
required Map<String, dynamic> body,
}) async {
final request =
await client.post('localhost', notifierPort, 'githubWebhook');
request.headers.add('content-type', 'application/json');
if (delivery != null) {
request.headers.add('X-GitHub-Delivery', delivery);
}
if (event != null) {
request.headers.add('X-GitHub-Event', event);
}
final encodedBody = jsonEncode(body);
if (signature != '') {
request.headers.add(
'X-Hub-Signature', signature ?? signEvent(utf8.encode(encodedBody)));
}
request.write(encodedBody);
return request.close();
}
// Create mock event body.
Map<String, dynamic> makeLabeledEvent(
{String labelName = 'bug', int number = 1}) =>
{
'action': 'labeled',
'issue': {
'html_url':
'https://github.com/dart-lang/webhook-test/issues/$number',
'number': number,
'title': 'TEST ISSUE TITLE',
'user': {
'login': 'hest',
'html_url': 'https://github.com/hest',
},
},
'label': {
'name': labelName,
},
'repository': {
'full_name': 'dart-lang/webhook-test',
},
'sender': {
'login': 'fisk',
'html_url': 'https://github.com/fisk',
}
};
Map<String, dynamic> makeIssueOpenedEvent(
{int number = 1,
String repositoryName = 'dart-lang/webhook-test',
String body =
'This is an amazing ../third_party/dart/runtime/vm solution'}) =>
{
'action': 'opened',
'issue': {
'html_url':
'https://github.com/dart-lang/webhook-test/issues/$number',
'number': number,
'title': 'TEST ISSUE TITLE',
'body': body,
'user': {
'login': 'hest',
'html_url': 'https://github.com/hest',
},
},
'repository': {
'full_name': 'dart-lang/webhook-test',
},
};
Map<String, dynamic> makeIssueCommentEvent(
{int number = 1,
String repositoryName = 'dart-lang/webhook-test',
String issueBody =
'This is an amazing ../third_party/dart/runtime/vm solution',
String commentBody = 'comment body goes here',
String authorAssociation = 'NONE'}) =>
{
'action': 'created',
'issue': {
'html_url':
'https://github.com/dart-lang/webhook-test/issues/$number',
'number': number,
'title': 'TEST ISSUE TITLE',
'body': issueBody,
'user': {
'login': 'hest',
'html_url': 'https://github.com/hest',
},
},
'comment': {
'body': commentBody,
'author_association': authorAssociation,
},
'repository': {
'full_name': 'dart-lang/webhook-test',
},
};
test('signing', () {
expect(
signEvent(utf8.encode(jsonEncode(makeLabeledEvent(labelName: 'bug')))),
equals('sha1=2a997eeaf8fda3069018e00b03ca105c875f365b'));
});
test('reject malformed request - no delivery', () async {
final rs = await sendEvent(body: {}, delivery: null);
expect(rs.statusCode, equals(HttpStatus.badRequest));
expect(sendgridRequests, isEmpty);
});
test('reject malformed request - no event', () async {
final rs = await sendEvent(body: {}, event: null);
expect(rs.statusCode, equals(HttpStatus.badRequest));
expect(sendgridRequests, isEmpty);
});
test('reject malformed request - no signature', () async {
final rs = await sendEvent(
body: makeLabeledEvent(labelName: 'bug'), signature: '');
expect(rs.statusCode, equals(HttpStatus.badRequest));
expect(sendgridRequests, isEmpty);
});
test('reject incorrectly signed request', () async {
final rs = await sendEvent(
body: makeLabeledEvent(labelName: 'bug'),
signature: 'sha1=76af51cdb9c7a43b246d4df721ac8f83e53182e5');
expect(rs.statusCode, equals(HttpStatus.unauthorized));
expect(sendgridRequests, isEmpty);
});
test('ok - ignore wrong event', () async {
final rs = await sendEvent(
body: makeLabeledEvent(labelName: 'bug'), event: 'wrong-type');
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests, isEmpty);
});
test('ok - ignore wrong action', () async {
final rs = await sendEvent(body: {'action': 'deleted'});
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests, isEmpty);
});
test('ok - no subscribers', () async {
final rs =
await sendEvent(body: makeLabeledEvent(labelName: 'performance'));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests, isEmpty);
});
test('ok - single subscriber', () async {
final rs = await sendEvent(body: makeLabeledEvent(labelName: 'bug'));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
final request = sendgridRequests.first;
expect(request.headers['authorization']!.single,
equals('Bearer ${Platform.environment['SENDGRID_SECRET']!}'));
expect(request.body['subject'], contains('bug'));
expect(request.body['subject'], contains('#1'));
expect(request.body['subject'], contains('TEST ISSUE TITLE'));
expect(request.body['personalizations'].single,
emailAsPersonalization('second@example.com'));
});
test('ok - multiple subscribers', () async {
final rs = await sendEvent(
body: makeLabeledEvent(labelName: 'feature', number: 2));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
final request = sendgridRequests.first;
expect(request.headers['authorization'],
contains('Bearer ${Platform.environment['SENDGRID_SECRET']!}'));
expect(request.body['subject'], contains('feature'));
expect(request.body['subject'], contains('#2'));
expect(request.body['subject'], contains('TEST ISSUE TITLE'));
expect(
request.body['personalizations'],
unorderedEquals([
emailAsPersonalization('second@example.com'),
emailAsPersonalization('first@example.com')
]));
});
test('ok - issue opened', () async {
final rs = await sendEvent(body: makeIssueOpenedEvent());
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
final request = sendgridRequests.first;
expect(request.headers['authorization'],
contains('Bearer ${Platform.environment['SENDGRID_SECRET']!}'));
expect(request.body['subject'], contains('#1'));
expect(request.body['subject'], contains('TEST ISSUE TITLE'));
expect(request.body['personalizations'].single,
emailAsPersonalization('second@example.com'));
expect(
request.body['content']
.firstWhere((c) => c['type'] == 'text/plain')['value'],
contains('Matches keyword: third_party/dart'));
});
test('ok - issue opened - test underscore as word boundary', () async {
final rs = await sendEvent(
body: makeIssueOpenedEvent(
body: 'xyz, something_jit_something_else, foobar'));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
final request = sendgridRequests.first;
expect(request.headers['authorization']!.single,
equals('Bearer ${Platform.environment['SENDGRID_SECRET']!}'));
expect(request.body['subject'], contains('#1'));
expect(request.body['subject'], contains('TEST ISSUE TITLE'));
expect(request.body['personalizations'].single,
emailAsPersonalization('second@example.com'));
expect(
request.body['content']
.firstWhere((c) => c['type'] == 'text/plain')['value'],
contains('Matches keyword: jit'));
});
test('ok - issue opened - no matching keyword', () async {
final rs = await sendEvent(
body: makeIssueOpenedEvent(
body: 'xyz, somethingjitsomething_else, foobar'));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(0));
});
test('ok - issue opened - crash', () async {
final rs = await sendEvent(body: makeIssueOpenedEvent(body: '''
I had Flutter engine c_r_a_s_h on me with the following message
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Such c_r_a_s_h
Much information
'''));
expect(rs.statusCode, equals(HttpStatus.ok));
expect(sendgridRequests.length, equals(1));
final request = sendgridRequests.first;
expect(
request.body['content']
.firstWhere((c) => c['type'] == 'text/plain')['value'],
contains('Matches keyword: crash'));
});
}
/// Request that arrived to the mock SendGrid server.
class SendgridRequest {
final HttpHeaders headers;
final Map<String, dynamic> body;
SendgridRequest({required this.headers, required this.body});
Map<String, dynamic> toJson() => {'headers': headers, 'body': body};
}
Map<String, dynamic> emailAsPersonalization(String email) =>
Personalization([Address(email)]).toJson();