package main import ( "encoding/json" "strings" "testing" ) func TestTheManifestIsComposesPackageAndNothingElse(t *testing.T) { m := readManifest(t) holdsTheBundle(t, m, "docker_compose") if got := strings.Join(m.packages(), ","); got != "docker-compose" { t.Errorf("packages %s: buildx and the runtime are the docker module's", got) } if len(m.Resources) != 1 { t.Errorf("one resource, the package: %v", m.Resources) } } const lsJSON = `[{"Name":"anton-lavinmq","Status":"running(1)","ConfigFiles":"/home/op/hub/lavinmq/docker-compose.yml"},{"Name":"old","Status":"exited(2)","ConfigFiles":"/srv/old/compose.yaml,/srv/old/compose.override.yaml"}]` const psLabels = "anton-lavinmq\t/home/op/hub/lavinmq\t/home/op/hub/lavinmq/docker-compose.yml\tlavinmq\trunning\n" + "old\t/srv/old\t/srv/old/compose.yaml,/srv/old/compose.override.yaml\tweb\texited\n" + "old\t/srv/old\t/srv/old/compose.yaml,/srv/old/compose.override.yaml\tdb\texited\n" // aDocker answers compose ls and the labelled ps, and hands every other line to rest. func aDocker(t *testing.T, rest func(line string) Result) *fake { return using(t, func(line string, c Cmd) Result { switch { case strings.HasPrefix(line, "docker compose ls"): return ok(lsJSON) case strings.HasPrefix(line, "docker ps -a --filter label=com.docker.compose.project"): return ok(psLabels) } if rest != nil { return rest(line) } return ok("") }) } func onDisk(t *testing.T, dirs, files []string) { t.Helper() wasD, wasF := statDir, statFile in := func(set []string) func(string) bool { return func(p string) bool { for _, s := range set { if s == p { return true } } return false } } statDir, statFile = in(dirs), in(files) t.Cleanup(func() { statDir, statFile = wasD, wasF }) } func TestProjectsMergesComposesListWithTheContainersLabels(t *testing.T) { aDocker(t, nil) got, err := Projects() if err != nil || got.Count != 2 { t.Fatalf("%+v %v", got, err) } p := got.Projects[0] if p.Name != "anton-lavinmq" || p.WorkingDir != "/home/op/hub/lavinmq" || p.Running != 1 || p.Containers != 1 || p.Status != "running(1)" { t.Errorf("%+v", p) } o := got.Projects[1] if strings.Join(o.Services, ",") != "db,web" || len(o.ConfigFiles) != 2 || o.Running != 0 || o.Containers != 2 { t.Errorf("%+v", o) } } func TestProjectsNamesADaemonTheAccountCannotReach(t *testing.T) { using(t, func(string, Cmd) Result { return Result{Status: 1, Stderr: "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock"} }) if _, err := Projects(); err == nil || !strings.Contains(err.Error(), "docker group") { t.Fatalf("%v", err) } } func TestATargetIsAKnownProjectWithItsFilesOrADirectoryHoldingAComposeFile(t *testing.T) { aDocker(t, nil) onDisk(t, []string{"/home/op/hub/lavinmq", "/srv/new", "/srv/empty", "/srv/old"}, []string{"/home/op/hub/lavinmq/docker-compose.yml", "/srv/new/compose.yaml"}) got, err := targetOf(map[string]any{"dir": "/home/op/hub/lavinmq/"}, true) if err != nil || got.Project != "anton-lavinmq" || len(got.Files) != 1 { t.Fatalf("a known directory: %+v %v", got, err) } if a := strings.Join(got.args(), " "); a != "compose --project-directory /home/op/hub/lavinmq -f /home/op/hub/lavinmq/docker-compose.yml -p anton-lavinmq" { t.Errorf("args %s", a) } got, err = targetOf(map[string]any{"dir": "/srv/new"}, true) if err != nil || got.Project != "" || got.Dir != "/srv/new" { t.Fatalf("a new directory: %+v %v", got, err) } got, err = targetOf(map[string]any{"project": "old"}, false) if err != nil || got.Dir != "/srv/old" || got.Files != nil { t.Fatalf("a project whose files are gone, for an act that needs none: %+v %v", got, err) } if _, err := targetOf(map[string]any{"project": "old"}, true); err == nil || !strings.Contains(err.Error(), "no longer there") { t.Errorf("its files are needed: %v", err) } for _, bad := range []map[string]any{{}, {"dir": "rel/path"}, {"dir": "/nope"}, {"dir": "/srv/empty"}, {"project": "Bad Name"}, {"project": "unknown"}} { if _, err := targetOf(bad, false); err == nil { t.Errorf("%v was accepted", bad) } } } func TestPsReadsEitherJSONShapeAndKeepsOnlyPublishedPorts(t *testing.T) { aDocker(t, func(line string) Result { return ok(`{"Name":"a-web-1","Service":"web","State":"running","Status":"Up 2 hours","Health":"healthy","ExitCode":0,"Image":"nginx","Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"},{"URL":"","TargetPort":443,"PublishedPort":0,"Protocol":"tcp"}]} {"Name":"a-db-1","Service":"db","State":"exited","Status":"Exited (1)","ExitCode":1,"Image":"postgres","Publishers":[]}`) }) got, err := Ps(Target{Project: "a"}) if err != nil || len(got.Containers) != 2 { t.Fatalf("%+v %v", got, err) } if c := got.Containers[0]; strings.Join(c.Ports, ",") != "0.0.0.0:8080->80/tcp" || c.Health != "healthy" { t.Errorf("%+v", c) } var arr []map[string]any if err := jsonObjects(`[{"Name":"x"}]`, &arr); err != nil || len(arr) != 1 { t.Errorf("an array: %v %v", arr, err) } } func TestLogsAreBoundedAndRefuseAnOptionInAName(t *testing.T) { f := aDocker(t, func(line string) Result { return Result{Stdout: "web-1 | a\nweb-1 | b\n", Truncated: true} }) got, err := Logs(Target{Project: "a"}, []string{"web"}, 50, "10m") if err != nil || len(got.Lines) != 2 || !got.Truncated { t.Fatalf("%+v %v", got, err) } if l := f.lines()[0]; l != "docker compose -p a logs --no-color --timestamps --tail 50 --since 10m web" { t.Errorf("%s", l) } if _, err := Logs(Target{Project: "a"}, []string{"--follow"}, 5, ""); err == nil { t.Error("an option as a service") } if _, err := Logs(Target{Project: "a"}, nil, 5, "1h; rm"); err == nil { t.Error("a since that is neither") } } func TestConfigRedactsSecretLookingValuesAndInlineContent(t *testing.T) { aDocker(t, func(string) Result { return ok(`{"name":"a","services":{"web":{"image":"nginx","environment":{"DB_PASSWORD":"hunter2","PORT":"80","API_TOKEN":"t"},"build":{"args":{"NPM_TOKEN":"n","NODE_ENV":"production"}}}},"secrets":{"s":{"content":"raw"}}}`) }) got, err := Config(Target{Dir: "/srv/a"}) if err != nil || got.Redacted != 4 || strings.Join(got.Services, ",") != "web" { t.Fatalf("%+v %v", got, err) } raw, _ := json.Marshal(got.Rendered) for _, secret := range []string{"hunter2", `"t"`, `"n"`, "raw"} { if strings.Contains(string(raw), secret) { t.Errorf("%s shown: %s", secret, raw) } } for _, kept := range []string{`"PORT":"80"`, `"NODE_ENV":"production"`, `"image":"nginx"`} { if !strings.Contains(string(raw), kept) { t.Errorf("%s hidden: %s", kept, raw) } } } func TestConfigAnInvalidFileIsComposesError(t *testing.T) { aDocker(t, func(string) Result { return Result{Status: 15, Stderr: "services.web Additional property foo is not allowed"} }) if _, err := Config(Target{Dir: "/srv/a"}); err == nil || !strings.Contains(err.Error(), "Additional property") { t.Fatalf("%v", err) } } func TestActsAreJobsAsTheAccountKeepVolumesAndCheckTheirArguments(t *testing.T) { f := aDocker(t, nil) tg := Target{Dir: "/srv/a", Project: "a"} if got, err := Up(tg, []string{"web"}, true, "always"); err != nil || got.Job.Running || got.Act != "up" { t.Fatalf("%+v %v", got, err) } if _, err := Down(tg); err != nil { t.Fatal(err) } if _, err := Restart(tg, nil); err != nil { t.Fatal(err) } if _, err := Pull(tg, nil); err != nil { t.Fatal(err) } want := []string{ "docker compose --project-directory /srv/a -p a up --detach --pull always --build web", "docker compose --project-directory /srv/a -p a down", "docker compose --project-directory /srv/a -p a restart", "docker compose --project-directory /srv/a -p a pull", } if got := strings.Join(f.lines(), "\n"); got != strings.Join(want, "\n") { t.Errorf("asked\n%s\nwant\n%s", got, strings.Join(want, "\n")) } for _, l := range f.lines() { if strings.Contains(l, "sudo") || strings.Contains(l, "-v") || strings.Contains(l, "--volumes") { t.Errorf("%s", l) } } if _, err := Up(tg, nil, false, "sometimes"); err == nil { t.Error("an unknown pull policy") } if _, err := Restart(tg, []string{"-t"}); err == nil { t.Error("an option as a service") } } func TestAFailedActIsAnError(t *testing.T) { aDocker(t, func(string) Result { return Result{Status: 1, Stderr: "Error response from daemon: port is already allocated"} }) if _, err := Up(Target{Dir: "/srv/a"}, nil, false, "missing"); err == nil || !strings.Contains(err.Error(), "already allocated") { t.Fatalf("%v", err) } }