0

我按照 Aqueduct 教程创建测试,但它缺少一个我急需的示例;我无法使用我的控制器测试文件上传端点。

我已经实现了一个控制器:

class FileController extends ResourceController {

  FileController() {
    acceptedContentTypes = [ContentType("multipart", "form-data")];
  }

  @Operation.post()
  Future<Response> postForm() async {

    final transformer = MimeMultipartTransformer(request.raw.headers.contentType.parameters["boundary"]);
    final bodyStream = Stream.fromIterable([await request.body.decode<List<int>>()]);
    final parts = await transformer.bind(bodyStream).toList();

    for (var part in parts) {
      final headers = part.headers;

      HttpMultipartFormData multipart = HttpMultipartFormData.parse(part);
      final content = multipart.cast<List<int>>();

      final filePath = "uploads/test.txt";

      await new File(filePath).create(recursive: true);

      IOSink sink = File(filePath).openWrite();
      await content.forEach(sink.add);

      await sink.flush();
      await sink.close();
    }

    return Response.ok({});   
  }
}

使用 Postman 上传文件时效果很好。

现在我正在尝试为此端点编写一个测试:

test("POST /upload-file uploads a file to the server", () async {

    final file = File('test.txt');
    final sink = file.openWrite();
    sink.write('test');
    await sink.close();

    final bytes = file.readAsBytesSync();

    harness.agent.headers['Content-Type'] = 'multipart/form-data; boundary=MultipartBoundry';
    harness.agent.headers['Content-Disposition'] = 'form-data; name="file"; filename="test.txt"';


    final response = await harness.agent.post("/upload-file", body: bytes);

    expectResponse(response, 200);
  });

并在 vscode 调试器中得到这个:

Expected: --- HTTP Response ---
          - Status code must be 200
          - Headers can be anything
          - Body can be anything
          ---------------------
  Actual: TestResponse:<-----------
          - Status code is 415
          - Headers are the following:
            - x-frame-options: SAMEORIGIN
            - x-xss-protection: 1; mode=block
            - x-content-type-options: nosniff
            - server: aqueduct/1
            - content-length: 0
          - Body is empty
          -------------------------
          >
   Which: Status codes are different. Expected: 200. Actual: 415
4

2 回答 2

0

415 状态码响应表明 ResourceController 拒绝了请求的内容类型。您已经正确设置了acceptedContentTypes,但是,对于隐藏在文档中的测试代理有一个(诚然令人困惑的)细微差别Agent.headers

Default headers to be added to requests made by this agent.

By default, this value is the empty map.

Do not provide a 'content-type' key. If the key 'content-type' is present, it will be removed prior to sending the request. It is replaced by the value of TestRequest.contentType, which also controls body encoding.

See also setBasicAuthorization, bearerAuthorization, accept, contentType for setting common headers.

请参阅此处的 API 参考。至于为什么会这样存在:就像您的响应一样,TestRequest 的内容类型(这是您使用代理发出请求时创建和执行的对象)决定了 CodecRegistry 中的哪个编解码器用作编码器。这使您可以始终处理“飞镖对象”并让 Aqueduct 处理编码/解码。

于 2019-11-14T19:54:59.747 回答
0

我写了一堆类来简化和阐明多部分请求测试。因此,如果有人仍然为此苦苦挣扎,欢迎尝试我的解决方案:

测试

import 'multipart_body_parser.dart';
//[...]
    test('POST /upload-file uploads a file to the server', () async {
      final boundary = '7d82a244f2ea5xd0s046';
      final file = File('test.txt');

      var encodedBody = MultipartBodyParser(boundary).parse([
        FileBodyPart(
          'file',
          'test.txt',
          File('test.txt'),
        ),
      ]);

      final response = await harness.agent.post(
        '/upload-file',
        body: encodedBody,
      );

      expectResponse(response, 200);
    });

multipart_body_parser.dart

import 'dart:convert';
import 'dart:io';

class MultipartBodyParser {
  final String boundary;

  MultipartBodyParser(this.boundary)
      : assert(
          boundary != null,
          'The boundary is empty. Please set it ' +
              'and keep on mind that it MUST NOT appear inside any of the ' +
              'encapsulated parts. Example: "sampleBoundary7da24f2e50046".',
        );

  List<int> get encodedNonLastBoundary =>
      ascii.encode('\r\n--' + boundary + '\r\n');

  List<int> get encodedLastBoundary =>
      ascii.encode('\r\n--' + boundary + '--\r\n\r\n');

  List<int> parse(List<_BodyPart> parts) {
    if (parts == null || parts.isEmpty) {
      throw MultipartBodyParserException(
        'Parts CAN NOT be empty. Please set at least one part of body.',
      );
    }
    var body = encodedNonLastBoundary;
    parts.forEach((part) {
      body += part.parse();
      if (parts.last != part) {
        body += encodedNonLastBoundary;
      }
    });
    body += encodedLastBoundary;
    return body;
  }
}

class TextBodyPart extends _BodyPart {
  final String content;

  TextBodyPart(formFieldName, _content)
      : content = _content ?? '',
        super(
          _ContentDisposition(
            formFieldName,
            'form-data',
          ),
          _ContentType(),
        );

  @override
  List<int> get encodedContent => ascii.encode(content);
}

class FileBodyPart extends _BodyPart {
  final File file;
  final String fileName;

  FileBodyPart(formFieldName, this.fileName, this.file)
      : super(
          _ContentDisposition(
            formFieldName,
            'form-data',
            '; filename="$fileName"',
          ),
          _ContentType('application/octet-stream'),
        );

  @override
  List<int> get encodedContent => file.readAsBytesSync();
}

abstract class _BodyPart {
  final _ContentDisposition contentDisposition;
  final _ContentType contentType;

  _BodyPart(this.contentDisposition, this.contentType)
      : assert(contentDisposition != null),
        assert(contentType != null);

  String get partHeader =>
      contentDisposition.toString() + contentType.toString();

  List<int> get encodedContent;

  List<int> parse() => ascii.encode(partHeader) + encodedContent;
}

class _ContentDisposition {
  final String formFieldName;
  final String formFieldType;
  final String additionalParams;
  _ContentDisposition(this.formFieldName, [_formFieldType, _additionalParams])
      : formFieldType = _formFieldType ?? 'form-data',
        additionalParams = _additionalParams ?? '',
        assert(formFieldName != null);

  @override
  String toString() =>
      'content-disposition: $formFieldType; name="$formFieldName"$additionalParams\r\n';
}

class _ContentType {
  final String type;
  _ContentType([this.type = 'text/plain']) : assert(type != null);

  @override
  String toString() => 'content-type: $type\r\n\r\n';
}

class MultipartBodyParserException implements Exception {
  final String message;

  const MultipartBodyParserException([this.message]);
}

于 2020-05-25T11:59:07.947 回答