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
8 changes: 7 additions & 1 deletion app_flutter/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Build Dashboard v2 Frontend

[Design Doc](https://flutter.dev/go/build-dashboard-v2)
[Design Doc](https://flutter.dev/go/build-dashboard-v2)

## Running for web locally

`flutter run -d chrome --web-port=8080`

Must run on port 8080 for Google Sign In to work since that is the only enabled port for localhost.
28 changes: 28 additions & 0 deletions app_flutter/lib/build_dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:app_flutter/service/google_authentication.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -52,6 +53,9 @@ class BuildDashboard extends StatelessWidget {
backgroundColor: buildState.isTreeBuilding.data
? theme.primaryColor
: theme.errorColor,
actions: <Widget>[
UserAvatar(buildState: buildState),
],
),
body: Column(
children: const <Widget>[
Expand All @@ -62,3 +66,27 @@ class BuildDashboard extends StatelessWidget {
);
}
}

/// Widget for displaying sign in information for the current user.
///
/// If logged in, it will display the user's avatar. Otherwise, it will show
/// a button for sign in.
class UserAvatar extends StatelessWidget {
const UserAvatar({@required this.buildState, Key key}) : super(key: key);

final FlutterBuildState buildState;

@override
Widget build(BuildContext context) {
final GoogleSignInService authService = buildState.authService;

if (authService.isAuthenticated) {
return Image.network(authService.avatarUrl);
}

return FlatButton(
child: const Text('Sign in'),
onPressed: () => buildState.signIn(),
);
}
}
53 changes: 53 additions & 0 deletions app_flutter/lib/service/google_authentication.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2019 The Chromium 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:google_sign_in_all/google_sign_in_all.dart';

/// Service class for interacting with Google Sign In authentication for Cocoon backend.
class GoogleSignInService {
/// Creates a new [GoogleSignIn].
GoogleSignInService({GoogleSignIn googleSignIn})
: _googleSignIn = googleSignIn ??
setupGoogleSignIn(
scopes: _googleScopes,
webClientId:
'308150028417-vlj9mqlm3gk1d03fb0efif1fu5nagdtt.apps.googleusercontent.com',
);

/// A list of Google API OAuth Scopes this project needs access to.
///
/// Currently, the project shows just basic user profile information
/// when logged in.
///
/// See https://developers.google.com/identity/protocols/googlescopes
static const List<String> _googleScopes = <String>[
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
];

// TODO(chillers): Switch to official Flutter plugin when it supports web.
final GoogleSignIn _googleSignIn;

AuthCredentials _credentials;

GoogleAccount _user;

/// Whether or not the application has been signed in to.
bool get isAuthenticated => _credentials?.accessToken != null;

/// The profile photo url of the current user signed in.
String get avatarUrl => _user?.photoUrl;

/// The email of the current user signed in.
String get email => _user?.email;

/// Authentication token to be sent to Cocoon Backend to verify API calls.
String get accessToken => _credentials?.accessToken;

/// Initiate the Google Sign In process.
Future<void> signIn() async {
_credentials = await _googleSignIn.signIn();
_user = await _googleSignIn.getCurrentUser();
}
}
16 changes: 14 additions & 2 deletions app_flutter/lib/state/flutter_build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ import 'package:flutter/foundation.dart';
import 'package:cocoon_service/protos.dart' show CommitStatus;

import '../service/cocoon.dart';
import '../service/google_authentication.dart';

/// State for the Flutter Build Dashboard
class FlutterBuildState extends ChangeNotifier {
/// Creates a new [FlutterBuildState].
///
/// If [CocoonService] is not specified, a new [CocoonService] instance is created.
FlutterBuildState({CocoonService cocoonService})
: _cocoonService = cocoonService ?? CocoonService();
FlutterBuildState({
CocoonService cocoonService,
GoogleSignInService authService,
}) : authService = authService ?? GoogleSignInService(),
_cocoonService = cocoonService ?? CocoonService();

/// Cocoon backend service that retrieves the data needed for this state.
final CocoonService _cocoonService;

/// Authentication service for managing Google Sign In.
final GoogleSignInService authService;

/// How often to query the Cocoon backend for the current build state.
@visibleForTesting
final Duration refreshRate = const Duration(seconds: 10);
Expand Down Expand Up @@ -69,6 +76,11 @@ class FlutterBuildState extends ChangeNotifier {
notifyListeners();
}

Future<void> signIn() async {
await authService.signIn();
notifyListeners();
}

@override
void dispose() {
refreshTimer?.cancel();
Expand Down
1 change: 1 addition & 0 deletions app_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies:

cocoon_service:
path: ../app_dart
google_sign_in_all: ^0.0.3
provider: ^3.0.0
url_launcher: ^5.1.0
url_launcher_web:
Expand Down
75 changes: 75 additions & 0 deletions app_flutter/test/build_dashboard_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2019 The Chromium 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:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart' as test;

import 'package:app_flutter/build_dashboard.dart';
import 'package:app_flutter/service/google_authentication.dart';
import 'package:app_flutter/service/fake_cocoon.dart';
import 'package:app_flutter/state/flutter_build.dart';

void main() {
group('UserAvatar', () {
GoogleSignInService authService;

setUp(() {
authService = MockGoogleSignInService();
});

testWidgets('shows sign in button when not signed in',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(false);
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

expect(find.text('Sign in'), findsOneWidget);
});

testWidgets('sign in button activates google sign in when pressed',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(false);
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

verifyNever(authService.signIn());

await tester.tap(find.byType(UserAvatar));

verify(authService.signIn()).called(1);
});

testWidgets('shows user avatar when signed in',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(true);
when(authService.avatarUrl).thenReturn('https://flutter.dev');
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

expect(tester.takeException(),
const test.TypeMatcher<NetworkImageLoadException>());
expect(find.byType(Image), findsOneWidget);
});
});
}

/// Mock [GoogleSignInService] for testing interactions.
class MockGoogleSignInService extends Mock implements GoogleSignInService {}
86 changes: 86 additions & 0 deletions app_flutter/test/service/google_authentication_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2019 The Chromium 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:google_sign_in_all/google_sign_in_all.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import 'package:app_flutter/service/google_authentication.dart';

void main() {
group('GoogleSignInService not signed in', () {
GoogleSignInService authService;

setUp(() {
authService = GoogleSignInService(googleSignIn: MockGoogleSignIn());
});

test('not authenticated', () {
expect(authService.isAuthenticated, false);
});

test('no user information', () {
expect(authService.avatarUrl, null);
expect(authService.email, null);
expect(authService.accessToken, null);
});
});

group('GoogleSignInService sign in', () {
GoogleSignInService authService;
GoogleSignIn mockSignIn;

setUp(() {
mockSignIn = MockGoogleSignIn();
final AuthCredentials fakeCredentials = FakeAuthCredentials();
when(mockSignIn.signIn())
.thenAnswer((_) => Future<AuthCredentials>.value(fakeCredentials));
when(mockSignIn.getCurrentUser()).thenAnswer((_) =>
Future<GoogleAccount>.value(GoogleAccount(
email: '[email protected]', photoUrl: 'fake://fake.png')));

authService = GoogleSignInService(googleSignIn: mockSignIn);
});

test('is authenticated after successful sign in', () async {
await authService.signIn();

expect(authService.isAuthenticated, true);
});

test('there is user information after successful sign in', () async {
await authService.signIn();

expect(authService.email, '[email protected]');
expect(authService.avatarUrl, 'fake://fake.png');
expect(authService.accessToken, 'fake');
});

test('is not authenticated after failure in sign in', () async {
when(mockSignIn.signIn())
.thenAnswer((_) => Future<AuthCredentials>.value(null));
when(mockSignIn.getCurrentUser())
.thenAnswer((_) => Future<GoogleAccount>.value(null));

await authService.signIn();

expect(authService.isAuthenticated, false);
expect(authService.email, null);
expect(authService.avatarUrl, null);
expect(authService.accessToken, null);
});
});
}

/// Mock [GoogleSignIn] for testing interactions.
class MockGoogleSignIn extends Mock implements GoogleSignIn {}

/// Fake [AuthCredentials] for [MockGoogleSignIn].
class FakeAuthCredentials implements AuthCredentials {
@override
final String accessToken = 'fake';

@override
final String idToken = 'faker';
}