blob: aa1a8814c1ff6ad451ec98286734e9ce41abf099 [file] [edit]
// Copyright 2023 The Flutter Authors. 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 'package:auto_submit/configuration/repository_configuration.dart';
import 'package:auto_submit/model/auto_submit_query_result.dart';
import 'package:auto_submit/validations/approval.dart';
import 'package:auto_submit/validations/validation.dart';
import 'package:cocoon_server_test/mocks.dart';
import 'package:cocoon_server_test/test_logging.dart';
import 'package:test/test.dart';
import '../configuration/repository_configuration_data.dart';
import '../requests/github_webhook_test_data.dart';
import '../src/service/fake_config.dart';
import '../src/service/fake_github_service.dart';
import '../src/service/fake_graphql_client.dart';
import 'approval_test_data.dart';
void main() {
useTestLoggerPerTest();
late Approval approval;
late FakeConfig config;
late FakeGithubService githubService;
late FakeGraphQLClient githubGraphQLClient;
setUp(() {
final gitHub = MockGitHub();
githubGraphQLClient = FakeGraphQLClient();
githubService = FakeGithubService();
config = FakeConfig(
githubService: githubService,
githubGraphQLClient: githubGraphQLClient,
githubClient: gitHub,
);
config.repositoryConfigurationMock = RepositoryConfiguration.fromYaml(
sampleConfigNoOverride,
);
approval = Approval(config: config);
});
group('Approval group tests', () {
Future<ValidationResult> computeValidationResult(String review) async {
final queryResultJsonDecode = jsonDecode(review) as Map<String, dynamic>;
final queryResult = QueryResult.fromJson(queryResultJsonDecode);
final pullRequest = generatePullRequest();
return approval.validate(queryResult, pullRequest);
}
test('Author and reviewer in flutter-hackers, pr approved', () async {
final review = constructSingleReviewerReview(reviewState: 'APPROVED');
// githubService.isTeamMemberMockList = [true, true];
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['keyonghan'] = true;
final result = await computeValidationResult(review);
expect(result.result, isTrue);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has met approval requirements for merging.',
),
isTrue,
);
});
test(
'Author is a NON member and reviewer is a member, needs 1 more review',
() async {
// githubService.isTeamMemberMockList = [true, true];
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['keyonghan'] = true;
final review = constructSingleReviewerReview(reviewState: 'APPROVED');
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 1 more review'), isTrue);
},
);
test(
'Author is a NON member and reviewer is a NON member, needs 2 more reviews',
() async {
// githubService.isTeamMemberMockList = [true, true];
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['keyonghan'] = false;
final review = constructSingleReviewerReview(reviewState: 'APPROVED');
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 2 more review'), isTrue);
},
);
test(
'Author is a member and reviewer is NON member, needs 1 more review',
() async {
// githubService.isTeamMemberMockList = [true, true];
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['keyonghan'] = false;
final review = constructSingleReviewerReview(reviewState: 'APPROVED');
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.IGNORE_TEMPORARILY);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 1 more review'), isTrue);
},
);
test(
'Author is NON member and reviewers are members, pr approved',
() async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['author2'] = true;
githubService.isTeamMemberMockMap['author3'] = true;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isTrue);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has met approval requirements for merging.',
),
isTrue,
);
},
);
test(
'Author is NON member and one reviewer is a NON member, needs 1 more review',
() async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['author2'] = true;
githubService.isTeamMemberMockMap['author3'] = false;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 1 more review'), isTrue);
},
);
test(
'Author is member and reviewers are NON members, needs 1 more review',
() async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['author2'] = false;
githubService.isTeamMemberMockMap['author3'] = false;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.IGNORE_TEMPORARILY);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 1 more review'), isTrue);
},
);
test(
'Author is NON member and reviewers are NON members, needs 2 reviews',
() async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['author2'] = false;
githubService.isTeamMemberMockMap['author3'] = false;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 2 more review'), isTrue);
},
);
test('Verify author review count does not go negative', () async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['ricardoamador'] = false;
githubService.isTeamMemberMockMap['keyonghan'] = false;
githubService.isTeamMemberMockMap['nehalvpatel'] = false;
final review = constructMultipleReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
thirdReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('needs 2 more review'), isTrue);
});
test('Verify author review count does not go negative', () async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['ricardoamador'] = true;
githubService.isTeamMemberMockMap['keyonghan'] = true;
githubService.isTeamMemberMockMap['nehalvpatel'] = true;
final review = constructMultipleReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
thirdReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isTrue);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has met approval requirements for merging.',
),
isTrue,
);
});
test(
'Author is member and member requests changes, 1 review is needed',
() async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['keyonghan'] = true;
final review = constructSingleReviewerReview(
reviewState: 'CHANGES_REQUESTED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('Changes were requested by'), isTrue);
},
);
test(
'Author is member and two member reviews, 1 change request, review is not approved',
() async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['author2'] = true;
githubService.isTeamMemberMockMap['author3'] = true;
final review = constructTwoReviewerReview(
reviewState: 'CHANGES_REQUESTED',
secondReviewState: 'APPROVED',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging.',
),
isTrue,
);
expect(result.message.contains('Changes were requested by'), isTrue);
},
);
test(
'Multiple approving reviews from the same author are counted only 1 time.',
() async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['ricardoamador'] = true;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
author: 'ricardoamador',
secondAuthor: 'ricardoamador',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.',
),
isTrue,
);
},
);
test('Successful review overwrites previous changes requested.', () async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['keyonghan'] = true;
githubService.isTeamMemberMockMap['jmagman'] = true;
final result = await computeValidationResult(multipleReviewsSameAuthor);
expect(result.result, isTrue);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has met approval requirements for merging.',
),
isTrue,
);
});
test('Author cannot review own pr', () async {
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['author3'] = true;
final review = constructTwoReviewerReview(
reviewState: 'APPROVED',
secondReviewState: 'APPROVED',
author: 'author1',
);
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(result.action, Action.REMOVE_LABEL);
expect(
result.message.contains(
'This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers',
),
isTrue,
);
});
test(
'DISMISSED review should override previous CHANGES_REQUESTED',
() async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['jmagman'] = true;
githubService.isTeamMemberMockMap['Z'] = true;
const bugScenario = '''
{
"repository": {
"pullRequest": {
"author": {
"login": "author1"
},
"id": "PR_kwDOA8VHis43rs4_",
"title": "Test PR",
"commits": {
"nodes":[
{
"commit": {
"abbreviatedOid": "4009ecc",
"oid": "4009ecc0b6dbf5cb19cb97472147063e7368ec10",
"status": {
"contexts":[]
}
}
}
]
},
"reviews": {
"nodes": [
{
"author": {
"login": "jmagman"
},
"state": "CHANGES_REQUESTED"
},
{
"author": {
"login": "jmagman"
},
"state": "DISMISSED"
},
{
"author": {
"login": "Z"
},
"state": "APPROVED"
}
]
}
}
}
}
''';
final result = await computeValidationResult(bugScenario);
expect(result.result, isTrue);
},
);
test(
'COMMENTED review should NOT clear previous CHANGES_REQUESTED',
() async {
githubService.isTeamMemberMockMap['author1'] = true;
githubService.isTeamMemberMockMap['jmagman'] = true;
githubService.isTeamMemberMockMap['Z'] = true;
const scenario = '''
{
"repository": {
"pullRequest": {
"author": {
"login": "author1"
},
"id": "PR_kwDOA8VHis43rs4_",
"title": "Test PR",
"reviews": {
"nodes": [
{
"author": {
"login": "jmagman"
},
"state": "CHANGES_REQUESTED"
},
{
"author": {
"login": "jmagman"
},
"state": "COMMENTED"
},
{
"author": {
"login": "Z"
},
"state": "APPROVED"
}
]
}
}
}
}
''';
final result = await computeValidationResult(scenario);
expect(result.result, isFalse);
expect(result.message, contains('Changes were requested by {jmagman}'));
},
);
test(
'pullRequestApprovalRequirementsMessage takes approval_group from config',
() async {
config.repositoryConfigurationMock = RepositoryConfiguration.fromYaml(
'''
default_branch: main
allow_config_override: false
auto_approval_accounts:
- dependabot[bot]
- dependabot
- DartDevtoolWorkflowBot
approving_reviews: 2
approval_group: custom-group
run_ci: true
support_no_review_revert: true
required_checkruns_on_revert:
- ci.yaml validation
''',
);
approval = Approval(config: config);
githubService.isTeamMemberMockMap['author1'] = false;
githubService.isTeamMemberMockMap['keyonghan'] = false;
final review = constructSingleReviewerReview(reviewState: 'APPROVED');
final result = await computeValidationResult(review);
expect(result.result, isFalse);
expect(
result.message.contains(
'part of custom-group or two member reviews if the author is not a member of custom-group',
),
isTrue,
);
},
);
});
}