| // Copyright 2019 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 'package:cocoon_common/guard_status.dart'; |
| import 'package:cocoon_common/rpc_model.dart'; |
| import 'package:flutter_dashboard/service/cocoon.dart'; |
| import 'package:flutter_dashboard/state/presubmit.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import '../utils/mocks.mocks.dart'; |
| |
| void main() { |
| late PresubmitState presubmitState; |
| late MockCocoonService mockCocoonService; |
| late MockFirebaseAuthService mockAuthService; |
| |
| setUp(() { |
| mockCocoonService = MockCocoonService(); |
| mockAuthService = MockFirebaseAuthService(); |
| |
| when(mockAuthService.isAuthenticated).thenReturn(false); |
| when(mockAuthService.idToken).thenAnswer((_) async => 'fakeToken'); |
| |
| // Default stubs to avoid MissingStubError during auto-fetches |
| when( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| pr: anyNamed('pr'), |
| repo: anyNamed('repo'), |
| owner: anyNamed('owner'), |
| ), |
| ).thenAnswer( |
| (_) async => const CocoonResponse<List<PresubmitGuardSummary>>.data([]), |
| ); |
| when( |
| mockCocoonService.fetchPresubmitGuard( |
| sha: anyNamed('sha'), |
| repo: anyNamed('repo'), |
| owner: anyNamed('owner'), |
| ), |
| ).thenAnswer( |
| (_) async => const CocoonResponse<PresubmitGuardResponse>.data(null), |
| ); |
| when( |
| mockCocoonService.fetchPresubmitCheckDetails( |
| checkRunId: anyNamed('checkRunId'), |
| buildName: anyNamed('buildName'), |
| repo: anyNamed('repo'), |
| owner: anyNamed('owner'), |
| ), |
| ).thenAnswer( |
| (_) async => const CocoonResponse<List<PresubmitCheckResponse>>.data([]), |
| ); |
| when( |
| mockCocoonService.fetchCommitStatuses( |
| repo: anyNamed('repo'), |
| branch: anyNamed('branch'), |
| ), |
| ).thenAnswer( |
| (_) async => const CocoonResponse<List<CommitStatus>>.data([]), |
| ); |
| |
| presubmitState = PresubmitState( |
| cocoonService: mockCocoonService, |
| authService: mockAuthService, |
| ); |
| }); |
| |
| test('PresubmitState initializes with default values', () { |
| expect(presubmitState.repo, 'flutter'); |
| expect(presubmitState.pr, isNull); |
| expect(presubmitState.sha, isNull); |
| expect(presubmitState.isLoading, isFalse); |
| expect(presubmitState.guardResponse, isNull); |
| expect(presubmitState.availableSummaries, isEmpty); |
| }); |
| |
| test( |
| 'PresubmitState update method updates properties and notifies listeners', |
| () { |
| var notified = false; |
| presubmitState.addListener(() => notified = true); |
| |
| presubmitState.update(repo: 'cocoon', pr: '123', sha: 'abc'); |
| |
| expect(presubmitState.repo, 'cocoon'); |
| expect(presubmitState.pr, '123'); |
| expect(presubmitState.sha, 'abc'); |
| expect(notified, isTrue); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchAvailableShas updates availableSummaries and notifies listeners', |
| () async { |
| const summaries = [ |
| PresubmitGuardSummary( |
| commitSha: 'sha1', |
| creationTime: 123, |
| guardStatus: GuardStatus.succeeded, |
| ), |
| ]; |
| when( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| pr: '123', |
| repo: 'flutter', |
| ), |
| ).thenAnswer( |
| (_) async => |
| const CocoonResponse<List<PresubmitGuardSummary>>.data(summaries), |
| ); |
| |
| presubmitState.pr = '123'; |
| var notified = false; |
| presubmitState.addListener(() => notified = true); |
| |
| await presubmitState.fetchAvailableShas(); |
| |
| expect(presubmitState.availableSummaries, summaries); |
| expect(notified, isTrue); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchGuardStatus updates guardResponse and notifies listeners', |
| () async { |
| const guardResponse = PresubmitGuardResponse( |
| prNum: 123, |
| author: 'dash', |
| guardStatus: GuardStatus.succeeded, |
| checkRunId: 456, |
| stages: [], |
| ); |
| when( |
| mockCocoonService.fetchPresubmitGuard(sha: 'sha1', repo: 'flutter'), |
| ).thenAnswer( |
| (_) async => |
| const CocoonResponse<PresubmitGuardResponse>.data(guardResponse), |
| ); |
| |
| presubmitState.sha = 'sha1'; |
| var notified = false; |
| presubmitState.addListener(() => notified = true); |
| |
| await presubmitState.fetchGuardStatus(); |
| |
| expect(presubmitState.guardResponse, guardResponse); |
| expect(presubmitState.isLoading, isFalse); |
| expect(notified, isTrue); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchCheckDetails updates checks and notifies listeners', |
| () async { |
| final checks = [ |
| PresubmitCheckResponse( |
| attemptNumber: 1, |
| buildName: 'check1', |
| creationTime: 0, |
| status: 'Succeeded', |
| ), |
| ]; |
| const guardResponse = PresubmitGuardResponse( |
| prNum: 123, |
| author: 'dash', |
| guardStatus: GuardStatus.succeeded, |
| checkRunId: 456, |
| stages: [], |
| ); |
| presubmitState.setGuardResponseForTest(guardResponse); |
| |
| when( |
| mockCocoonService.fetchPresubmitCheckDetails( |
| checkRunId: 456, |
| buildName: 'check1', |
| repo: 'flutter', |
| ), |
| ).thenAnswer( |
| (_) async => CocoonResponse<List<PresubmitCheckResponse>>.data(checks), |
| ); |
| |
| presubmitState.selectCheck('check1'); |
| var notified = false; |
| presubmitState.addListener(() => notified = true); |
| |
| await presubmitState.fetchCheckDetails(); |
| |
| expect(presubmitState.checks, checks); |
| expect(notified, isTrue); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState update does not notify if values are the same and no fetch triggered', |
| () { |
| presubmitState.update(repo: 'flutter', pr: '123', sha: 'sha1'); |
| var notifiedCount = 0; |
| presubmitState.addListener(() => notifiedCount++); |
| |
| presubmitState.update(repo: 'flutter', pr: '123', sha: 'sha1'); |
| |
| expect(notifiedCount, 0); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchAvailableShas returns early if pr is null', |
| () async { |
| await presubmitState.fetchAvailableShas(); |
| verifyNever( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| repo: anyNamed('repo'), |
| pr: anyNamed('pr'), |
| ), |
| ); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchAvailableShas defaults sha to latest if sha is null', |
| () async { |
| const summaries = [ |
| PresubmitGuardSummary( |
| commitSha: 'latest', |
| creationTime: 123, |
| guardStatus: GuardStatus.succeeded, |
| ), |
| ]; |
| when( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| pr: '123', |
| repo: 'flutter', |
| ), |
| ).thenAnswer( |
| (_) async => |
| const CocoonResponse<List<PresubmitGuardSummary>>.data(summaries), |
| ); |
| |
| presubmitState.pr = '123'; |
| await presubmitState.fetchAvailableShas(); |
| |
| expect(presubmitState.sha, 'latest'); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState fetchGuardStatus returns early if sha is null', |
| () async { |
| await presubmitState.fetchGuardStatus(); |
| verifyNever( |
| mockCocoonService.fetchPresubmitGuard( |
| repo: anyNamed('repo'), |
| sha: anyNamed('sha'), |
| ), |
| ); |
| }, |
| ); |
| |
| test('PresubmitState refresh timer management', () async { |
| presubmitState.addListener(() {}); // Trigger timer start |
| expect(presubmitState.refreshTimer, isNotNull); |
| |
| presubmitState.dispose(); |
| expect(presubmitState.refreshTimer?.isActive, isFalse); |
| }); |
| |
| test( |
| 'PresubmitState refreshes on auth change when becoming authenticated', |
| () async { |
| // Create a local state initialized with PR |
| final localPresubmitState = PresubmitState( |
| cocoonService: mockCocoonService, |
| authService: mockAuthService, |
| pr: '123', |
| ); |
| // Wait for constructor fetch |
| await Future<void>.delayed(Duration.zero); |
| clearInteractions(mockCocoonService); |
| |
| // Now login |
| when(mockAuthService.isAuthenticated).thenReturn(true); |
| localPresubmitState.onAuthChanged(); |
| |
| // onAuthChanged triggers _fetchRefreshUpdate -> fetchIfNeeded -> fetchAvailableShas |
| // which is async. We just need to wait for it to complete. |
| await localPresubmitState.fetchAvailableShas(); |
| |
| verify( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| repo: anyNamed('repo'), |
| pr: anyNamed('pr'), |
| ), |
| ).called(greaterThanOrEqualTo(1)); |
| }, |
| ); |
| |
| test( |
| 'PresubmitState refreshes on auth change when becoming unauthenticated', |
| () async { |
| when(mockAuthService.isAuthenticated).thenReturn(true); |
| final localPresubmitState = PresubmitState( |
| cocoonService: mockCocoonService, |
| authService: mockAuthService, |
| pr: '123', |
| ); |
| await Future<void>.delayed(Duration.zero); |
| clearInteractions(mockCocoonService); |
| |
| when(mockAuthService.isAuthenticated).thenReturn(false); |
| localPresubmitState.onAuthChanged(); |
| await Future<void>.delayed(Duration.zero); |
| |
| // Should not refresh when logout |
| verifyNever( |
| mockCocoonService.fetchPresubmitGuardSummaries( |
| repo: anyNamed('repo'), |
| pr: anyNamed('pr'), |
| ), |
| ); |
| }, |
| ); |
| |
| test('rerunFailedJob triggers API and updates loading state', () async { |
| when( |
| mockCocoonService.rerunFailedJob( |
| idToken: anyNamed('idToken'), |
| repo: anyNamed('repo'), |
| pr: anyNamed('pr'), |
| buildName: anyNamed('buildName'), |
| ), |
| ).thenAnswer((_) async => const CocoonResponse<void>.data(null)); |
| |
| presubmitState.pr = '123'; |
| final error = await presubmitState.rerunFailedJob('check1'); |
| |
| expect(error, isNull); |
| verify( |
| mockCocoonService.rerunFailedJob( |
| idToken: anyNamed('idToken'), |
| repo: 'flutter', |
| pr: 123, |
| buildName: 'check1', |
| ), |
| ).called(1); |
| }); |
| |
| test('rerunAllFailedJobs triggers API and updates loading state', () async { |
| when( |
| mockCocoonService.rerunAllFailedJobs( |
| idToken: anyNamed('idToken'), |
| repo: anyNamed('repo'), |
| pr: anyNamed('pr'), |
| ), |
| ).thenAnswer((_) async => const CocoonResponse<void>.data(null)); |
| |
| presubmitState.pr = '123'; |
| final error = await presubmitState.rerunAllFailedJobs(); |
| |
| expect(error, isNull); |
| verify( |
| mockCocoonService.rerunAllFailedJobs( |
| idToken: anyNamed('idToken'), |
| repo: 'flutter', |
| pr: 123, |
| ), |
| ).called(1); |
| }); |
| } |