API-Docker

 view release on metacpan or  search on metacpan

t/lib/Test/API/Docker/Mock.pm  view on Meta::CPAN

    eval { $cleanup->() };
    warn "Cleanup failed: $@" if $@;
  }
  @_cleanups = ();
}

sub test_docker {
  my (%routes) = @_;

  if (is_live()) {
    require API::Docker;
    return API::Docker->new(host => $ENV{API_DOCKER_TEST_HOST});
  }

  return _mock_docker(%routes);
}

sub _mock_docker {
  my (%routes) = @_;

  unless (grep { /version/ } keys %routes) {
    $routes{'GET /version'} = load_fixture('system_version');
  }

  require API::Docker;

  # Pinned rather than left to negotiate: setting api_version here means
  # negotiate_version's own guard ("return if defined $self->api_version",
  # API::Docker.pm) short-circuits for every mocked client, so this number
  # is never actually read off the injected 'GET /version' route -- it is
  # read back as-is by anything that asserts $docker->api_version (only
  # t/version.t does). Kept equal to t/fixtures/system_version.json's own
  # ApiVersion (karr k101: Docker Engine Community 29.7.2, API 1.55) so the
  # two do not silently drift apart the way they did when the fixture was
  # hand-rolled at '1.47' to match this literal instead of the other way
  # round.
  my $docker = API::Docker->new(
    host        => 'unix:///var/run/docker.sock',
    api_version => '1.55',
  );

  my $mock_request = sub {
    my ($self, $method, $path, %opts) = @_;

    my $clean_path = $path;
    $clean_path =~ s{^/v[\d.]+}{};

    my $key = "$method $clean_path";

    my $handler;
    my $matched = 0;
    if (exists $routes{$key}) {
      $handler = $routes{$key};
      $matched = 1;
    }
    else {
      for my $pattern (keys %routes) {
        my ($route_method, $route_path) = split /\s+/, $pattern, 2;
        next unless $method eq $route_method;
        # \Q...\E: $route_path is a literal path, not a regex a test author
        # wrote on purpose -- interpolated unescaped, a route key containing
        # a regex metacharacter (`.`, `?`, `+`, `(` ...) was read as a
        # pattern rather than as the literal string it looks like, and could
        # match a different path than the one it was registered for (or
        # croak on an unbalanced `(`). This tier still tolerates only
        # whitespace between method and path -- the exact-match branch above
        # is the fast path for everything else. See t/mock_harness.t.
        next unless $clean_path =~ m{^\Q$route_path\E$};
        $handler = $routes{$pattern};
        $matched = 1;
        last;
      }
    }

    croak "No mock route for: $key (available: " . join(', ', sort keys %routes) . ")"
      unless $matched;

    # A mocked request can never time out -- nothing here reads a socket, and
    # nothing here opens one -- so read_timeout and connect_timeout are both
    # accepted and ignored, exactly as a route ignores every other transport
    # option. What is not ignored is their shape: that is part of the contract
    # this stands in for, and a value the real transport would refuse has to
    # fail here rather than only once a test is run live. The role's own
    # checks are called rather than copied, so the two cannot drift.
    $self->_read_timeout_value($opts{read_timeout})
      if exists $opts{read_timeout};
    $self->_connect_timeout_value($opts{connect_timeout})
      if exists $opts{connect_timeout};

    my $result = ref $handler eq 'CODE'
      ? $handler->($method, $clean_path, %opts)
      : $handler;

    # A route that says nothing about the status gets the one the daemon
    # would have sent for that body: 200 with one, 204 without. A route built
    # with mock_response() carries its own status and headers.
    my $response = ref $result eq 'Test::API::Docker::Mock::Response'
      ? $result
      : mock_response(status => (defined $result ? 200 : 204), data => $result);

    if (my $out = $opts{response}) {
      %$out = (
        status  => $response->{status},
        reason  => $response->{reason},
        headers => $response->{headers},
      );
    }

    # Filled above, croaked below -- the same order _request keeps, so an
    # eval-ing caller can still read the status of a mocked failure. A >= 400
    # route never reaches a streaming callback either: the real transport
    # reads such a body whole before the croak (_read_streaming_response
    # returns it with no summary), so the callback is not invoked here.
    _mock_croak($response->{status}, $response->{reason}, $response->{data})
      if $response->{status} >= 400;

    my @streaming = grep { exists $opts{$_} } qw( on_event on_frame on_chunk );
    croak "Mock route $key got more than one of on_event, on_frame, on_chunk: "
      . join(' and ', @streaming) if @streaming > 1;
    return _mock_stream($opts{$streaming[0]}, $response) if @streaming;



( run in 1.161 second using v1.01-cache-2.11-cpan-54e63673c56 )