From 9a06489be58e309ab91357a25fa6aa0230025299 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 24 Sep 2016 14:30:01 -0400 Subject: [PATCH] Multipart support thanks to http_server --- .idea/libraries/Dart_Packages.xml | 188 +++++--- README.md | 6 +- Test Results - Run_All_Tests.html | 707 ---------------------------- lib/body_parser.dart | 162 +------ lib/src/body_parse_result.dart | 13 + lib/src/body_parser.dart | 21 + lib/src/chunk.dart | 8 + lib/{ => src}/file_upload_info.dart | 2 - lib/src/get_value.dart | 14 + lib/src/map_from_uri.dart | 41 ++ lib/src/parse_body.dart | 59 +++ pubspec.yaml | 10 +- test/all_tests.dart | 171 +------ test/server.dart | 110 +++++ test/uploads.dart | 135 ++++++ 15 files changed, 529 insertions(+), 1118 deletions(-) delete mode 100644 Test Results - Run_All_Tests.html create mode 100644 lib/src/body_parse_result.dart create mode 100644 lib/src/body_parser.dart create mode 100644 lib/src/chunk.dart rename lib/{ => src}/file_upload_info.dart (94%) create mode 100644 lib/src/get_value.dart create mode 100644 lib/src/map_from_uri.dart create mode 100644 lib/src/parse_body.dart create mode 100644 test/server.dart create mode 100644 test/uploads.dart diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 407e0ca6..6f7ec188 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,318 +5,350 @@ - - - - - - - - - - - - - - - + + + + + + - + + + + + + - - - - + + + + + + - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 1f705e3f..7975e410 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Body Parser -![version 1.0.0-dev](https://img.shields.io/badge/version-1.0.0--dev-red.svg) +![version 1.0.0-dev+1](https://img.shields.io/badge/version-1.0.0--dev-red.svg) **NOT YET PRODUCTION READY** @@ -68,6 +68,4 @@ Thank you for using this library. I hope you like it. Feel free to follow me on Twitter: -[@thosakwe](http://twitter.com/thosakwe) -or -[@regios_tech](http://twitter.com/regios_tech) \ No newline at end of file +[@_wapaa_](http://twitter.com/_wapaa_) \ No newline at end of file diff --git a/Test Results - Run_All_Tests.html b/Test Results - Run_All_Tests.html deleted file mode 100644 index f974794f..00000000 --- a/Test Results - Run_All_Tests.html +++ /dev/null @@ -1,707 +0,0 @@ - - - - -Test Results — All Tests - - - - - - - - - -
- -
-
    -
  • - -
    1.86 s
    -
    Test server support
    -
      -
    • - -
      1.03 s
      -
      query string
      -
        -
      • - -
        910 ms
        -
        passedGET Simple
        -
          -
        • -Test server listening on http://localhost:45244
          GET http://localhost:45244/?hello=world
          Response: {"body":{},"query":{"hello":"world"},"files":[]}
          -
        • -
        -
      • -
      • - -
        121 ms
        -
        passedGET Complex
        -
          -
        • -Test server listening on http://localhost:54627
          Body: hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=2&map.foo.bar=baz
          Response: {"body":{},"query":{"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}},"files":[]}
          -
        • -
        -
      • -
      -
    • -
    • - -
      454 ms
      -
      urlencoded
      -
        -
      • - -
        334 ms
        -
        passedPOST Simple
        -
          -
        • -Test server listening on http://localhost:38381
          Body: hello=world
          Response: {"body":{"hello":"world"},"query":{},"files":[]}
          -
        • -
        -
      • -
      • - -
        120 ms
        -
        passedPost Complex
        -
          -
        • -Test server listening on http://localhost:52437
          -
        • -
        -
      • -
      -
    • -
    • - -
      195 ms
      -
      JSON
      -
        -
      • - -
        83 ms
        -
        passedPost Simple
        -
          -
        • -Test server listening on http://localhost:59756
          Body: {"hello":"world"}
          Response: {"body":{"hello":"world"},"query":{},"files":[]}
          -
        • -
        -
      • -
      • - -
        112 ms
        -
        passedPost Complex
        -
          -
        • -Test server listening on http://localhost:39215
          Body: {"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}}
          Response: {"body":{"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}},"query":{},"files":[]}
          -
        • -
        -
      • -
      -
    • -
    • - -
      181 ms
      -
      File
      -
        -
      • - -
        181 ms
        -
        passedSingle upload
        -
          -
        • -Test server listening on http://localhost:55740
          Form Data:

          ----myBoundary
          Content-Disposition: form-data; name="hello"
          world
          ----myBoundary
          Content-Disposition: file; name="file"; filename="app.dart"
          Content-Type: text/plain
          Hello world
          ----myBoundary--
          Response: {"body":{"hello":"world"},"query":{},"files":[{"mimeType":"text/plain","name":"file","filename":"app.dart","data":[72,101,108,108,111,32,119,111,114,108,100]}]}
          -
        • -
        -
      • -
      • -ignoredMultiple upload -
      • -
      -
    • -
    -
  • -
-
-
- - - diff --git a/lib/body_parser.dart b/lib/body_parser.dart index 1f1b22ba..9bb69e35 100644 --- a/lib/body_parser.dart +++ b/lib/body_parser.dart @@ -4,161 +4,7 @@ library body_parser; import 'dart:async'; import 'dart:convert'; import 'dart:io'; - -part 'file_upload_info.dart'; - -/// A representation of data from an incoming request. -class BodyParseResult { - /// The parsed body. - Map body = {}; - - /// The parsed query string. - Map query = {}; - - /// All files uploaded within this request. - List files = []; -} - -/// Grabs data from an incoming request. -/// -/// Supports urlencoded and JSON, as well as multipart/form-data uploads. -/// On a file upload request, only fields with the name **'file'** are processed -/// as files. Anything else is put in the body. You can change the upload file name -/// via the *fileUploadName* parameter. :) -Future parseBody(HttpRequest request, - {String fileUploadName: 'file'}) async { - BodyParseResult result = new BodyParseResult(); - ContentType contentType = request.headers.contentType; - - // Parse body - if (contentType != null) { - if (contentType.mimeType == 'application/json') - result.body = JSON.decode(await request.transform(UTF8.decoder).join()); - else if (contentType.mimeType == 'application/x-www-form-urlencoded') { - String body = await request.transform(UTF8.decoder).join(); - buildMapFromUri(result.body, body); - } - } - - // Parse query - RegExp queryRgx = new RegExp(r'\?(.+)$'); - String uriString = request.requestedUri.toString(); - if (queryRgx.hasMatch(uriString)) { - Match queryMatch = queryRgx.firstMatch(uriString); - buildMapFromUri(result.query, queryMatch.group(1)); - } - - // Accept file - if (contentType != null && request.method == 'POST') { - RegExp parseBoundaryRgx = new RegExp( - r'multipart\/form-data;\s*boundary=([^\s;]+)'); - if (parseBoundaryRgx.hasMatch(contentType.toString())) { - Match boundaryMatch = parseBoundaryRgx.firstMatch(contentType.toString()); - String boundary = boundaryMatch.group(1); - String body = await request.transform(UTF8.decoder).join(); - for (String chunk in body.split(boundary)) { - var fileData = getFileDataFromChunk( - chunk, boundary, fileUploadName, result.body); - if (fileData != null) - fileData.forEach((x) => result.files.add(x)); - } - } - } - - return result; -} - -/// Parses file data from a multipart/form-data chunk. -List getFileDataFromChunk(String chunk, String boundary, String fileUploadName, - Map body) { - FileUploadInfo result = new FileUploadInfo(); - RegExp isFormDataRgx = new RegExp( - r'Content-Disposition:\s*([^;]+);\s*name="([^"]+)"'); - - if (isFormDataRgx.hasMatch(chunk)) { - Match formDataMatch = isFormDataRgx.firstMatch(chunk); - String disposition = formDataMatch.group(1); - String name = formDataMatch.group(2); - String restOfChunk = chunk.substring(formDataMatch.end); - - RegExp parseFilenameRgx = new RegExp(r'filename="([^"]+)"'); - if (parseFilenameRgx.hasMatch(chunk)) { - result.filename = parseFilenameRgx.firstMatch(chunk).group(1); - } - - RegExp contentTypeRgx = new RegExp(r'Content-Type:\s*([^\r\n]+)\r\n'); - if (contentTypeRgx.hasMatch(restOfChunk)) { - Match contentTypeMatch = contentTypeRgx.firstMatch(restOfChunk); - restOfChunk = restOfChunk.substring(contentTypeMatch.end); - result.mimeType = contentTypeMatch.group(1); - } else restOfChunk = restOfChunk.replaceAll(new RegExp(r'^(\r\n)+'), ""); - - restOfChunk = restOfChunk - .replaceAll(boundary, "") - .replaceFirst(new RegExp(r'\r\n$'), ""); - - if (disposition == 'file' && name == fileUploadName) { - result.name = name; - result.data = UTF8.encode(restOfChunk); - return [result]; - } else { - buildMapFromUri(body, "$name=$restOfChunk"); - return null; - } - } - - return null; -} - -/// Parses a URI-encoded string into real data! **Wow!** -/// -/// Whichever map you provide will be automatically populated from the urlencoded body string you provide. -buildMapFromUri(Map map, String body) { - RegExp parseArrayRgx = new RegExp(r'^(.+)\[\]$'); - - for (String keyValuePair in body.split('&')) { - if (keyValuePair.contains('=')) { - List split = keyValuePair.split('='); - String key = Uri.decodeQueryComponent(split[0]); - String value = Uri.decodeQueryComponent(split[1]); - - if (parseArrayRgx.hasMatch(key)) { - Match queryMatch = parseArrayRgx.firstMatch(key); - key = queryMatch.group(1); - if (!(map[key] is List)) { - map[key] = []; - } - - map[key].add(getValue(value)); - } else if (key.contains('.')) { - // i.e. map.foo.bar => [map, foo, bar] - List keys = key.split('.'); - - Map targetMap = map[keys[0]] ?? {}; - map[keys[0]] = targetMap; - for (int i = 1; i < keys.length; i++) { - if (i < keys.length - 1) { - targetMap[keys[i]] = targetMap[keys[i]] ?? {}; - targetMap = targetMap[keys[i]]; - } else { - targetMap[keys[i]] = getValue(value); - } - } - } - else map[key] = getValue(value); - } else map[Uri.decodeQueryComponent(keyValuePair)] = true; - } -} - -getValue(String value) { - num numValue = num.parse(value, (_) => double.NAN); - if (!numValue.isNaN) - return numValue; - else if (value.startsWith('[') && value.endsWith(']')) - return JSON.decode(value); - else if (value.startsWith('{') && value.endsWith('}')) - return JSON.decode(value); - else if (value.trim().toLowerCase() == 'null') - return null; - else return value; -} \ No newline at end of file +export 'src/body_parser.dart'; +export 'src/body_parse_result.dart'; +export 'src/file_upload_info.dart'; +export 'src/parse_body.dart'; diff --git a/lib/src/body_parse_result.dart b/lib/src/body_parse_result.dart new file mode 100644 index 00000000..98131fa9 --- /dev/null +++ b/lib/src/body_parse_result.dart @@ -0,0 +1,13 @@ +import 'file_upload_info.dart'; + +/// A representation of data from an incoming request. +class BodyParseResult { + /// The parsed body. + Map body = {}; + + /// The parsed query string. + Map query = {}; + + /// All files uploaded within this request. + List files = []; +} diff --git a/lib/src/body_parser.dart b/lib/src/body_parser.dart new file mode 100644 index 00000000..ad199e7b --- /dev/null +++ b/lib/src/body_parser.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'dart:io'; +import 'body_parse_result.dart'; + +class BodyParser implements StreamTransformer, BodyParseResult> { + + @override + Stream bind(HttpRequest stream) { + var _stream = new StreamController(); + + stream.toList().then((lists) { + var ints = []; + lists.forEach(ints.addAll); + _stream.close(); + + + }); + + return _stream.stream; + } +} \ No newline at end of file diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart new file mode 100644 index 00000000..2dd36d0b --- /dev/null +++ b/lib/src/chunk.dart @@ -0,0 +1,8 @@ +import 'file_upload_info.dart'; + +List getFileDataFromChunk(String chunk, String boundary, + String fileUploadName, + Map body) { + List result = []; + return result; +} \ No newline at end of file diff --git a/lib/file_upload_info.dart b/lib/src/file_upload_info.dart similarity index 94% rename from lib/file_upload_info.dart rename to lib/src/file_upload_info.dart index d0556f39..85be74dd 100644 --- a/lib/file_upload_info.dart +++ b/lib/src/file_upload_info.dart @@ -1,5 +1,3 @@ -part of body_parser; - /// Represents a file uploaded to the server. class FileUploadInfo { /// The MIME type of the uploaded file. diff --git a/lib/src/get_value.dart b/lib/src/get_value.dart new file mode 100644 index 00000000..73e01507 --- /dev/null +++ b/lib/src/get_value.dart @@ -0,0 +1,14 @@ +import 'dart:convert'; + +getValue(String value) { + num numValue = num.parse(value, (_) => double.NAN); + if (!numValue.isNaN) + return numValue; + else if (value.startsWith('[') && value.endsWith(']')) + return JSON.decode(value); + else if (value.startsWith('{') && value.endsWith('}')) + return JSON.decode(value); + else if (value.trim().toLowerCase() == 'null') + return null; + else return value; +} \ No newline at end of file diff --git a/lib/src/map_from_uri.dart b/lib/src/map_from_uri.dart new file mode 100644 index 00000000..22b5603b --- /dev/null +++ b/lib/src/map_from_uri.dart @@ -0,0 +1,41 @@ +import 'get_value.dart'; + +/// Parses a URI-encoded string into real data! **Wow!** +/// +/// Whichever map you provide will be automatically populated from the urlencoded body string you provide. +buildMapFromUri(Map map, String body) { + RegExp parseArrayRgx = new RegExp(r'^(.+)\[\]$'); + + for (String keyValuePair in body.split('&')) { + if (keyValuePair.contains('=')) { + List split = keyValuePair.split('='); + String key = Uri.decodeQueryComponent(split[0]); + String value = Uri.decodeQueryComponent(split[1]); + + if (parseArrayRgx.hasMatch(key)) { + Match queryMatch = parseArrayRgx.firstMatch(key); + key = queryMatch.group(1); + if (!(map[key] is List)) { + map[key] = []; + } + + map[key].add(getValue(value)); + } else if (key.contains('.')) { + // i.e. map.foo.bar => [map, foo, bar] + List keys = key.split('.'); + + Map targetMap = map[keys[0]] ?? {}; + map[keys[0]] = targetMap; + for (int i = 1; i < keys.length; i++) { + if (i < keys.length - 1) { + targetMap[keys[i]] = targetMap[keys[i]] ?? {}; + targetMap = targetMap[keys[i]]; + } else { + targetMap[keys[i]] = getValue(value); + } + } + } + else map[key] = getValue(value); + } else map[Uri.decodeQueryComponent(keyValuePair)] = true; + } +} \ No newline at end of file diff --git a/lib/src/parse_body.dart b/lib/src/parse_body.dart new file mode 100644 index 00000000..e9c953ca --- /dev/null +++ b/lib/src/parse_body.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:http_server/http_server.dart'; +import 'package:mime/mime.dart'; +import 'body_parse_result.dart'; +import 'chunk.dart'; +import 'file_upload_info.dart'; +import 'map_from_uri.dart'; + +/// Grabs data from an incoming request. +/// +/// Supports URL-encoded and JSON, as well as multipart/* forms. +/// On a file upload request, only fields with the name **'file'** are processed +/// as files. Anything else is put in the body. You can change the upload file name +/// via the *fileUploadName* parameter. :) +Future parseBody(HttpRequest request) async { + var result = new BodyParseResult(); + + if (request.headers.contentType != null) { + if (request.headers.contentType.primaryType == 'multipart' && + request.headers.contentType.parameters.containsKey('boundary')) { + var parts = request + .transform(new MimeMultipartTransformer( + request.headers.contentType.parameters['boundary'])) + .map((part) => + HttpMultipartFormData.parse(part, defaultEncoding: UTF8)); + + await for (HttpMultipartFormData part in parts) { + if (part.isBinary || part.contentDisposition.parameters.containsKey("filename")) { + BytesBuilder builder = await part.fold(new BytesBuilder(), (BytesBuilder b, d) => b..add(d is! String ? d : d.codeUnits)); + var upload = new FileUploadInfo( + mimeType: part.contentType.mimeType, + name: part.contentDisposition.parameters['name'], + filename: part.contentDisposition.parameters['filename'] ?? 'file', + data: builder.takeBytes()); + result.files.add(upload); + } else if (part.isText) { + var text = await part.join(); + buildMapFromUri(result.body, '${part.contentDisposition.parameters["name"]}=$text'); + } else { + print("Found something else : ${part.contentDisposition}"); + } + } + } else if (request.headers.contentType.mimeType == + ContentType.JSON.mimeType) { + result.body + .addAll(JSON.decode(await request.transform(UTF8.decoder).join())); + } else if (request.headers.contentType.mimeType == + 'application/x-www-form-urlencoded') { + String body = await request.transform(UTF8.decoder).join(); + buildMapFromUri(result.body, body); + } + } else if (request.uri.hasQuery) { + buildMapFromUri(result.query, request.uri.query); + } + + return result; +} diff --git a/pubspec.yaml b/pubspec.yaml index 6a99e196..93b5f323 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,11 @@ name: body_parser author: Tobe O -version: 1.0.0-dev +version: 1.0.0-dev+1 description: Parse request bodies and query strings in Dart. homepage: https://github.com/thosakwe/body_parser +dependencies: + http_server: ">=0.9.6 <1.0.0" dev_dependencies: - http: any - json_god: any - test: any \ No newline at end of file + http: ">=0.11.3 <0.12.0" + json_god: ">=2.0.0-beta <3.0.0" + test: ">=0.12.15 <0.13.0" \ No newline at end of file diff --git a/test/all_tests.dart b/test/all_tests.dart index 7adf25a4..0686d5f0 100644 --- a/test/all_tests.dart +++ b/test/all_tests.dart @@ -1,168 +1,9 @@ -import 'dart:io'; - -import 'package:body_parser/body_parser.dart'; -import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart'; import 'package:test/test.dart'; +import 'server.dart' as server; +import 'uploads.dart' as uploads; + main() { - group('Test server support', () { - HttpServer server; - String url; - http.Client client; - God god; - - setUp(() async { - server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); - server.listen((HttpRequest request) async { - //Server will simply return a JSON representation of the parsed body - request.response.write(god.serialize(await parseBody(request))); - await request.response.close(); - }); - url = 'http://localhost:${server.port}'; - print('Test server listening on $url'); - client = new http.Client(); - god = new God(); - }); - tearDown(() async { - await server.close(force: true); - client.close(); - server = null; - url = null; - client = null; - god = null; - }); - - group('query string', () { - test('GET Simple', () async { - print('GET $url/?hello=world'); - var response = await client.get('$url/?hello=world'); - print('Response: ${response.body}'); - expect(response.body, - equals('{"body":{},"query":{"hello":"world"},"files":[]}')); - }); - - test('GET Complex', () async { - var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - - 1}&map.foo.bar=baz'; - print('Body: $postData'); - var response = await client.get('$url/?$postData'); - print('Response: ${response.body}'); - var query = god.deserialize(response.body)['query']; - expect(query['hello'], equals('world')); - expect(query['nums'][2], equals(2)); - expect(query['map'] is Map, equals(true)); - expect(query['map']['foo'], equals({'bar': 'baz'})); - }); - }); - - group('urlencoded', () { - Map headers = { - HttpHeaders.CONTENT_TYPE: 'application/x-www-form-urlencoded' - }; - test('POST Simple', () async { - print('Body: hello=world'); - var response = await client.post( - url, headers: headers, body: 'hello=world'); - print('Response: ${response.body}'); - expect(response.body, - equals('{"body":{"hello":"world"},"query":{},"files":[]}')); - }); - - test('Post Complex', () async { - var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - - 1}&map.foo.bar=baz'; - var response = await client.post(url, headers: headers, body: postData); - var body = god.deserialize(response.body)['body']; - expect(body['hello'], equals('world')); - expect(body['nums'][2], equals(2)); - expect(body['map'] is Map, equals(true)); - expect(body['map']['foo'], equals({'bar': 'baz'})); - }); - }); - - group('JSON', () { - Map headers = { - HttpHeaders.CONTENT_TYPE: ContentType.JSON.toString() - }; - test('Post Simple', () async { - var postData = god.serialize({ - 'hello': 'world' - }); - print('Body: $postData'); - var response = await client.post( - url, headers: headers, body: postData); - print('Response: ${response.body}'); - expect(response.body, - equals('{"body":{"hello":"world"},"query":{},"files":[]}')); - }); - - test('Post Complex', () async { - var postData = god.serialize({ - 'hello': 'world', - 'nums': [1, 2.0, 3 - 1], - 'map': { - 'foo': { - 'bar': 'baz' - } - } - }); - print('Body: $postData'); - var response = await client.post(url, headers: headers, body: postData); - print('Response: ${response.body}'); - var body = god.deserialize(response.body)['body']; - expect(body['hello'], equals('world')); - expect(body['nums'][2], equals(2)); - expect(body['map'] is Map, equals(true)); - expect(body['map']['foo'], equals({'bar': 'baz'})); - }); - }); - - group('File', () { - test('Single upload', () async { - String boundary = '----myBoundary'; - Map headers = { - HttpHeaders.CONTENT_TYPE: 'multipart/form-data; boundary=$boundary' - }; - String postData = '\r\n$boundary\r\n' + - 'Content-Disposition: form-data; name="hello"\r\nworld\r\n$boundary\r\n' + - 'Content-Disposition: file; name="file"; filename="app.dart"\r\n' + - 'Content-Type: text/plain\r\nHello world\r\n$boundary--'; - - print('Form Data: \n$postData'); - var response = await client.post(url, headers: headers, body: postData); - print('Response: ${response.body}'); - Map json = god.deserialize(response.body); - List files = json['files']; - expect(files.length, equals(1)); - expect(files[0]['name'], equals('file')); - expect(files[0]['mimeType'], equals('text/plain')); - expect(files[0]['data'].length, equals(11)); - expect(files[0]['filename'], equals('app.dart')); - expect(json['body']['hello'], equals('world')); - }); - - test('Multiple upload', () async { - String boundary = '----myBoundary'; - Map headers = { - HttpHeaders.CONTENT_TYPE: 'multipart/form-data; boundary=$boundary' - }; - String postData = '\r\n$boundary\r\n' + - 'Content-Disposition: form-data; name="json"\r\ngod\r\n$boundary\r\n' + - 'Content-Disposition: file; name="file"; filename="app.dart"\r\n' + - 'Content-Type: text/plain\r\nHello world\r\n$boundary--'; - - print('Form Data: \n$postData'); - var response = await client.post(url, headers: headers, body: postData); - print('Response: ${response.body}'); - Map json = god.deserialize(response.body); - List files = json['files']; - expect(files.length, equals(1)); - expect(files[0]['name'], equals('file')); - expect(files[0]['mimeType'], equals('text/plain')); - expect(files[0]['data'].length, equals(11)); - expect(json['body']['json'], equals('god')); - }, skip: 'Multiple file uploads are yet to come.'); - }); - }); -} \ No newline at end of file + group('server', server.main); + group('uploads', uploads.main); +} diff --git a/test/server.dart b/test/server.dart new file mode 100644 index 00000000..6b205684 --- /dev/null +++ b/test/server.dart @@ -0,0 +1,110 @@ +import 'dart:io'; +import 'package:body_parser/body_parser.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart' as god; +import 'package:test/test.dart'; + +main() { + HttpServer server; + String url; + http.Client client; + + setUp(() async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + server.listen((HttpRequest request) async { + //Server will simply return a JSON representation of the parsed body + request.response.write(god.serialize(await parseBody(request))); + await request.response.close(); + }); + url = 'http://localhost:${server.port}'; + print('Test server listening on $url'); + client = new http.Client(); + }); + tearDown(() async { + await server.close(force: true); + client.close(); + server = null; + url = null; + client = null; + }); + + group('query string', () { + test('GET Simple', () async { + print('GET $url/?hello=world'); + var response = await client.get('$url/?hello=world'); + print('Response: ${response.body}'); + expect(response.body, + equals('{"body":{},"query":{"hello":"world"},"files":[]}')); + }); + + test('GET Complex', () async { + var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - + 1}&map.foo.bar=baz'; + print('Body: $postData'); + var response = await client.get('$url/?$postData'); + print('Response: ${response.body}'); + var query = god.deserialize(response.body)['query']; + expect(query['hello'], equals('world')); + expect(query['nums'][2], equals(2)); + expect(query['map'] is Map, equals(true)); + expect(query['map']['foo'], equals({'bar': 'baz'})); + }); + }); + + group('urlencoded', () { + Map headers = { + HttpHeaders.CONTENT_TYPE: 'application/x-www-form-urlencoded' + }; + test('POST Simple', () async { + print('Body: hello=world'); + var response = + await client.post(url, headers: headers, body: 'hello=world'); + print('Response: ${response.body}'); + expect(response.body, + equals('{"body":{"hello":"world"},"query":{},"files":[]}')); + }); + + test('Post Complex', () async { + var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - + 1}&map.foo.bar=baz'; + var response = await client.post(url, headers: headers, body: postData); + var body = god.deserialize(response.body)['body']; + expect(body['hello'], equals('world')); + expect(body['nums'][2], equals(2)); + expect(body['map'] is Map, equals(true)); + expect(body['map']['foo'], equals({'bar': 'baz'})); + }); + }); + + group('JSON', () { + Map headers = { + HttpHeaders.CONTENT_TYPE: ContentType.JSON.toString() + }; + test('Post Simple', () async { + var postData = god.serialize({'hello': 'world'}); + print('Body: $postData'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + expect(response.body, + equals('{"body":{"hello":"world"},"query":{},"files":[]}')); + }); + + test('Post Complex', () async { + var postData = god.serialize({ + 'hello': 'world', + 'nums': [1, 2.0, 3 - 1], + 'map': { + 'foo': {'bar': 'baz'} + } + }); + print('Body: $postData'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + var body = god.deserialize(response.body)['body']; + expect(body['hello'], equals('world')); + expect(body['nums'][2], equals(2)); + expect(body['map'] is Map, equals(true)); + expect(body['map']['foo'], equals({'bar': 'baz'})); + }); + }); +} diff --git a/test/uploads.dart b/test/uploads.dart new file mode 100644 index 00000000..496279cc --- /dev/null +++ b/test/uploads.dart @@ -0,0 +1,135 @@ +import 'dart:io'; +import 'package:body_parser/body_parser.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart' as god; +import 'package:test/test.dart'; + +main() { + HttpServer server; + String url; + http.Client client; + + setUp(() async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0); + server.listen((HttpRequest request) async { + //Server will simply return a JSON representation of the parsed body + request.response.write(god.serialize(await parseBody(request))); + await request.response.close(); + }); + url = 'http://localhost:${server.port}'; + print('Test server listening on $url'); + client = new http.Client(); + }); + + tearDown(() async { + await server.close(force: true); + client.close(); + server = null; + url = null; + client = null; + }); + + test('No upload', () async { + String boundary = 'myBoundary'; + Map headers = { + HttpHeaders.CONTENT_TYPE: 'multipart/form-data; boundary=$boundary' + }; + String postData = ''' +--$boundary +Content-Disposition: form-data; name="hello" + +world +--$boundary-- +''' + .replaceAll("\n", "\r\n"); + + print( + 'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + Map json = god.deserialize(response.body); + List files = json['files']; + expect(files.length, equals(0)); + expect(json['body']['hello'], equals('world')); + }); + + test('Single upload', () async { + String boundary = 'myBoundary'; + Map headers = { + HttpHeaders.CONTENT_TYPE: new ContentType("multipart", "form-data", + parameters: {"boundary": boundary}).toString() + }; + String postData = ''' +--$boundary +Content-Disposition: form-data; name="hello" + +world +--$boundary +Content-Disposition: form-data; name="file"; filename="app.dart" +Content-Type: application/dart + +Hello world +--$boundary-- +''' + .replaceAll("\n", "\r\n"); + + print( + 'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + Map json = god.deserialize(response.body); + List files = json['files']; + expect(files.length, equals(1)); + expect(files[0]['name'], equals('file')); + expect(files[0]['mimeType'], equals('application/dart')); + expect(files[0]['data'].length, equals(11)); + expect(files[0]['filename'], equals('app.dart')); + expect(json['body']['hello'], equals('world')); + }); + + test('Multiple upload', () async { + String boundary = 'myBoundary'; + Map headers = { + HttpHeaders.CONTENT_TYPE: 'multipart/form-data; boundary=$boundary' + }; + String postData = ''' +--$boundary +Content-Disposition: form-data; name="json" + +god +--$boundary +Content-Disposition: form-data; name="num" + +14.50000 +--$boundary +Content-Disposition: form-data; name="file"; filename="app.dart" +Content-Type: text/plain + +Hello world +--$boundary +Content-Disposition: form-data; name="entry-point"; filename="main.js" +Content-Type: text/javascript + +function main() { + console.log("Hello, world!"); +} +--$boundary-- +''' + .replaceAll("\n", "\r\n"); + + print( + 'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + Map json = god.deserialize(response.body); + List files = json['files']; + expect(files.length, equals(2)); + expect(files[0]['name'], equals('file')); + expect(files[0]['mimeType'], equals('text/plain')); + expect(files[0]['data'].length, equals(11)); + expect(files[1]['name'], equals('entry-point')); + expect(files[1]['mimeType'], equals('text/javascript')); + expect(json['body']['json'], equals('god')); + expect(json['body']['num'], equals(14.5)); + }); +}