feat: Add tests, update notes implementation

This commit is contained in:
Anton Stubenbord
2023-12-31 15:26:20 +01:00
parent d7f297a4df
commit 55aa42e4ab
29 changed files with 273 additions and 115 deletions

View File

@@ -4,3 +4,4 @@ export 'src/models/models.dart';
export 'src/modules/modules.dart';
export 'src/converters/converters.dart';
export 'config/hive/hive_type_ids.dart';
export 'src/interceptor/dio_http_error_interceptor.dart';

View File

@@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
class DioHttpErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) {
final data = err.response!.data;
if (PaperlessServerMessageException.canParse(data)) {
final exception = PaperlessServerMessageException.fromJson(data);
final message = exception.detail;
handler.reject(
DioException(
message: message,
requestOptions: err.requestOptions,
error: exception,
response: err.response,
type: DioExceptionType.badResponse,
),
);
} else if (PaperlessFormValidationException.canParse(data)) {
final exception = PaperlessFormValidationException.fromJson(data);
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: exception,
response: err.response,
type: DioExceptionType.badResponse,
),
);
} else if (data is String) {
if (data.contains("No required SSL certificate was sent")) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
type: DioExceptionType.badResponse,
error: const PaperlessApiException(
ErrorCode.missingClientCertificate),
),
);
} else {
handler.reject(
DioException(
requestOptions: err.requestOptions,
message: data,
error: PaperlessApiException(
ErrorCode.documentLoadFailed,
details: data,
),
response: err.response,
stackTrace: err.stackTrace,
type: DioExceptionType.badResponse,
),
);
}
} else {
handler.reject(err);
}
}
}
}

View File

@@ -125,8 +125,8 @@ class DocumentFilter extends Equatable {
return queryParams;
}
@override
String toString() => toQueryParameters().toString();
// @override
// String toString() => toQueryParameters().toString();
DocumentFilter copyWith({
int? pageSize,
@@ -249,9 +249,4 @@ class DocumentFilter extends Equatable {
moreLike,
selectedView,
];
// factory DocumentFilter.fromJson(Map<String, dynamic> json) =>
// _$DocumentFilterFromJson(json);
// Map<String, dynamic> toJson() => _$DocumentFilterToJson(this);
}

View File

@@ -82,7 +82,6 @@ class FilterRule with EquatableMixin {
assert(filter.tags is IdsTagsQuery);
return filter.copyWith(
tags: switch (filter.tags) {
// TODO: Handle this case.
IdsTagsQuery(include: var i, exclude: var e) => IdsTagsQuery(
include: [...i, int.parse(value!)],
exclude: e,

View File

@@ -28,4 +28,4 @@ export 'task/task.dart';
export 'task/task_status.dart';
export 'user_model.dart';
export 'exception/exceptions.dart';
export 'note_model.dart';
export 'note_model.dart' show NoteModel;

View File

@@ -1,3 +1,5 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'note_model.freezed.dart';
part 'note_model.g.dart';
@@ -9,9 +11,19 @@ class NoteModel with _$NoteModel {
required String? note,
required DateTime? created,
required int? document,
required int? user,
@JsonKey(fromJson: parseNoteUserFromJson) required int? user,
}) = _NoteModel;
factory NoteModel.fromJson(Map<String, dynamic> json) =>
_$NoteModelFromJson(json);
}
int? parseNoteUserFromJson(dynamic json) {
if (json == null) return null;
if (json is Map) {
return json['id'];
} else if (json is int) {
return json;
}
return null;
}

View File

@@ -11,7 +11,16 @@ class PaperlessApiException implements Exception {
this.httpStatusCode,
});
const PaperlessApiException.unknown() : this(ErrorCode.unknown);
const PaperlessApiException.unknown({
String? details,
StackTrace? stackTrace,
int? httpStatusCode,
}) : this(
ErrorCode.unknown,
details: details,
stackTrace: stackTrace,
httpStatusCode: httpStatusCode,
);
@override
String toString() {
@@ -71,5 +80,7 @@ enum ErrorCode {
updateSavedViewError,
customFieldCreateFailed,
customFieldLoadFailed,
customFieldDeleteFailed;
customFieldDeleteFailed,
deleteNoteFailed,
addNoteFailed;
}

View File

@@ -11,7 +11,7 @@ import 'date_range_unit.dart';
part 'date_range_query.g.dart';
sealed class DateRangeQuery {
sealed class DateRangeQuery with EquatableMixin {
const DateRangeQuery();
Map<String, String> toQueryParameter(DateRangeQueryField field);
@@ -28,10 +28,13 @@ class UnsetDateRangeQuery extends DateRangeQuery {
@override
bool matches(DateTime dt) => true;
@override
List<Object?> get props => [];
}
@HiveType(typeId: PaperlessApiHiveTypeIds.relativeDateRangeQuery)
class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
class RelativeDateRangeQuery extends DateRangeQuery {
@HiveField(0)
final int offset;
@HiveField(1)
@@ -84,7 +87,7 @@ class RelativeDateRangeQuery extends DateRangeQuery with EquatableMixin {
@JsonSerializable()
@HiveType(typeId: PaperlessApiHiveTypeIds.absoluteDateRangeQuery)
class AbsoluteDateRangeQuery extends DateRangeQuery with EquatableMixin {
class AbsoluteDateRangeQuery extends DateRangeQuery {
@LocalDateTimeJsonConverter()
@HiveField(0)
final DateTime? after;

View File

@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
part 'id_query_parameter.g.dart';
sealed class IdQueryParameter {
sealed class IdQueryParameter with EquatableMixin {
const IdQueryParameter();
Map<String, String> toQueryParameter(String field);
bool matches(int? id);
@@ -23,6 +23,9 @@ class UnsetIdQueryParameter extends IdQueryParameter {
@override
bool matches(int? id) => true;
@override
List<Object?> get props => [];
}
// @HiveType(typeId: PaperlessApiHiveTypeIds.notAssignedIdQueryParameter)
@@ -36,6 +39,8 @@ class NotAssignedIdQueryParameter extends IdQueryParameter {
@override
bool matches(int? id) => id == null;
@override
List<Object?> get props => [];
}
// @HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedIdQueryParameter)
@@ -48,6 +53,8 @@ class AnyAssignedIdQueryParameter extends IdQueryParameter {
@override
bool matches(int? id) => id != null;
@override
List<Object?> get props => [];
}
@HiveType(typeId: PaperlessApiHiveTypeIds.setIdQueryParameter)

View File

@@ -4,7 +4,7 @@ import 'package:paperless_api/config/hive/hive_type_ids.dart';
part 'tags_query.g.dart';
sealed class TagsQuery {
sealed class TagsQuery with EquatableMixin {
const TagsQuery();
Map<String, String> toQueryParameter();
bool matches(Iterable<int> ids);
@@ -20,10 +20,13 @@ class NotAssignedTagsQuery extends TagsQuery {
@override
bool matches(Iterable<int> ids) => ids.isEmpty;
@override
List<Object?> get props => [];
}
@HiveType(typeId: PaperlessApiHiveTypeIds.anyAssignedTagsQuery)
class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
class AnyAssignedTagsQuery extends TagsQuery {
@HiveField(0)
final List<int> tagIds;
const AnyAssignedTagsQuery({
@@ -54,7 +57,7 @@ class AnyAssignedTagsQuery extends TagsQuery with EquatableMixin {
}
@HiveType(typeId: PaperlessApiHiveTypeIds.idsTagsQuery)
class IdsTagsQuery extends TagsQuery with EquatableMixin {
class IdsTagsQuery extends TagsQuery {
@HiveField(0)
final List<int> include;
@HiveField(1)

View File

@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/config/hive/hive_type_ids.dart';
@@ -91,6 +92,11 @@ class TextQuery {
return other.queryText == queryText && other.queryType == queryType;
}
@override
String toString() {
return "TextQuery($queryText, $queryType)";
}
@override
int get hashCode => Object.hash(queryText, queryType);
}

View File

@@ -1,9 +1,4 @@
import 'package:paperless_api/src/models/exception/exceptions.dart';
abstract class PaperlessAuthenticationApi {
///
/// @throws [PaperlessUnauthorizedException]
///
Future<String> login({
required String username,
required String password,

View File

@@ -37,6 +37,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
// return AuthenticationTemporaryRedirect(redirectUrl!);
} on DioException catch (exception) {
throw exception.unravel();
} catch (error, stackTrace) {
throw PaperlessApiException.unknown(
details: error.toString(),
stackTrace: stackTrace,
);
}
}
}

View File

@@ -36,4 +36,7 @@ abstract class PaperlessDocumentsApi {
Future<FieldSuggestions> findSuggestions(DocumentModel document);
Future<List<String>> autocomplete(String query, [int limit = 10]);
Future<DocumentModel> addNote(
{required DocumentModel document, required String text});
}

View File

@@ -337,7 +337,30 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
return document.copyWith(notes: notes);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed),
orElse: const PaperlessApiException(ErrorCode.deleteNoteFailed),
);
}
}
@override
Future<DocumentModel> addNote({
required DocumentModel document,
required String text,
}) async {
try {
final response = await client.post(
"/api/documents/${document.id}/notes/",
options: Options(validateStatus: (status) => status == 200),
data: {'note': text},
);
final notes =
(response.data as List).map((e) => NoteModel.fromJson(e)).toList();
return document.copyWith(notes: notes);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.addNoteFailed),
);
}
}

View File

@@ -22,6 +22,8 @@ dependencies:
jiffy: ^5.0.0
freezed_annotation: ^2.4.1
hive: ^2.2.3
mockito: ^5.4.4
http_mock_adapter: ^0.6.1
dev_dependencies:
flutter_test:

View File

@@ -0,0 +1,90 @@
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:mockito/mockito.dart';
import 'package:paperless_api/paperless_api.dart';
void main() {
group('AuthenticationApi with DioHttpErrorIncerceptor', () {
late PaperlessAuthenticationApi authenticationApi;
late DioAdapter mockAdapter;
const token = "abcde";
const invalidCredentialsServerMessage =
"Unable to log in with provided credentials.";
setUp(() {
final dio = Dio()..interceptors.add(DioHttpErrorInterceptor());
authenticationApi = PaperlessAuthenticationApiImpl(dio);
mockAdapter = DioAdapter(dio: dio);
// Valid credentials
mockAdapter.onPost(
"/api/token/",
data: {
"username": "username",
"password": "password",
},
(server) => server.reply(200, {"token": token}),
);
// Invalid credentials
mockAdapter.onPost(
"/api/token/",
data: {
"username": "wrongUsername",
"password": "wrongPassword",
},
(server) => server.reply(400, {
"non_field_errors": [invalidCredentialsServerMessage]
}),
);
});
// tearDown(() {});
test(
'should return a valid token when logging in with valid credentials',
() {
expect(
authenticationApi.login(
username: "username",
password: "password",
),
completion(token),
);
},
);
test(
'should throw a PaperlessFormValidationException containing a reason '
'when logging in with invalid credentials',
() {
expect(
authenticationApi.login(
username: "wrongUsername",
password: "wrongPassword",
),
throwsA(isA<PaperlessFormValidationException>().having(
(e) => e.unspecificErrorMessage(),
"non-field specific error message",
equals(invalidCredentialsServerMessage),
)),
);
},
);
test(
'should return an error when logging in with invalid credentials',
() {
expect(
authenticationApi.login(
username: "wrongUsername",
password: "wrongPassword",
),
throwsA(isA<PaperlessFormValidationException>().having(
(e) => e.unspecificErrorMessage(),
"non-field specific error message",
equals(invalidCredentialsServerMessage),
)),
);
},
);
});
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:paperless_api/paperless_api.dart';
void main() {
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
group('Parsing [SavedView] to [DocumentFilter]:', () {
test('Values are correctly parsed if set.', () {
expect(
SavedView.fromJson({
@@ -64,7 +64,7 @@ void main() {
]
}).toDocumentFilter(),
equals(
DocumentFilter.initial.copyWith(
DocumentFilter(
correspondent: const SetIdQueryParameter(id: 42),
documentType: const SetIdQueryParameter(id: 69),
storagePath: const SetIdQueryParameter(id: 14),
@@ -83,6 +83,7 @@ void main() {
sortField: SortField.created,
sortOrder: SortOrder.descending,
query: const TextQuery.extended("Never gonna give you up"),
selectedView: 1,
),
),
);
@@ -99,7 +100,11 @@ void main() {
"sort_reverse": true,
"filter_rules": [],
}).toDocumentFilter(),
equals(DocumentFilter.initial),
equals(
const DocumentFilter(
selectedView: 1,
),
),
);
});
@@ -130,11 +135,12 @@ void main() {
},
],
}).toDocumentFilter();
final expected = DocumentFilter.initial.copyWith(
correspondent: const NotAssignedIdQueryParameter(),
documentType: const NotAssignedIdQueryParameter(),
storagePath: const NotAssignedIdQueryParameter(),
tags: const NotAssignedTagsQuery(),
const expected = DocumentFilter(
correspondent: NotAssignedIdQueryParameter(),
documentType: NotAssignedIdQueryParameter(),
storagePath: NotAssignedIdQueryParameter(),
tags: NotAssignedTagsQuery(),
selectedView: 1,
);
expect(
actual,
@@ -148,6 +154,7 @@ void main() {
expect(
SavedView.fromDocumentFilter(
DocumentFilter(
selectedView: 1,
correspondent: const SetIdQueryParameter(id: 1),
documentType: const SetIdQueryParameter(id: 2),
storagePath: const SetIdQueryParameter(id: 3),
@@ -173,6 +180,7 @@ void main() {
),
equals(
SavedView(
id: 1,
name: "test_name",
showOnDashboard: false,
showInSidebar: false,