*
* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* <http://license.coscl.org.cn/MulanPSL2>
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*
*/
package docker
import (
"bytes"
"context"
"errors"
"io"
"net"
"os"
"testing"
"time"
"github.com/agiledragon/gomonkey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
dockerapi "github.com/docker/docker/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"gopkg.openfuyao.cn/bkeadm/utils"
)
const (
testNumericZero = 0
testNumericOne = 1
testNumericTwo = 2
testNumericThree = 3
testRetryCount = 3
testDelaySeconds = 1
testContainerID = "test-container-id-12345"
testImageID = "sha256:test-image-id-123456"
testNumericPort = 8080
testIPv4SegmentA = 192
testIPv4SegmentB = 168
testIPv4SegmentC = 1
testIPv4SegmentD = 100
)
const (
testShortTimeout = 1 * time.Second
testMediumTimeout = 5 * time.Second
testFileMode0644 = 0644
)
var (
testRetryOptions = utils.RetryOptions{
MaxRetry: testRetryCount,
Delay: testDelaySeconds,
}
testLoopbackIP = net.IPv4(
testIPv4SegmentA,
testIPv4SegmentB,
testIPv4SegmentC,
testIPv4SegmentD,
).String()
testTimeout = 5 * time.Second
)
func TestNewDockerClient(t *testing.T) {
tests := []struct {
name string
socketExists bool
newClientErr error
expectedError bool
}{
{
name: "socket exists and client created successfully",
socketExists: true,
newClientErr: nil,
expectedError: false,
},
{
name: "socket does not exist",
socketExists: false,
newClientErr: nil,
expectedError: true,
},
{
name: "socket exists but client creation fails",
socketExists: true,
newClientErr: errors.New("client creation error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.Exists, func(path string) bool {
return tt.socketExists
})
patches.ApplyFunc(dockerapi.NewClientWithOpts, func(opts ...dockerapi.Opt) (*dockerapi.Client, error) {
return nil, tt.newClientErr
})
client, err := NewDockerClient()
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, client)
} else {
assert.NoError(t, err)
assert.NotNil(t, client)
}
})
}
}
func TestGetClient(t *testing.T) {
t.Run("get client returns the underlying client", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
mockDockerClient := &dockerapi.Client{}
patches.ApplyFunc(utils.Exists, func(path string) bool {
return true
})
patches.ApplyFunc(dockerapi.NewClientWithOpts, func(opts ...dockerapi.Opt) (*dockerapi.Client, error) {
return mockDockerClient, nil
})
dockerClient, err := NewDockerClient()
assert.NoError(t, err)
assert.NotNil(t, dockerClient)
client := dockerClient.GetClient()
assert.NotNil(t, client)
})
}
func TestImageRefStruct(t *testing.T) {
t.Run("image ref struct initialization", func(t *testing.T) {
imageRef := ImageRef{
Image: "nginx:latest",
Username: "testuser",
Password: "testpass",
Platform: "linux/amd64",
}
assert.Equal(t, "nginx:latest", imageRef.Image)
assert.Equal(t, "testuser", imageRef.Username)
assert.Equal(t, "testpass", imageRef.Password)
assert.Equal(t, "linux/amd64", imageRef.Platform)
})
t.Run("image ref struct with empty credentials", func(t *testing.T) {
imageRef := ImageRef{
Image: "public-image:latest",
Username: "",
Password: "",
Platform: "",
}
assert.Equal(t, "public-image:latest", imageRef.Image)
assert.Empty(t, imageRef.Username)
assert.Empty(t, imageRef.Password)
assert.Empty(t, imageRef.Platform)
})
}
func TestContainerRefStruct(t *testing.T) {
t.Run("container ref struct initialization", func(t *testing.T) {
containerRef := ContainerRef{
Id: testContainerID,
Name: "test-container",
}
assert.Equal(t, testContainerID, containerRef.Id)
assert.Equal(t, "test-container", containerRef.Name)
})
}
func TestImageList(t *testing.T) {
t.Run("image list returns local images", func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageList,
func(_ *dockerapi.Client, _ context.Context, opts image.ListOptions) ([]image.Summary, error) {
return []image.Summary{
{
ID: testImageID,
RepoTags: []string{"nginx:latest", "nginx:alpine"},
},
{
ID: "sha256:alpine-image-id",
RepoTags: []string{"alpine:latest"},
},
}, nil
})
images, err := client.ImageList()
assert.NoError(t, err)
assert.Len(t, images, testNumericThree)
})
}
func TestHasImage(t *testing.T) {
tests := []struct {
name string
image string
imageList []ImageRef
expectedResult bool
}{
{
name: "image exists in the list",
image: "nginx:latest",
imageList: []ImageRef{{Image: "nginx:latest"}, {Image: "alpine:latest"}},
expectedResult: true,
},
{
name: "image does not exist in the list",
image: "postgres:latest",
imageList: []ImageRef{{Image: "nginx:latest"}, {Image: "alpine:latest"}},
expectedResult: false,
},
{
name: "empty image list",
image: "nginx:latest",
imageList: []ImageRef{},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageList,
func(_ *dockerapi.Client, _ context.Context, opts image.ListOptions) ([]image.Summary, error) {
imageSummaries := make([]image.Summary, len(tt.imageList))
for i, img := range tt.imageList {
imageSummaries[i] = image.Summary{
RepoTags: []string{img.Image},
}
}
return imageSummaries, nil
})
result := client.HasImage(tt.image)
assert.Equal(t, tt.expectedResult, result)
})
}
}
func TestContainerExists(t *testing.T) {
tests := []struct {
name string
containerInspect types.ContainerJSON
expectedResult bool
}{
{
name: "container exists",
containerInspect: types.ContainerJSON{
ContainerJSONBase: &container.ContainerJSONBase{
ID: testContainerID,
Name: "/test-container",
},
},
expectedResult: true,
},
{
name: "container does not exist",
containerInspect: types.ContainerJSON{},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ContainerInspect,
func(_ *dockerapi.Client, _ context.Context, containerName string) (types.ContainerJSON, error) {
return tt.containerInspect, nil
})
result, exists := client.ContainerExists("test-container")
assert.Equal(t, tt.expectedResult, exists)
if tt.expectedResult {
assert.Equal(t, testContainerID, result.ID)
}
})
}
}
func TestContainerRemove(t *testing.T) {
tests := []struct {
name string
removeErr error
expectedError bool
}{
{
name: "container removed successfully",
removeErr: nil,
expectedError: false,
},
{
name: "container removal fails",
removeErr: errors.New("remove error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ContainerRemove,
func(_ *dockerapi.Client, _ context.Context, containerID string, options container.RemoveOptions) error {
return tt.removeErr
})
err := client.ContainerRemove(testContainerID)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestContainerStop(t *testing.T) {
tests := []struct {
name string
stopErr error
expectedError bool
}{
{
name: "container stopped successfully",
stopErr: nil,
expectedError: false,
},
{
name: "container stop fails",
stopErr: errors.New("stop error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ContainerStop,
func(_ *dockerapi.Client, _ context.Context, containerID string, options container.StopOptions) error {
return tt.stopErr
})
err := client.ContainerStop(testContainerID)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestTag(t *testing.T) {
tests := []struct {
name string
tagErr error
expectedError bool
}{
{
name: "image tagged successfully",
tagErr: nil,
expectedError: false,
},
{
name: "image tagging fails",
tagErr: errors.New("tag error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageTag,
func(_ *dockerapi.Client, _ context.Context, sourceImage, targetImage string) error {
return tt.tagErr
})
err := client.Tag("nginx:latest", "my-registry.com/nginx:latest")
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSave(t *testing.T) {
tests := []struct {
name string
imageSaveErr error
expectedError bool
}{
{
name: "image saved successfully",
imageSaveErr: nil,
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
mockBody := io.NopCloser(bytes.NewReader([]byte("test image data")))
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageSave,
func(_ *dockerapi.Client, _ context.Context, imageIDs []string) (io.ReadCloser, error) {
return mockBody, tt.imageSaveErr
})
patches.ApplyFunc(os.WriteFile, func(name string, data []byte, perm os.FileMode) error {
return nil
})
err := client.Save("nginx:latest", "/tmp/test-image.tar")
assert.NoError(t, err)
})
}
}
func TestLoad(t *testing.T) {
tests := []struct {
name string
loadErr error
mockBody string
expectedError bool
}{
{
name: "image loaded successfully",
loadErr: nil,
mockBody: `{"status":"Loaded","id":"sha256:abc123","ProgressDetail":{}}`,
expectedError: false,
},
{
name: "load fails",
loadErr: errors.New("load error"),
mockBody: "",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
mockBody := io.NopCloser(bytes.NewReader([]byte(tt.mockBody)))
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageLoad,
func(_ *dockerapi.Client, _ context.Context, input io.Reader) (image.LoadResponse, error) {
return image.LoadResponse{
Body: mockBody,
}, tt.loadErr
})
patches.ApplyFunc(os.OpenFile, func(name string, flag int, perm os.FileMode) (*os.File, error) {
return &os.File{}, nil
})
image, err := client.Load("/tmp/test-image.tar")
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotEmpty(t, image)
}
})
}
}
func TestRemove(t *testing.T) {
tests := []struct {
name string
removeErr error
expectedError bool
}{
{
name: "image removed successfully",
removeErr: nil,
expectedError: false,
},
{
name: "image removal fails",
removeErr: errors.New("remove error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageRemove,
func(_ *dockerapi.Client, _ context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
return []image.DeleteResponse{}, tt.removeErr
})
err := client.Remove(ImageRef{Image: "nginx:latest"})
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestRun(t *testing.T) {
tests := []struct {
name string
createErr error
startErr error
expectedError bool
}{
{
name: "container created and started successfully",
createErr: nil,
startErr: nil,
expectedError: false,
},
{
name: "container creation fails",
createErr: errors.New("create error"),
startErr: nil,
expectedError: true,
},
{
name: "container start fails",
createErr: nil,
startErr: errors.New("start error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ContainerCreate,
func(_ *dockerapi.Client, _ context.Context, config *container.Config, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, platform *specs.Platform, containerName string) (container.CreateResponse, error) {
return container.CreateResponse{ID: testContainerID}, tt.createErr
})
patches.ApplyFunc((*dockerapi.Client).ContainerStart,
func(_ *dockerapi.Client, _ context.Context, containerID string, options container.StartOptions) error {
return tt.startErr
})
config := &container.Config{
Image: "nginx:latest",
}
hostConfig := &container.HostConfig{}
networkingConfig := &network.NetworkingConfig{}
err := client.Run(config, hostConfig, networkingConfig, nil, "test-container")
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestEnsureImageExists(t *testing.T) {
sleepPatches := gomonkey.ApplyFunc(time.Sleep, func(time.Duration) {})
defer sleepPatches.Reset()
tests := []struct {
name string
imageInspect types.ImageInspect
inspectErr error
pullErr error
expectedError bool
}{
{
name: "image already exists",
imageInspect: types.ImageInspect{
ID: testImageID,
},
inspectErr: nil,
pullErr: nil,
expectedError: false,
},
{
name: "image does not exist, pull succeeds",
imageInspect: types.ImageInspect{},
inspectErr: errors.New("not found"),
pullErr: nil,
expectedError: false,
},
{
name: "image does not exist, pull fails",
imageInspect: types.ImageInspect{},
inspectErr: errors.New("not found"),
pullErr: errors.New("pull error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImageInspectWithRaw,
func(_ *dockerapi.Client, _ context.Context, imageID string) (types.ImageInspect, []byte, error) {
return tt.imageInspect, nil, tt.inspectErr
})
if tt.inspectErr != nil || tt.imageInspect.ID == "" {
mockReader := io.NopCloser(bytes.NewReader([]byte("pulling")))
patches.ApplyFunc((*dockerapi.Client).ImagePull,
func(_ *dockerapi.Client, _ context.Context, image string, options image.PullOptions) (io.ReadCloser, error) {
if tt.pullErr != nil {
return nil, tt.pullErr
}
return mockReader, nil
})
patches.ApplyFunc(io.Copy, func(dst io.Writer, src io.Reader) (int64, error) {
return 0, nil
})
}
err := client.EnsureImageExists(ImageRef{Image: "nginx:latest"}, testRetryOptions)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestEnsureContainerRun(t *testing.T) {
tests := []struct {
name string
containerInspect types.ContainerJSON
startErr error
removeErr error
expectedResult bool
expectedError bool
}{
{
name: "container exists and is running",
containerInspect: types.ContainerJSON{
ContainerJSONBase: &container.ContainerJSONBase{
ID: testContainerID,
Name: "/test-container",
State: &container.State{
Running: true,
},
},
Config: &container.Config{},
},
startErr: nil,
removeErr: nil,
expectedResult: true,
expectedError: false,
},
{
name: "container exists but is not running, start succeeds",
containerInspect: types.ContainerJSON{
ContainerJSONBase: &container.ContainerJSONBase{
ID: testContainerID,
Name: "/test-container",
State: &container.State{
Running: false,
},
},
Config: &container.Config{},
},
startErr: nil,
removeErr: nil,
expectedResult: true,
expectedError: false,
},
{
name: "container does not exist",
containerInspect: types.ContainerJSON{
ContainerJSONBase: nil,
},
startErr: nil,
removeErr: nil,
expectedResult: false,
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ContainerInspect,
func(_ *dockerapi.Client, _ context.Context, containerID string) (types.ContainerJSON, error) {
return tt.containerInspect, nil
})
if tt.containerInspect.ContainerJSONBase != nil && tt.containerInspect.State != nil && !tt.containerInspect.State.Running {
patches.ApplyFunc((*dockerapi.Client).ContainerStart,
func(_ *dockerapi.Client, _ context.Context, containerID string, options container.StartOptions) error {
return tt.startErr
})
if tt.startErr != nil {
patches.ApplyFunc((*dockerapi.Client).ContainerRemove,
func(_ *dockerapi.Client, _ context.Context, containerID string, options container.RemoveOptions) error {
return tt.removeErr
})
}
}
result, err := client.EnsureContainerRun(testContainerID)
assert.Equal(t, tt.expectedResult, result)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestCopyFromContainer(t *testing.T) {
tests := []struct {
name string
copyErr error
expectedError bool
}{
{
name: "copy from container successfully",
copyErr: nil,
expectedError: true,
},
{
name: "copy from container fails",
copyErr: errors.New("copy error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
mockContent := io.NopCloser(bytes.NewReader([]byte("test content")))
mockStat := container.PathStat{
Mode: testFileMode0644,
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).CopyFromContainer,
func(_ *dockerapi.Client, _ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
return mockContent, mockStat, tt.copyErr
})
err := client.CopyFromContainer(testContainerID, "/tmp/test", "/tmp/dest")
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestClientStruct(t *testing.T) {
t.Run("client struct fields are properly initialized", func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
mockDockerClient := &dockerapi.Client{}
patches.ApplyFunc(utils.Exists, func(path string) bool {
return true
})
patches.ApplyFunc(dockerapi.NewClientWithOpts, func(opts ...dockerapi.Opt) (*dockerapi.Client, error) {
return mockDockerClient, nil
})
dockerClient, err := NewDockerClient()
assert.NoError(t, err)
assert.NotNil(t, dockerClient)
client, ok := dockerClient.(*Client)
assert.True(t, ok)
assert.NotNil(t, client.Client)
assert.NotNil(t, client.ctx)
})
}
func TestPull(t *testing.T) {
sleepPatches := gomonkey.ApplyFunc(time.Sleep, func(time.Duration) {})
defer sleepPatches.Reset()
tests := []struct {
name string
pullErr error
expectedError bool
}{
{
name: "image pulled successfully",
pullErr: nil,
expectedError: false,
},
{
name: "image pull fails",
pullErr: errors.New("pull error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
mockReader := io.NopCloser(bytes.NewReader([]byte("pulling...")))
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImagePull,
func(_ *dockerapi.Client, _ context.Context, image string, options image.PullOptions) (io.ReadCloser, error) {
return mockReader, tt.pullErr
})
patches.ApplyFunc(io.Copy, func(dst io.Writer, src io.Reader) (int64, error) {
return 0, nil
})
err := client.Pull(ImageRef{Image: "nginx:latest"}, testRetryOptions)
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestPush(t *testing.T) {
tests := []struct {
name string
pushErr error
expectedError bool
}{
{
name: "image pushed successfully",
pushErr: nil,
expectedError: false,
},
{
name: "image push fails",
pushErr: errors.New("push error"),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
mockReader := io.NopCloser(bytes.NewReader([]byte("pushing...")))
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).ImagePush,
func(_ *dockerapi.Client, _ context.Context, image string, options image.PushOptions) (io.ReadCloser, error) {
return mockReader, tt.pushErr
})
patches.ApplyFunc(io.Copy, func(dst io.Writer, src io.Reader) (int64, error) {
return 0, nil
})
err := client.Push(ImageRef{Image: "nginx:latest"})
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestClose(t *testing.T) {
tests := []struct {
name string
closeErr error
}{
{
name: "close succeeds",
closeErr: nil,
},
{
name: "close fails",
closeErr: errors.New("close error"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Client: &dockerapi.Client{},
ctx: context.Background(),
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*dockerapi.Client).Close,
func(_ *dockerapi.Client) error {
return tt.closeErr
})
client.Close()
})
}
}