Skip to content

Commit

Permalink
Merge pull request #51 from nimblehq/feature/9-backend-sign-in
Browse files Browse the repository at this point in the history
[#9] [Backend] As a user, I can sign in with email and password
  • Loading branch information
nkhanh44 authored Aug 11, 2023
2 parents a3a409d + 7504b5f commit 76d4efd
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 99 deletions.
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
SECRET=
REST_API_ENDPOINT=
CLIENT_ID=
CLIENT_SECRET=
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 632d6ac0b577d6e268ff7a13a105bbc4f7941989

COCOAPODS: 1.12.0
COCOAPODS: 1.12.1
14 changes: 0 additions & 14 deletions lib/api/api_service.dart

This file was deleted.

17 changes: 17 additions & 0 deletions lib/api/authentication_api_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:dio/dio.dart';
import 'package:survey_flutter/model/request/login_request.dart';
import 'package:survey_flutter/model/response/login_response.dart';
import 'package:retrofit/retrofit.dart';

part 'authentication_api_service.g.dart';

@RestApi()
abstract class AuthenticationApiService {
factory AuthenticationApiService(Dio dio, {String baseUrl}) =
_AuthenticationApiService;

@POST('/oauth/token')
Future<LoginResponse> login(
@Body() LoginRequest body,
);
}
22 changes: 0 additions & 22 deletions lib/api/repository/credential_repository.dart

This file was deleted.

7 changes: 7 additions & 0 deletions lib/api/response_decoder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:japx/japx.dart';

class ResponseDecoder {
static Map<String, dynamic> decode(Map<String, dynamic> json) {
return Japx.decode(json)['data'];
}
}
8 changes: 8 additions & 0 deletions lib/env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ class Env {
static String get restApiEndpoint {
return FlutterConfig.get('REST_API_ENDPOINT');
}

static String get clientId {
return FlutterConfig.get('CLIENT_ID');
}

static String get clientSecret {
return FlutterConfig.get('CLIENT_SECRET');
}
}
23 changes: 23 additions & 0 deletions lib/model/login_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:equatable/equatable.dart';

class LoginModel extends Equatable {
final String id;
final String accessToken;
final double expiresIn;
final String refreshToken;

const LoginModel({
required this.id,
required this.accessToken,
required this.expiresIn,
required this.refreshToken,
});

@override
List<Object?> get props => [
id,
accessToken,
expiresIn,
refreshToken,
];
}
22 changes: 22 additions & 0 deletions lib/model/request/login_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';

part 'login_request.g.dart';

@JsonSerializable()
class LoginRequest {
final String grantType;
final String email;
final String password;
final String clientId;
final String clientSecret;

LoginRequest({
required this.grantType,
required this.email,
required this.password,
required this.clientId,
required this.clientSecret,
});

Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
}
45 changes: 45 additions & 0 deletions lib/model/response/login_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:survey_flutter/api/response_decoder.dart';
import 'package:survey_flutter/model/login_model.dart';

part 'login_response.g.dart';

@JsonSerializable()
class LoginResponse {
final String id;
final String accessToken;
final String tokenType;
final double expiresIn;
final String refreshToken;
final int createdAt;

LoginResponse({
required this.id,
required this.accessToken,
required this.tokenType,
required this.expiresIn,
required this.refreshToken,
required this.createdAt,
});

factory LoginResponse.fromJson(Map<String, dynamic> json) =>
_$LoginResponseFromJson(ResponseDecoder.decode(json));

LoginModel toLoginModel() => LoginModel(
id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken,
);

static LoginResponse dummy() {
return LoginResponse(
id: "",
accessToken: "",
tokenType: "",
expiresIn: 0,
refreshToken: "",
createdAt: 0,
);
}
}
16 changes: 0 additions & 16 deletions lib/model/response/user_response.dart

This file was deleted.

41 changes: 41 additions & 0 deletions lib/repositories/authentication_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:survey_flutter/api/authentication_api_service.dart';
import 'package:survey_flutter/api/exception/network_exceptions.dart';
import 'package:survey_flutter/env.dart';
import 'package:survey_flutter/model/login_model.dart';
import 'package:survey_flutter/model/request/login_request.dart';
import 'package:injectable/injectable.dart';

const String _grantType = "password";

abstract class AuthenticationRepository {
Future<LoginModel> login({
required String email,
required String password,
});
}

@Singleton(as: AuthenticationRepository)
class AuthenticationRepositoryImpl extends AuthenticationRepository {
final AuthenticationApiService _authenticationApiService;

AuthenticationRepositoryImpl(this._authenticationApiService);

@override
Future<LoginModel> login({
required String email,
required String password,
}) async {
try {
final response = await _authenticationApiService.login(LoginRequest(
email: email,
password: password,
clientId: Env.clientId,
clientSecret: Env.clientSecret,
grantType: _grantType,
));
return response.toLoginModel();
} catch (exception) {
throw NetworkExceptions.fromDioException(exception);
}
}
}
35 changes: 35 additions & 0 deletions lib/usecases/login_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'dart:async';
import 'package:survey_flutter/model/login_model.dart';
import 'package:survey_flutter/repositories/authentication_repository.dart';
import 'package:survey_flutter/usecases/base/base_use_case.dart';
import 'package:injectable/injectable.dart';

class LoginParams {
final String email;
final String password;

LoginParams({
required this.email,
required this.password,
});
}

@Injectable()
class LoginUseCase extends UseCase<LoginModel, LoginParams> {
final AuthenticationRepository _repository;

const LoginUseCase(this._repository);

@override
Future<Result<LoginModel>> call(LoginParams params) async {
try {
final result = await _repository.login(
email: params.email,
password: params.password,
);
return Success(result);
} catch (exception) {
return Failed(UseCaseException(exception));
}
}
}
32 changes: 32 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.2"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
fake_async:
dependency: transitive
description:
Expand Down Expand Up @@ -319,6 +327,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: transitive
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
glob:
dependency: transitive
description:
Expand Down Expand Up @@ -367,6 +383,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
injectable:
dependency: "direct main"
description:
name: injectable
sha256: f71eb879124ed286cbd2210337b91ff5f345f146187c1f1891c172e0ac06443a
url: "https://pub.dev"
source: hosted
version: "1.5.4"
integration_test:
dependency: "direct dev"
description: flutter
Expand All @@ -388,6 +412,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
japx:
dependency: "direct main"
description:
name: japx
sha256: d43074dd5d7d4e9d7a4325fe27e4cefeb1db600ea6b69ea9fb94b7fc372baabe
url: "https://pub.dev"
source: hosted
version: "2.1.0"
js:
dependency: transitive
description:
Expand Down
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ dependencies:
package_info_plus: ^4.0.0
permission_handler: ^10.2.0
retrofit: ^4.0.1
japx: ^2.0.5
equatable: ^2.0.0
injectable: ^1.5.0

dev_dependencies:
build_runner: ^2.4.4
Expand Down
50 changes: 50 additions & 0 deletions test/api/repositories/authentication_repository_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter_config/flutter_config.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:survey_flutter/repositories/authentication_repository.dart';
import 'package:survey_flutter/api/exception/network_exceptions.dart';
import 'package:survey_flutter/model/response/login_response.dart';

import '../../mocks/generate_mocks.mocks.dart';

void main() {
group('AuthenticationRepositoryTest', () {
late MockAuthenticationApiService mockAuthApiService;
late AuthenticationRepositoryImpl authRepository;

const email = "email";
const password = "password";

setUpAll(() {
FlutterConfig.loadValueForTesting({
'CLIENT_ID': 'CLIENT_ID',
'CLIENT_SECRET': 'CLIENT_SECRET',
});
});

setUp(() {
mockAuthApiService = MockAuthenticationApiService();
authRepository = AuthenticationRepositoryImpl(mockAuthApiService);
});

test('When login successfully, it returns correct model', () async {
final loginResponse = LoginResponse.dummy();

when(mockAuthApiService.login(any))
.thenAnswer((_) async => loginResponse);

final result =
await authRepository.login(email: email, password: password);

expect(result, loginResponse.toLoginModel());
});

test('When login fail, it returns failed exception', () async {
when(mockAuthApiService.login(any)).thenThrow(Exception());

final result = authRepository.login(email: email, password: password);

expect(result, throwsA(isA<NetworkExceptions>()));
});
});
}
Loading

0 comments on commit 76d4efd

Please sign in to comment.