Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@

# VSCode
.vscode/

build/*
1 change: 1 addition & 0 deletions app_dart/lib/cocoon_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export 'src/request_handlers/get_green_commits.dart';
export 'src/request_handlers/get_release_branches.dart';
export 'src/request_handlers/get_repos.dart';
export 'src/request_handlers/get_status.dart';
export 'src/request_handlers/get_test_suppression.dart';
export 'src/request_handlers/github/webhook_subscription.dart';
export 'src/request_handlers/github_rate_limit_status.dart';
export 'src/request_handlers/github_webhook.dart';
Expand Down
6 changes: 6 additions & 0 deletions app_dart/lib/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ Server createServer({
),
ttl: const Duration(seconds: 15),
),
'/api/public/get-test-suppression': CacheRequestHandler(
cache: cache,
config: config,
delegate: GetTestSuppression(config: config, firestore: firestore),
ttl: const Duration(seconds: 15),
),
'/api/public/update-discord-status': CacheRequestHandler(
cache: cache,
config: config,
Expand Down
71 changes: 71 additions & 0 deletions app_dart/lib/src/request_handlers/get_test_suppression.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2025 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:async';

import 'package:github/github.dart';
import 'package:meta/meta.dart';

import '../../cocoon_service.dart';
import '../model/firestore/suppressed_test.dart';
import '../request_handling/exceptions.dart';

/// Request handler to get a list of suppressed tests.
///
/// GET /api/public/get-test-suppression
///
/// Parameters:
/// repo: (string in query) default: 'flutter/flutter'. Name of the repo.
///
/// Response:
/// [
/// {
/// "name": "foo_test",
/// "repository": "flutter/flutter",
/// "issueLink": "...",
/// "createTimestamp": 123456789
/// }
/// ]
@immutable
final class GetTestSuppression extends RequestHandler {
const GetTestSuppression({required super.config, required this.firestore});

final FirestoreService firestore;

static const String kRepoParam = 'repo';

@override
Future<Response> get(Request request) async {
if (!config.flags.dynamicTestSuppression) {
throw const MethodNotAllowed('Tree status suppression is disabled.');
}

final repoName =
request.uri.queryParameters[kRepoParam] ?? Config.flutterSlug.fullName;
final slug = RepositorySlug.full(repoName);

final suppressedTests = await SuppressedTest.getSuppressedTests(
firestore,
slug.fullName,
);

return Response.json([
for (final test in suppressedTests)
{
'name': test.testName,
'repository': test.repository,
'issueLink': test.issueLink,
'createTimestamp': test.createTimestamp.millisecondsSinceEpoch,
'updates': [
for (var update in test.updates)
{
...update,
'updateTimestamp':
update['updateTimestamp'].millisecondsSinceEpoch,
},
],
},
]);
}
}
151 changes: 151 additions & 0 deletions app_dart/test/request_handlers/get_test_suppression_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2025 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:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/src/model/firestore/suppressed_test.dart';

import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/flags/dynamic_config.dart';

import 'package:test/test.dart';

import '../src/fake_config.dart';
import '../src/request_handling/api_request_handler_tester.dart';
import '../src/service/fake_firestore_service.dart';

void main() {
useTestLoggerPerTest();

late FakeFirestoreService firestore;
late ApiRequestHandlerTester tester;
late GetTestSuppression handler;
late FakeConfig config;

final fakeNow = DateTime.now().toUtc();

setUp(() {
firestore = FakeFirestoreService();
tester = ApiRequestHandlerTester();
config = FakeConfig(
dynamicConfig: DynamicConfig(dynamicTestSuppression: true),
);

handler = GetTestSuppression(config: config, firestore: firestore);
});

test('throws MethodNotAllowed if feature flag is disabled', () async {
config = FakeConfig(
dynamicConfig: DynamicConfig(dynamicTestSuppression: false),
);
handler = GetTestSuppression(config: config, firestore: firestore);

expect(() => tester.get(handler), throwsA(isA<MethodNotAllowed>()));
});

test('returns empty list if no suppressed tests', () async {
final response = await tester.get(handler);
final body = await utf8.decodeStream(response.body);
expect(body, '[]');
});

test('returns suppressed tests for default repo', () async {
final doc = SuppressedTest(
name: 'foo_test',
repository: 'flutter/flutter',
issueLink: 'https://github.com/flutter/flutter/issues/123',
isSuppressed: true,
createTimestamp: fakeNow,
updates: [],
)..name = '$kDatabase/documents/${SuppressedTest.kCollectionId}/foo_test';
firestore.putDocument(doc);

// Add a non-suppressed test
final doc2 = SuppressedTest(
name: 'bar_test',
repository: 'flutter/flutter',
issueLink: 'https://github.com/flutter/flutter/issues/456',
isSuppressed: false,
createTimestamp: fakeNow,
updates: [],
)..name = '$kDatabase/documents/${SuppressedTest.kCollectionId}/bar_test';
firestore.putDocument(doc2);

final response = await tester.get(handler);
final body = await utf8.decodeStream(response.body);
final json = jsonDecode(body) as List<dynamic>;

expect(json.length, 1);
expect(json[0]['name'], 'foo_test');
expect(json[0]['repository'], 'flutter/flutter');
expect(
json[0]['issueLink'],
'https://github.com/flutter/flutter/issues/123',
);
});

test('returns suppressed tests for specific repo', () async {
tester.request.uri = Uri(queryParameters: {'repo': 'flutter/engine'});

firestore.putDocuments([
SuppressedTest(
name: 'engine_test',
repository: 'flutter/engine',
issueLink: 'https://github.com/flutter/flutter/issues/789',
isSuppressed: true,
createTimestamp: fakeNow,
updates: [],
)
..name =
'$kDatabase/documents/${SuppressedTest.kCollectionId}/engine_test',
SuppressedTest(
name: 'engine_test2',
repository: 'flutter/engine',
issueLink: 'https://github.com/flutter/flutter/issues/123',
isSuppressed: true,
createTimestamp: fakeNow,
updates: [
{
'user': 'fake@example.com',
'note': 'This is a note',
'updateTimestamp': fakeNow.toUtc(),
'action': 'SUPPRESS',
},
{
'user': 'fu@example.com',
'note': 'this is an update',
'updateTimestamp': fakeNow.toUtc().add(
const Duration(minutes: 42),
),
'action': 'SUPPRESS',
},
],
)
..name =
'$kDatabase/documents/${SuppressedTest.kCollectionId}/engine_test2',
]);

final response = await tester.get(handler);
final body = await utf8.decodeStream(response.body);
final json = jsonDecode(body) as List<dynamic>;

expect(json.length, 2);
expect(json[0]['name'], 'engine_test');
expect(json[0]['repository'], 'flutter/engine');
expect(json[1]['name'], 'engine_test2');
expect(json[1]['repository'], 'flutter/engine');

final updates = json[1]['updates'] as List<dynamic>;
expect(updates.length, 2);
expect(updates[0]['user'], 'fake@example.com');
expect(updates[0]['updateTimestamp'], fakeNow.millisecondsSinceEpoch);
expect(updates[1]['user'], 'fu@example.com');
expect(
updates[1]['updateTimestamp'],
fakeNow.add(const Duration(minutes: 42)).millisecondsSinceEpoch,
);
});
}