blob: cbb6d2a3494c6fb49e862379365ba05fdb98d714 [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:convert';
import 'dart:js_util';
import 'package:firebase_admin_interop/firebase_admin_interop.dart';
import 'package:js/js.dart';
import 'package:node_http/node_http.dart' as http_client;
import 'package:node_interop/http.dart' show http, HttpServer;
import 'package:node_interop/node_interop.dart';
import 'package:node_interop/util.dart' show dartify, jsify;
import 'package:node_io/node_io.dart' hide HttpServer;
import 'package:test/test.dart';
import 'package:github_label_notifier/github_utils.dart';
// Note: must match project-id used in test.sh
final String projectId = 'github-label-notifier';
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.
HttpServer sendgridMockServer;
final sendgridRequests = <SendgridRequest>[];
setUpAll(() async {
// Populate firestore with mock data.
final firestore = FirebaseAdmin.instance
.initializeApp(AppOptions(projectId: projectId))
.firestore();
final subscriptions = firestore.collection('github-label-subscriptions');
for (var doc in (await subscriptions.get()).documents) {
await doc.reference.delete();
}
await subscriptions.add(DocumentData.fromMap({
'email': 'first@example.com',
'subscriptions': [
'dart-lang/webhook-test:some-label',
'dart-lang/webhook-test:feature',
],
}));
await subscriptions.add(DocumentData.fromMap({
'email': 'second@example.com',
'subscriptions': [
'dart-lang/webhook-test:bug',
'dart-lang/webhook-test:feature',
'dart-lang/webhook-test:special-label',
],
}));
await firestore
.document('github-keyword-subscriptions/dart-lang\$webhook-test')
.setData(DocumentData.fromMap({
'keywords': [
'jit',
'aot',
'third_party/dart',
],
'label': 'special-label',
}));
// 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.
sendgridMockServer = http.createServer(allowInterop((rq, rs) {
final body = [];
rq.on('data', allowInterop((chunk) {
body.add(chunk);
}));
rq.on('end', allowInterop(() {
sendgridRequests.add(SendgridRequest(
headers: dartify(rq.headers),
body: jsonDecode(_bufferToString(Buffer.concat(body)))));
// Reply with 200 OK.
rs.writeHead(200, 'OK', jsify({'Content-Type': 'text/plain'}));
rs.write('OK');
rs.end();
}));
}));
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');
sendgridMockServer.listen(serverUri.port, serverUri.host);
});
setUp(() {
sendgridRequests.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<http_client.Response> sendEvent({
String delivery = '7e754400-fa9b-11e9-9421-bb996f50ac6b',
String event = 'issues',
String signature,
Map<String, dynamic> body,
}) async {
final headers = {
'content-type': 'application/json',
};
if (delivery != null) {
headers['X-GitHub-Delivery'] = delivery;
}
if (event != null) {
headers['X-GitHub-Event'] = event;
}
if (signature != '') {
headers['X-Hub-Signature'] = signature ?? signEvent(body);
}
// Note: there does not seem to be a good way to get a trigger uri for
// a function when running the test suite.
return await http_client.post(
'http://localhost:5001/$projectId/us-central1/githubWebhook',
headers: headers,
body: jsonEncode(body));
}
// 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',
},
};
test('signing', () {
expect(signEvent(makeLabeledEvent(labelName: 'bug')),
equals('sha1=76af51cdb9c7a43b146d4df721ac8f83e53182e5'));
});
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 rq = sendgridRequests.first;
expect(rq.headers['authorization'],
equals('Bearer ' + Platform.environment['SENDGRID_SECRET']));
expect(rq.body['subject'], contains('bug'));
expect(rq.body['subject'], contains('#1'));
expect(rq.body['subject'], contains('TEST ISSUE TITLE'));
expect(
rq.body['personalizations'],
unorderedEquals([
{
'to': [
{'email': '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 rq = sendgridRequests.first;
expect(rq.headers['authorization'],
equals('Bearer ' + Platform.environment['SENDGRID_SECRET']));
expect(rq.body['subject'], contains('feature'));
expect(rq.body['subject'], contains('#2'));
expect(rq.body['subject'], contains('TEST ISSUE TITLE'));
expect(
rq.body['personalizations'],
unorderedEquals([
{
'to': [
{'email': 'second@example.com'}
]
},
{
'to': [
{'email': '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 rq = sendgridRequests.first;
expect(rq.headers['authorization'],
equals('Bearer ' + Platform.environment['SENDGRID_SECRET']));
expect(rq.body['subject'], contains('#1'));
expect(rq.body['subject'], contains('TEST ISSUE TITLE'));
expect(
rq.body['personalizations'],
unorderedEquals([
{
'to': [
{'email': 'second@example.com'}
]
}
]));
expect(
rq.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 rq = sendgridRequests.first;
expect(rq.headers['authorization'],
equals('Bearer ' + Platform.environment['SENDGRID_SECRET']));
expect(rq.body['subject'], contains('#1'));
expect(rq.body['subject'], contains('TEST ISSUE TITLE'));
expect(
rq.body['personalizations'],
unorderedEquals([
{
'to': [
{'email': 'second@example.com'}
]
}
]));
expect(
rq.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));
});
}
/// Helper method to convert a [Buffer] to a [String].
///
/// Just calling [Buffer.toString] invokes the wrong method
/// (see https://dartbug.com/30096).
String _bufferToString(Buffer buf) {
return callMethod(buf, 'toString', []);
}
/// Request that arrived to the mock SendGrid server.
class SendgridRequest {
final Map<String, dynamic> headers;
final Map<String, dynamic> body;
SendgridRequest({this.headers, this.body});
}