From 39b2f9e6ae5fbd10957c26bbdf9ea51a5d392365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 30 Apr 2025 23:23:57 +0100 Subject: [PATCH 1/5] :heavy_plus_sign: add qs_dart and validators dependencies --- pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 7a48f4f..66eb69a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,8 @@ environment: dependencies: http: ^1.2.1 + qs_dart: ^1.3.5+1 + validators: ^3.0.0 dev_dependencies: lints: ^4.0.0 From e6d2cc3ec5fc4b46fb1f76984dee178fe6a38fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 30 Apr 2025 23:24:14 +0100 Subject: [PATCH 2/5] :zap: improve URL validation and query parameter handling in buildUrlString --- lib/utils/query_parameters.dart | 61 +++++++++++++++------------------ 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/lib/utils/query_parameters.dart b/lib/utils/query_parameters.dart index 2dc6ac4..3c4246a 100644 --- a/lib/utils/query_parameters.dart +++ b/lib/utils/query_parameters.dart @@ -1,41 +1,36 @@ +import 'package:qs_dart/qs_dart.dart' as qs; +import 'package:validators/validators.dart' as validators; + /// Takes a string and appends [parameters] as query parameters of [url]. /// -/// It does not check if [url] is valid, it just appends the parameters. +/// Throws [ArgumentError] if [url] is not a valid absolute HTTP(S) URL. String buildUrlString(String url, Map? parameters) { - // Avoids unnecessary processing. - if (parameters == null) return url; + late final Uri uri; - // Check if there are parameters to add. - if (parameters.isNotEmpty) { - // Checks if the string url already has parameters. - if (url.contains("?")) { - url += "&"; - } else { - url += "?"; + try { + if (!validators.isURL(url)) { + throw FormatException('Invalid URL format'); } - - // Concat every parameter to the string url. - parameters.forEach((key, value) { - if (value is List) { - if (value is List) { - for (String singleValue in value) { - url += "$key=${Uri.encodeQueryComponent(singleValue)}&"; - } - } else { - for (dynamic singleValue in value) { - url += "$key=${Uri.encodeQueryComponent(singleValue.toString())}&"; - } - } - } else if (value is String) { - url += "$key=${Uri.encodeQueryComponent(value)}&"; - } else { - url += "$key=${Uri.encodeQueryComponent(value.toString())}&"; - } - }); - - // Remove last '&' character. - url = url.substring(0, url.length - 1); + uri = Uri.parse(url); + } on FormatException { + throw ArgumentError.value(url, 'url', 'Must be a valid URL'); } - return url; + return parameters?.isNotEmpty ?? false + ? uri + .replace( + query: qs.encode( + { + ...uri.queryParametersAll, + ...?parameters, + }, + qs.EncodeOptions( + listFormat: qs.ListFormat.repeat, + skipNulls: false, + strictNullHandling: false, + ), + ), + queryParameters: null) + .toString() + : url; } From 52ddf3c0183491ebf528bb03025600ea8a8720eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 30 Apr 2025 23:24:34 +0100 Subject: [PATCH 3/5] :white_check_mark: enhance URL parameter handling tests in buildUrlString --- test/utils/utils_test.dart | 271 ++++++++++++++++++++++++++++++++++--- 1 file changed, 255 insertions(+), 16 deletions(-) diff --git a/test/utils/utils_test.dart b/test/utils/utils_test.dart index 5bcd64e..33c58f5 100644 --- a/test/utils/utils_test.dart +++ b/test/utils/utils_test.dart @@ -5,44 +5,283 @@ main() { group("buildUrlString", () { test("Adds parameters to a URL string without parameters", () { // Arrange - String url = "https://www.google.com/helloworld"; - Map parameters = {"foo": "bar", "num": "0"}; + final String url = "https://www.google.com/helloworld"; + final Map parameters = {"foo": "bar", "num": "0"}; // Act - String parameterUrl = buildUrlString(url, parameters); + final String parameterUrl = buildUrlString(url, parameters); // Assert - expect(parameterUrl, - equals("https://www.google.com/helloworld?foo=bar&num=0")); + expect( + parameterUrl, + equals("https://www.google.com/helloworld?foo=bar&num=0"), + ); }); + test("Adds parameters to a URL string with parameters", () { // Arrange - String url = "https://www.google.com/helloworld?foo=bar&num=0"; - Map parameters = {"extra": "1", "extra2": "anotherone"}; + final String url = "https://www.google.com/helloworld?foo=bar&num=0"; + final Map parameters = { + "extra": "1", + "extra2": "anotherone" + }; // Act - String parameterUrl = buildUrlString(url, parameters); + final String parameterUrl = buildUrlString(url, parameters); // Assert expect( - parameterUrl, - equals( - "https://www.google.com/helloworld?foo=bar&num=0&extra=1&extra2=anotherone")); + parameterUrl, + equals( + "https://www.google.com/helloworld?foo=bar&num=0&extra=1&extra2=anotherone", + ), + ); }); + test("Adds parameters with array to a URL string without parameters", () { // Arrange - String url = "https://www.google.com/helloworld"; - Map parameters = { + final String url = "https://www.google.com/helloworld"; + final Map parameters = { "foo": "bar", "num": ["0", "1"], }; // Act - String parameterUrl = buildUrlString(url, parameters); + final String parameterUrl = buildUrlString(url, parameters); // Assert - expect(parameterUrl, - equals("https://www.google.com/helloworld?foo=bar&num=0&num=1")); + expect( + parameterUrl, + equals("https://www.google.com/helloworld?foo=bar&num=0&num=1"), + ); + }); + + test("Null parameters returns original URL", () { + final url = "https://example.com/path"; + expect( + buildUrlString(url, null), + equals(url), + ); + }); + + test("Empty parameters returns original URL", () { + final url = "https://example.com/path"; + expect( + buildUrlString(url, {}), + equals(url), + ); + }); + + test("Null parameter value becomes empty assignment", () { + final url = "https://example.com/path"; + final params = {"a": null}; + expect( + buildUrlString(url, params), + equals("https://example.com/path?a="), + ); + }); + + test("Overrides existing parameter", () { + final url = "https://example.com/path?foo=bar"; + final params = {"foo": "baz", "x": "y"}; + expect( + buildUrlString(url, params), + equals("https://example.com/path?foo=baz&x=y"), + ); + }); + + test("Preserves fragment without existing query", () { + final url = "https://example.com/path#section"; + final params = {"a": "1"}; + expect(buildUrlString(url, params), + equals("https://example.com/path?a=1#section")); + }); + + test("Preserves fragment with existing query", () { + final url = "https://example.com/path?foo=bar#section"; + final params = {"baz": "qux"}; + expect(buildUrlString(url, params), + equals("https://example.com/path?foo=bar&baz=qux#section")); + }); + + test("Invalid URL does not trigger concatenation fallback", () { + final url = "not a valid url"; + final params = {"a": "b"}; + expect(() => buildUrlString(url, params), throwsArgumentError); + }); + + test("Encodes special characters in keys and values", () { + final url = "https://example.com"; + final params = {"a b": "c d", "ä": "ö"}; + expect( + buildUrlString(url, params), + equals("https://example.com?a%20b=c%20d&%C3%A4=%C3%B6"), + ); + }); + + test("Numeric and boolean values are stringified", () { + final url = "https://example.com"; + final params = {"int": 42, "bool": true}; + expect( + buildUrlString(url, params), + equals("https://example.com?int=42&bool=true"), + ); + }); + + test("List parameter overrides existing singular key", () { + final url = "https://example.com/path?x=1"; + final params = { + "x": ["2", "3"] + }; + expect( + buildUrlString(url, params), + equals("https://example.com/path?x=2&x=3"), + ); + }); + + test('encodes a query string object (basic key/value)', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a': 'b'}), + equals('$testUrl?a=b'), + ); + expect( + buildUrlString(testUrl, {'a': '1'}), + equals('$testUrl?a=1'), + ); + expect( + buildUrlString(testUrl, {'a': '1', 'b': '2'}), + equals('$testUrl?a=1&b=2'), + ); + expect( + buildUrlString(testUrl, {'a': 'A_Z'}), + equals('$testUrl?a=A_Z'), + ); + }); + + test('encodes various unicode characters', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a': '€'}), + equals('$testUrl?a=%E2%82%AC'), + ); + expect( + buildUrlString(testUrl, {'a': ''}), + equals('$testUrl?a=%EE%80%80'), + ); + expect( + buildUrlString(testUrl, {'a': 'א'}), + equals('$testUrl?a=%D7%90'), + ); + expect( + buildUrlString(testUrl, {'a': '𐐷'}), + equals('$testUrl?a=%F0%90%90%B7'), + ); + }); + + test('increasing number of pairs', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a': 'b', 'c': 'd'}), + equals('$testUrl?a=b&c=d'), + ); + expect( + buildUrlString(testUrl, {'a': 'b', 'c': 'd', 'e': 'f'}), + equals('$testUrl?a=b&c=d&e=f'), + ); + expect( + buildUrlString(testUrl, {'a': 'b', 'c': 'd', 'e': 'f', 'g': 'h'}), + equals('$testUrl?a=b&c=d&e=f&g=h'), + ); + }); + + test('list values get repeated keys', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, { + 'a': ['b', 'c', 'd'], + 'e': 'f' + }), + equals('$testUrl?a=b&a=c&a=d&e=f'), + ); + }); + + test('empty map yields no query string', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {}), + buildUrlString(testUrl, {}).toString(), + ); + }); + + test('single key with empty string value', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a': ''}), + equals('$testUrl?a='), + ); + }); + + test('null value is not skipped', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a': null, 'b': '2'}), + equals('$testUrl?a=&b=2'), + ); + }); + + test('keys with special characters are encoded', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'a b': 'c d'}), + equals('$testUrl?a%20b=c%20d'), + ); + expect( + buildUrlString(testUrl, {'ä': 'ö'}), + equals('$testUrl?%C3%A4=%C3%B6'), + ); + }); + + test('values containing reserved characters', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'q': 'foo@bar.com'}), + equals('$testUrl?q=foo%40bar.com'), + ); + expect( + buildUrlString(testUrl, {'path': '/home'}), + equals('$testUrl?path=%2Fhome'), + ); + }); + + test('plus sign and space in value', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, {'v': 'a+b c'}), + equals('$testUrl?v=a%2Bb%20c'), + ); + }); + + test('list values including numbers and empty strings', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, { + 'x': ['1', '', '3'], + }), + equals('$testUrl?x=1&x=&x=3'), + ); + }); + + test('multiple keys maintain insertion order', () { + final String testUrl = 'https://example.com/path'; + expect( + buildUrlString(testUrl, { + 'first': '1', + 'second': '2', + 'third': '3', + }), + equals('$testUrl?first=1&second=2&third=3'), + ); }); }); } From 71706f32dade353c9bf6f6782ee0d6c1037a3ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Wed, 30 Apr 2025 23:39:50 +0100 Subject: [PATCH 4/5] :memo: update error message for invalid URL in buildUrlString --- lib/utils/query_parameters.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/query_parameters.dart b/lib/utils/query_parameters.dart index 3c4246a..6e39c39 100644 --- a/lib/utils/query_parameters.dart +++ b/lib/utils/query_parameters.dart @@ -3,7 +3,7 @@ import 'package:validators/validators.dart' as validators; /// Takes a string and appends [parameters] as query parameters of [url]. /// -/// Throws [ArgumentError] if [url] is not a valid absolute HTTP(S) URL. +/// Throws [ArgumentError] if [url] is not a valid URL. String buildUrlString(String url, Map? parameters) { late final Uri uri; From fd686b1876caea5c410097d1692ab868a55129a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemen=20Tu=C5=A1ar?= Date: Tue, 22 Jul 2025 09:05:11 +0100 Subject: [PATCH 5/5] :arrow_up: bump qs dependency --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 66eb69a..fc917a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ environment: dependencies: http: ^1.2.1 - qs_dart: ^1.3.5+1 + qs_dart: ^1.3.8 validators: ^3.0.0 dev_dependencies: