Files
Dominik Gašparić 5e780eb767
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
refactor: Switch from docker go modules to moby (#4676)
Co-authored-by: codedoga <codedoga@users.noreply.github.com>
2026-05-06 05:27:02 -07:00

351 lines
12 KiB
Go

package docker
import (
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"time"
"testing"
"github.com/amir20/dozzle/internal/container"
docker "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockedProxy struct {
mock.Mock
DockerCLI
}
func (m *mockedProxy) ContainerList(context.Context, client.ContainerListOptions) (client.ContainerListResult, error) {
args := m.Called()
containers, ok := args.Get(0).([]docker.Summary)
if !ok && args.Get(0) != nil {
panic("containers is not of type []docker.Summary")
}
return client.ContainerListResult{Items: containers}, args.Error(1)
}
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
args := m.Called(ctx, id, options)
reader, ok := args.Get(0).(io.ReadCloser)
if !ok && args.Get(0) != nil {
panic("reader is not of type io.ReadCloser")
}
return reader, args.Error(1)
}
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
args := m.Called(ctx, containerID)
return client.ContainerInspectResult{Container: args.Get(0).(docker.InspectResponse)}, args.Error(1)
}
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, options client.ContainerStatsOptions) (client.ContainerStatsResult, error) {
_ = ctx
_ = containerID
_ = options
return client.ContainerStatsResult{}, nil
}
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
args := m.Called(ctx, containerID, options)
return client.ContainerStartResult{}, args.Error(0)
}
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
args := m.Called(ctx, containerID, options)
return client.ContainerStopResult{}, args.Error(0)
}
func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
args := m.Called(ctx, containerID, options)
return client.ContainerRestartResult{}, args.Error(0)
}
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ListContainers_happy(t *testing.T) {
containers := []docker.Summary{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
require.NoError(t, err, "error should not return an error.")
Ids := []string{"1234567890_a", "abcdefghijkl"}
for i, container := range list {
assert.Equal(t, container.ID, Ids[i])
}
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
id := "123456"
proxy := new(mockedProxy)
expected := "INFO Testing logs..."
b := make([]byte, 8)
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
b = append(b, []byte(expected)...)
reader := io.NopCloser(bytes.NewReader(b))
since := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
options := client.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Tail: "100",
Timestamps: true,
Since: "2020-12-31T23:59:59.95Z"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
logReader, _ := client.ContainerLogs(context.Background(), id, since, container.STDALL)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerLogs_error(t *testing.T) {
id := "123456"
proxy := new(mockedProxy)
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, container.STDALL)
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
proxy.AssertExpectations(t)
}
func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy := new(mockedProxy)
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := docker.InspectResponse{
ID: "abcdefghijklmnopqrst",
State: state,
HostConfig: &docker.HostConfig{},
Config: &docker.Config{Tty: false},
}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container.ID, "abcdefghijkl")
proxy.AssertExpectations(t)
}
func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(docker.InspectResponse{}, errors.New("not found"))
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
_, err := client.FindContainer(context.Background(), "not_valid")
require.Error(t, err, "error should be thrown")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
proxy := new(mockedProxy)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := docker.InspectResponse{ID: "abcdefghijkl", State: state, HostConfig: &docker.HostConfig{}, Config: &docker.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerRestart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
c, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, c.ID, "abcdefghijkl")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, err, nil)
}
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerActions_error(t *testing.T) {
proxy := new(mockedProxy)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(docker.InspectResponse{}, errors.New("not found"))
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
c, err := client.FindContainer(context.Background(), "random-id")
require.Error(t, err, "error should be thrown")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
require.Error(t, err, "error should be thrown")
assert.Error(t, err, "error should have been returned")
}
proxy.AssertExpectations(t)
}
func Test_newContainer_labelPriority(t *testing.T) {
tests := []struct {
name string
labels map[string]string
containerName string
expectedName string
expectedGroup string
}{
{
name: "dozzle labels take priority",
labels: map[string]string{"dev.dozzle.name": "dozzle-name", "coolify.serviceName": "coolify-name", "dev.dozzle.group": "dozzle-group", "coolify.projectName": "coolify-project"},
containerName: "/docker-name",
expectedName: "dozzle-name",
expectedGroup: "dozzle-group",
},
{
name: "coolify labels as fallback",
labels: map[string]string{"coolify.serviceName": "coolify-name", "coolify.projectName": "coolify-project"},
containerName: "/docker-name",
expectedName: "coolify-name",
expectedGroup: "coolify-project",
},
{
name: "docker name as final fallback",
labels: map[string]string{},
containerName: "/docker-name",
expectedName: "docker-name",
expectedGroup: "",
},
{
name: "coolify name with dozzle group",
labels: map[string]string{"coolify.serviceName": "coolify-name", "dev.dozzle.group": "dozzle-group"},
containerName: "/docker-name",
expectedName: "coolify-name",
expectedGroup: "dozzle-group",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
summary := docker.Summary{
ID: "abcdefghijklmnopqrst",
Names: []string{tt.containerName},
Labels: tt.labels,
}
c := newContainer(summary, "localhost")
assert.Equal(t, tt.expectedName, c.Name)
assert.Equal(t, tt.expectedGroup, c.Group)
})
}
}
func Test_newContainerFromJSON_labelPriority(t *testing.T) {
tests := []struct {
name string
labels map[string]string
containerName string
expectedName string
expectedGroup string
}{
{
name: "dozzle labels take priority",
labels: map[string]string{"dev.dozzle.name": "dozzle-name", "coolify.serviceName": "coolify-name", "dev.dozzle.group": "dozzle-group", "coolify.projectName": "coolify-project"},
containerName: "/docker-name",
expectedName: "dozzle-name",
expectedGroup: "dozzle-group",
},
{
name: "coolify labels as fallback",
labels: map[string]string{"coolify.serviceName": "coolify-name", "coolify.projectName": "coolify-project"},
containerName: "/docker-name",
expectedName: "coolify-name",
expectedGroup: "coolify-project",
},
{
name: "docker name as final fallback",
labels: map[string]string{},
containerName: "/docker-name",
expectedName: "docker-name",
expectedGroup: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := docker.InspectResponse{
ID: "abcdefghijklmnopqrst",
Name: tt.containerName,
State: state,
HostConfig: &docker.HostConfig{},
Config: &docker.Config{Labels: tt.labels},
}
c := newContainerFromJSON(json, "localhost")
assert.Equal(t, tt.expectedName, c.Name)
assert.Equal(t, tt.expectedGroup, c.Group)
})
}
}