package discovery_test import ( "context" "errors" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/shcizo/package-updater/internal/discovery" "github.com/stretchr/testify/require" ) type fakeDockerClient struct { containers []types.Container err error } func (f *fakeDockerClient) ContainerList(_ context.Context, _ container.ListOptions) ([]types.Container, error) { return f.containers, f.err } func (f *fakeDockerClient) Ping(_ context.Context) (types.Ping, error) { return types.Ping{}, nil } func mkContainer(image string, labels map[string]string) types.Container { return types.Container{Image: image, Labels: labels} } func mkComposeLabels(project, service, workingDir, configFile string, optIn bool) map[string]string { m := map[string]string{ "com.docker.compose.project": project, "com.docker.compose.service": service, "com.docker.compose.project.working_dir": workingDir, "com.docker.compose.project.config_files": configFile, } if optIn { m["se.shcizo.auto-update"] = "true" } return m } func TestFindJobs_MatchAndOptIn(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp-prod", "web", "/home/shcizo/self-hosted/myapp-prod", "/home/shcizo/self-hosted/myapp-prod/docker-compose.yml", true, )), mkContainer("registry.example.com/other:v1", mkComposeLabels( "other", "web", "/home/shcizo/self-hosted/other", "/home/shcizo/self-hosted/other/docker-compose.yml", true, )), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Len(t, jobs, 1) require.Equal(t, "myapp-prod", jobs[0].Project) require.Equal(t, "web", jobs[0].Service) require.Equal(t, "/home/shcizo/self-hosted/myapp-prod", jobs[0].WorkingDir) } func TestFindJobs_SkipsWithoutOptIn(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp-prod", "web", "/home/shcizo/self-hosted/myapp-prod", "/home/shcizo/self-hosted/myapp-prod/docker-compose.yml", false, )), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Empty(t, jobs) } func TestFindJobs_DedupReplicas(t *testing.T) { labels := mkComposeLabels( "myapp", "web", "/home/shcizo/self-hosted/myapp", "/home/shcizo/self-hosted/myapp/docker-compose.yml", true, ) fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", labels), mkContainer("registry.example.com/myapp:v1", labels), mkContainer("registry.example.com/myapp:v1", labels), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Len(t, jobs, 1) } func TestFindJobs_OutsideRootProducesRefusedJob(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp", "web", "/opt/elsewhere/myapp", "/opt/elsewhere/myapp/docker-compose.yml", true, )), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Len(t, jobs, 1) require.True(t, jobs[0].Refused) } func TestFindJobs_MultipleStacksSameImage(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp-prod", "web", "/home/shcizo/self-hosted/myapp-prod", "/home/shcizo/self-hosted/myapp-prod/docker-compose.yml", true, )), mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp-staging", "web", "/home/shcizo/self-hosted/myapp-staging", "/home/shcizo/self-hosted/myapp-staging/docker-compose.yml", true, )), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Len(t, jobs, 2) } func TestFindJobs_DockerError(t *testing.T) { fake := &fakeDockerClient{err: errors.New("connection refused")} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") _, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.Error(t, err) } func TestFindJobs_NonComposeContainerIsSkipped(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", map[string]string{ "se.shcizo.auto-update": "true", }), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Empty(t, jobs) } func TestFindJobs_PopulatesImageField(t *testing.T) { fake := &fakeDockerClient{containers: []types.Container{ mkContainer("registry.example.com/myapp:v1", mkComposeLabels( "myapp-prod", "web", "/home/shcizo/self-hosted/myapp-prod", "/home/shcizo/self-hosted/myapp-prod/docker-compose.yml", true, )), }} d := discovery.New(fake, "/home/shcizo/self-hosted", "se.shcizo.auto-update") jobs, err := d.FindJobs(context.Background(), "registry.example.com/myapp") require.NoError(t, err) require.Len(t, jobs, 1) require.Equal(t, "registry.example.com/myapp", jobs[0].Image) require.Empty(t, jobs[0].ServiceID) }