package cli
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"github.com/openeuler/Conch/internal/cli/client"
cmd "github.com/openeuler/Conch/internal/cli/cmd"
)
func TestPrintHelpListsSubcommands(t *testing.T) {
var buf bytes.Buffer
printHelp(&buf)
got := buf.String()
if strings.Contains(got, "CONTAINERD_ADDRESS") {
t.Fatalf("help output still references CONTAINERD_ADDRESS:\n%s", got)
}
for _, want := range []string{
"conch <command> [options]",
"Commands:",
" image ",
" sandbox ",
" template ",
" debug ",
"conch <command> --help",
} {
if !strings.Contains(got, want) {
t.Fatalf("help output missing %q:\n%s", want, got)
}
}
for _, unwanted := range []string{
"CONCH_API_TIMEOUT",
"CONCH_API_URL",
"conch image push [options]",
"conch sandbox create",
"conch sandbox checkpoint",
"conch template create",
"conch debug snapshot",
} {
if strings.Contains(got, unwanted) {
t.Fatalf("help output should not expose %q:\n%s", unwanted, got)
}
}
if strings.Contains(got, "conch snapshots ls [options]") {
t.Fatalf("help output still contains deprecated snapshots command:\n%s", got)
}
if strings.Contains(got, "snapshot export") {
t.Fatalf("help should not expose removed snapshot export command:\n%s", got)
}
if strings.Contains(got, "conch convert") {
t.Fatalf("help output still contains removed convert command:\n%s", got)
}
}
func TestPrintHelpAlignsCommandDescriptions(t *testing.T) {
var buf bytes.Buffer
printHelp(&buf)
for _, want := range []string{
" image Pull, push, unpack, list, or remove images.",
" sandbox Create, checkpoint, or control sandboxes from Template Names.",
" template Build, list, inspect, or remove templates.",
" debug Low-level inspection and repair commands.",
} {
if !strings.Contains(buf.String(), want) {
t.Fatalf("help output missing aligned command line %q:\n%s", want, buf.String())
}
}
}
func TestRunRejectsRemovedTopLevelCommands(t *testing.T) {
for _, command := range []string{"pull", "push", "unpack", "snapshot", "snapshots", "checkpoint", "convert"} {
t.Run(command, func(t *testing.T) {
oldStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
os.Stderr = w
code := Run([]string{command, "--help"})
_ = w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
_, _ = buf.ReadFrom(r)
if code != 2 {
t.Fatalf("Run(%q --help) exit code = %d, want 2; stderr:\n%s", command, code, buf.String())
}
if !strings.Contains(buf.String(), "unknown command "+strconv.Quote(command)) {
t.Fatalf("Run(%q --help) stderr missing unknown command:\n%s", command, buf.String())
}
})
}
}
func TestPrintImagePushHelpIncludesExample(t *testing.T) {
var buf bytes.Buffer
cmd.PrintImagePushHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch image push [options] <local-image> <remote-image>",
"conchd/containerd",
"--plain-http",
"--username string",
"--timeout duration",
"timeout for this push operation",
"conch image push --timeout 30m",
} {
if !strings.Contains(got, want) {
t.Fatalf("push help output missing %q:\n%s", want, got)
}
}
}
func TestPrintSandboxCreateHelpExplainsDefaultSpec(t *testing.T) {
var buf bytes.Buffer
cmd.PrintSandboxCreateHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch sandbox create [--template-name <template-name> | --template-id <template-id>] [options]",
"sandbox.default_spec",
"sandbox.default_spec.template_name",
"sandbox.default_spec.template_id",
"sandbox.default_spec.ram_mb",
} {
if !strings.Contains(got, want) {
t.Fatalf("sandbox create help missing %q:\n%s", want, got)
}
}
}
func TestParseImagePushArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantLocal string
wantRemote string
wantPlain bool
wantTimeout string
wantErr bool
}{
{
name: "default",
args: []string{"localhost/demo:latest", "hub.oepkgs.net/conch/demo:latest"},
wantLocal: "localhost/demo:latest",
wantRemote: "hub.oepkgs.net/conch/demo:latest",
},
{
name: "plain http",
args: []string{"--plain-http", "localhost/demo:latest", "conch.example.com/conch/demo:latest"},
wantLocal: "localhost/demo:latest",
wantRemote: "conch.example.com/conch/demo:latest",
wantPlain: true,
},
{
name: "timeout",
args: []string{"--timeout=10m", "localhost/demo:latest", "conch.example.com/conch/demo:latest"},
wantLocal: "localhost/demo:latest",
wantRemote: "conch.example.com/conch/demo:latest",
wantTimeout: "10m",
},
{
name: "missing image",
args: []string{"localhost/demo:latest"},
wantErr: true,
},
{
name: "unknown option",
args: []string{"--user", "demo:demo", "localhost/demo:latest", "remote/demo:latest"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cmd.ParseImagePushArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Fatalf("parsePushArgs() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if got.LocalImage != tt.wantLocal || got.RemoteImage != tt.wantRemote || got.PlainHTTP != tt.wantPlain || got.Timeout != tt.wantTimeout {
t.Fatalf("parsePushArgs() = %#v", got)
}
})
}
}
func TestRunImagePushPassesRequest(t *testing.T) {
var got client.PushImageRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/image/push" {
t.Fatalf("path = %q, want /api/image/push", r.URL.Path)
}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode push request: %v", err)
}
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
t.Setenv("CONCH_API_URL", server.URL)
err := cmd.RunImagePush(context.Background(), []string{"localhost/demo:latest", "remote/demo:latest"})
if err != nil {
t.Fatalf("runPush: %v", err)
}
if got.LocalImage != "localhost/demo:latest" || got.RemoteImage != "remote/demo:latest" {
t.Fatalf("push request = %#v", got)
}
}
func TestRunImagePushAcceptsOperationTimeout(t *testing.T) {
var got map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode push request: %v", err)
}
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
t.Setenv("CONCH_API_URL", server.URL)
err := cmd.RunImagePush(context.Background(), []string{"--timeout", "30m", "localhost/demo:latest", "remote/demo:latest"})
if err != nil {
t.Fatalf("runPush: %v", err)
}
if _, ok := got["registry_timeout"]; ok {
t.Fatalf("push request unexpectedly contains registry_timeout: %#v", got)
}
}
func TestPrintImagePullHelpIncludesExample(t *testing.T) {
var buf bytes.Buffer
cmd.PrintImagePullHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch image pull [options] <image-name>",
"conchd API base URL",
"config file path",
"--plain-http",
"--user string",
"docker.io/library/nginx:latest",
"Conch does not unpack OCI images",
"`conch template pull`",
} {
if !strings.Contains(got, want) {
t.Fatalf("pull help output missing %q:\n%s", want, got)
}
}
for _, unwanted := range []string{"--kernel-plain-http", "--kernel-user", "--skip-unpack"} {
if strings.Contains(got, unwanted) {
t.Fatalf("pull help output should not expose %q:\n%s", unwanted, got)
}
}
}
func TestRunImagePullSendsContentOnlyRequest(t *testing.T) {
var got client.PullImageRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/image/pull" {
t.Fatalf("path = %q, want /api/image/pull", r.URL.Path)
}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode pull request: %v", err)
}
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
t.Setenv("CONCH_API_URL", server.URL)
err := cmd.RunImagePull(context.Background(), []string{"docker.io/library/nginx:latest"})
if err != nil {
t.Fatalf("RunImagePull() error = %v", err)
}
if got.ImageName != "docker.io/library/nginx:latest" {
t.Fatalf("pull request = %#v", got)
}
}
func TestParseRegistryUser(t *testing.T) {
tests := []struct {
name string
input string
wantUser string
wantPass string
wantErr bool
}{
{name: "empty", input: "", wantUser: "", wantPass: ""},
{name: "valid", input: "example-user:example-password", wantUser: "example-user", wantPass: "example-password"},
{name: "missing colon", input: "conch", wantErr: true},
{name: "missing password", input: "conch:", wantErr: true},
{name: "missing username", input: ":secret", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, pass, err := cmd.ParseRegistryUser(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("parseRegistryUser() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if user != tt.wantUser || pass != tt.wantPass {
t.Fatalf("parseRegistryUser() = (%q, %q), want (%q, %q)", user, pass, tt.wantUser, tt.wantPass)
}
})
}
}
func TestPrintTemplateUnpackHelpIncludesExample(t *testing.T) {
var buf bytes.Buffer
cmd.PrintTemplateUnpackHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch template unpack [options] <template-name>",
"conchd API base URL",
"config file path",
"every component",
"conch template unpack registry.example.com/conch/template:latest",
} {
if !strings.Contains(got, want) {
t.Fatalf("unpack help output missing %q:\n%s", want, got)
}
}
}
func TestResolveConchAPIURLUsesOverrideAndAlias(t *testing.T) {
if got := cmd.ResolveConchAPIURL("http://explicit", "http://alias"); got != "http://explicit" {
t.Fatalf("api url = %q, want explicit", got)
}
if got := cmd.ResolveConchAPIURL("", "http://alias"); got != "http://alias" {
t.Fatalf("api url alias = %q, want alias", got)
}
}
func TestPrintTemplateCreateHelpIncludesUsageAndInputs(t *testing.T) {
var buf bytes.Buffer
cmd.PrintTemplateCreateHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch template create --name <name> --source <image> --kernel <path> --initrd <path> [options]",
"--name string",
"--source string",
"--kernel string",
"--initrd string",
"--user string",
"--username string",
} {
if !strings.Contains(got, want) {
t.Fatalf("template create help output missing %q:\n%s", want, got)
}
}
}
func TestRunTemplateCreateUsesTemplateCreateAPI(t *testing.T) {
dir := t.TempDir()
kernelPath := dir + "/vmlinuz"
initrdPath := dir + "/conch.initrd"
cfgPath := dir + "/config.yaml"
if err := os.WriteFile(kernelPath, []byte("kernel-content"), 0o644); err != nil {
t.Fatalf("write kernel: %v", err)
}
if err := os.WriteFile(initrdPath, []byte("initrd-content"), 0o644); err != nil {
t.Fatalf("write initrd: %v", err)
}
if err := os.WriteFile(cfgPath, []byte("{}\n"), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
var metadata client.TemplateCreateMetadata
var kernelBody string
var initrdBody string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/template/create" {
t.Fatalf("path = %q, want /api/template/create", r.URL.Path)
}
if err := r.ParseMultipartForm(4096); err != nil {
t.Fatalf("ParseMultipartForm: %v", err)
}
if err := json.Unmarshal([]byte(r.FormValue("metadata")), &metadata); err != nil {
t.Fatalf("decode metadata: %v", err)
}
file, _, err := r.FormFile("kernel")
if err != nil {
t.Fatalf("kernel FormFile: %v", err)
}
raw, err := io.ReadAll(file)
_ = file.Close()
if err != nil {
t.Fatalf("ReadAll kernel: %v", err)
}
kernelBody = string(raw)
file, _, err = r.FormFile("initrd")
if err != nil {
t.Fatalf("initrd FormFile: %v", err)
}
raw, err = io.ReadAll(file)
_ = file.Close()
if err != nil {
t.Fatalf("ReadAll initrd: %v", err)
}
initrdBody = string(raw)
_ = json.NewEncoder(w).Encode(client.TemplateCreateResponse{
Status: "ok",
TemplateName: "registry.example/conch/test:latest",
TemplateID: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
})
}))
defer server.Close()
err := cmd.RunTemplateCreate(context.Background(), []string{
"--config", cfgPath,
"--api-url", server.URL,
"--name", "registry.example/conch/test:latest",
"--source", "public.ecr.aws/docker/library/busybox:latest",
"--kernel", kernelPath,
"--initrd", initrdPath,
})
if err != nil {
t.Fatalf("runTemplateCreate: %v", err)
}
if metadata.Name != "registry.example/conch/test:latest" || metadata.Source != "public.ecr.aws/docker/library/busybox:latest" {
t.Fatalf("metadata = %#v", metadata)
}
if kernelBody != "kernel-content" || initrdBody != "initrd-content" {
t.Fatalf("uploaded bodies kernel=%q initrd=%q", kernelBody, initrdBody)
}
}
func TestPrintSnapshotHelpUsesDebugCommand(t *testing.T) {
var buf bytes.Buffer
cmd.PrintSnapshotHelp(&buf)
got := buf.String()
for _, want := range []string{
"conch debug snapshot ls [options]",
"conch debug snapshot rm [options] <snapshot-key>",
"ls List EROFS snapshots",
"rm Remove one EROFS snapshot",
} {
if !strings.Contains(got, want) {
t.Fatalf("snapshot help output missing %q:\n%s", want, got)
}
}
for _, removed := range []string{"conch snapshot ls", "conch snapshots ls"} {
if strings.Contains(got, removed) {
t.Fatalf("snapshot help output still contains removed top-level command %q:\n%s", removed, got)
}
}
}