package publish import ( "bytes" "context" "errors" "io" "os" "path/filepath" "reflect" "strings" "testing" ) func TestPkgAndSitePublishReplaceDestination(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "dist", "index.html"), []byte("new"), 0o644); err != nil { t.Fatal(err) } for _, adapter := range []string{"pkg", "site"} { destinationRoot := filepath.Join(root, adapter) destination := filepath.Join(destinationRoot, "project") if err := os.MkdirAll(destination, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(destination, "old"), []byte("old"), 0o644); err != nil { t.Fatal(err) } service := Service{PkgRoot: destinationRoot, SiteRoot: destinationRoot} err := service.Publish(context.Background(), Request{Adapter: adapter, From: "dist", To: "project", Workspace: workspace}, &bytes.Buffer{}) if err != nil { t.Fatal(err) } data, err := os.ReadFile(filepath.Join(destination, "index.html")) if err != nil || string(data) != "new" { t.Fatalf("%s data=%q err=%v", adapter, data, err) } if _, err := os.Stat(filepath.Join(destination, "old")); !os.IsNotExist(err) { t.Fatalf("%s old file remains: %v", adapter, err) } } } func TestPublishReplacementFailureLeavesPreviousTargetVisible(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "dist", "new"), []byte("new"), 0o644); err != nil { t.Fatal(err) } targetRoot := filepath.Join(root, "site") target := filepath.Join(targetRoot, "project") if err := os.MkdirAll(target, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(target, "old"), []byte("old"), 0o644); err != nil { t.Fatal(err) } boom := errors.New("exchange unavailable") oldExchange := renameExchange renameExchange = func(string, string) error { return boom } defer func() { renameExchange = oldExchange }() err := (Service{SiteRoot: targetRoot}).Publish(context.Background(), Request{Adapter: "site", From: "dist", To: "project", Workspace: workspace}, &bytes.Buffer{}) if !errors.Is(err, boom) { t.Fatalf("err=%v", err) } data, err := os.ReadFile(filepath.Join(target, "old")) if err != nil || string(data) != "old" { t.Fatalf("previous target missing or changed: %q %v", data, err) } if _, err := os.Stat(filepath.Join(target, "new")); !os.IsNotExist(err) { t.Fatalf("partial new target visible: %v", err) } } func TestPublishFirstCreateRenameFailureLeavesNoTarget(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "dist", "new"), []byte("new"), 0o644); err != nil { t.Fatal(err) } boom := errors.New("rename failed") oldRename := renamePublish renamePublish = func(string, string) error { return boom } defer func() { renamePublish = oldRename }() targetRoot := filepath.Join(root, "site") err := (Service{SiteRoot: targetRoot}).Publish(context.Background(), Request{Adapter: "site", From: "dist", To: "project", Workspace: workspace}, &bytes.Buffer{}) if !errors.Is(err, boom) { t.Fatalf("err=%v", err) } if _, err := os.Stat(filepath.Join(targetRoot, "project")); !os.IsNotExist(err) { t.Fatalf("partial first target visible: %v", err) } } func TestPublishContextCancellationLeavesDestinationUntouched(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "dist", "new"), []byte("new"), 0o644); err != nil { t.Fatal(err) } destinationRoot := filepath.Join(root, "site") destination := filepath.Join(destinationRoot, "project") if err := os.MkdirAll(destination, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(destination, "old"), []byte("old"), 0o644); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) cancel() err := (Service{SiteRoot: destinationRoot}).Publish(ctx, Request{Adapter: "site", From: "dist", To: "project", Workspace: workspace}, &bytes.Buffer{}) if err != context.Canceled { t.Fatalf("err=%v", err) } data, err := os.ReadFile(filepath.Join(destination, "old")) if err != nil || string(data) != "old" { t.Fatalf("destination=%q err=%v", data, err) } } func TestRegistryPublishStagesArchiveAndUsesSkopeo(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "image.oci"), []byte("archive"), 0o600); err != nil { t.Fatal(err) } var gotArgs, gotEnv []string var staged []byte var output bytes.Buffer service := Service{RegistryAuthFile: "/auth.json", Run: func(_ context.Context, args, env []string, _ io.Writer) error { gotArgs, gotEnv = args, env var err error staged, err = os.ReadFile(args[5][len("oci-archive:"):]) if err != nil { return err } return os.WriteFile(args[4], []byte("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n"), 0o600) }} err := service.Publish(context.Background(), Request{Adapter: "registry", From: "image.oci", To: "registry.invalid/app:v1", Workspace: workspace}, &output) if err != nil { t.Fatal(err) } if len(gotArgs) != 7 || gotArgs[0] != "copy" || gotArgs[1] != "--authfile" || gotArgs[2] != "/auth.json" || gotArgs[3] != "--digestfile" || !strings.HasPrefix(gotArgs[5], "oci-archive:") || gotArgs[6] != "docker://registry.invalid/app:v1" { t.Fatalf("args=%v", gotArgs) } if string(staged) != "archive" { t.Fatalf("staged=%q", staged) } if !reflect.DeepEqual(gotEnv, os.Environ()) { t.Fatalf("env=%v", gotEnv) } if !strings.Contains(output.String(), "[luci] registry-published destination=registry.invalid/app:v1 digest=sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef reference=registry.invalid/app@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") { t.Fatalf("output=%q", output.String()) } if _, err := os.Stat(gotArgs[5][len("oci-archive:"):]); !os.IsNotExist(err) { t.Fatalf("staged archive remains: %v", err) } } func TestCanonicalRegistryReference(t *testing.T) { actual := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" for _, test := range []struct { destination string want string wantErr bool }{ {"registry.invalid/app", "registry.invalid/app@" + actual, false}, {"localhost:5000/app:v1", "localhost:5000/app@" + actual, false}, {"registry.invalid/team/app:v1", "registry.invalid/team/app@" + actual, false}, {"registry.invalid/app@" + actual, "registry.invalid/app@" + actual, false}, {"registry.invalid/app:v1@" + actual, "registry.invalid/app@" + actual, false}, {"registry.invalid/app@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "", true}, } { got, err := canonicalRegistryReference(test.destination, actual) if (err != nil) != test.wantErr || got != test.want { t.Fatalf("canonicalRegistryReference(%q) = %q, %v; want %q, error=%t", test.destination, got, err, test.want, test.wantErr) } } } func TestRegistryPublishRejectsMissingMalformedOrFailedDigestWithoutEvidence(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "image.oci"), []byte("archive"), 0o600); err != nil { t.Fatal(err) } actual := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" for name, test := range map[string]struct { to string run func([]string) error }{ "failure": {"registry.invalid/app:v1", func([]string) error { return errors.New("publish failed") }}, "missing": {"registry.invalid/app:v1", func([]string) error { return nil }}, "malformed": {"registry.invalid/app:v1", func(args []string) error { return os.WriteFile(args[4], []byte("sha256:nope"), 0o600) }}, "conflicting pinned target": {"registry.invalid/app@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", func(args []string) error { return os.WriteFile(args[4], []byte(actual), 0o600) }}, } { test := test t.Run(name, func(t *testing.T) { var output bytes.Buffer service := Service{RegistryAuthFile: "/auth.json", Run: func(_ context.Context, args, _ []string, _ io.Writer) error { return test.run(args) }} if err := service.Publish(context.Background(), Request{Adapter: "registry", From: "image.oci", To: test.to, Workspace: workspace}, &output); err == nil { t.Fatal("publish succeeded") } if strings.Contains(output.String(), "registry-published") { t.Fatalf("false evidence: %q", output.String()) } }) } } func TestRegistryRejectsUnsafeArchiveAndTarget(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatal(err) } if err := os.Symlink(root, filepath.Join(workspace, "image.oci")); err != nil { t.Fatal(err) } service := Service{RegistryAuthFile: "/auth.json"} for _, request := range []Request{ {Adapter: "registry", From: "missing.oci", To: "registry.invalid/app:latest", Workspace: workspace}, {Adapter: "registry", From: "image.oci", To: "registry.invalid/app:latest", Workspace: workspace}, {Adapter: "registry", From: "missing.oci", To: "--tls-verify=false", Workspace: workspace}, {Adapter: "registry", From: "missing.oci", To: "docker://registry.invalid/app", Workspace: workspace}, } { if err := service.Publish(context.Background(), request, &bytes.Buffer{}); err == nil { t.Fatalf("accepted %#v", request) } } } func TestPublishRejectsMissingConfigBadShapeUnsafePathsAndSymlinks(t *testing.T) { root := t.TempDir() workspace := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(workspace, "dist"), 0o755); err != nil { t.Fatal(err) } service := Service{} cases := []Request{ {Adapter: "registry", Image: "app", To: "target"}, {Adapter: "registry", From: "dist", To: "file:///tmp/target", Workspace: workspace}, {Adapter: "pkg", From: "dist", To: "target", Workspace: workspace}, {Adapter: "site", From: "dist", To: "target", Workspace: workspace}, {Adapter: "unknown", From: "dist", To: "target", Workspace: workspace}, } for _, request := range cases { if err := service.Publish(context.Background(), request, &bytes.Buffer{}); err == nil { t.Fatalf("accepted %#v", request) } } service.SiteRoot = filepath.Join(root, "site") if err := service.Publish(context.Background(), Request{Adapter: "site", From: "../outside", To: "target", Workspace: workspace}, &bytes.Buffer{}); err == nil { t.Fatal("unsafe source accepted") } if err := os.Symlink(root, filepath.Join(workspace, "link")); err != nil { t.Fatal(err) } if err := service.Publish(context.Background(), Request{Adapter: "site", From: "link", To: "target", Workspace: workspace}, &bytes.Buffer{}); err == nil { t.Fatal("source symlink accepted") } if err := os.MkdirAll(service.SiteRoot, 0o755); err != nil { t.Fatal(err) } if err := os.Symlink(root, filepath.Join(service.SiteRoot, "escape")); err != nil { t.Fatal(err) } if err := service.Publish(context.Background(), Request{Adapter: "site", From: "dist", To: "escape/target", Workspace: workspace}, &bytes.Buffer{}); err == nil { t.Fatal("target parent symlink accepted") } if err := os.Symlink(root, filepath.Join(service.SiteRoot, "target")); err != nil { t.Fatal(err) } if err := service.Publish(context.Background(), Request{Adapter: "site", From: "dist", To: "target", Workspace: workspace}, &bytes.Buffer{}); err == nil { t.Fatal("target leaf symlink accepted") } }