diff --git a/packages/body_parser/.gitignore b/packages/body_parser/.gitignore new file mode 100644 index 00000000..b4d6e266 --- /dev/null +++ b/packages/body_parser/.gitignore @@ -0,0 +1,64 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.packages +.pub/ +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ diff --git a/packages/body_parser/.idea/body_parser.iml b/packages/body_parser/.idea/body_parser.iml new file mode 100644 index 00000000..ae9af975 --- /dev/null +++ b/packages/body_parser/.idea/body_parser.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/body_parser/.idea/libraries/Dart_Packages.xml b/packages/body_parser/.idea/libraries/Dart_Packages.xml new file mode 100644 index 00000000..3a4aead0 --- /dev/null +++ b/packages/body_parser/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/body_parser/.idea/modules.xml b/packages/body_parser/.idea/modules.xml new file mode 100644 index 00000000..9c283837 --- /dev/null +++ b/packages/body_parser/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/body_parser/.idea/runConfigurations/main_dart.xml b/packages/body_parser/.idea/runConfigurations/main_dart.xml new file mode 100644 index 00000000..750f7262 --- /dev/null +++ b/packages/body_parser/.idea/runConfigurations/main_dart.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/body_parser/.idea/vcs.xml b/packages/body_parser/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/packages/body_parser/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/body_parser/.travis.yml b/packages/body_parser/.travis.yml new file mode 100644 index 00000000..a9e2c109 --- /dev/null +++ b/packages/body_parser/.travis.yml @@ -0,0 +1,4 @@ +language: dart +dart: + - dev + - stable \ No newline at end of file diff --git a/packages/body_parser/CHANGELOG.md b/packages/body_parser/CHANGELOG.md new file mode 100644 index 00000000..2304947d --- /dev/null +++ b/packages/body_parser/CHANGELOG.md @@ -0,0 +1,5 @@ +# 1.1.1 +* Dart 2 updates; should fix Angel in Travis. + +# 1.1.0 +* Add `parseBodyFromStream` \ No newline at end of file diff --git a/packages/body_parser/LICENSE b/packages/body_parser/LICENSE new file mode 100644 index 00000000..3832f450 --- /dev/null +++ b/packages/body_parser/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Tobe O + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/body_parser/README.md b/packages/body_parser/README.md new file mode 100644 index 00000000..6c577eab --- /dev/null +++ b/packages/body_parser/README.md @@ -0,0 +1,82 @@ +# body_parser +[![Pub](https://img.shields.io/pub/v/body_parser.svg)](https://pub.dartlang.org/packages/body_parser) +[![build status](https://travis-ci.org/angel-dart/body_parser.svg)](https://travis-ci.org/angel-dart/body_parser) + +Parse request bodies and query strings in Dart, as well multipart/form-data uploads. No external +dependencies required. + +This is the request body parser powering the +[Angel](https://angel-dart.github.io) +framework. If you are looking for a server-side solution with dependency injection, +WebSockets, and more, then I highly recommend it as your first choice. Bam! + +### Contents + +* [Body Parser](#body-parser) +* [About](#about) +* [Installation](#installation) +* [Usage](#usage) +* [Thanks](#thank-you-for-using-body-parser) + +# About + +I needed something like Express.js's `body-parser` module, so I made it here. It fully supports JSON requests. +x-www-form-urlencoded fully supported, as well as query strings. You can also include arrays in your query, +in the same way you would for a PHP application. Full file upload support will also be present by the production 1.0.0 release. + +A benefit of this is that primitive types are automatically deserialized correctly. As in, if you have a `hello=1.5` request, then +`body['hello']` will equal `1.5` and not `'1.5'`. A very semantic difference, yes, but it relieves stress in my head. + +# Installation + +To install Body Parser for your Dart project, simply add body_parser to your +pub dependencies. + + dependencies: + body_parser: any + +# Usage + +Body Parser exposes a simple class called `BodyParseResult`. +You can easily parse the query string and request body for a request by calling `Future parseBody`. + +```dart +import 'dart:convert'; +import 'package:body_parser/body_parser.dart'; + +main() async { + // ... + await for (HttpRequest request in server) { + request.response.write(JSON.encode(await parseBody(request).body)); + await request.response.close(); + } +} +``` + +You can also use `buildMapFromUri(Map, String)` to populate a map from a URL encoded string. + +This can easily be used with a library like [JSON God](https://github.com/thosakwe/json_god) +to build structured JSON/REST APIs. Add validation and you've got an instant backend. + +```dart +MyClass create(HttpRequest request) async { + return god.deserialize(await parseBody(request).body, MyClass); +} +``` + +## Custom Body Parsing +In cases where you need to parse unrecognized content types, `body_parser` won't be of any help to you +on its own. However, you can use the `originalBuffer` property of a `BodyParseResult` to see the original +request buffer. To get this functionality, pass `storeOriginalBuffer` as `true` when calling `parseBody`. + +For example, if you wanted to +[parse GraphQL queries within your server](https://github.com/angel-dart/graphql)... + +```dart +app.get('/graphql', (req, res) async { + if (req.headers.contentType.mimeType == 'application/graphql') { + var graphQlString = new String.fromCharCodes(req.originalBuffer); + // ... + } +}); +``` \ No newline at end of file diff --git a/packages/body_parser/analysis_options.yaml b/packages/body_parser/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/packages/body_parser/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/body_parser/example/main.dart b/packages/body_parser/example/main.dart new file mode 100644 index 00000000..ce0af703 --- /dev/null +++ b/packages/body_parser/example/main.dart @@ -0,0 +1,41 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:body_parser/body_parser.dart'; + +main() async { + var address = '127.0.0.1'; + var port = 3000; + var futures = []; + + for (int i = 1; i < Platform.numberOfProcessors; i++) { + futures.add(Isolate.spawn(start, [address, port, i])); + } + + Future.wait(futures).then((_) { + print('All instances started.'); + print( + 'Test with "wrk -t12 -c400 -d30s -s ./example/post.lua http://localhost:3000" or similar'); + start([address, port, 0]); + }); +} + +void start(List args) { + var address = new InternetAddress(args[0] as String); + int port = args[1], id = args[2]; + + HttpServer.bind(address, port, shared: true).then((server) { + server.listen((request) async { + // ignore: deprecated_member_use + var body = await parseBody(request); + request.response + ..headers.contentType = new ContentType('application', 'json') + ..write(json.encode(body.body)) + ..close(); + }); + + print( + 'Server #$id listening at http://${server.address.address}:${server.port}'); + }); +} diff --git a/packages/body_parser/example/post.lua b/packages/body_parser/example/post.lua new file mode 100644 index 00000000..524febc6 --- /dev/null +++ b/packages/body_parser/example/post.lua @@ -0,0 +1,6 @@ +-- example HTTP POST script which demonstrates setting the +-- HTTP method, body, and adding a header + +wrk.method = "POST" +wrk.body = "foo=bar&baz=quux" +wrk.headers["Content-Type"] = "application/x-www-form-urlencoded" \ No newline at end of file diff --git a/packages/body_parser/lib/body_parser.dart b/packages/body_parser/lib/body_parser.dart new file mode 100644 index 00000000..0541b3e6 --- /dev/null +++ b/packages/body_parser/lib/body_parser.dart @@ -0,0 +1,6 @@ +/// A library for parsing HTTP request bodies and queries. +library body_parser; + +export 'src/body_parse_result.dart'; +export 'src/file_upload_info.dart'; +export 'src/parse_body.dart'; diff --git a/packages/body_parser/lib/src/body_parse_result.dart b/packages/body_parser/lib/src/body_parse_result.dart new file mode 100644 index 00000000..806335c3 --- /dev/null +++ b/packages/body_parser/lib/src/body_parse_result.dart @@ -0,0 +1,28 @@ +import 'file_upload_info.dart'; + +/// A representation of data from an incoming request. +abstract class BodyParseResult { + /// The parsed body. + Map get body; + + /// The parsed query string. + Map get query; + + /// All files uploaded within this request. + List get files; + + /// The original body bytes sent with this request. + /// + /// You must set [storeOriginalBuffer] to `true` to see this. + List get originalBuffer; + + /// If an error was encountered while parsing the body, it will appear here. + /// + /// Otherwise, this is `null`. + dynamic get error; + + /// If an error was encountered while parsing the body, the call stack will appear here. + /// + /// Otherwise, this is `null`. + StackTrace get stack; +} diff --git a/packages/body_parser/lib/src/chunk.dart b/packages/body_parser/lib/src/chunk.dart new file mode 100644 index 00000000..58776ca0 --- /dev/null +++ b/packages/body_parser/lib/src/chunk.dart @@ -0,0 +1,7 @@ +import 'file_upload_info.dart'; + +List getFileDataFromChunk( + String chunk, String boundary, String fileUploadName, Map body) { + List result = []; + return result; +} diff --git a/packages/body_parser/lib/src/file_upload_info.dart b/packages/body_parser/lib/src/file_upload_info.dart new file mode 100644 index 00000000..5db8b785 --- /dev/null +++ b/packages/body_parser/lib/src/file_upload_info.dart @@ -0,0 +1,17 @@ +/// Represents a file uploaded to the server. +class FileUploadInfo { + /// The MIME type of the uploaded file. + String mimeType; + + /// The name of the file field from the request. + String name; + + /// The filename of the file. + String filename; + + /// The bytes that make up this file. + List data; + + FileUploadInfo( + {this.mimeType, this.name, this.filename, this.data: const []}) {} +} diff --git a/packages/body_parser/lib/src/get_value.dart b/packages/body_parser/lib/src/get_value.dart new file mode 100644 index 00000000..d3c6c3f1 --- /dev/null +++ b/packages/body_parser/lib/src/get_value.dart @@ -0,0 +1,20 @@ +import 'package:dart2_constant/convert.dart'; + +getValue(String value) { + try { + num numValue = num.parse(value); + if (!numValue.isNaN) + return numValue; + else + return value; + } on FormatException { + 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; + } +} diff --git a/packages/body_parser/lib/src/map_from_uri.dart b/packages/body_parser/lib/src/map_from_uri.dart new file mode 100644 index 00000000..0945ce6b --- /dev/null +++ b/packages/body_parser/lib/src/map_from_uri.dart @@ -0,0 +1,43 @@ +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('=')) { + var equals = keyValuePair.indexOf('='); + String key = Uri.decodeQueryComponent(keyValuePair.substring(0, equals)); + String value = + Uri.decodeQueryComponent(keyValuePair.substring(equals + 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]] != null ? map[keys[0]] as Map : {}; + 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]] as Map; + } else { + targetMap[keys[i]] = getValue(value); + } + } + } else + map[key] = getValue(value); + } else + map[Uri.decodeQueryComponent(keyValuePair)] = true; + } +} diff --git a/packages/body_parser/lib/src/parse_body.dart b/packages/body_parser/lib/src/parse_body.dart new file mode 100644 index 00000000..8fdb94b9 --- /dev/null +++ b/packages/body_parser/lib/src/parse_body.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dart2_constant/convert.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:http_server/http_server.dart'; +import 'package:mime/mime.dart'; + +import 'body_parse_result.dart'; +import 'file_upload_info.dart'; +import 'map_from_uri.dart'; + +/// Forwards to [parseBodyFromStream]. +@deprecated +Future parseBody(HttpRequest request, + {bool storeOriginalBuffer: false}) { + return parseBodyFromStream( + request, + request.headers.contentType != null + ? new MediaType.parse(request.headers.contentType.toString()) + : null, + request.uri, + storeOriginalBuffer: storeOriginalBuffer); +} + +/// 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. :) +/// +/// Use [storeOriginalBuffer] to add the original request bytes to the result. +Future parseBodyFromStream( + Stream data, MediaType contentType, Uri requestUri, + {bool storeOriginalBuffer: false}) async { + var result = new _BodyParseResultImpl(); + + Future getBytes() { + return data + .fold(new BytesBuilder(copy: false), (a, b) => a..add(b)) + .then((b) => b.takeBytes()); + } + + Future getBody() { + if (storeOriginalBuffer) { + return getBytes().then((bytes) { + result.originalBuffer = bytes; + return utf8.decode(bytes); + }); + } else { + return utf8.decoder.bind(data).join(); + } + } + + try { + if (contentType != null) { + if (contentType.type == 'multipart' && + contentType.parameters.containsKey('boundary')) { + Stream stream; + + if (storeOriginalBuffer) { + var bytes = result.originalBuffer = await getBytes(); + var ctrl = new StreamController() + ..add(bytes) + ..close(); + stream = ctrl.stream; + } else { + stream = data; + } + + var parts = MimeMultipartTransformer( + contentType.parameters['boundary']).bind(stream) + .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(copy: false), + (BytesBuilder b, d) => b + ..add(d is! String + ? (d as List) + : (d as String).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 if (contentType.mimeType == 'application/json') { + result.body + .addAll(_foldToStringDynamic(json.decode(await getBody()) as Map)); + } else if (contentType.mimeType == 'application/x-www-form-urlencoded') { + String body = await getBody(); + buildMapFromUri(result.body, body); + } else if (storeOriginalBuffer == true) { + result.originalBuffer = await getBytes(); + } + } else { + if (requestUri.hasQuery) { + buildMapFromUri(result.query, requestUri.query); + } + + if (storeOriginalBuffer == true) { + result.originalBuffer = await getBytes(); + } + } + } catch (e, st) { + result.error = e; + result.stack = st; + } + + return result; +} + +class _BodyParseResultImpl implements BodyParseResult { + @override + Map body = {}; + + @override + List files = []; + + @override + List originalBuffer; + + @override + Map query = {}; + + @override + var error = null; + + @override + StackTrace stack = null; +} + +Map _foldToStringDynamic(Map map) { + return map == null + ? null + : map.keys.fold>( + {}, (out, k) => out..[k.toString()] = map[k]); +} diff --git a/packages/body_parser/pubspec.yaml b/packages/body_parser/pubspec.yaml new file mode 100644 index 00000000..567852ca --- /dev/null +++ b/packages/body_parser/pubspec.yaml @@ -0,0 +1,15 @@ +name: body_parser +author: Tobe O +version: 1.1.1 +description: Parse request bodies and query strings in Dart. Supports JSON, URL-encoded, and multi-part bodies. +homepage: https://github.com/angel-dart/body_parser +environment: + sdk: ">=1.8.0 <3.0.0" +dependencies: + dart2_constant: ^1.0.0 + http_parser: ">=3.1.1 <4.0.0" + http_server: ">=0.9.6 <1.0.0" + mime: ">=0.9.3 <1.0.0" +dev_dependencies: + http: ">=0.11.3 <0.12.0" + test: ">=0.12.15" \ No newline at end of file diff --git a/packages/body_parser/test/form_data_test.dart b/packages/body_parser/test/form_data_test.dart new file mode 100644 index 00000000..354c42db --- /dev/null +++ b/packages/body_parser/test/form_data_test.dart @@ -0,0 +1,142 @@ +import 'dart:io'; +import 'package:body_parser/body_parser.dart'; +import 'package:dart2_constant/convert.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; +import 'server_test.dart'; + +main() { + HttpServer server; + String url; + http.Client client; + + setUp(() async { + server = await HttpServer.bind('127.0.0.1', 0); + server.listen((HttpRequest request) async { + //Server will simply return a JSON representation of the parsed body + // ignore: deprecated_member_use + request.response.write(jsonEncodeBody(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 = { + '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 jsons = json.decode(response.body); + var files = jsons['files'].map((map) { + return map == null + ? null + : map.keys.fold>( + {}, (out, k) => out..[k.toString()] = map[k]); + }); + expect(files.length, equals(0)); + expect(jsons['body']['hello'], equals('world')); + }); + + test('Single upload', () async { + String boundary = 'myBoundary'; + Map headers = { + '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 jsons = json.decode(response.body); + var files = jsons['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(jsons['body']['hello'], equals('world')); + }); + + test('Multiple upload', () async { + String boundary = 'myBoundary'; + Map headers = { + '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 jsons = json.decode(response.body); + var files = jsons['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(jsons['body']['json'], equals('god')); + expect(jsons['body']['num'], equals(14.5)); + }); +} diff --git a/packages/body_parser/test/server_test.dart b/packages/body_parser/test/server_test.dart new file mode 100644 index 00000000..277ff495 --- /dev/null +++ b/packages/body_parser/test/server_test.dart @@ -0,0 +1,159 @@ +import 'dart:io' show HttpRequest, HttpServer; + +import 'package:body_parser/body_parser.dart'; +import 'package:dart2_constant/convert.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +const TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjcuMC4wLjEiLCJleHAiOi0xLCJpYXQiOiIyMDE2LTEyLTIyVDEyOjQ5OjUwLjM2MTQ0NiIsImlzcyI6ImFuZ2VsX2F1dGgiLCJzdWIiOiIxMDY2OTQ4Mzk2MDIwMjg5ODM2NTYifQ==.PYw7yUb-cFWD7N0sSLztP7eeRvO44nu1J2OgDNyT060='; + +String jsonEncodeBody(BodyParseResult result) { + return json.encode({ + 'query': result.query, + 'body': result.body, + 'error': result.error?.toString(), + 'files': result.files.map((f) { + return { + 'name': f.name, + 'mimeType': f.mimeType, + 'filename': f.filename, + 'data': f.data, + }; + }).toList(), + 'originalBuffer': result.originalBuffer, + 'stack': null, //result.stack.toString(), + }); +} + +main() { + HttpServer server; + String url; + http.Client client; + + setUp(() async { + server = await HttpServer.bind('127.0.0.1', 0); + server.listen((HttpRequest request) async { + //Server will simply return a JSON representation of the parsed body + request.response.write( + // ignore: deprecated_member_use + jsonEncodeBody(await parseBody(request, storeOriginalBuffer: true))); + 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}'); + var result = json.decode(response.body); + expect(result['body'], equals({})); + expect(result['query'], equals({'hello': 'world'})); + expect(result['files'], equals([])); + //expect(result['originalBuffer'], isNull); + }); + + 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 = json.decode(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'})); + }); + + test('JWT', () async { + var postData = 'token=$TOKEN'; + print('Body: $postData'); + var response = await client.get('$url/?$postData'); + print('Response: ${response.body}'); + var query = json.decode(response.body)['query']; + expect(query['token'], equals(TOKEN)); + }); + }); + + group('urlencoded', () { + Map headers = { + '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}'); + var result = json.decode(response.body); + expect(result['query'], equals({})); + expect(result['body'], equals({'hello': 'world'})); + expect(result['files'], equals([])); + expect(result['originalBuffer'], isList); + expect(result['originalBuffer'], isNotEmpty); + }); + + 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); + print('Response: ${response.body}'); + var body = json.decode(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'})); + }); + + test('JWT', () async { + var postData = 'token=$TOKEN'; + var response = await client.post(url, headers: headers, body: postData); + var body = json.decode(response.body)['body']; + expect(body['token'], equals(TOKEN)); + }); + }); + + group('json', () { + Map headers = {'content-type': 'application/json'}; + test('Post Simple', () async { + var postData = json.encode({'hello': 'world'}); + print('Body: $postData'); + var response = await client.post(url, headers: headers, body: postData); + print('Response: ${response.body}'); + var result = json.decode(response.body); + expect(result['body'], equals({'hello': 'world'})); + expect(result['query'], equals({})); + expect(result['files'], equals([])); + expect(result['originalBuffer'], allOf(isList, isNotEmpty)); + }); + + test('Post Complex', () async { + var postData = json.encode({ + '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 = json.decode(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'})); + }); + }); +}