| // 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(); |