Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions lib/container/ministack_container.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule Testcontainers.MinistackContainer do
Comment thread
jarlah marked this conversation as resolved.
@moduledoc """
Provides functionality for creating and managing Ministack container configurations.
"""

alias Testcontainers.Container
alias Testcontainers.ContainerBuilder
alias Testcontainers.LogWaitStrategy
alias Testcontainers.MinistackContainer

@default_image "ministackorg/ministack"
@default_tag "1.3.42"
@default_image_with_tag "#{@default_image}:#{@default_tag}"
@default_username "111111111111"
@default_password "anything"
@default_s3_port 4566
@default_ui_port 2222
@default_wait_timeout 60_000

@type t :: %__MODULE__{}

@enforce_keys [:image, :username, :password, :wait_timeout]
defstruct [
:image,
:username,
:password,
:wait_timeout,
reuse: false
]

def new,
do: %__MODULE__{
image: @default_image_with_tag,
username: @default_username,
password: @default_password,
wait_timeout: @default_wait_timeout
}

@doc """
Set the reuse flag to reuse the container if it is already running.
"""
def with_reuse(%__MODULE__{} = config, reuse) when is_boolean(reuse) do
%__MODULE__{config | reuse: reuse}
end

def get_username, do: @default_username
def get_password, do: @default_password
def default_ui_port, do: @default_ui_port
def default_s3_port, do: @default_s3_port

@doc """
Retrieves the port mapped by the Docker host for the Ministack container.
"""
def port(%Container{} = container), do: Testcontainers.get_port(container, @default_s3_port)

@doc """
Generates the connection URL for accessing the Ministack service running within the container.
"""
def connection_url(%Container{} = container) do
"http://#{Testcontainers.get_host(container)}:#{port(container)}"
end

@doc """
Generates the connection options for accessing the Ministack service running within the container.
Compatible with what ex_aws expects in `ExAws.request(options)`
"""
def connection_opts(%Container{} = container) do
[
port: MinistackContainer.port(container),
scheme: "http://",
host: Testcontainers.get_host(container),
access_key_id: container.environment[:AWS_ACCESS_KEY_ID],
secret_access_key: container.environment[:AWS_SECRET_ACCESS_KEY]
]
end

defimpl ContainerBuilder do
import Container

@spec build(MinistackContainer.t()) :: Container.t()
@impl true
def build(%MinistackContainer{} = config) do
new(config.image)
|> with_exposed_ports([
MinistackContainer.default_s3_port(),
MinistackContainer.default_ui_port()
])
|> with_environment(:AWS_ACCESS_KEY_ID, config.username)
|> with_environment(:AWS_SECRET_ACCESS_KEY, config.password)
|> with_reuse(config.reuse)
|> with_waiting_strategy(
LogWaitStrategy.new(
~r/.*Ready .* services available on port #{MinistackContainer.default_s3_port()}\./,
config.wait_timeout,
1000
)
)
end

@impl true
def after_start(_config, _container, _conn), do: :ok
end
end
109 changes: 109 additions & 0 deletions test/container/ministack_container_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: MIT
defmodule Testcontainers.Container.MinistackContainerTest do
use ExUnit.Case, async: true

import Testcontainers.ExUnit

alias Testcontainers.ContainerBuilder
Comment thread
jarlah marked this conversation as resolved.
alias Testcontainers.MinistackContainer

@ministack_container MinistackContainer.new()

describe "new/0 and builder options" do
test "returns default ministack configuration" do
config = MinistackContainer.new()

assert config.image == "ministackorg/ministack:1.3.42"
assert config.username == "111111111111"
assert config.password == "anything"
assert config.wait_timeout == 60_000
assert config.reuse == false
end

test "exposes default S3 and UI ports and sets AWS credentials" do
container =
MinistackContainer.new()
|> MinistackContainer.with_reuse(true)
|> ContainerBuilder.build()

assert {MinistackContainer.default_s3_port(), nil} in container.exposed_ports
assert {MinistackContainer.default_ui_port(), nil} in container.exposed_ports
assert container.environment[:AWS_ACCESS_KEY_ID] == MinistackContainer.get_username()
assert container.environment[:AWS_SECRET_ACCESS_KEY] == MinistackContainer.get_password()
assert container.reuse == true
end
end

describe "runtime behavior" do
container(:ministack, @ministack_container)

test "provides connection helpers", %{
ministack: ministack
} do
host = Testcontainers.get_host(ministack)
port = MinistackContainer.port(ministack)
conn_opts = MinistackContainer.connection_opts(ministack)

assert is_integer(port)
assert MinistackContainer.connection_url(ministack) == "http://#{host}:#{port}"

assert conn_opts == [
port: port,
scheme: "http://",
host: host,
access_key_id: MinistackContainer.get_username(),
secret_access_key: MinistackContainer.get_password()
]
end

test "responds from health-check endpoint", %{
ministack: ministack
} do
health_url = "#{MinistackContainer.connection_url(ministack)}/_ministack/health"

{:ok, %{status: 200, body: body}} = Tesla.get(health_url)
{:ok, health} = Jason.decode(body)

assert is_map(health)
assert map_size(health) > 0
end

test "supports bucket and file object operations", %{
ministack: ministack
} do
conn_opts = MinistackContainer.connection_opts(ministack)

bucket = bucket_name("files")
object_key = "fixtures/hello.txt"
file_contents = "Hello from a Ministack-backed S3 object"

{:ok, _result} =
ExAws.S3.put_bucket(bucket, "")
|> ExAws.request(conn_opts)

{:ok, %{body: %{buckets: buckets}}} =
ExAws.S3.list_buckets()
|> ExAws.request(conn_opts)

assert Enum.any?(buckets, &(&1.name == bucket))

{:ok, _result} =
ExAws.S3.put_object(bucket, object_key, file_contents)
|> ExAws.request(conn_opts)

{:ok, %{body: %{contents: objects}}} =
ExAws.S3.list_objects(bucket, prefix: "fixtures/")
|> ExAws.request(conn_opts)

assert Enum.any?(objects, &(&1.key == object_key))

{:ok, %{body: ^file_contents}} =
ExAws.S3.get_object(bucket, object_key)
|> ExAws.request(conn_opts)
end
end

defp bucket_name(prefix) do
"ministack-#{prefix}-#{System.unique_integer([:positive])}"
end
end
Comment thread
jarlah marked this conversation as resolved.
Loading