Files
fl-query/packages/fl_query/test/graphql_client_test.dart
T
Kingkor Roy Tirtho 3ec81de606 +QueryObserver
+Query
+notifyManager
+onlineManager
+QueryCache
2022-02-10 18:29:42 +06:00

889 lines
24 KiB
Dart

import 'package:fl_query/src/core/result_parser.dart';
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:fl_query/fl_query.dart';
import 'package:gql/language.dart';
import './helpers.dart';
void main() {
const String readSingle = r'''
query ReadSingle($id: ID!) {
single(id: $id) {
id,
__typename,
name
}
}
''';
const String writeSingle = r'''
mutation WriteSingle($id: ID!, $name: String!) {
updateSingle(id: $id, name: $name) {
id,
__typename,
name
}
}
''';
const String readRepositories = r'''
query ReadRepositories($nRepositories: Int!) {
viewer {
repositories(last: $nRepositories) {
nodes {
__typename
id
name
viewerHasStarred
}
}
}
}
''';
Map<String, dynamic> readRepositoryData({
bool withTypenames = true,
bool withIds = true,
bool viewerHasStarred = false,
}) {
return {
'viewer': {
'repositories': {
'nodes': [
{
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==',
'name': 'pq',
'viewerHasStarred': viewerHasStarred
},
{
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==',
'name': 'go-evercookie',
'viewerHasStarred': viewerHasStarred
},
{
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==',
'name': 'watchbot',
'viewerHasStarred': viewerHasStarred
},
]
.map((map) =>
withTypenames ? {'__typename': 'Repository', ...map} : map)
.toList(),
},
},
};
}
const String addStar = r'''
mutation AddStar($starrableId: ID!) {
action: addStar(input: {starrableId: $starrableId}) {
starrable {
viewerHasStarred
}
}
}
''';
late MockLink link;
late GraphQLClient client;
group('simple json', () {
setUp(() {
link = MockLink();
client = GraphQLClient(
cache: getTestCache(),
link: link,
);
});
group('query', () {
test('successful response', () async {
final _options = QueryOptions(
document: parseString(readRepositories),
variables: <String, dynamic>{
'nRepositories': 42,
},
);
final repoData = readRepositoryData(withTypenames: true);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable([
Response(
data: repoData,
context: Context().withEntry(
HttpLinkResponseContext(
statusCode: 200,
headers: {'foo': 'bar'},
),
),
),
]),
);
final QueryResult r = await client.query(_options);
verify(
link.request(
Request(
operation: Operation(
document: parseString(readRepositories),
//operationName: 'ReadRepositories',
),
variables: <String, dynamic>{
'nRepositories': 42,
},
context: Context(),
),
),
);
expect(r.exception, isNull);
expect(r.data, equals(repoData));
expect(
r.context.entry<HttpLinkResponseContext>()!.statusCode,
equals(200),
);
expect(
r.context.entry<HttpLinkResponseContext>()!.headers['foo'],
equals('bar'),
);
});
test('successful response with parser', () async {
final ResultParserFn<List<String>> parserFn = (data) {
return data['viewer']['repositories']['nodes']
.map<String>((node) => node['name'] as String)
.toList();
};
final _options = QueryOptions(
document: parseString(readRepositories),
variables: <String, dynamic>{
'nRepositories': 42,
},
parserFn: parserFn,
);
final repoData = readRepositoryData(withTypenames: true);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable([
Response(
data: repoData,
context: Context().withEntry(
HttpLinkResponseContext(
statusCode: 200,
headers: {'foo': 'bar'},
),
),
),
]),
);
final QueryResult<List<String>> r = await client.query(_options);
verify(
link.request(
Request(
operation: Operation(
document: parseString(readRepositories),
//operationName: 'ReadRepositories',
),
variables: <String, dynamic>{
'nRepositories': 42,
},
context: Context(),
),
),
);
expect(r.exception, isNull);
expect(r.data, equals(repoData));
List<String>? parsedData = r.parsedData;
expect(
parsedData,
equals([
'pq',
'go-evercookie',
'watchbot',
]));
expect(
r.context.entry<HttpLinkResponseContext>()!.statusCode,
equals(200),
);
expect(
r.context.entry<HttpLinkResponseContext>()!.headers['foo'],
equals('bar'),
);
});
test('successful response without normalization', () async {
final readUnidentifiedRepositories = parseString(r'''
query ReadRepositories($nRepositories: Int!) {
viewer {
repositories(last: $nRepositories) {
nodes {
name
viewerHasStarred
}
}
}
}
''');
final repoData = readRepositoryData(
withTypenames: false,
withIds: false,
);
final _options = QueryOptions(
document: readUnidentifiedRepositories,
variables: {'nRepositories': 42},
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable([
Response(data: repoData),
]),
);
final QueryResult r = await client.query(_options);
verify(link.request(_options.asRequest));
expect(r.data, equals(repoData));
});
test('correct consecutive responses', () async {
final _options = QueryOptions(
fetchPolicy: FetchPolicy.networkOnly,
document: parseString(readRepositories),
variables: <String, dynamic>{
'nRepositories': 42,
},
);
final firstData =
readRepositoryData(withTypenames: true, viewerHasStarred: false);
final secondData =
readRepositoryData(withTypenames: true, viewerHasStarred: true);
final resp = (d) => Stream.fromIterable([
Response(
data: d,
context: Context().withEntry(
HttpLinkResponseContext(
statusCode: 200,
headers: {'foo': 'bar'},
),
),
)
]);
when(link.request(any)).thenAnswer((_) => resp(firstData));
QueryResult r = await client.query(_options);
expect(r.exception, isNull);
expect(r.data, equals(firstData));
when(link.request(any)).thenAnswer((_) => resp(secondData));
r = await client.query(_options);
expect(r.exception, isNull);
expect(r.data, equals(secondData));
});
test('malformed server response', () async {
final _options = QueryOptions(
document: parseString(readRepositories),
variables: {'nRepositories': 42},
);
final malformedRepoData = {
'viewer': {
// maybe the server doesn't validate response structures properly,
// or a user generates a response on the client, etc
'repos': readRepositoryData()['viewer']!['repositories']
},
};
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable([
Response(data: malformedRepoData),
]),
);
final QueryResult r = await client.query(_options);
expect(r.data, equals(malformedRepoData),
reason: 'Malformed data should be passed along with errors');
throwsA(isA<PartialDataException>().having(
(e) => e.path,
'An accurate path to the first missing subfield',
['a', 'b', '__typename'],
));
});
test('failed query because of an exception with null string', () async {
final e = Exception();
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromFuture(Future.error(e)),
);
final QueryResult r = await client.query(
WatchQueryOptions(
document: parseString(readRepositories),
),
);
expect(
r.exception!.exception!.originalException,
e,
);
});
test('failed query because of an exception with empty string', () async {
final e = Exception('');
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromFuture(Future.error(e)),
);
final QueryResult r = await client.query(
WatchQueryOptions(
document: parseString(readRepositories),
),
);
expect(
r.exception!.exception!.originalException,
e,
);
});
// test('failed query because of because of error response', {});
// test('failed query because of because of invalid response', () {
// String responseBody =
// '{\"message\":\"Bad credentials\",\"documentation_url\":\"https://developer.github.com/v4\"}';
// int responseCode = 401;
// });
// test('partially success query with some errors', {});
});
group('mutation', () {
test('query stream notified', () async {
final initialQueryResponse = Response(
data: <String, dynamic>{
'single': {
'id': '1',
'__typename': 'Single',
'name': 'initialQueryName',
},
},
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(
[initialQueryResponse],
),
);
final ObservableQuery observable = client.watchQuery(
WatchQueryOptions(
document: parseString(readSingle),
eagerlyFetchResults: true,
variables: {'id': '1'},
),
);
expect(
observable.stream,
emitsInOrder(
[
// we have no optimistic result
isA<QueryResult>().having(
(result) => result.isLoading,
'loading result',
true,
),
isA<QueryResult>().having(
(result) => result.data!['single']['name'],
'initial query result',
'initialQueryName',
),
isA<QueryResult>().having(
(result) => result.data!['single']['name'],
'result caused by mutation',
'newNameFromMutation',
)
],
),
);
final mutationResponseWithNewName = Response(
data: <String, dynamic>{
'updateSingle': {
'id': '1',
'__typename': 'Single',
'name': 'newNameFromMutation',
},
},
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(
[mutationResponseWithNewName],
),
);
final variables = {'id': '1', 'name': 'newNameFromMutation'};
final QueryResult response = await client.mutate(MutationOptions(
document: parseString(writeSingle), variables: variables));
expect(response.data!['updateSingle']['name'], variables['name']);
});
test('successful mutation', () async {
final MutationOptions _options = MutationOptions(
document: parseString(addStar),
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(
[
Response(
data: <String, dynamic>{
'action': {
'starrable': {
'viewerHasStarred': true,
},
},
},
),
],
),
);
final QueryResult response = await client.mutate(_options);
verify(
link.request(
Request(
operation: Operation(
document: parseString(addStar),
//operationName: 'AddStar',
),
variables: <String, dynamic>{},
context: Context(),
),
),
);
expect(response.exception, isNull);
expect(response.data, isNotNull);
final bool? viewerHasStarred =
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
expect(viewerHasStarred, true);
});
test('successful mutation with parser', () async {
final ResultParserFn<bool> resultParser =
(data) => data['action']['starrable']['viewerHasStarred'] as bool;
final MutationOptions _options = MutationOptions(
document: parseString(addStar),
parserFn: resultParser,
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(
[
Response(
data: <String, dynamic>{
'action': {
'starrable': {
'viewerHasStarred': true,
},
},
},
),
],
),
);
final QueryResult response = await client.mutate(_options);
verify(
link.request(
Request(
operation: Operation(
document: parseString(addStar),
//operationName: 'AddStar',
),
variables: <String, dynamic>{},
context: Context(),
),
),
);
final bool parsedResult = response.parsedData;
expect(parsedResult, isTrue);
expect(response.exception, isNull);
expect(response.data, isNotNull);
final bool? viewerHasStarred =
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
expect(viewerHasStarred, true);
});
test('successful mutation through watchQuery', () async {
final _options = MutationOptions(
document: parseString(addStar),
variables: {},
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(
[
Response(
data: <String, dynamic>{
'action': {
'starrable': {
'viewerHasStarred': true,
},
},
},
),
],
),
);
final observableQuery = client.watchQuery(WatchQueryOptions(
document: _options.document,
variables: _options.variables,
fetchResults: false,
));
final result = await observableQuery.fetchResults().networkResult!;
verify(
link.request(
Request(
operation: Operation(
document: parseString(addStar),
//operationName: 'AddStar',
),
variables: <String, dynamic>{},
context: Context(),
),
),
);
expect(result.hasException, isFalse);
expect(result.data, isNotNull);
final bool? viewerHasStarred =
result.data!['action']['starrable']['viewerHasStarred'] as bool?;
expect(viewerHasStarred, true);
});
});
group('subscription', () {
test('results', () async {
final responses = [
{
'id': '1',
'name': 'first',
},
{
'id': '2',
'name': 'second',
},
].map((item) => Response(
data: <String, dynamic>{
'item': {
'__typename': 'Item',
...item,
},
},
));
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(responses),
);
final stream = client.subscribe(
SubscriptionOptions(
document: parseString(
r'''
subscription {
item {
id
name
}
}
''',
),
),
);
expect(
stream,
emitsInOrder(
[
isA<QueryResult>().having(
(result) => result.data!['item']['name'],
'first subscription item',
'first',
),
isA<QueryResult>().having(
(result) => result.data!['item']['name'],
'second subscription item',
'second',
)
],
),
);
});
test('parses results', () async {
final responses = [
{
'id': '1',
'name': 'first',
},
{
'id': '2',
'name': 'second',
},
].map((item) => Response(
data: <String, dynamic>{
'item': {
'__typename': 'Item',
...item,
},
},
));
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(responses),
);
final ResultParserFn<String> parserFn =
(data) => data['item']['name'] as String;
;
final stream = client.subscribe(
SubscriptionOptions(
parserFn: parserFn,
document: parseString(
r'''
subscription {
item {
id
name
}
}
''',
),
),
);
expect(
stream,
emitsInOrder(['first', 'second']
.map((e) => isA<QueryResult<String>>().having((result) {
final String? parsed = result.parsedData;
return parsed;
}, "Parsed item", e))),
);
});
test('wraps stream exceptions', () async {
final ex = ServerException(
parsedResponse: null,
originalException: Error(),
);
when(
link.request(any),
).thenAnswer(
(_) => Stream.error(ex),
);
final stream = client.subscribe(
SubscriptionOptions(
document: parseString(
r'''
subscription {
item {
id
name
}
}
''',
),
),
);
expect(
stream,
emitsInOrder(
[
isA<QueryResult>().having(
(result) => result.exception!.exception,
'wrapped exception',
ex,
),
],
),
);
});
test('wraps all exceptions from outside of stream', () async {
final err = Error();
when(
link.request(any),
).thenThrow(err);
final stream = client.subscribe(
SubscriptionOptions(
document: parseString(
r'''
subscription {
item {
id
name
}
}
''',
),
),
);
expect(
stream,
emitsInOrder(
[
isA<QueryResult>().having(
(result) => result.exception!.exception!.originalException,
'wrapped exception',
err,
),
],
),
);
});
});
});
group('direct cache access', () {
setUp(() {
link = MockLink();
client = GraphQLClient(
cache: getTestCache(),
link: link,
);
});
test('all methods with exposition', () {
/// entity identifiers for normalization
final idFields = {'__typename': 'MyType', 'id': 1};
/// The direct cache API uses `gql_link` Requests directly
/// These can also be obtained via `options.asRequest` from any `Options` object,
/// or via `Operation(document: gql(...)).asRequest()`
final queryRequest = Request(
operation: Operation(
document: gql(
r'''{
someField {
id,
myField
}
}''',
),
),
);
final queryData = {
'__typename': 'Query',
'someField': {
...idFields,
'myField': 'originalValue',
},
};
/// `broadcast: true` (the default) would rebroadcast cache updates to all safe instances of `ObservableQuery`
/// **NOTE**: only `GraphQLClient` can immediately call for a query rebroadcast. if you request a rebroadcast directly
/// from the cache, it still has to wait for the client to check in on it
client.writeQuery(queryRequest, data: queryData, broadcast: false);
/// `optimistic: true` (the default) integrates optimistic data
/// written to the cache into your read.
expect(
client.readQuery(queryRequest, optimistic: false), equals(queryData));
/// While fragments are never executed themselves, we provide a `gql_link`-like API for consistency.
/// These can also be obtained via `Fragment(document: gql(...)).asRequest()`.
final fragmentRequest = FragmentRequest(
fragment: Fragment(
document: gql(
r'''
fragment mySmallSubset on MyType {
myField,
someNewField
}
''',
),
),
idFields: idFields,
);
/// We've specified `idFields` and are only editing a subset of the data
final fragmentData = {
'myField': 'updatedValue',
'someNewField': [
{'newData': false}
],
};
/// We didn't disable `broadcast`, so all instances of `ObservableQuery` will be notified of any changes
client.writeFragment(fragmentRequest, data: fragmentData);
/// __typename is automatically included in all reads
expect(
client.readFragment(fragmentRequest),
equals({
'__typename': 'MyType',
...fragmentData,
}),
);
final updatedQueryData = {
'__typename': 'Query',
'someField': {
...idFields,
'myField': 'updatedValue',
},
};
/// `myField` is updated, but we don't have `someNewField`, as expected.
expect(client.readQuery(queryRequest), equals(updatedQueryData));
});
});
}