forked from molecule-ai/molecule-core
Merge pull request #227 from Molecule-AI/test/issue-217-plugin-pipeline-tests
test(handlers): unit test suite for plugins_install_pipeline.go
This commit is contained in:
commit
2616f2e4a1
574
platform/internal/handlers/plugins_install_pipeline_test.go
Normal file
574
platform/internal/handlers/plugins_install_pipeline_test.go
Normal file
@ -0,0 +1,574 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/plugins"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ==================== validatePluginName ====================
|
||||
|
||||
func TestValidatePluginName_Valid(t *testing.T) {
|
||||
valid := []string{
|
||||
"my-plugin",
|
||||
"plugin_name",
|
||||
"MyPlugin",
|
||||
"123abc",
|
||||
"a",
|
||||
"superpowers",
|
||||
"molecule-dev",
|
||||
}
|
||||
for _, name := range valid {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := validatePluginName(name); err != nil {
|
||||
t.Errorf("validatePluginName(%q) returned unexpected error: %v", name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_Empty(t *testing.T) {
|
||||
if err := validatePluginName(""); err == nil {
|
||||
t.Error("expected error for empty plugin name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_ForwardSlash(t *testing.T) {
|
||||
if err := validatePluginName("foo/bar"); err == nil {
|
||||
t.Error("expected error for plugin name containing '/'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_Backslash(t *testing.T) {
|
||||
if err := validatePluginName(`foo\bar`); err == nil {
|
||||
t.Error("expected error for plugin name containing '\\'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_DotDot(t *testing.T) {
|
||||
if err := validatePluginName(".."); err == nil {
|
||||
t.Error("expected error for '..'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_DotDotEmbedded(t *testing.T) {
|
||||
if err := validatePluginName("foo..bar"); err == nil {
|
||||
t.Error("expected error for name containing '..'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginName_PathTraversalCases(t *testing.T) {
|
||||
cases := []string{
|
||||
"../etc",
|
||||
"foo/../bar",
|
||||
"../../secrets",
|
||||
}
|
||||
for _, name := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := validatePluginName(name); err == nil {
|
||||
t.Errorf("validatePluginName(%q): expected error for path traversal", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== dirSize ====================
|
||||
|
||||
func TestDirSize_EmptyDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
size, err := dirSize(dir, 1000)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on empty dir: %v", err)
|
||||
}
|
||||
if size != 0 {
|
||||
t.Errorf("expected size 0 for empty dir, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSize_SingleFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := []byte("hello world") // 11 bytes
|
||||
if err := os.WriteFile(filepath.Join(dir, "file.txt"), content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
size, err := dirSize(dir, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if size != int64(len(content)) {
|
||||
t.Errorf("expected size %d, got %d", len(content), size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSize_MultipleFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
files := map[string][]byte{
|
||||
"a.txt": []byte("hello"), // 5
|
||||
"b.txt": []byte("world!"), // 6
|
||||
}
|
||||
for name, data := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
size, err := dirSize(dir, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if size != 11 {
|
||||
t.Errorf("expected size 11, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSize_Subdirectories(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sub := filepath.Join(dir, "subdir")
|
||||
if err := os.MkdirAll(sub, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sub, "nested.txt"), []byte("nested"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
size, err := dirSize(dir, 1000)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if size != 6 {
|
||||
t.Errorf("expected size 6, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSize_ExceedsLimit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Write a 100-byte file, set limit to 50.
|
||||
if err := os.WriteFile(filepath.Join(dir, "big.bin"), make([]byte, 100), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := dirSize(dir, 50)
|
||||
if err == nil {
|
||||
t.Error("expected error when dir size exceeds limit")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cap") {
|
||||
t.Errorf("expected error to mention cap, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSize_ExactlyAtLimit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// A 10-byte file with limit=10 should succeed (not exceed).
|
||||
if err := os.WriteFile(filepath.Join(dir, "exact.bin"), make([]byte, 10), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
size, err := dirSize(dir, 10)
|
||||
if err != nil {
|
||||
t.Errorf("exactly at limit should not error, got: %v", err)
|
||||
}
|
||||
if size != 10 {
|
||||
t.Errorf("expected size 10, got %d", size)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== httpErr / newHTTPErr ====================
|
||||
|
||||
func TestHTTPErr_Error_ContainsStatus(t *testing.T) {
|
||||
e := newHTTPErr(http.StatusBadRequest, gin.H{"error": "bad input"})
|
||||
msg := e.Error()
|
||||
if !strings.Contains(msg, "400") {
|
||||
t.Errorf("Error() should contain status code 400, got: %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPErr_StatusPreserved(t *testing.T) {
|
||||
cases := []int{
|
||||
http.StatusBadRequest,
|
||||
http.StatusNotFound,
|
||||
http.StatusBadGateway,
|
||||
http.StatusGatewayTimeout,
|
||||
http.StatusRequestEntityTooLarge,
|
||||
http.StatusInternalServerError,
|
||||
}
|
||||
for _, code := range cases {
|
||||
e := newHTTPErr(code, gin.H{"error": "test"})
|
||||
if e.Status != code {
|
||||
t.Errorf("newHTTPErr(%d): Status = %d, want %d", code, e.Status, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPErr_ErrorsAs_Unwraps(t *testing.T) {
|
||||
original := newHTTPErr(http.StatusBadGateway, gin.H{"error": "upstream"})
|
||||
wrapped := fmt.Errorf("outer: %w", original)
|
||||
var he *httpErr
|
||||
if !errors.As(wrapped, &he) {
|
||||
t.Fatal("errors.As should unwrap *httpErr through fmt.Errorf %w")
|
||||
}
|
||||
if he.Status != http.StatusBadGateway {
|
||||
t.Errorf("expected 502, got %d", he.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== regexpEscapeForAwk ====================
|
||||
|
||||
func TestRegexpEscapeForAwk_PlainName(t *testing.T) {
|
||||
// Alphanumeric + hyphen + underscore should be returned unchanged.
|
||||
input := "my-plugin_123"
|
||||
got := regexpEscapeForAwk(input)
|
||||
if got != input {
|
||||
t.Errorf("regexpEscapeForAwk(%q) = %q, want unchanged", input, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpEscapeForAwk_Slash(t *testing.T) {
|
||||
// Slash is the awk regex delimiter and MUST be escaped.
|
||||
got := regexpEscapeForAwk("a/b")
|
||||
if got != `a\/b` {
|
||||
t.Errorf("regexpEscapeForAwk(%q) = %q, want %q", "a/b", got, `a\/b`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpEscapeForAwk_Dot(t *testing.T) {
|
||||
got := regexpEscapeForAwk("a.b")
|
||||
if got != `a\.b` {
|
||||
t.Errorf("regexpEscapeForAwk(%q) = %q, want %q", "a.b", got, `a\.b`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpEscapeForAwk_Plus(t *testing.T) {
|
||||
got := regexpEscapeForAwk("a+b")
|
||||
if got != `a\+b` {
|
||||
t.Errorf("regexpEscapeForAwk(%q) = %q, want %q", "a+b", got, `a\+b`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpEscapeForAwk_FullMarkerString(t *testing.T) {
|
||||
// The actual marker used in stripPluginMarkersFromMemory.
|
||||
// "# Plugin: my-plugin /" must have "/" escaped but " " unescaped.
|
||||
marker := "# Plugin: my-plugin /"
|
||||
got := regexpEscapeForAwk(marker)
|
||||
if !strings.Contains(got, `\/`) {
|
||||
t.Errorf("expected escaped slash in output for %q, got %q", marker, got)
|
||||
}
|
||||
if strings.Contains(got, `\ `) {
|
||||
t.Errorf("space should NOT be escaped in %q", marker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpEscapeForAwk_NoDoubleEscape(t *testing.T) {
|
||||
// A backslash in the input should itself be escaped.
|
||||
got := regexpEscapeForAwk(`a\b`)
|
||||
if !strings.HasPrefix(got, `a\\`) {
|
||||
t.Errorf("backslash should be escaped, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== streamDirAsTar ====================
|
||||
|
||||
func TestStreamDirAsTar_EmptyDir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
if err := streamDirAsTar(root, tw); err != nil {
|
||||
t.Fatalf("unexpected error on empty dir: %v", err)
|
||||
}
|
||||
tw.Close()
|
||||
|
||||
tr := tar.NewReader(&buf)
|
||||
count := 0
|
||||
for {
|
||||
if _, err := tr.Next(); err != nil {
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 tar entries for empty dir, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamDirAsTar_SingleFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
content := []byte("plugin manifest content")
|
||||
if err := os.WriteFile(filepath.Join(root, "plugin.yaml"), content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
if err := streamDirAsTar(root, tw); err != nil {
|
||||
t.Fatalf("streamDirAsTar failed: %v", err)
|
||||
}
|
||||
tw.Close()
|
||||
|
||||
entries := tarEntries(t, &buf)
|
||||
if _, ok := entries["plugin.yaml"]; !ok {
|
||||
t.Error("tar should contain plugin.yaml")
|
||||
}
|
||||
if string(entries["plugin.yaml"]) != string(content) {
|
||||
t.Errorf("plugin.yaml content mismatch: got %q", entries["plugin.yaml"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamDirAsTar_NestedDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "rules"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "rules", "main.md"), []byte("# Rule"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "plugin.yaml"), []byte("name: test\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
if err := streamDirAsTar(root, tw); err != nil {
|
||||
t.Fatalf("streamDirAsTar failed: %v", err)
|
||||
}
|
||||
tw.Close()
|
||||
|
||||
entries := tarEntries(t, &buf)
|
||||
if _, ok := entries["plugin.yaml"]; !ok {
|
||||
t.Error("tar should contain plugin.yaml")
|
||||
}
|
||||
// Nested paths must use forward slashes regardless of OS.
|
||||
if _, ok := entries["rules/main.md"]; !ok {
|
||||
t.Errorf("tar should contain rules/main.md; got entries: %v", entryKeys(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamDirAsTar_PathsAreRelative(t *testing.T) {
|
||||
// Entries must be relative paths — no leading slash, no tempdir prefix.
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "file.txt"), []byte("x"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
if err := streamDirAsTar(root, tw); err != nil {
|
||||
t.Fatalf("streamDirAsTar failed: %v", err)
|
||||
}
|
||||
tw.Close()
|
||||
|
||||
tr := tar.NewReader(&buf)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(hdr.Name, "/") {
|
||||
t.Errorf("tar entry %q has absolute path", hdr.Name)
|
||||
}
|
||||
if strings.Contains(hdr.Name, "tmp") || strings.Contains(hdr.Name, "var") {
|
||||
t.Errorf("tar entry %q leaks tempdir path", hdr.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== resolveAndStage (with stub resolver) ====================
|
||||
|
||||
// stubResolver is a minimal SourceResolver for testing resolveAndStage
|
||||
// without requiring a real Docker client or live plugin registry.
|
||||
type stubResolver struct {
|
||||
scheme string
|
||||
name string // plugin name returned from Fetch
|
||||
content string // file content written into dst
|
||||
fetchErr error // non-nil causes Fetch to return this error
|
||||
}
|
||||
|
||||
func (s *stubResolver) Scheme() string { return s.scheme }
|
||||
func (s *stubResolver) Fetch(_ context.Context, _ string, dst string) (string, error) {
|
||||
if s.fetchErr != nil {
|
||||
return "", s.fetchErr
|
||||
}
|
||||
// Write a minimal file so dirSize has something to measure.
|
||||
if err := os.WriteFile(filepath.Join(dst, "plugin.yaml"), []byte(s.content), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.name, nil
|
||||
}
|
||||
|
||||
func TestResolveAndStage_EmptySource(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil)
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: ""})
|
||||
assertHTTPErrStatus(t, err, http.StatusBadRequest, "empty source")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_UnknownScheme(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil)
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "nosuchthing://plugin"})
|
||||
assertHTTPErrStatus(t, err, http.StatusBadRequest, "unknown scheme")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_HappyPath(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
name: "my-plugin",
|
||||
content: "name: my-plugin\nversion: 1.0.0\n",
|
||||
})
|
||||
|
||||
result, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://my-plugin"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on happy path: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.StagedDir)
|
||||
|
||||
if result.PluginName != "my-plugin" {
|
||||
t.Errorf("expected PluginName 'my-plugin', got %q", result.PluginName)
|
||||
}
|
||||
if result.Source.Scheme != "stub" {
|
||||
t.Errorf("expected Source.Scheme 'stub', got %q", result.Source.Scheme)
|
||||
}
|
||||
// The staged directory must exist and contain the file.
|
||||
if _, err := os.Stat(filepath.Join(result.StagedDir, "plugin.yaml")); os.IsNotExist(err) {
|
||||
t.Error("staged dir should contain plugin.yaml after successful fetch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAndStage_StagedDirCleanedOnFetchError(t *testing.T) {
|
||||
// resolveAndStage must remove the staging tempdir if Fetch fails.
|
||||
// We verify this by capturing the stagedDir path from the error path;
|
||||
// since we can't inspect it directly, we verify that no extra tempdirs
|
||||
// are left behind after the function returns.
|
||||
beforeCount := tempDirCount(t)
|
||||
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
fetchErr: errors.New("simulated fetch failure"),
|
||||
})
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://plugin"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from fetch failure")
|
||||
}
|
||||
|
||||
afterCount := tempDirCount(t)
|
||||
if afterCount > beforeCount {
|
||||
t.Errorf("resolveAndStage left %d orphaned tempdir(s) after fetch error", afterCount-beforeCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAndStage_FetchReturnsErrPluginNotFound(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
fetchErr: plugins.ErrPluginNotFound,
|
||||
})
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://missing"})
|
||||
assertHTTPErrStatus(t, err, http.StatusNotFound, "ErrPluginNotFound")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_FetchReturnsDeadlineExceeded(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
fetchErr: context.DeadlineExceeded,
|
||||
})
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://slow"})
|
||||
assertHTTPErrStatus(t, err, http.StatusGatewayTimeout, "DeadlineExceeded")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_FetchReturnsGenericError(t *testing.T) {
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
fetchErr: errors.New("connection refused"),
|
||||
})
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://anything"})
|
||||
assertHTTPErrStatus(t, err, http.StatusBadGateway, "generic fetch error")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_ResolverReturnsInvalidName(t *testing.T) {
|
||||
// A resolver returning a name with path traversal must yield 400.
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil).WithSourceResolver(&stubResolver{
|
||||
scheme: "stub",
|
||||
name: "foo/bar", // invalid: contains slash
|
||||
content: "name: test\n",
|
||||
})
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "stub://anything"})
|
||||
assertHTTPErrStatus(t, err, http.StatusBadRequest, "invalid name from resolver")
|
||||
}
|
||||
|
||||
func TestResolveAndStage_LocalSchemePathTraversal(t *testing.T) {
|
||||
// "local://../../etc/passwd" must be rejected before Fetch is called,
|
||||
// preventing path-traversal on the platform's plugin registry directory.
|
||||
h := NewPluginsHandler(t.TempDir(), nil, nil)
|
||||
_, err := h.resolveAndStage(context.Background(), installRequest{Source: "local://../../etc/passwd"})
|
||||
assertHTTPErrStatus(t, err, http.StatusBadRequest, "local path traversal")
|
||||
}
|
||||
|
||||
// ==================== helpers ====================
|
||||
|
||||
// assertHTTPErrStatus is a test helper that checks err is a *httpErr with
|
||||
// the expected status code. Fails with a clear message if either condition
|
||||
// is not met.
|
||||
func assertHTTPErrStatus(t *testing.T, err error, want int, label string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatalf("[%s] expected *httpErr with status %d, got nil error", label, want)
|
||||
}
|
||||
var he *httpErr
|
||||
if !errors.As(err, &he) {
|
||||
t.Fatalf("[%s] expected *httpErr, got %T: %v", label, err, err)
|
||||
}
|
||||
if he.Status != want {
|
||||
t.Errorf("[%s] expected status %d, got %d", label, want, he.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// tarEntries reads all entries from a tar.Reader backed by buf and returns
|
||||
// a map of entry name → string content. The caller must have already
|
||||
// written and closed the tar.Writer before calling this.
|
||||
func tarEntries(t *testing.T, buf *bytes.Buffer) map[string]string {
|
||||
t.Helper()
|
||||
tr := tar.NewReader(bytes.NewReader(buf.Bytes()))
|
||||
entries := make(map[string]string)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if _, err := io.Copy(&b, tr); err != nil {
|
||||
t.Fatalf("failed to read tar entry %s: %v", hdr.Name, err)
|
||||
}
|
||||
// Normalize OS path separators so tests pass on Windows too.
|
||||
entries[filepath.ToSlash(hdr.Name)] = b.String()
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// entryKeys returns the sorted list of keys from a map[string]string for
|
||||
// inclusion in error messages.
|
||||
func entryKeys(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// tempDirCount counts the number of entries in os.TempDir() that look like
|
||||
// molecule-plugin-fetch-* staging dirs. Used to verify cleanup on error.
|
||||
func tempDirCount(t *testing.T) int {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(os.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read tempdir: %v", err)
|
||||
}
|
||||
count := 0
|
||||
for _, e := range entries {
|
||||
if e.IsDir() && strings.HasPrefix(e.Name(), "molecule-plugin-fetch-") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user