Skip to content

Commit 894f9c3

Browse files
binding [nfc]: Move userAgentHeader function to ZulipBinding
This is a preparatory commit for the work of embedding the device info in user-agent header.
1 parent 2f12fc4 commit 894f9c3

File tree

9 files changed

+216
-148
lines changed

9 files changed

+216
-148
lines changed

lib/api/core.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:io';
44
import 'package:http/http.dart' as http;
55

66
import '../log.dart';
7+
import '../model/binding.dart';
78
import '../model/localizations.dart';
89
import 'exception.dart';
910

@@ -37,11 +38,14 @@ class ApiConnection {
3738
String? email,
3839
String? apiKey,
3940
required http.Client client,
41+
Map<String, String> Function()? userAgentHeader,
4042
}) : assert((email != null) == (apiKey != null)),
4143
_authValue = (email != null && apiKey != null)
4244
? _authHeaderValue(email: email, apiKey: apiKey)
4345
: null,
44-
_client = client;
46+
_client = client,
47+
_userAgentHeader =
48+
userAgentHeader ?? (() => <String, String>{'User-Agent': 'ZulipFlutter'});
4549

4650
/// Construct an API connection that talks to a live Zulip server over the real network.
4751
ApiConnection.live({
@@ -51,7 +55,8 @@ class ApiConnection {
5155
String? apiKey,
5256
}) : this(client: http.Client(),
5357
realmUrl: realmUrl, zulipFeatureLevel: zulipFeatureLevel,
54-
email: email, apiKey: apiKey);
58+
email: email, apiKey: apiKey,
59+
userAgentHeader: ZulipBinding.instance.userAgentHeader);
5560

5661
final Uri realmUrl;
5762

@@ -69,6 +74,8 @@ class ApiConnection {
6974
/// * API docs at <https://zulip.com/api/changelog>.
7075
int? zulipFeatureLevel;
7176

77+
final Map<String, String> Function() _userAgentHeader;
78+
7279
final String? _authValue;
7380

7481
void addAuth(http.BaseRequest request) {
@@ -88,7 +95,7 @@ class ApiConnection {
8895
assert(debugLog("${request.method} ${request.url}"));
8996

9097
addAuth(request);
91-
request.headers.addAll(userAgentHeader());
98+
request.headers.addAll(_userAgentHeader());
9299
if (overrideUserAgent != null) {
93100
request.headers['User-Agent'] = overrideUserAgent;
94101
}
@@ -213,13 +220,6 @@ Map<String, String> authHeader({required String email, required String apiKey})
213220
};
214221
}
215222

216-
Map<String, String> userAgentHeader() {
217-
return {
218-
// TODO(#467) include platform, platform version, and app version
219-
'User-Agent': 'ZulipFlutter',
220-
};
221-
}
222-
223223
Map<String, String>? encodeParameters(Map<String, dynamic>? params) {
224224
return params?.map((k, v) =>
225225
MapEntry(k, v is RawParameter ? v.value : jsonEncode(v)));

lib/model/binding.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ abstract class ZulipBinding {
160160

161161
/// Wraps the [AndroidNotificationHostApi] constructor.
162162
AndroidNotificationHostApi get androidNotificationHost;
163+
164+
/// Generates a user agent header for HTTP requests.
165+
Map<String, String> userAgentHeader();
163166
}
164167

165168
/// Like [device_info_plus.BaseDeviceInfo], but without things we don't use.
@@ -275,6 +278,9 @@ class LiveZulipBinding extends ZulipBinding {
275278
return ZulipBinding.instance as LiveZulipBinding;
276279
}
277280

281+
// Stored user agent header, since it remains constant.
282+
Map<String, String>? _userAgentHeader;
283+
278284
@override
279285
Future<BaseDeviceInfo?> get deviceInfo => _deviceInfo;
280286
late Future<BaseDeviceInfo?> _deviceInfo;
@@ -379,4 +385,17 @@ class LiveZulipBinding extends ZulipBinding {
379385

380386
@override
381387
AndroidNotificationHostApi get androidNotificationHost => AndroidNotificationHostApi();
388+
389+
@override
390+
Map<String, String> userAgentHeader() {
391+
return _userAgentHeader ??= buildUserAgentHeader();
392+
}
393+
}
394+
395+
@visibleForTesting
396+
Map<String, String> buildUserAgentHeader() {
397+
return {
398+
// TODO(#467) include platform, platform version, and app version
399+
'User-Agent': 'ZulipFlutter',
400+
};
382401
}

lib/widgets/content.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,7 @@ class RealmContentNetworkImage extends StatelessWidget {
13081308
if (src.origin == account.realmUrl.origin) ...authHeader(
13091309
email: account.email, apiKey: account.apiKey,
13101310
),
1311-
...userAgentHeader(),
1311+
...ZulipBinding.instance.userAgentHeader(),
13121312
},
13131313
cacheWidth: cacheWidth,
13141314
cacheHeight: cacheHeight,

lib/widgets/lightbox.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:video_player/video_player.dart';
77
import '../api/core.dart';
88
import '../api/model/model.dart';
99
import '../log.dart';
10+
import '../model/binding.dart';
1011
import 'content.dart';
1112
import 'dialog.dart';
1213
import 'page.dart';
@@ -389,7 +390,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
389390
email: store.account.email,
390391
apiKey: store.account.apiKey,
391392
),
392-
...userAgentHeader()
393+
...ZulipBinding.instance.userAgentHeader(),
393394
});
394395
_controller!.addListener(_handleVideoControllerUpdate);
395396

test/api/core_test.dart

Lines changed: 69 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,31 @@ import 'package:zulip/api/core.dart';
88
import 'package:zulip/api/exception.dart';
99
import 'package:zulip/model/localizations.dart';
1010

11+
import '../model/binding.dart';
1112
import '../stdlib_checks.dart';
1213
import 'exception_checks.dart';
1314
import 'fake_api.dart';
1415
import '../example_data.dart' as eg;
1516

1617
void main() {
18+
TestZulipBinding.ensureInitialized();
19+
tearDown(testBinding.reset);
20+
1721
test('ApiConnection.get', () async {
1822
Future<void> checkRequest(Map<String, dynamic>? params, String expectedRelativeUrl) {
19-
return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
20-
connection.prepare(json: {});
21-
await connection.get(kExampleRouteName, (json) => json, 'example/route', params);
22-
check(connection.lastRequest!).isA<http.Request>()
23-
..method.equals('GET')
24-
..url.asString.equals('${eg.realmUrl.origin}$expectedRelativeUrl')
25-
..headers.deepEquals({
26-
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
27-
...userAgentHeader(),
28-
})
29-
..body.equals('');
23+
return FakeApiConnection.with_(account: eg.selfAccount,
24+
userAgentHeader: testBinding.userAgentHeader,
25+
(connection) async {
26+
connection.prepare(json: {});
27+
await connection.get(kExampleRouteName, (json) => json, 'example/route', params);
28+
check(connection.lastRequest!).isA<http.Request>()
29+
..method.equals('GET')
30+
..url.asString.equals('${eg.realmUrl.origin}$expectedRelativeUrl')
31+
..headers.deepEquals({
32+
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
33+
...testBinding.userAgentHeader(),
34+
})
35+
..body.equals('');
3036
});
3137
}
3238

@@ -47,19 +53,21 @@ void main() {
4753

4854
test('ApiConnection.post', () async {
4955
Future<void> checkRequest(Map<String, dynamic>? params, String expectedBody, {bool expectContentType = true}) {
50-
return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
51-
connection.prepare(json: {});
52-
await connection.post(kExampleRouteName, (json) => json, 'example/route', params);
53-
check(connection.lastRequest!).isA<http.Request>()
54-
..method.equals('POST')
55-
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
56-
..headers.deepEquals({
57-
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
58-
...userAgentHeader(),
59-
if (expectContentType)
60-
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
61-
})
62-
..body.equals(expectedBody);
56+
return FakeApiConnection.with_(account: eg.selfAccount,
57+
userAgentHeader: testBinding.userAgentHeader,
58+
(connection) async {
59+
connection.prepare(json: {});
60+
await connection.post(kExampleRouteName, (json) => json, 'example/route', params);
61+
check(connection.lastRequest!).isA<http.Request>()
62+
..method.equals('POST')
63+
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
64+
..headers.deepEquals({
65+
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
66+
...testBinding.userAgentHeader(),
67+
if (expectContentType)
68+
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
69+
})
70+
..body.equals(expectedBody);
6371
});
6472
}
6573

@@ -78,26 +86,28 @@ void main() {
7886

7987
test('ApiConnection.postFileFromStream', () async {
8088
Future<void> checkRequest(List<List<int>> content, int length, String? filename) {
81-
return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
82-
connection.prepare(json: {});
83-
await connection.postFileFromStream(
84-
kExampleRouteName, (json) => json, 'example/route',
85-
Stream.fromIterable(content), length, filename: filename);
86-
check(connection.lastRequest!).isA<http.MultipartRequest>()
87-
..method.equals('POST')
88-
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
89-
..headers.deepEquals({
90-
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
91-
...userAgentHeader(),
92-
})
93-
..fields.deepEquals({})
94-
..files.single.which((it) => it
95-
..field.equals('file')
96-
..length.equals(length)
97-
..filename.equals(filename)
98-
..has<Future<List<int>>>((f) => f.finalize().toBytes(), 'contents')
99-
.completes((it) => it.deepEquals(content.expand((l) => l)))
100-
);
89+
return FakeApiConnection.with_(account: eg.selfAccount,
90+
userAgentHeader: testBinding.userAgentHeader,
91+
(connection) async {
92+
connection.prepare(json: {});
93+
await connection.postFileFromStream(
94+
kExampleRouteName, (json) => json, 'example/route',
95+
Stream.fromIterable(content), length, filename: filename);
96+
check(connection.lastRequest!).isA<http.MultipartRequest>()
97+
..method.equals('POST')
98+
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
99+
..headers.deepEquals({
100+
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
101+
...testBinding.userAgentHeader(),
102+
})
103+
..fields.deepEquals({})
104+
..files.single.which((it) => it
105+
..field.equals('file')
106+
..length.equals(length)
107+
..filename.equals(filename)
108+
..has<Future<List<int>>>((f) => f.finalize().toBytes(), 'contents')
109+
.completes((it) => it.deepEquals(content.expand((l) => l)))
110+
);
101111
});
102112
}
103113

@@ -113,19 +123,21 @@ void main() {
113123

114124
test('ApiConnection.delete', () async {
115125
Future<void> checkRequest(Map<String, dynamic>? params, String expectedBody, {bool expectContentType = true}) {
116-
return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
117-
connection.prepare(json: {});
118-
await connection.delete(kExampleRouteName, (json) => json, 'example/route', params);
119-
check(connection.lastRequest!).isA<http.Request>()
120-
..method.equals('DELETE')
121-
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
122-
..headers.deepEquals({
123-
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
124-
...userAgentHeader(),
125-
if (expectContentType)
126-
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
127-
})
128-
..body.equals(expectedBody);
126+
return FakeApiConnection.with_(account: eg.selfAccount,
127+
userAgentHeader: testBinding.userAgentHeader,
128+
(connection) async {
129+
connection.prepare(json: {});
130+
await connection.delete(kExampleRouteName, (json) => json, 'example/route', params);
131+
check(connection.lastRequest!).isA<http.Request>()
132+
..method.equals('DELETE')
133+
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
134+
..headers.deepEquals({
135+
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
136+
...testBinding.userAgentHeader(),
137+
if (expectContentType)
138+
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
139+
})
140+
..body.equals(expectedBody);
129141
});
130142
}
131143

test/api/fake_api.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,27 +134,32 @@ class FakeApiConnection extends ApiConnection {
134134
int? zulipFeatureLevel = eg.futureZulipFeatureLevel,
135135
String? email,
136136
String? apiKey,
137+
Map<String, String> Function()? userAgentHeader,
137138
}) : this._(
138139
realmUrl: realmUrl ?? eg.realmUrl,
139140
zulipFeatureLevel: zulipFeatureLevel,
140141
email: email,
141142
apiKey: apiKey,
142143
client: FakeHttpClient(),
144+
userAgentHeader: userAgentHeader,
143145
);
144146

145-
FakeApiConnection.fromAccount(Account account)
146-
: this(
147+
FakeApiConnection.fromAccount(Account account, [
148+
Map<String, String> Function()? userAgentHeader,
149+
]) : this(
147150
realmUrl: account.realmUrl,
148151
zulipFeatureLevel: account.zulipFeatureLevel,
149152
email: account.email,
150-
apiKey: account.apiKey);
153+
apiKey: account.apiKey,
154+
userAgentHeader: userAgentHeader);
151155

152156
FakeApiConnection._({
153157
required super.realmUrl,
154158
required super.zulipFeatureLevel,
155159
super.email,
156160
super.apiKey,
157161
required this.client,
162+
required super.userAgentHeader,
158163
}) : super(client: client);
159164

160165
final FakeHttpClient client;
@@ -171,12 +176,16 @@ class FakeApiConnection extends ApiConnection {
171176
Uri? realmUrl,
172177
int? zulipFeatureLevel = eg.futureZulipFeatureLevel,
173178
Account? account,
179+
Map<String, String> Function()? userAgentHeader,
174180
}) async {
175181
assert((account == null)
176182
|| (realmUrl == null && zulipFeatureLevel == eg.futureZulipFeatureLevel));
177183
final connection = (account != null)
178-
? FakeApiConnection.fromAccount(account)
179-
: FakeApiConnection(realmUrl: realmUrl, zulipFeatureLevel: zulipFeatureLevel);
184+
? FakeApiConnection.fromAccount(account, userAgentHeader)
185+
: FakeApiConnection(
186+
realmUrl: realmUrl,
187+
zulipFeatureLevel: zulipFeatureLevel,
188+
userAgentHeader: userAgentHeader);
180189
try {
181190
return await fn(connection);
182191
} finally {

0 commit comments

Comments
 (0)