Как протестировать загрузку файлов с помощью жгута Aqueduct?

Я следовал учебнику 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

person JereK    schedule 13.11.2019    source источник


Ответы (2)


Ответ с кодом состояния 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 использовать в качестве кодировщика. Это позволяет вам всегда иметь дело с «объектами Dart» и позволить Aqueduct обрабатывать кодирование/декодирование.

person Joe Conway    schedule 14.11.2019
comment
Спасибо за ответ. Я уже некоторое время борюсь с этим включением/выключением. Кажется, что класс ContentType не предоставляет свойства для multipart/form-data. Итак, какое свойство я должен использовать? - person JereK; 22.11.2019
comment
Хорошо, я смог сделать это в тестах: harness.agent.contentType = ContentType("multipart", "form-data");, и это устранило мою проблему с неподдерживаемым типом носителя, но теперь реализация дает сбой с другим сообщением об ошибке, которое может выходить за рамки этого вопроса. Был ли мой подход правильным? - person JereK; 22.11.2019

Я написал кучу классов, чтобы упростить и прояснить тестирование составных запросов. Итак, если кто-то все еще борется с этим, можете попробовать мое решение:

проверить

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]);
}

person Owczar    schedule 25.05.2020