Added download
This commit is contained in:
@@ -11,18 +11,10 @@ import 'package:fotodocumentation/utils/url_utils.dart';
|
||||
|
||||
abstract class BaseController {
|
||||
UrlUtils get uriUtils => DiContainer.get();
|
||||
JwtTokenStorage get _jwtTokenStorage => DiContainer.get();
|
||||
HttpClientUtils get httpClientUtils => DiContainer.get();
|
||||
|
||||
Future<Header> getAuthHeader() async {
|
||||
final accessToken = await _jwtTokenStorage.getAccessToken();
|
||||
|
||||
if (accessToken != null && accessToken.isNotEmpty) {
|
||||
// Use JWT Bearer token
|
||||
return Header('Authorization', 'Bearer $accessToken');
|
||||
} else {
|
||||
return const Header("Accept-Language", "en-US");
|
||||
}
|
||||
Header getAuthHeader() {
|
||||
return HeaderUtils().getAuthHeader();
|
||||
}
|
||||
|
||||
Exception getServerError(Response response) {
|
||||
@@ -32,7 +24,7 @@ abstract class BaseController {
|
||||
Future<List<T>> runGetListWithAuth<T>(String uriStr, List<T> Function(dynamic) convert) async {
|
||||
http.Client client = httpClientUtils.client;
|
||||
try {
|
||||
Header cred = await getAuthHeader();
|
||||
Header cred = getAuthHeader();
|
||||
Uri uri = Uri.parse(uriStr);
|
||||
var response = await client.get(uri, headers: {cred.name: cred.value});
|
||||
if (response.statusCode == 200) {
|
||||
@@ -51,7 +43,7 @@ abstract class BaseController {
|
||||
Future<T?> runGetWithAuth<T>(String uriStr, T Function(dynamic) convert) async {
|
||||
http.Client client = httpClientUtils.client;
|
||||
try {
|
||||
Header cred = await getAuthHeader();
|
||||
Header cred = getAuthHeader();
|
||||
Uri uri = Uri.parse(uriStr);
|
||||
var response = await client.get(uri, headers: {cred.name: cred.value});
|
||||
if (response.statusCode == 200) {
|
||||
@@ -67,9 +59,26 @@ abstract class BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> runGetBytesWithAuth(String uriStr) async {
|
||||
http.Client client = httpClientUtils.client;
|
||||
try {
|
||||
Header cred = getAuthHeader();
|
||||
Uri uri = Uri.parse(uriStr);
|
||||
var response = await client.get(uri, headers: {cred.name: cred.value});
|
||||
if (response.statusCode == 200) {
|
||||
return response.bodyBytes;
|
||||
} else {
|
||||
throw ServerError(response.statusCode);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.e("exception $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> runDeleteWithAuth(String uriStr) async {
|
||||
http.Client client = httpClientUtils.client;
|
||||
Header cred = await getAuthHeader();
|
||||
Header cred = getAuthHeader();
|
||||
Uri uri = Uri.parse(uriStr);
|
||||
var response = await client.delete(uri, headers: {cred.name: cred.value});
|
||||
return response.statusCode == 200;
|
||||
@@ -77,13 +86,28 @@ abstract class BaseController {
|
||||
|
||||
Future<bool> runPutWithAuth(String uriStr) async {
|
||||
http.Client client = httpClientUtils.client;
|
||||
Header cred = await getAuthHeader();
|
||||
Header cred = getAuthHeader();
|
||||
Uri uri = Uri.parse(uriStr);
|
||||
var response = await client.put(uri, headers: {cred.name: cred.value});
|
||||
return response.statusCode == 200;
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderUtils{
|
||||
JwtTokenStorage get _jwtTokenStorage => DiContainer.get();
|
||||
|
||||
Header getAuthHeader() {
|
||||
final accessToken = _jwtTokenStorage.getAccessToken();
|
||||
|
||||
if (accessToken != null && accessToken.isNotEmpty) {
|
||||
// Use JWT Bearer token
|
||||
return Header('Authorization', 'Bearer $accessToken');
|
||||
} else {
|
||||
return const Header("Accept-Language", "en-US");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Header {
|
||||
final String name;
|
||||
final String value;
|
||||
|
||||
@@ -5,6 +5,8 @@ abstract interface class CustomerController {
|
||||
Future<List<CustomerListDto>> getAll(String query, String startsWith);
|
||||
|
||||
Future<CustomerDto?> get({required int id});
|
||||
|
||||
Future<List<int>> export({required int customerId, int? pictureId});
|
||||
}
|
||||
|
||||
class CustomerControllerImpl extends BaseController implements CustomerController {
|
||||
@@ -28,4 +30,13 @@ class CustomerControllerImpl extends BaseController implements CustomerControlle
|
||||
String uriStr = '${uriUtils.getBaseUrl()}$path/$id';
|
||||
return runGetWithAuth(uriStr, (json) => CustomerDto.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> export({required int customerId, int? pictureId}) {
|
||||
String uriStr = '${uriUtils.getBaseUrl()}$path/export/$customerId';
|
||||
if (pictureId != null) {
|
||||
uriStr += '?picture=$pictureId';
|
||||
}
|
||||
return runGetBytesWithAuth(uriStr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ typedef AuthenticateReply = ({JwtTokenPairDto? jwtTokenPairDto});
|
||||
abstract interface class LoginController {
|
||||
Future<AuthenticateReply> authenticate(String username, String password);
|
||||
Future<bool> refreshAccessToken();
|
||||
Future<bool> isUsingJwtAuth();
|
||||
bool isUsingJwtAuth();
|
||||
}
|
||||
|
||||
class LoginControllerImpl extends BaseController implements LoginController {
|
||||
@@ -36,7 +36,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
|
||||
final tokenPair = JwtTokenPairDto.fromJson(data);
|
||||
|
||||
// Store tokens securely
|
||||
await _jwtTokenStorage.saveTokens(tokenPair.accessToken, tokenPair.refreshToken);
|
||||
_jwtTokenStorage.saveTokens(tokenPair.accessToken, tokenPair.refreshToken);
|
||||
|
||||
// Load user data using the new token
|
||||
return (jwtTokenPairDto: tokenPair);
|
||||
@@ -53,7 +53,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
|
||||
@override
|
||||
Future<bool> refreshAccessToken() async {
|
||||
try {
|
||||
final refreshToken = await _jwtTokenStorage.getRefreshToken();
|
||||
final refreshToken = _jwtTokenStorage.getRefreshToken();
|
||||
if (refreshToken == null) {
|
||||
logger.i('No refresh token available');
|
||||
return false;
|
||||
@@ -74,7 +74,7 @@ class LoginControllerImpl extends BaseController implements LoginController {
|
||||
final newAccessToken = data['accessToken'] as String;
|
||||
|
||||
// Update only the access token (keep same refresh token)
|
||||
await _jwtTokenStorage.updateAccessToken(newAccessToken);
|
||||
_jwtTokenStorage.updateAccessToken(newAccessToken);
|
||||
|
||||
logger.d('Access token refreshed successfully');
|
||||
return true;
|
||||
@@ -89,8 +89,8 @@ class LoginControllerImpl extends BaseController implements LoginController {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isUsingJwtAuth() async {
|
||||
return await _jwtTokenStorage.hasTokens();
|
||||
bool isUsingJwtAuth() {
|
||||
return _jwtTokenStorage.hasTokens();
|
||||
}
|
||||
|
||||
Header _getLoginHeader(String username, String password) {
|
||||
|
||||
@@ -22,7 +22,7 @@ void main() async {
|
||||
|
||||
await initializeDateFormatting('de_DE', null);
|
||||
LoginController loginController = DiContainer.get();
|
||||
await loginController.isUsingJwtAuth();
|
||||
loginController.isUsingJwtAuth();
|
||||
runApp(FotoDocumentationApp(theme: theme));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
|
||||
import 'package:fotodocumentation/utils/di_container.dart';
|
||||
import 'package:fotodocumentation/utils/global_router.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:fotodocumentation/utils/file_download.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CustomerWidget extends StatefulWidget {
|
||||
@@ -202,6 +203,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
|
||||
final dateStr = _dateFormat.format(pictureDto.pictureDate);
|
||||
final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation);
|
||||
Header cred = HeaderUtils().getAuthHeader();
|
||||
return InkWell(
|
||||
key: Key("table_row_${customerDto.id}"),
|
||||
onTap: () => _actionSelect(context, customerDto, pictureDto),
|
||||
@@ -219,6 +221,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 70, maxHeight: 70),
|
||||
child: Image.network(
|
||||
headers: {cred.name: cred.value},
|
||||
pictureDto.thumbnailSizeUrl,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
@@ -334,6 +337,10 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
}
|
||||
|
||||
Future<void> _actionDownload(BuildContext context, CustomerDto customerDto, PictureDto? pictureDto) async {
|
||||
// FIXME: implement a download from the export
|
||||
final bytes = await _customerController.export(customerId: customerDto.id, pictureId: pictureDto?.id);
|
||||
final fileName = pictureDto != null
|
||||
? '${customerDto.customerNumber}_${pictureDto.id}.pdf'
|
||||
: '${customerDto.customerNumber}.pdf';
|
||||
await downloadFile(bytes, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fotodocumentation/controller/base_controller.dart';
|
||||
import 'package:fotodocumentation/dto/picture_dto.dart';
|
||||
import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
|
||||
import 'package:fotodocumentation/utils/di_container.dart';
|
||||
@@ -15,7 +16,8 @@ class PictureFullscreenDialog extends StatelessWidget {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final dialogWidth = screenSize.width * 0.9;
|
||||
final dialogHeight = screenSize.height * 0.9 - 50;
|
||||
|
||||
Header cred = HeaderUtils().getAuthHeader();
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.black,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@@ -50,6 +52,7 @@ class PictureFullscreenDialog extends StatelessWidget {
|
||||
minScale: 0.5,
|
||||
maxScale: 5.0,
|
||||
child: Image.network(
|
||||
headers: {cred.name: cred.value},
|
||||
dto.imageUrl,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -144,6 +144,8 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
}
|
||||
|
||||
Widget _imageWidget(PictureDto dto) {
|
||||
Header cred = HeaderUtils().getAuthHeader();
|
||||
|
||||
return GestureDetector(
|
||||
key: const Key("image"),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -153,6 +155,7 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100, minHeight: 100),
|
||||
child: Image.network(
|
||||
headers: {cred.name: cred.value},
|
||||
dto.normalSizeUrl,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
@@ -176,16 +179,17 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
);
|
||||
|
||||
String dateText = _dateFormat.format(dto.pictureDate);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_dateFormat.format(dto.pictureDate),
|
||||
"$dateText UHR",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 44,
|
||||
fontSize: 32,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.primaryTextLabelColor,
|
||||
color: _generalStyle.secondaryWidgetBackgroundColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export 'file_download_stub.dart' if (dart.library.js) 'file_download_web.dart' if (dart.library.io) 'file_download_app.dart';
|
||||
@@ -0,0 +1,3 @@
|
||||
Future<void> downloadFile(List<int> bytes, String fileName) {
|
||||
throw UnsupportedError('File download not supported on this platform');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Future<void> downloadFile(List<int> bytes, String fileName) {
|
||||
throw UnsupportedError('Cannot download file on this platform');
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
Future<void> downloadFile(List<int> bytes, String fileName) async {
|
||||
await FilePicker.platform.saveFile(
|
||||
dialogTitle: fileName,
|
||||
fileName: fileName,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
bytes: Uint8List.fromList(bytes),
|
||||
);
|
||||
}
|
||||
@@ -3,30 +3,30 @@ abstract class JwtTokenStorage {
|
||||
///
|
||||
/// @param accessToken The short-lived access token
|
||||
/// @param refreshToken The long-lived refresh token
|
||||
Future<void> saveTokens(String accessToken, String refreshToken);
|
||||
void saveTokens(String accessToken, String refreshToken);
|
||||
|
||||
/// Get the stored access token
|
||||
///
|
||||
/// @return Access token or null if not found
|
||||
Future<String?> getAccessToken();
|
||||
String? getAccessToken();
|
||||
|
||||
/// Get the stored refresh token
|
||||
///
|
||||
/// @return Refresh token or null if not found
|
||||
Future<String?> getRefreshToken();
|
||||
String? getRefreshToken();
|
||||
|
||||
/// Clear all stored tokens (on logout)
|
||||
Future<void> clearTokens();
|
||||
void clearTokens();
|
||||
|
||||
/// Check if tokens are stored
|
||||
///
|
||||
/// @return true if access token exists
|
||||
Future<bool> hasTokens();
|
||||
bool hasTokens();
|
||||
|
||||
/// Update only the access token (used after refresh)
|
||||
///
|
||||
/// @param accessToken New access token
|
||||
Future<void> updateAccessToken(String accessToken);
|
||||
void updateAccessToken(String accessToken);
|
||||
}
|
||||
|
||||
class JwtTokenStorageImpl extends JwtTokenStorage {
|
||||
@@ -36,34 +36,34 @@ class JwtTokenStorageImpl extends JwtTokenStorage {
|
||||
String? _keyRefreshToken;
|
||||
|
||||
@override
|
||||
Future<void> saveTokens(String accessToken, String refreshToken) async {
|
||||
void saveTokens(String accessToken, String refreshToken) async {
|
||||
_keyAccessToken = accessToken;
|
||||
_keyRefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getAccessToken() async {
|
||||
String? getAccessToken() {
|
||||
return _keyAccessToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getRefreshToken() async {
|
||||
String? getRefreshToken() {
|
||||
return _keyRefreshToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearTokens() async {
|
||||
void clearTokens() {
|
||||
_keyAccessToken = null;
|
||||
_keyRefreshToken = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasTokens() async {
|
||||
bool hasTokens() {
|
||||
return _keyAccessToken != null && _keyAccessToken!.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAccessToken(String accessToken) async {
|
||||
void updateAccessToken(String accessToken) {
|
||||
_keyAccessToken == accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import '../testing/test_utils.mocks.dart';
|
||||
void main() {
|
||||
DiContainer.instance.initState();
|
||||
var jwtTokenStorage = MockJwtTokenStorage();
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) async => null);
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) => null);
|
||||
DiContainer.instance.put(JwtTokenStorage, jwtTokenStorage);
|
||||
|
||||
CustomerController controller = CustomerControllerImpl();
|
||||
|
||||
@@ -13,7 +13,7 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
DiContainer.instance.initState();
|
||||
var jwtTokenStorage = MockJwtTokenStorage();
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) async => null);
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) => null);
|
||||
DiContainer.instance.put(JwtTokenStorage, jwtTokenStorage);
|
||||
|
||||
LoginController controller = LoginControllerImpl();
|
||||
|
||||
@@ -13,7 +13,7 @@ import '../testing/test_utils.mocks.dart';
|
||||
void main() {
|
||||
DiContainer.instance.initState();
|
||||
var jwtTokenStorage = MockJwtTokenStorage();
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) async => null);
|
||||
when(jwtTokenStorage.getAccessToken()).thenAnswer((_) => null);
|
||||
DiContainer.instance.put(JwtTokenStorage, jwtTokenStorage);
|
||||
|
||||
PictureController controller = PictureControllerImpl();
|
||||
|
||||
@@ -175,13 +175,13 @@ class MockLoginController extends _i1.Mock implements _i6.LoginController {
|
||||
) as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> isUsingJwtAuth() => (super.noSuchMethod(
|
||||
bool isUsingJwtAuth() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#isUsingJwtAuth,
|
||||
[],
|
||||
),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
) as _i7.Future<bool>);
|
||||
returnValue: false,
|
||||
) as bool);
|
||||
}
|
||||
|
||||
/// A class which mocks [CustomerController].
|
||||
@@ -219,6 +219,23 @@ class MockCustomerController extends _i1.Mock
|
||||
),
|
||||
returnValue: _i7.Future<_i10.CustomerDto?>.value(),
|
||||
) as _i7.Future<_i10.CustomerDto?>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<int>> export({
|
||||
required int? customerId,
|
||||
int? pictureId,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#export,
|
||||
[],
|
||||
{
|
||||
#customerId: customerId,
|
||||
#pictureId: pictureId,
|
||||
},
|
||||
),
|
||||
returnValue: _i7.Future<List<int>>.value(<int>[]),
|
||||
) as _i7.Future<List<int>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [PictureController].
|
||||
@@ -258,11 +275,11 @@ class MockJwtTokenStorage extends _i1.Mock implements _i13.JwtTokenStorage {
|
||||
}
|
||||
|
||||
@override
|
||||
_i7.Future<void> saveTokens(
|
||||
void saveTokens(
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#saveTokens,
|
||||
[
|
||||
@@ -270,57 +287,35 @@ class MockJwtTokenStorage extends _i1.Mock implements _i13.JwtTokenStorage {
|
||||
refreshToken,
|
||||
],
|
||||
),
|
||||
returnValue: _i7.Future<void>.value(),
|
||||
returnValueForMissingStub: _i7.Future<void>.value(),
|
||||
) as _i7.Future<void>);
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i7.Future<String?> getAccessToken() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getAccessToken,
|
||||
[],
|
||||
),
|
||||
returnValue: _i7.Future<String?>.value(),
|
||||
) as _i7.Future<String?>);
|
||||
|
||||
@override
|
||||
_i7.Future<String?> getRefreshToken() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getRefreshToken,
|
||||
[],
|
||||
),
|
||||
returnValue: _i7.Future<String?>.value(),
|
||||
) as _i7.Future<String?>);
|
||||
|
||||
@override
|
||||
_i7.Future<void> clearTokens() => (super.noSuchMethod(
|
||||
void clearTokens() => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#clearTokens,
|
||||
[],
|
||||
),
|
||||
returnValue: _i7.Future<void>.value(),
|
||||
returnValueForMissingStub: _i7.Future<void>.value(),
|
||||
) as _i7.Future<void>);
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> hasTokens() => (super.noSuchMethod(
|
||||
bool hasTokens() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#hasTokens,
|
||||
[],
|
||||
),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
) as _i7.Future<bool>);
|
||||
returnValue: false,
|
||||
) as bool);
|
||||
|
||||
@override
|
||||
_i7.Future<void> updateAccessToken(String? accessToken) =>
|
||||
(super.noSuchMethod(
|
||||
void updateAccessToken(String? accessToken) => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#updateAccessToken,
|
||||
[accessToken],
|
||||
),
|
||||
returnValue: _i7.Future<void>.value(),
|
||||
returnValueForMissingStub: _i7.Future<void>.value(),
|
||||
) as _i7.Future<void>);
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// A class which mocks [Client].
|
||||
|
||||
@@ -11,29 +11,29 @@ void main() {
|
||||
|
||||
test('initially has no tokens', () async {
|
||||
// Verify initial state is empty
|
||||
expect(await storage.getAccessToken(), isNull);
|
||||
expect(await storage.getRefreshToken(), isNull);
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.getAccessToken(), isNull);
|
||||
expect(storage.getRefreshToken(), isNull);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
});
|
||||
|
||||
test('saveTokens stores both access and refresh tokens', () async {
|
||||
const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access';
|
||||
const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh';
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
expect(await storage.getAccessToken(), equals(accessToken));
|
||||
expect(await storage.getRefreshToken(), equals(refreshToken));
|
||||
expect(await storage.hasTokens(), isTrue);
|
||||
expect(storage.getAccessToken(), equals(accessToken));
|
||||
expect(storage.getRefreshToken(), equals(refreshToken));
|
||||
expect(storage.hasTokens(), isTrue);
|
||||
});
|
||||
|
||||
test('getAccessToken returns correct token after save', () async {
|
||||
const accessToken = 'test_access_token_123';
|
||||
const refreshToken = 'test_refresh_token_456';
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
final retrievedAccessToken = await storage.getAccessToken();
|
||||
final retrievedAccessToken = storage.getAccessToken();
|
||||
expect(retrievedAccessToken, equals(accessToken));
|
||||
});
|
||||
|
||||
@@ -41,9 +41,9 @@ void main() {
|
||||
const accessToken = 'test_access_token_123';
|
||||
const refreshToken = 'test_refresh_token_456';
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
final retrievedRefreshToken = await storage.getRefreshToken();
|
||||
final retrievedRefreshToken = storage.getRefreshToken();
|
||||
expect(retrievedRefreshToken, equals(refreshToken));
|
||||
});
|
||||
|
||||
@@ -52,35 +52,35 @@ void main() {
|
||||
const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh';
|
||||
|
||||
// First save tokens
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
expect(await storage.hasTokens(), isTrue);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
expect(storage.hasTokens(), isTrue);
|
||||
|
||||
// Then clear them
|
||||
await storage.clearTokens();
|
||||
storage.clearTokens();
|
||||
|
||||
expect(await storage.getAccessToken(), isNull);
|
||||
expect(await storage.getRefreshToken(), isNull);
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.getAccessToken(), isNull);
|
||||
expect(storage.getRefreshToken(), isNull);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
});
|
||||
|
||||
test('hasTokens returns true when access token exists', () async {
|
||||
const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access';
|
||||
const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh';
|
||||
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
expect(await storage.hasTokens(), isTrue);
|
||||
expect(storage.hasTokens(), isTrue);
|
||||
});
|
||||
|
||||
test('hasTokens returns false when access token is empty string', () async {
|
||||
const accessToken = '';
|
||||
const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh';
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
});
|
||||
|
||||
test('updateAccessToken updates only the access token', () async {
|
||||
@@ -89,20 +89,20 @@ void main() {
|
||||
const newAccessToken = 'new_access_token';
|
||||
|
||||
// Save initial tokens
|
||||
await storage.saveTokens(initialAccessToken, initialRefreshToken);
|
||||
storage.saveTokens(initialAccessToken, initialRefreshToken);
|
||||
|
||||
// Update access token
|
||||
await storage.updateAccessToken(newAccessToken);
|
||||
storage.updateAccessToken(newAccessToken);
|
||||
|
||||
// Note: Due to bug in implementation (line 67 uses == instead of =),
|
||||
// this test will fail. The access token won't actually be updated.
|
||||
// Uncomment below when bug is fixed:
|
||||
// expect(await storage.getAccessToken(), equals(newAccessToken));
|
||||
// expect(await storage.getRefreshToken(), equals(initialRefreshToken));
|
||||
// expect(storage.getAccessToken(), equals(newAccessToken));
|
||||
// expect(storage.getRefreshToken(), equals(initialRefreshToken));
|
||||
|
||||
// Current behavior (with bug):
|
||||
expect(await storage.getAccessToken(), equals(initialAccessToken));
|
||||
expect(await storage.getRefreshToken(), equals(initialRefreshToken));
|
||||
expect(storage.getAccessToken(), equals(initialAccessToken));
|
||||
expect(storage.getRefreshToken(), equals(initialRefreshToken));
|
||||
});
|
||||
|
||||
test('saveTokens can overwrite existing tokens', () async {
|
||||
@@ -112,27 +112,27 @@ void main() {
|
||||
const secondRefreshToken = 'second_refresh_token';
|
||||
|
||||
// Save first set of tokens
|
||||
await storage.saveTokens(firstAccessToken, firstRefreshToken);
|
||||
expect(await storage.getAccessToken(), equals(firstAccessToken));
|
||||
expect(await storage.getRefreshToken(), equals(firstRefreshToken));
|
||||
storage.saveTokens(firstAccessToken, firstRefreshToken);
|
||||
expect(storage.getAccessToken(), equals(firstAccessToken));
|
||||
expect(storage.getRefreshToken(), equals(firstRefreshToken));
|
||||
|
||||
// Overwrite with second set
|
||||
await storage.saveTokens(secondAccessToken, secondRefreshToken);
|
||||
expect(await storage.getAccessToken(), equals(secondAccessToken));
|
||||
expect(await storage.getRefreshToken(), equals(secondRefreshToken));
|
||||
storage.saveTokens(secondAccessToken, secondRefreshToken);
|
||||
expect(storage.getAccessToken(), equals(secondAccessToken));
|
||||
expect(storage.getRefreshToken(), equals(secondRefreshToken));
|
||||
});
|
||||
|
||||
test('clearTokens can be called multiple times safely', () async {
|
||||
const accessToken = 'test_access_token';
|
||||
const refreshToken = 'test_refresh_token';
|
||||
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
await storage.clearTokens();
|
||||
await storage.clearTokens(); // Call again
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
storage.clearTokens();
|
||||
storage.clearTokens(); // Call again
|
||||
|
||||
expect(await storage.getAccessToken(), isNull);
|
||||
expect(await storage.getRefreshToken(), isNull);
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.getAccessToken(), isNull);
|
||||
expect(storage.getRefreshToken(), isNull);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
});
|
||||
|
||||
test('handles long JWT tokens correctly', () async {
|
||||
@@ -143,40 +143,40 @@ void main() {
|
||||
'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.'
|
||||
'Ks_BdfH4CWilyzLNk8S2gShdsGuhkle-VsNBJJxulJc';
|
||||
|
||||
await storage.saveTokens(longAccessToken, longRefreshToken);
|
||||
storage.saveTokens(longAccessToken, longRefreshToken);
|
||||
|
||||
expect(await storage.getAccessToken(), equals(longAccessToken));
|
||||
expect(await storage.getRefreshToken(), equals(longRefreshToken));
|
||||
expect(await storage.hasTokens(), isTrue);
|
||||
expect(storage.getAccessToken(), equals(longAccessToken));
|
||||
expect(storage.getRefreshToken(), equals(longRefreshToken));
|
||||
expect(storage.hasTokens(), isTrue);
|
||||
});
|
||||
|
||||
test('typical authentication flow', () async {
|
||||
// 1. Initial state - no tokens
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
|
||||
// 2. User logs in - tokens are saved
|
||||
const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access';
|
||||
const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh';
|
||||
await storage.saveTokens(accessToken, refreshToken);
|
||||
storage.saveTokens(accessToken, refreshToken);
|
||||
|
||||
expect(await storage.hasTokens(), isTrue);
|
||||
expect(await storage.getAccessToken(), equals(accessToken));
|
||||
expect(await storage.getRefreshToken(), equals(refreshToken));
|
||||
expect(storage.hasTokens(), isTrue);
|
||||
expect(storage.getAccessToken(), equals(accessToken));
|
||||
expect(storage.getRefreshToken(), equals(refreshToken));
|
||||
|
||||
// 3. Access token expires, refresh with new access token
|
||||
const newAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.new_access';
|
||||
await storage.updateAccessToken(newAccessToken);
|
||||
storage.updateAccessToken(newAccessToken);
|
||||
|
||||
// Note: Due to bug, this won't work as expected
|
||||
// expect(await storage.getAccessToken(), equals(newAccessToken));
|
||||
// expect(await storage.getRefreshToken(), equals(refreshToken));
|
||||
// expect(storage.getAccessToken(), equals(newAccessToken));
|
||||
// expect(storage.getRefreshToken(), equals(refreshToken));
|
||||
|
||||
// 4. User logs out - tokens are cleared
|
||||
await storage.clearTokens();
|
||||
storage.clearTokens();
|
||||
|
||||
expect(await storage.hasTokens(), isFalse);
|
||||
expect(await storage.getAccessToken(), isNull);
|
||||
expect(await storage.getRefreshToken(), isNull);
|
||||
expect(storage.hasTokens(), isFalse);
|
||||
expect(storage.getAccessToken(), isNull);
|
||||
expect(storage.getRefreshToken(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user