Merge pull request #14 from Snider/refactor-add-good-bad-ugly-tests

Add _Good, _Bad, and _Ugly tests
This commit is contained in:
Snider 2025-11-14 10:44:32 +00:00 committed by GitHub
commit 952a287872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1770 additions and 952 deletions

View file

@ -17,122 +17,130 @@ import (
"github.com/spf13/cobra"
)
// allCmd represents the all command
var allCmd = &cobra.Command{
Use: "all [url]",
Short: "Collect all resources from a URL",
Long: `Collect all resources from a URL, dispatching to the appropriate collector based on the URL type.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
url := args[0]
outputFile, _ := cmd.Flags().GetString("output")
format, _ := cmd.Flags().GetString("format")
compression, _ := cmd.Flags().GetString("compression")
var allCmd = NewAllCmd()
owner, err := parseGithubOwner(url)
if err != nil {
return err
}
func NewAllCmd() *cobra.Command {
allCmd := &cobra.Command{
Use: "all [url]",
Short: "Collect all resources from a URL",
Long: `Collect all resources from a URL, dispatching to the appropriate collector based on the URL type.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
url := args[0]
outputFile, _ := cmd.Flags().GetString("output")
format, _ := cmd.Flags().GetString("format")
compression, _ := cmd.Flags().GetString("compression")
repos, err := GithubClient.GetPublicRepos(cmd.Context(), owner)
if err != nil {
return err
}
prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote)
prompter.Start()
defer prompter.Stop()
var progressWriter io.Writer
if prompter.IsInteractive() {
bar := ui.NewProgressBar(len(repos), "Cloning repositories")
progressWriter = ui.NewProgressWriter(bar)
}
cloner := vcs.NewGitCloner()
allDataNodes := datanode.New()
for _, repoURL := range repos {
dn, err := cloner.CloneGitRepository(repoURL, progressWriter)
owner, err := parseGithubOwner(url)
if err != nil {
// Log the error and continue
fmt.Fprintln(cmd.ErrOrStderr(), "Error cloning repository:", err)
continue
return err
}
// This is not an efficient way to merge datanodes, but it's the only way for now
// A better approach would be to add a Merge method to the DataNode
repoName := strings.TrimSuffix(repoURL, ".git")
parts := strings.Split(repoName, "/")
repoName = parts[len(parts)-1]
err = dn.Walk(".", func(path string, de fs.DirEntry, err error) error {
repos, err := GithubClient.GetPublicRepos(cmd.Context(), owner)
if err != nil {
return err
}
prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote)
prompter.Start()
defer prompter.Stop()
var progressWriter io.Writer
if prompter.IsInteractive() {
bar := ui.NewProgressBar(len(repos), "Cloning repositories")
progressWriter = ui.NewProgressWriter(bar)
}
cloner := vcs.NewGitCloner()
allDataNodes := datanode.New()
for _, repoURL := range repos {
dn, err := cloner.CloneGitRepository(repoURL, progressWriter)
if err != nil {
return err
// Log the error and continue
fmt.Fprintln(cmd.ErrOrStderr(), "Error cloning repository:", err)
continue
}
if !de.IsDir() {
err := func() error {
file, err := dn.Open(path)
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
allDataNodes.AddData(repoName+"/"+path, data)
return nil
}()
// This is not an efficient way to merge datanodes, but it's the only way for now
// A better approach would be to add a Merge method to the DataNode
repoName := strings.TrimSuffix(repoURL, ".git")
parts := strings.Split(repoName, "/")
repoName = parts[len(parts)-1]
err = dn.Walk(".", func(path string, de fs.DirEntry, err error) error {
if err != nil {
return err
}
if !de.IsDir() {
err := func() error {
file, err := dn.Open(path)
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
allDataNodes.AddData(repoName+"/"+path, data)
return nil
}()
if err != nil {
return err
}
}
return nil
})
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "Error walking datanode:", err)
continue
}
return nil
})
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "Error walking datanode:", err)
continue
}
}
var data []byte
if format == "matrix" {
matrix, err := matrix.FromDataNode(allDataNodes)
if err != nil {
return fmt.Errorf("error creating matrix: %w", err)
var data []byte
if format == "matrix" {
matrix, err := matrix.FromDataNode(allDataNodes)
if err != nil {
return fmt.Errorf("error creating matrix: %w", err)
}
data, err = matrix.ToTar()
if err != nil {
return fmt.Errorf("error serializing matrix: %w", err)
}
} else {
data, err = allDataNodes.ToTar()
if err != nil {
return fmt.Errorf("error serializing DataNode: %w", err)
}
}
data, err = matrix.ToTar()
compressedData, err := compress.Compress(data, compression)
if err != nil {
return fmt.Errorf("error serializing matrix: %w", err)
return fmt.Errorf("error compressing data: %w", err)
}
} else {
data, err = allDataNodes.ToTar()
err = os.WriteFile(outputFile, compressedData, 0644)
if err != nil {
return fmt.Errorf("error serializing DataNode: %w", err)
return fmt.Errorf("error writing DataNode to file: %w", err)
}
}
compressedData, err := compress.Compress(data, compression)
if err != nil {
return fmt.Errorf("error compressing data: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), "All repositories saved to", outputFile)
err = os.WriteFile(outputFile, compressedData, 0644)
if err != nil {
return fmt.Errorf("error writing DataNode to file: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), "All repositories saved to", outputFile)
return nil
},
}
func init() {
RootCmd.AddCommand(allCmd)
return nil
},
}
allCmd.PersistentFlags().String("output", "all.dat", "Output file for the DataNode")
allCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or matrix)")
allCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)")
return allCmd
}
func GetAllCmd() *cobra.Command {
return allCmd
}
func init() {
RootCmd.AddCommand(GetAllCmd())
}
func parseGithubOwner(u string) (string, error) {

View file

@ -14,6 +14,7 @@ import (
)
func TestAllCmd_Good(t *testing.T) {
// Setup mock HTTP client for GitHub API
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
StatusCode: http.StatusOK,
@ -29,6 +30,7 @@ func TestAllCmd_Good(t *testing.T) {
github.NewAuthenticatedClient = oldNewAuthenticatedClient
}()
// Setup mock Git cloner
mockCloner := &mocks.MockGitCloner{
DN: datanode.New(),
Err: nil,
@ -40,8 +42,9 @@ func TestAllCmd_Good(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(allCmd)
rootCmd.AddCommand(GetAllCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "all", "https://github.com/testuser", "--output", out)
if err != nil {
@ -50,9 +53,16 @@ func TestAllCmd_Good(t *testing.T) {
}
func TestAllCmd_Bad(t *testing.T) {
// Setup mock HTTP client to return an error
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
"https://api.github.com/users/baduser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
},
"https://api.github.com/orgs/baduser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
},
})
@ -65,11 +75,42 @@ func TestAllCmd_Bad(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(allCmd)
rootCmd.AddCommand(GetAllCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "all", "https://github.com/testuser", "--output", out)
_, err := executeCommand(rootCmd, "all", "https://github.com/baduser", "--output", out)
if err == nil {
t.Fatalf("expected an error, but got none")
t.Fatal("expected an error, but got none")
}
}
func TestAllCmd_Ugly(t *testing.T) {
t.Run("User with no repos", func(t *testing.T) {
// Setup mock HTTP client for a user with no repos
mockGithubClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/emptyuser/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[]`)),
},
})
oldNewAuthenticatedClient := github.NewAuthenticatedClient
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
return mockGithubClient
}
defer func() {
github.NewAuthenticatedClient = oldNewAuthenticatedClient
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetAllCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "all", "https://github.com/emptyuser", "--output", out)
if err != nil {
t.Fatalf("all command failed for user with no repos: %v", err)
}
})
}

View file

@ -5,15 +5,19 @@ import (
)
// collectCmd represents the collect command
var collectCmd = &cobra.Command{
Use: "collect",
Short: "Collect a resource from a URI.",
Long: `Collect a resource from a URI and store it in a DataNode.`,
}
var collectCmd = NewCollectCmd()
func init() {
RootCmd.AddCommand(collectCmd)
RootCmd.AddCommand(GetCollectCmd())
}
func NewCollectCmd() *cobra.Command {
return &cobra.Command{
Use: "collect",
Short: "Collect a resource from a URI.",
Long: `Collect a resource from a URI and store it in a DataNode.`,
}
}
func GetCollectCmd() *cobra.Command {
return collectCmd
}

View file

@ -10,6 +10,7 @@ import (
)
func TestCollectGithubRepoCmd_Good(t *testing.T) {
// Setup mock Git cloner
mockCloner := &mocks.MockGitCloner{
DN: datanode.New(),
Err: nil,
@ -21,16 +22,18 @@ func TestCollectGithubRepoCmd_Good(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(collectCmd)
rootCmd.AddCommand(GetCollectCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1.git", "--output", out)
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1", "--output", out)
if err != nil {
t.Fatalf("collect github repo command failed: %v", err)
}
}
func TestCollectGithubRepoCmd_Bad(t *testing.T) {
// Setup mock Git cloner to return an error
mockCloner := &mocks.MockGitCloner{
DN: nil,
Err: fmt.Errorf("git clone error"),
@ -42,11 +45,23 @@ func TestCollectGithubRepoCmd_Bad(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(collectCmd)
rootCmd.AddCommand(GetCollectCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1.git", "--output", out)
_, err := executeCommand(rootCmd, "collect", "github", "repo", "https://github.com/testuser/repo1", "--output", out)
if err == nil {
t.Fatalf("expected an error, but got none")
t.Fatal("expected an error, but got none")
}
}
func TestCollectGithubRepoCmd_Ugly(t *testing.T) {
t.Run("Invalid repo URL", func(t *testing.T) {
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetCollectCmd())
_, err := executeCommand(rootCmd, "collect", "github", "repo", "not-a-github-url")
if err == nil {
t.Fatal("expected an error for invalid repo URL, but got none")
}
})
}

View file

@ -14,77 +14,83 @@ import (
)
// collectWebsiteCmd represents the collect website command
var collectWebsiteCmd = &cobra.Command{
Use: "website [url]",
Short: "Collect a single website",
Long: `Collect a single website and store it in a DataNode.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
websiteURL := args[0]
outputFile, _ := cmd.Flags().GetString("output")
depth, _ := cmd.Flags().GetInt("depth")
format, _ := cmd.Flags().GetString("format")
compression, _ := cmd.Flags().GetString("compression")
prompter := ui.NewNonInteractivePrompter(ui.GetWebsiteQuote)
prompter.Start()
defer prompter.Stop()
var bar *progressbar.ProgressBar
if prompter.IsInteractive() {
bar = ui.NewProgressBar(-1, "Crawling website")
}
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar)
if err != nil {
return fmt.Errorf("error downloading and packaging website: %w", err)
}
var data []byte
if format == "matrix" {
matrix, err := matrix.FromDataNode(dn)
if err != nil {
return fmt.Errorf("error creating matrix: %w", err)
}
data, err = matrix.ToTar()
if err != nil {
return fmt.Errorf("error serializing matrix: %w", err)
}
} else {
data, err = dn.ToTar()
if err != nil {
return fmt.Errorf("error serializing DataNode: %w", err)
}
}
compressedData, err := compress.Compress(data, compression)
if err != nil {
return fmt.Errorf("error compressing data: %w", err)
}
if outputFile == "" {
outputFile = "website." + format
if compression != "none" {
outputFile += "." + compression
}
}
err = os.WriteFile(outputFile, compressedData, 0644)
if err != nil {
return fmt.Errorf("error writing website to file: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), "Website saved to", outputFile)
return nil
},
}
var collectWebsiteCmd = NewCollectWebsiteCmd()
func init() {
collectCmd.AddCommand(collectWebsiteCmd)
GetCollectCmd().AddCommand(GetCollectWebsiteCmd())
}
func GetCollectWebsiteCmd() *cobra.Command {
return collectWebsiteCmd
}
func NewCollectWebsiteCmd() *cobra.Command {
collectWebsiteCmd := &cobra.Command{
Use: "website [url]",
Short: "Collect a single website",
Long: `Collect a single website and store it in a DataNode.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
websiteURL := args[0]
outputFile, _ := cmd.Flags().GetString("output")
depth, _ := cmd.Flags().GetInt("depth")
format, _ := cmd.Flags().GetString("format")
compression, _ := cmd.Flags().GetString("compression")
prompter := ui.NewNonInteractivePrompter(ui.GetWebsiteQuote)
prompter.Start()
defer prompter.Stop()
var bar *progressbar.ProgressBar
if prompter.IsInteractive() {
bar = ui.NewProgressBar(-1, "Crawling website")
}
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar)
if err != nil {
return fmt.Errorf("error downloading and packaging website: %w", err)
}
var data []byte
if format == "matrix" {
matrix, err := matrix.FromDataNode(dn)
if err != nil {
return fmt.Errorf("error creating matrix: %w", err)
}
data, err = matrix.ToTar()
if err != nil {
return fmt.Errorf("error serializing matrix: %w", err)
}
} else {
data, err = dn.ToTar()
if err != nil {
return fmt.Errorf("error serializing DataNode: %w", err)
}
}
compressedData, err := compress.Compress(data, compression)
if err != nil {
return fmt.Errorf("error compressing data: %w", err)
}
if outputFile == "" {
outputFile = "website." + format
if compression != "none" {
outputFile += "." + compression
}
}
err = os.WriteFile(outputFile, compressedData, 0644)
if err != nil {
return fmt.Errorf("error writing website to file: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), "Website saved to", outputFile)
return nil
},
}
collectWebsiteCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
collectWebsiteCmd.PersistentFlags().Int("depth", 2, "Recursion depth for downloading")
collectWebsiteCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or matrix)")
collectWebsiteCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)")
}
func NewCollectWebsiteCmd() *cobra.Command {
return collectWebsiteCmd
}

View file

@ -11,27 +11,8 @@ import (
"github.com/schollz/progressbar/v3"
)
func TestCollectWebsiteCmd_NoArgs(t *testing.T) {
rootCmd := NewRootCmd()
collectCmd := NewCollectCmd()
collectWebsiteCmd := NewCollectWebsiteCmd()
collectCmd.AddCommand(collectWebsiteCmd)
rootCmd.AddCommand(collectCmd)
_, err := executeCommand(rootCmd, "collect", "website")
if err == nil {
t.Fatalf("expected an error, but got none")
}
if !strings.Contains(err.Error(), "accepts 1 arg(s), received 0") {
t.Fatalf("unexpected error message: %v", err)
}
}
func Test_NewCollectWebsiteCmd(t *testing.T) {
if NewCollectWebsiteCmd() == nil {
t.Errorf("NewCollectWebsiteCmd is nil")
}
}
func TestCollectWebsiteCmd_Good(t *testing.T) {
// Mock the website downloader
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
return datanode.New(), nil
@ -41,8 +22,9 @@ func TestCollectWebsiteCmd_Good(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(collectCmd)
rootCmd.AddCommand(GetCollectCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "collect", "website", "https://example.com", "--output", out)
if err != nil {
@ -51,6 +33,7 @@ func TestCollectWebsiteCmd_Good(t *testing.T) {
}
func TestCollectWebsiteCmd_Bad(t *testing.T) {
// Mock the website downloader to return an error
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
return nil, fmt.Errorf("website error")
@ -60,11 +43,26 @@ func TestCollectWebsiteCmd_Bad(t *testing.T) {
}()
rootCmd := NewRootCmd()
rootCmd.AddCommand(collectCmd)
rootCmd.AddCommand(GetCollectCmd())
// Execute command
out := filepath.Join(t.TempDir(), "out")
_, err := executeCommand(rootCmd, "collect", "website", "https://example.com", "--output", out)
if err == nil {
t.Fatalf("expected an error, but got none")
t.Fatal("expected an error, but got none")
}
}
func TestCollectWebsiteCmd_Ugly(t *testing.T) {
t.Run("No arguments", func(t *testing.T) {
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetCollectCmd())
_, err := executeCommand(rootCmd, "collect", "website")
if err == nil {
t.Fatal("expected an error for no arguments, but got none")
}
if !strings.Contains(err.Error(), "accepts 1 arg(s), received 0") {
t.Errorf("unexpected error message: %v", err)
}
})
}

View file

@ -12,54 +12,63 @@ import (
var borgfile string
var output string
var compileCmd = &cobra.Command{
Use: "compile",
Short: "Compile a Borgfile into a Terminal Isolation Matrix.",
RunE: func(cmd *cobra.Command, args []string) error {
content, err := os.ReadFile(borgfile)
if err != nil {
return err
}
var compileCmd = NewCompileCmd()
m, err := matrix.New()
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) == 0 {
continue
func NewCompileCmd() *cobra.Command {
compileCmd := &cobra.Command{
Use: "compile",
Short: "Compile a Borgfile into a Terminal Isolation Matrix.",
RunE: func(cmd *cobra.Command, args []string) error {
content, err := os.ReadFile(borgfile)
if err != nil {
return err
}
switch parts[0] {
case "ADD":
if len(parts) != 3 {
return fmt.Errorf("invalid ADD instruction: %s", line)
}
src := parts[1]
dest := parts[2]
data, err := os.ReadFile(src)
if err != nil {
return err
}
m.RootFS.AddData(dest, data)
default:
return fmt.Errorf("unknown instruction: %s", parts[0])
m, err := matrix.New()
if err != nil {
return err
}
}
tarball, err := m.ToTar()
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
switch parts[0] {
case "ADD":
if len(parts) != 3 {
return fmt.Errorf("invalid ADD instruction: %s", line)
}
src := parts[1]
dest := parts[2]
data, err := os.ReadFile(src)
if err != nil {
return err
}
m.RootFS.AddData(strings.TrimPrefix(dest, "/"), data)
default:
return fmt.Errorf("unknown instruction: %s", parts[0])
}
}
return os.WriteFile(output, tarball, 0644)
},
tarball, err := m.ToTar()
if err != nil {
return err
}
return os.WriteFile(output, tarball, 0644)
},
}
compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.")
compileCmd.Flags().StringVarP(&output, "output", "o", "a.matrix", "Path to the output matrix file.")
return compileCmd
}
func GetCompileCmd() *cobra.Command {
return compileCmd
}
func init() {
RootCmd.AddCommand(compileCmd)
compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.")
compileCmd.Flags().StringVarP(&output, "output", "o", "a.matrix", "Path to the output matrix file.")
RootCmd.AddCommand(GetCompileCmd())
}

View file

@ -29,7 +29,7 @@ func TestCompileCmd_Good(t *testing.T) {
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(compileCmd)
rootCmd.AddCommand(GetCompileCmd())
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err != nil {
t.Fatalf("compile command failed: %v", err)
@ -43,9 +43,7 @@ func TestCompileCmd_Good(t *testing.T) {
defer matrixFile.Close()
tr := tar.NewReader(matrixFile)
foundConfig := false
foundRootFS := false
foundTestFile := false
found := make(map[string]bool)
for {
header, err := tr.Next()
if err == io.EOF {
@ -54,66 +52,79 @@ func TestCompileCmd_Good(t *testing.T) {
if err != nil {
t.Fatalf("failed to read tar header: %v", err)
}
found[header.Name] = true
}
switch header.Name {
case "config.json":
foundConfig = true
case "rootfs/":
foundRootFS = true
case "rootfs/test.txt":
foundTestFile = true
expectedFiles := []string{"config.json", "rootfs/", "rootfs/test.txt"}
for _, f := range expectedFiles {
if !found[f] {
t.Errorf("%s not found in matrix tarball", f)
}
}
if !foundConfig {
t.Error("config.json not found in matrix")
}
if !foundRootFS {
t.Error("rootfs/ not found in matrix")
}
if !foundTestFile {
t.Error("rootfs/test.txt not found in matrix")
}
}
func TestCompileCmd_Bad_InvalidBorgfile(t *testing.T) {
tempDir := t.TempDir()
borgfilePath := filepath.Join(tempDir, "Borgfile")
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
func TestCompileCmd_Bad(t *testing.T) {
t.Run("Invalid Borgfile instruction", func(t *testing.T) {
tempDir := t.TempDir()
borgfilePath := filepath.Join(tempDir, "Borgfile")
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
// Create a dummy Borgfile with an invalid instruction.
borgfileContent := "INVALID_INSTRUCTION"
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
if err != nil {
t.Fatalf("failed to create Borgfile: %v", err)
}
// Create a dummy Borgfile with an invalid instruction.
borgfileContent := "INVALID_INSTRUCTION"
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
if err != nil {
t.Fatalf("failed to create Borgfile: %v", err)
}
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(compileCmd)
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err == nil {
t.Fatal("compile command should have failed but did not")
}
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetCompileCmd())
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err == nil {
t.Fatal("compile command should have failed but did not")
}
})
t.Run("Missing input file", func(t *testing.T) {
tempDir := t.TempDir()
borgfilePath := filepath.Join(tempDir, "Borgfile")
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
// Create a dummy Borgfile that references a non-existent file.
borgfileContent := "ADD /non/existent/file /test.txt"
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
if err != nil {
t.Fatalf("failed to create Borgfile: %v", err)
}
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetCompileCmd())
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err == nil {
t.Fatal("compile command should have failed but did not")
}
})
}
func TestCompileCmd_Bad_MissingInputFile(t *testing.T) {
tempDir := t.TempDir()
borgfilePath := filepath.Join(tempDir, "Borgfile")
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
func TestCompileCmd_Ugly(t *testing.T) {
t.Run("Empty Borgfile", func(t *testing.T) {
tempDir := t.TempDir()
borgfilePath := filepath.Join(tempDir, "Borgfile")
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
// Create a dummy Borgfile that references a non-existent file.
borgfileContent := "ADD /non/existent/file /test.txt"
err := os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
if err != nil {
t.Fatalf("failed to create Borgfile: %v", err)
}
// Create an empty Borgfile.
err := os.WriteFile(borgfilePath, []byte(""), 0644)
if err != nil {
t.Fatalf("failed to create Borgfile: %v", err)
}
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(compileCmd)
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err == nil {
t.Fatal("compile command should have failed but did not")
}
// Run the compile command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetCompileCmd())
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
if err != nil {
t.Fatalf("compile command failed for empty Borgfile: %v", err)
}
})
}

View file

@ -1,25 +0,0 @@
package cmd
import (
"bytes"
"github.com/spf13/cobra"
)
// executeCommand is a helper function to execute a cobra command and return the output.
func executeCommand(root *cobra.Command, args ...string) (string, error) {
_, output, err := executeCommandC(root, args...)
return output, err
}
// executeCommandC is a helper function to execute a cobra command and return the output.
func executeCommandC(root *cobra.Command, args ...string) (*cobra.Command, string, error) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
c, err := root.ExecuteC()
return c, buf.String(), err
}

View file

@ -1,54 +1,84 @@
package cmd
import (
"bytes"
"io"
"log/slog"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestExecute(t *testing.T) {
// executeCommand is a helper function to execute a cobra command and return the output.
func executeCommand(root *cobra.Command, args ...string) (string, error) {
_, output, err := executeCommandC(root, args...)
return output, err
}
// executeCommandC is a helper function to execute a cobra command and return the output.
func executeCommandC(root *cobra.Command, args ...string) (*cobra.Command, string, error) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
c, err := root.ExecuteC()
return c, buf.String(), err
}
func TestExecute_Good(t *testing.T) {
// This is a basic test to ensure the command runs without panicking.
err := Execute(slog.New(slog.NewTextHandler(io.Discard, nil)))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRootCmd_Good(t *testing.T) {
t.Run("No args", func(t *testing.T) {
_, err := executeCommand(RootCmd)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
func Test_NewRootCmd(t *testing.T) {
if NewRootCmd() == nil {
t.Errorf("NewRootCmd is nil")
}
t.Run("Help flag", func(t *testing.T) {
// We need to reset the command's state before each run.
RootCmd.ResetFlags()
RootCmd.ResetCommands()
initAllCommands()
output, err := executeCommand(RootCmd, "--help")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(output, "Usage:") {
t.Errorf("expected help output to contain 'Usage:', but it did not")
}
})
}
func Test_executeCommand(t *testing.T) {
type args struct {
cmd *cobra.Command
args []string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Test with no args",
args: args{
cmd: NewRootCmd(),
args: []string{},
},
want: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := executeCommand(tt.args.cmd, tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("executeCommand() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
func TestRootCmd_Bad(t *testing.T) {
t.Run("Unknown command", func(t *testing.T) {
// We need to reset the command's state before each run.
RootCmd.ResetFlags()
RootCmd.ResetCommands()
initAllCommands()
_, err := executeCommand(RootCmd, "unknown-command")
if err == nil {
t.Fatal("expected an error for an unknown command, but got none")
}
})
}
// initAllCommands re-initializes all commands for testing.
func initAllCommands() {
RootCmd.AddCommand(GetAllCmd())
RootCmd.AddCommand(GetCollectCmd())
RootCmd.AddCommand(GetCompileCmd())
RootCmd.AddCommand(GetRunCmd())
RootCmd.AddCommand(GetServeCmd())
}

View file

@ -9,65 +9,73 @@ import (
"github.com/spf13/cobra"
)
var runCmd = &cobra.Command{
Use: "run [matrix file]",
Short: "Run a Terminal Isolation Matrix.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
matrixFile := args[0]
var runCmd = NewRunCmd()
// Create a temporary directory to unpack the matrix file.
tempDir, err := os.MkdirTemp("", "borg-run-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
func NewRunCmd() *cobra.Command {
return &cobra.Command{
Use: "run [matrix file]",
Short: "Run a Terminal Isolation Matrix.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
matrixFile := args[0]
// Unpack the matrix file.
file, err := os.Open(matrixFile)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
// Create a temporary directory to unpack the matrix file.
tempDir, err := os.MkdirTemp("", "borg-run-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
path := filepath.Join(tempDir, header.Name)
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(path, 0755); err != nil {
// Unpack the matrix file.
file, err := os.Open(matrixFile)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
path := filepath.Join(tempDir, header.Name)
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
continue
}
outFile, err := os.Create(path)
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tr); err != nil {
return err
}
continue
}
outFile, err := os.Create(path)
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tr); err != nil {
return err
}
}
// Run the matrix.
runc := execCommand("runc", "run", "borg-container")
runc.Dir = tempDir
runc.Stdout = os.Stdout
runc.Stderr = os.Stderr
runc.Stdin = os.Stdin
return runc.Run()
},
}
}
// Run the matrix.
runc := execCommand("runc", "run", "borg-container")
runc.Dir = tempDir
runc.Stdout = os.Stdout
runc.Stderr = os.Stderr
runc.Stdin = os.Stdin
return runc.Run()
},
func GetRunCmd() *cobra.Command {
return runCmd
}
func init() {
RootCmd.AddCommand(runCmd)
RootCmd.AddCommand(GetRunCmd())
}

View file

@ -27,6 +27,59 @@ func TestHelperProcess(t *testing.T) {
func TestRunCmd_Good(t *testing.T) {
// Create a dummy matrix file.
matrixPath := createDummyMatrix(t)
// Mock the exec.Command function.
origExecCommand := execCommand
execCommand = helperProcess
t.Cleanup(func() {
execCommand = origExecCommand
})
// Run the run command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetRunCmd())
_, err := executeCommand(rootCmd, "run", matrixPath)
if err != nil {
t.Fatalf("run command failed: %v", err)
}
}
func TestRunCmd_Bad(t *testing.T) {
t.Run("Missing input file", func(t *testing.T) {
// Run the run command with a non-existent file.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetRunCmd())
_, err := executeCommand(rootCmd, "run", "/non/existent/file.matrix")
if err == nil {
t.Fatal("run command should have failed but did not")
}
})
}
func TestRunCmd_Ugly(t *testing.T) {
t.Run("Invalid matrix file", func(t *testing.T) {
// Create an invalid (non-tar) matrix file.
tempDir := t.TempDir()
matrixPath := filepath.Join(tempDir, "invalid.matrix")
err := os.WriteFile(matrixPath, []byte("this is not a tar file"), 0644)
if err != nil {
t.Fatalf("failed to create invalid matrix file: %v", err)
}
// Run the run command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(GetRunCmd())
_, err = executeCommand(rootCmd, "run", matrixPath)
if err == nil {
t.Fatal("run command should have failed but did not")
}
})
}
// createDummyMatrix creates a valid, empty matrix file for testing.
func createDummyMatrix(t *testing.T) string {
t.Helper()
tempDir := t.TempDir()
matrixPath := filepath.Join(tempDir, "test.matrix")
matrixFile, err := os.Create(matrixPath)
@ -36,6 +89,7 @@ func TestRunCmd_Good(t *testing.T) {
defer matrixFile.Close()
tw := tar.NewWriter(matrixFile)
// Add a dummy config.json.
configContent := []byte(matrix.DefaultConfigJSON)
hdr := &tar.Header{
@ -63,29 +117,5 @@ func TestRunCmd_Good(t *testing.T) {
if err := tw.Close(); err != nil {
t.Fatalf("failed to close tar writer: %v", err)
}
// Mock the exec.Command function.
origExecCommand := execCommand
execCommand = helperProcess
t.Cleanup(func() {
execCommand = origExecCommand
})
// Run the run command.
rootCmd := NewRootCmd()
rootCmd.AddCommand(runCmd)
_, err = executeCommand(rootCmd, "run", matrixPath)
if err != nil {
t.Fatalf("run command failed: %v", err)
}
}
func TestRunCmd_Bad_MissingInputFile(t *testing.T) {
// Run the run command with a non-existent file.
rootCmd := NewRootCmd()
rootCmd.AddCommand(runCmd)
_, err := executeCommand(rootCmd, "run", "/non/existent/file.matrix")
if err == nil {
t.Fatal("run command should have failed but did not")
}
return matrixPath
}

View file

@ -14,51 +14,60 @@ import (
)
// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve [file]",
Short: "Serve a packaged PWA file",
Long: `Serves the contents of a packaged PWA file using a static file server.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dataFile := args[0]
port, _ := cmd.Flags().GetString("port")
var serveCmd = NewServeCmd()
rawData, err := os.ReadFile(dataFile)
if err != nil {
return fmt.Errorf("Error reading data file: %w", err)
}
func NewServeCmd() *cobra.Command {
serveCmd := &cobra.Command{
Use: "serve [file]",
Short: "Serve a packaged PWA file",
Long: `Serves the contents of a packaged PWA file using a static file server.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dataFile := args[0]
port, _ := cmd.Flags().GetString("port")
data, err := compress.Decompress(rawData)
if err != nil {
return fmt.Errorf("Error decompressing data: %w", err)
}
var fs http.FileSystem
if strings.HasSuffix(dataFile, ".matrix") {
fs, err = tarfs.New(data)
rawData, err := os.ReadFile(dataFile)
if err != nil {
return fmt.Errorf("Error creating TarFS from matrix tarball: %w", err)
return fmt.Errorf("Error reading data file: %w", err)
}
} else {
dn, err := datanode.FromTar(data)
data, err := compress.Decompress(rawData)
if err != nil {
return fmt.Errorf("Error creating DataNode from tarball: %w", err)
return fmt.Errorf("Error decompressing data: %w", err)
}
fs = http.FS(dn)
}
http.Handle("/", http.FileServer(fs))
var fs http.FileSystem
if strings.HasSuffix(dataFile, ".matrix") {
fs, err = tarfs.New(data)
if err != nil {
return fmt.Errorf("Error creating TarFS from matrix tarball: %w", err)
}
} else {
dn, err := datanode.FromTar(data)
if err != nil {
return fmt.Errorf("Error creating DataNode from tarball: %w", err)
}
fs = http.FS(dn)
}
fmt.Printf("Serving PWA on http://localhost:%s\n", port)
err = http.ListenAndServe(":"+port, nil)
if err != nil {
return fmt.Errorf("Error starting server: %w", err)
}
return nil
},
http.Handle("/", http.FileServer(fs))
fmt.Printf("Serving PWA on http://localhost:%s\n", port)
err = http.ListenAndServe(":"+port, nil)
if err != nil {
return fmt.Errorf("Error starting server: %w", err)
}
return nil
},
}
serveCmd.PersistentFlags().String("port", "8080", "Port to serve the PWA on")
return serveCmd
}
func GetServeCmd() *cobra.Command {
return serveCmd
}
func init() {
RootCmd.AddCommand(serveCmd)
serveCmd.PersistentFlags().String("port", "8080", "Port to serve the PWA on")
RootCmd.AddCommand(GetServeCmd())
}

View file

@ -5,55 +5,115 @@ import (
"testing"
)
func TestCompressDecompress(t *testing.T) {
testData := []byte("hello, world")
// Test gzip compression
compressedGz, err := Compress(testData, "gz")
func TestGzip_Good(t *testing.T) {
originalData := []byte("hello, gzip world")
compressed, err := Compress(originalData, "gz")
if err != nil {
t.Fatalf("gzip compression failed: %v", err)
}
if bytes.Equal(originalData, compressed) {
t.Fatal("gzip compressed data is the same as the original")
}
decompressedGz, err := Decompress(compressedGz)
decompressed, err := Decompress(compressed)
if err != nil {
t.Fatalf("gzip decompression failed: %v", err)
}
if !bytes.Equal(testData, decompressedGz) {
if !bytes.Equal(originalData, decompressed) {
t.Errorf("gzip decompressed data does not match original data")
}
}
// Test xz compression
compressedXz, err := Compress(testData, "xz")
func TestXz_Good(t *testing.T) {
originalData := []byte("hello, xz world")
compressed, err := Compress(originalData, "xz")
if err != nil {
t.Fatalf("xz compression failed: %v", err)
}
if bytes.Equal(originalData, compressed) {
t.Fatal("xz compressed data is the same as the original")
}
decompressedXz, err := Decompress(compressedXz)
decompressed, err := Decompress(compressed)
if err != nil {
t.Fatalf("xz decompression failed: %v", err)
}
if !bytes.Equal(testData, decompressedXz) {
if !bytes.Equal(originalData, decompressed) {
t.Errorf("xz decompressed data does not match original data")
}
}
// Test no compression
compressedNone, err := Compress(testData, "none")
func TestNone_Good(t *testing.T) {
originalData := []byte("hello, none world")
compressed, err := Compress(originalData, "none")
if err != nil {
t.Fatalf("no compression failed: %v", err)
t.Fatalf("'none' compression failed: %v", err)
}
if !bytes.Equal(originalData, compressed) {
t.Errorf("'none' compression should not change data")
}
if !bytes.Equal(testData, compressedNone) {
t.Errorf("no compression data does not match original data")
}
decompressedNone, err := Decompress(compressedNone)
decompressed, err := Decompress(compressed)
if err != nil {
t.Fatalf("no compression decompression failed: %v", err)
t.Fatalf("'none' decompression failed: %v", err)
}
if !bytes.Equal(testData, decompressedNone) {
t.Errorf("no compression decompressed data does not match original data")
if !bytes.Equal(originalData, decompressed) {
t.Errorf("'none' decompressed data does not match original data")
}
}
func TestCompress_Bad(t *testing.T) {
originalData := []byte("test")
// The function should return the original data for an unknown format.
compressed, err := Compress(originalData, "invalid-format")
if err != nil {
t.Fatalf("expected no error for invalid compression format, got %v", err)
}
if !bytes.Equal(originalData, compressed) {
t.Errorf("expected original data for unknown format, got %q", compressed)
}
}
func TestDecompress_Bad(t *testing.T) {
// A truncated gzip stream should cause a decompression error.
originalData := []byte("hello, gzip world")
compressed, _ := Compress(originalData, "gz")
truncated := compressed[:len(compressed)-5] // Corrupt the stream
_, err := Decompress(truncated)
if err == nil {
t.Fatal("expected an error when decompressing a truncated stream, got nil")
}
}
func TestCompress_Ugly(t *testing.T) {
// Test compressing empty data
originalData := []byte{}
compressed, err := Compress(originalData, "gz")
if err != nil {
t.Fatalf("compressing empty data failed: %v", err)
}
decompressed, err := Decompress(compressed)
if err != nil {
t.Fatalf("decompressing empty compressed data failed: %v", err)
}
if !bytes.Equal(originalData, decompressed) {
t.Errorf("expected empty data, got %q", decompressed)
}
}
func TestDecompress_Ugly(t *testing.T) {
// Test decompressing empty byte slice
result, err := Decompress([]byte{})
if err != nil {
t.Fatalf("decompressing an empty slice should not produce an error, got %v", err)
}
if len(result) != 0 {
t.Errorf("expected empty result from decompressing empty slice, got %q", result)
}
}

View file

@ -119,6 +119,11 @@ func (d *DataNode) ReadDir(name string) ([]fs.DirEntry, error) {
name = ""
}
// Disallow reading a file as a directory.
if info, err := d.Stat(name); err == nil && !info.IsDir() {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
}
entries := []fs.DirEntry{}
seen := make(map[string]bool)

View file

@ -1,96 +1,298 @@
package datanode
import (
"errors"
"io/fs"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
)
func TestDataNode(t *testing.T) {
func TestNew_Good(t *testing.T) {
dn := New()
if dn == nil {
t.Fatal("New() returned nil")
}
if dn.files == nil {
t.Error("New() did not initialize the files map")
}
}
func TestAddData_Good(t *testing.T) {
dn := New()
path := "foo.txt"
data := []byte("foo")
dn.AddData(path, data)
file, ok := dn.files[path]
if !ok {
t.Fatalf("file %q not found in datanode", path)
}
if string(file.content) != string(data) {
t.Errorf("expected data %q, got %q", data, file.content)
}
info, err := file.Stat()
if err != nil {
t.Fatalf("file.Stat() failed: %v", err)
}
if info.Name() != "foo.txt" {
t.Errorf("expected name foo.txt, got %s", info.Name())
}
}
func TestAddData_Ugly(t *testing.T) {
t.Run("Overwrite", func(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("foo.txt", []byte("bar"))
file, _ := dn.files["foo.txt"]
if string(file.content) != "bar" {
t.Errorf("expected data to be overwritten to 'bar', got %q", file.content)
}
})
t.Run("Weird Path", func(t *testing.T) {
dn := New()
// path.Clean treats "a/../b/./c.txt" as "b/c.txt" but our implementation is simpler
// and doesn't handle `..`. Let's test what it does handle.
path := "./b/./c.txt"
dn.AddData(path, []byte("c"))
if _, ok := dn.files["./b/./c.txt"]; !ok {
t.Errorf("expected path to be stored as is")
}
})
}
func TestOpen_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("bar/qux.txt", []byte("qux"))
// Test Open
file, err := dn.Open("foo.txt")
if err != nil {
t.Fatalf("Open failed: %v", err)
}
file.Close()
defer file.Close()
}
_, err = dn.Open("nonexistent.txt")
func TestOpen_Bad(t *testing.T) {
dn := New()
_, err := dn.Open("nonexistent.txt")
if err == nil {
t.Fatalf("Expected error opening nonexistent file, got nil")
t.Fatal("expected error opening nonexistent file, got nil")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected fs.ErrNotExist, got %v", err)
}
}
// Test Stat
func TestOpen_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
file, err := dn.Open("bar") // Opening a directory
if err != nil {
t.Fatalf("expected no error when opening a directory, got %v", err)
}
defer file.Close()
// Reading from a directory should fail
_, err = file.Read(make([]byte, 1))
if err == nil {
t.Fatal("expected error reading from a directory, got nil")
}
var pathErr *fs.PathError
if !errors.As(err, &pathErr) || pathErr.Err != fs.ErrInvalid {
t.Errorf("expected fs.ErrInvalid when reading a directory, got %v", err)
}
}
func TestStat_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
// Test file
info, err := dn.Stat("bar/baz.txt")
if err != nil {
t.Fatalf("Stat failed: %v", err)
}
if info.Name() != "baz.txt" {
t.Errorf("Expected name baz.txt, got %s", info.Name())
t.Errorf("expected name baz.txt, got %s", info.Name())
}
if info.Size() != 3 {
t.Errorf("Expected size 3, got %d", info.Size())
t.Errorf("expected size 3, got %d", info.Size())
}
if info.IsDir() {
t.Errorf("Expected baz.txt to not be a directory")
t.Error("expected baz.txt to not be a directory")
}
// Test directory
dirInfo, err := dn.Stat("bar")
if err != nil {
t.Fatalf("Stat directory failed: %v", err)
}
if !dirInfo.IsDir() {
t.Errorf("Expected 'bar' to be a directory")
t.Error("expected 'bar' to be a directory")
}
if dirInfo.Name() != "bar" {
t.Errorf("expected dir name 'bar', got %s", dirInfo.Name())
}
}
func TestStat_Bad(t *testing.T) {
dn := New()
_, err := dn.Stat("nonexistent")
if err == nil {
t.Fatal("expected error stating nonexistent file, got nil")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected fs.ErrNotExist, got %v", err)
}
}
func TestStat_Ugly(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
// Test root
info, err := dn.Stat(".")
if err != nil {
t.Fatalf("Stat('.') failed: %v", err)
}
if !info.IsDir() {
t.Error("expected '.' to be a directory")
}
if info.Name() != "." {
t.Errorf("expected name '.', got %s", info.Name())
}
}
func TestExists_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
// Test Exists
exists, err := dn.Exists("foo.txt")
if err != nil || !exists {
t.Errorf("Expected foo.txt to exist, err: %v", err)
}
exists, err = dn.Exists("bar")
if err != nil || !exists {
t.Errorf("Expected 'bar' directory to exist, err: %v", err)
}
exists, err = dn.Exists("nonexistent")
if err != nil || exists {
t.Errorf("Expected 'nonexistent' to not exist, err: %v", err)
t.Errorf("expected foo.txt to exist, err: %v", err)
}
// Test ReadDir
exists, err = dn.Exists("bar")
if err != nil || !exists {
t.Errorf("expected 'bar' directory to exist, err: %v", err)
}
}
func TestExists_Bad(t *testing.T) {
dn := New()
exists, err := dn.Exists("nonexistent")
if err != nil {
t.Errorf("unexpected error for nonexistent file: %v", err)
}
if exists {
t.Error("expected 'nonexistent' to not exist")
}
}
func TestExists_Ugly(t *testing.T) {
dn := New()
dn.AddData("dummy.txt", []byte("dummy"))
// Test root
exists, err := dn.Exists(".")
if err != nil || !exists {
t.Error("expected root '.' to exist")
}
// Test empty path
exists, err = dn.Exists("")
if err != nil {
// our stat treats "" as "."
if !strings.Contains(err.Error(), "exists") {
t.Errorf("unexpected error for empty path: %v", err)
}
}
if !exists {
t.Error("expected empty path '' to exist (as root)")
}
}
func TestReadDir_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("bar/qux.txt", []byte("qux"))
// Read root
entries, err := dn.ReadDir(".")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
expectedRootEntries := []string{"bar", "foo.txt"}
if len(entries) != len(expectedRootEntries) {
t.Errorf("Expected %d entries in root, got %d", len(expectedRootEntries), len(entries))
}
var rootEntryNames []string
for _, e := range entries {
rootEntryNames = append(rootEntryNames, e.Name())
}
sort.Strings(rootEntryNames)
if !reflect.DeepEqual(rootEntryNames, expectedRootEntries) {
t.Errorf("Expected entries %v, got %v", expectedRootEntries, rootEntryNames)
entryNames := toSortedNames(entries)
if !reflect.DeepEqual(entryNames, expectedRootEntries) {
t.Errorf("expected entries %v, got %v", expectedRootEntries, entryNames)
}
// Read subdirectory
barEntries, err := dn.ReadDir("bar")
if err != nil {
t.Fatalf("ReadDir('bar') failed: %v", err)
}
expectedBarEntries := []string{"baz.txt", "qux.txt"}
if len(barEntries) != len(expectedBarEntries) {
t.Errorf("Expected %d entries in 'bar', got %d", len(expectedBarEntries), len(barEntries))
barEntryNames := toSortedNames(barEntries)
if !reflect.DeepEqual(barEntryNames, expectedBarEntries) {
t.Errorf("expected entries %v, got %v", expectedBarEntries, barEntryNames)
}
}
func TestReadDir_Bad(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
// Read nonexistent dir
entries, err := dn.ReadDir("nonexistent")
if err != nil {
t.Fatalf("expected no error reading nonexistent dir, got %v", err)
}
if len(entries) != 0 {
t.Errorf("expected 0 entries for nonexistent dir, got %d", len(entries))
}
// Test Walk
// Read file
_, err = dn.ReadDir("foo.txt")
if err == nil {
t.Fatal("expected error reading a file")
}
var pathErr *fs.PathError
if !errors.As(err, &pathErr) || pathErr.Err != fs.ErrInvalid {
t.Errorf("expected fs.ErrInvalid when reading a file, got %v", err)
}
}
func TestReadDir_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("empty_dir/", nil)
// Read dir with another dir but no files
entries, err := dn.ReadDir(".")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
expected := []string{"bar"} // empty_dir/ is ignored by AddData
names := toSortedNames(entries)
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestWalk_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
dn.AddData("bar/baz.txt", []byte("baz"))
dn.AddData("bar/qux.txt", []byte("qux"))
var paths []string
dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
paths = append(paths, path)
@ -101,24 +303,105 @@ func TestDataNode(t *testing.T) {
if !reflect.DeepEqual(paths, expectedPaths) {
t.Errorf("Walk expected paths %v, got %v", expectedPaths, paths)
}
}
// Test CopyFile
tmpfile, err := os.CreateTemp("", "datanode-test-")
if err != nil {
t.Fatalf("CreateTemp failed: %v", err)
func TestWalk_Bad(t *testing.T) {
dn := New()
// Walk non-existent path. fs.WalkDir will call the func with the error.
var called bool
err := dn.Walk("nonexistent", func(path string, d fs.DirEntry, err error) error {
called = true
if err == nil {
t.Error("expected error for nonexistent path")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("unexpected error message: %v", err)
}
return err // propagate error
})
if !called {
t.Fatal("walk function was not called for nonexistent root")
}
defer os.Remove(tmpfile.Name())
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("expected Walk to return fs.ErrNotExist, got %v", err)
}
}
err = dn.CopyFile("foo.txt", tmpfile.Name(), 0644)
func TestWalk_Ugly(t *testing.T) {
dn := New()
dn.AddData("a/b.txt", []byte("b"))
dn.AddData("a/c.txt", []byte("c"))
// Test stopping walk
walkErr := errors.New("stop walking")
var paths []string
err := dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
if path == "a/b.txt" {
return walkErr
}
paths = append(paths, path)
return nil
})
if err != walkErr {
t.Errorf("expected walk to return the callback error, got %v", err)
}
}
func TestCopyFile_Good(t *testing.T) {
dn := New()
dn.AddData("foo.txt", []byte("foo"))
tmpfile := filepath.Join(t.TempDir(), "test.txt")
err := dn.CopyFile("foo.txt", tmpfile, 0644)
if err != nil {
t.Fatalf("CopyFile failed: %v", err)
}
content, err := os.ReadFile(tmpfile.Name())
content, err := os.ReadFile(tmpfile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if string(content) != "foo" {
t.Errorf("Expected foo, got %s", string(content))
t.Errorf("expected foo, got %s", string(content))
}
}
func TestCopyFile_Bad(t *testing.T) {
dn := New()
tmpfile := filepath.Join(t.TempDir(), "test.txt")
// Source does not exist
err := dn.CopyFile("nonexistent.txt", tmpfile, 0644)
if err == nil {
t.Fatal("expected error for nonexistent source file")
}
// Destination is not writable
dn.AddData("foo.txt", []byte("foo"))
err = dn.CopyFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
if err == nil {
t.Fatal("expected error for unwritable destination")
}
}
func TestCopyFile_Ugly(t *testing.T) {
dn := New()
dn.AddData("bar/baz.txt", []byte("baz"))
tmpfile := filepath.Join(t.TempDir(), "test.txt")
// Attempting to copy a directory
err := dn.CopyFile("bar", tmpfile, 0644)
if err == nil {
t.Fatal("expected error when trying to copy a directory")
}
}
func toSortedNames(entries []fs.DirEntry) []string {
var names []string
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
return names
}

View file

@ -47,6 +47,7 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
client := NewAuthenticatedClient(ctx)
var allCloneURLs []string
url := fmt.Sprintf("%s/users/%s/repos", apiURL, userOrOrg)
isFirstRequest := true
for {
if err := ctx.Err(); err != nil {
@ -63,24 +64,19 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
}
if resp.StatusCode != http.StatusOK {
// If it's the first request for a user and it's a 404, we can try the org endpoint.
if isFirstRequest && strings.Contains(url, "/users/") && resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
url = fmt.Sprintf("%s/orgs/%s/repos", apiURL, userOrOrg)
isFirstRequest = false // We are now trying the org endpoint.
continue // Re-run the loop with the org URL.
}
status := resp.Status
resp.Body.Close()
// Try organization endpoint
url = fmt.Sprintf("%s/orgs/%s/repos", apiURL, userOrOrg)
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Borg-Data-Collector")
resp, err = client.Do(req)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("failed to fetch repos: %s", status)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("failed to fetch repos: %s", resp.Status)
}
isFirstRequest = false // Subsequent requests are for pagination.
var repos []Repo
if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
@ -94,9 +90,6 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
}
linkHeader := resp.Header.Get("Link")
if linkHeader == "" {
break
}
nextURL := g.findNextURL(linkHeader)
if nextURL == "" {
break
@ -111,8 +104,15 @@ func (g *githubClient) findNextURL(linkHeader string) string {
links := strings.Split(linkHeader, ",")
for _, link := range links {
parts := strings.Split(link, ";")
if len(parts) == 2 && strings.TrimSpace(parts[1]) == `rel="next"` {
return strings.Trim(strings.TrimSpace(parts[0]), "<>")
if len(parts) < 2 {
continue
}
if strings.TrimSpace(parts[1]) == `rel="next"` {
urlPart := strings.TrimSpace(parts[0])
if strings.HasPrefix(urlPart, "<") && strings.HasSuffix(urlPart, ">") {
return urlPart[1 : len(urlPart)-1]
}
}
}
return ""

View file

@ -5,120 +5,180 @@ import (
"context"
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/Snider/Borg/pkg/mocks"
)
func TestGetPublicRepos(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testuser/repo1.git"}]`)),
},
"https://api.github.com/orgs/testorg/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}, "Link": []string{`<https://api.github.com/organizations/123/repos?page=2>; rel="next"`}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo1.git"}]`)),
},
"https://api.github.com/organizations/123/repos?page=2": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo2.git"}]`)),
},
func TestGetPublicRepos_Good(t *testing.T) {
t.Run("User Repos", func(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testuser/repo1.git"}]`)),
},
})
client := setupMockClient(t, mockClient)
repos, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
if err != nil {
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err)
}
if len(repos) != 1 || repos[0] != "https://github.com/testuser/repo1.git" {
t.Errorf("unexpected user repos: %v", repos)
}
})
client := &githubClient{}
oldClient := NewAuthenticatedClient
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
return mockClient
}
defer func() {
NewAuthenticatedClient = oldClient
}()
// Test user repos
repos, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
if err != nil {
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err)
}
if len(repos) != 1 || repos[0] != "https://github.com/testuser/repo1.git" {
t.Errorf("unexpected user repos: %v", repos)
}
// Test org repos with pagination
repos, err = client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testorg")
if err != nil {
t.Fatalf("getPublicReposWithAPIURL for org failed: %v", err)
}
if len(repos) != 2 || repos[0] != "https://github.com/testorg/repo1.git" || repos[1] != "https://github.com/testorg/repo2.git" {
t.Errorf("unexpected org repos: %v", repos)
}
}
func TestGetPublicRepos_Error(t *testing.T) {
u, _ := url.Parse("https://api.github.com/users/testuser/repos")
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString("")),
Request: &http.Request{Method: "GET", URL: u},
},
"https://api.github.com/orgs/testuser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString("")),
Request: &http.Request{Method: "GET", URL: u},
},
t.Run("Org Repos with Pagination", func(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testorg/repos": {
StatusCode: http.StatusNotFound, // Trigger fallback to org
Status: "404 Not Found",
Body: io.NopCloser(bytes.NewBufferString(`{}`)),
},
"https://api.github.com/orgs/testorg/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}, "Link": []string{`<https://api.github.com/organizations/123/repos?page=2>; rel="next"`}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo1.git"}]`)),
},
"https://api.github.com/organizations/123/repos?page=2": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "https://github.com/testorg/repo2.git"}]`)),
},
})
client := setupMockClient(t, mockClient)
repos, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testorg")
if err != nil {
t.Fatalf("getPublicReposWithAPIURL for org failed: %v", err)
}
if len(repos) != 2 || repos[0] != "https://github.com/testorg/repo1.git" || repos[1] != "https://github.com/testorg/repo2.git" {
t.Errorf("unexpected org repos: %v", repos)
}
})
expectedErr := "failed to fetch repos: 404 Not Found"
client := &githubClient{}
oldClient := NewAuthenticatedClient
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
return mockClient
}
defer func() {
NewAuthenticatedClient = oldClient
}()
// Test user repos
_, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
if err.Error() != expectedErr {
t.Fatalf("getPublicReposWithAPIURL for user failed: %v", err)
}
}
func TestFindNextURL(t *testing.T) {
func TestGetPublicRepos_Bad(t *testing.T) {
t.Run("Not Found", func(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/testuser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
},
"https://api.github.com/orgs/testuser/repos": {
StatusCode: http.StatusNotFound,
Status: "404 Not Found",
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
},
})
client := setupMockClient(t, mockClient)
_, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "testuser")
if err == nil {
t.Fatal("expected an error but got nil")
}
if !strings.Contains(err.Error(), "404 Not Found") {
t.Errorf("expected '404 Not Found' in error message, got %q", err)
}
})
t.Run("Invalid JSON", func(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/badjson/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[{"clone_url": "invalid}`)),
},
})
client := setupMockClient(t, mockClient)
_, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "badjson")
if err == nil {
t.Fatal("expected an error for invalid JSON, but got nil")
}
})
}
func TestGetPublicRepos_Ugly(t *testing.T) {
t.Run("Empty Repo List", func(t *testing.T) {
mockClient := mocks.NewMockClient(map[string]*http.Response{
"https://api.github.com/users/empty/repos": {
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`[]`)),
},
})
client := setupMockClient(t, mockClient)
repos, err := client.getPublicReposWithAPIURL(context.Background(), "https://api.github.com", "empty")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(repos) != 0 {
t.Errorf("expected 0 repos, got %d", len(repos))
}
})
}
func TestFindNextURL_Good(t *testing.T) {
client := &githubClient{}
linkHeader := `<https://api.github.com/organizations/123/repos?page=2>; rel="next", <https://api.github.com/organizations/123/repos?page=1>; rel="prev"`
nextURL := client.findNextURL(linkHeader)
if nextURL != "https://api.github.com/organizations/123/repos?page=2" {
t.Errorf("unexpected next URL: %s", nextURL)
}
}
linkHeader = `<https://api.github.com/organizations/123/repos?page=1>; rel="prev"`
nextURL = client.findNextURL(linkHeader)
func TestFindNextURL_Bad(t *testing.T) {
client := &githubClient{}
linkHeader := `<https://api.github.com/organizations/123/repos?page=1>; rel="prev"`
nextURL := client.findNextURL(linkHeader)
if nextURL != "" {
t.Errorf("unexpected next URL: %s", nextURL)
t.Errorf("unexpected next URL for header with no 'next': %s", nextURL)
}
nextURL = client.findNextURL("")
if nextURL != "" {
t.Errorf("unexpected next URL for empty header: %s", nextURL)
}
}
func TestNewAuthenticatedClient(t *testing.T) {
// Test with no token
func TestFindNextURL_Ugly(t *testing.T) {
client := &githubClient{}
// Malformed: missing angle brackets
linkHeader := `https://api.github.com/organizations/123/repos?page=2; rel="next"`
nextURL := client.findNextURL(linkHeader)
if nextURL != "" {
t.Errorf("unexpected next URL for malformed header: %s", nextURL)
}
}
func TestNewAuthenticatedClient_Good(t *testing.T) {
t.Setenv("GITHUB_TOKEN", "test-token")
client := NewAuthenticatedClient(context.Background())
if client == http.DefaultClient {
t.Error("expected an authenticated client, but got http.DefaultClient")
}
}
func TestNewAuthenticatedClient_Bad(t *testing.T) {
// Unset the variable to ensure it's not present
t.Setenv("GITHUB_TOKEN", "")
client := NewAuthenticatedClient(context.Background())
if client != http.DefaultClient {
t.Errorf("expected http.DefaultClient, but got something else")
}
// Test with token
t.Setenv("GITHUB_TOKEN", "test-token")
client = NewAuthenticatedClient(context.Background())
if client == http.DefaultClient {
t.Errorf("expected an authenticated client, but got http.DefaultClient")
t.Error("expected http.DefaultClient when no token is set, but got something else")
}
}
// setupMockClient is a helper function to inject a mock http.Client.
func setupMockClient(t *testing.T, mock *http.Client) *githubClient {
client := &githubClient{}
originalNewAuthenticatedClient := NewAuthenticatedClient
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
return mock
}
// Restore the original function after the test
t.Cleanup(func() {
NewAuthenticatedClient = originalNewAuthenticatedClient
})
return client
}

View file

@ -4,11 +4,17 @@ import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"io/fs"
"github.com/Snider/Borg/pkg/datanode"
)
var (
ErrDataNodeRequired = errors.New("datanode is required")
ErrConfigIsNil = errors.New("config is nil")
)
// TerminalIsolationMatrix represents a runc bundle.
type TerminalIsolationMatrix struct {
Config []byte
@ -37,6 +43,9 @@ func New() (*TerminalIsolationMatrix, error) {
// FromDataNode creates a new TerminalIsolationMatrix from a DataNode.
func FromDataNode(dn *datanode.DataNode) (*TerminalIsolationMatrix, error) {
if dn == nil {
return nil, ErrDataNodeRequired
}
m, err := New()
if err != nil {
return nil, err
@ -47,6 +56,9 @@ func FromDataNode(dn *datanode.DataNode) (*TerminalIsolationMatrix, error) {
// ToTar serializes the TerminalIsolationMatrix to a tarball.
func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) {
if m.Config == nil {
return nil, ErrConfigIsNil
}
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
@ -76,6 +88,10 @@ func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) {
// Add the rootfs files.
err := m.RootFS.Walk(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
// If the root directory doesn't exist (i.e. empty datanode), it's not an error.
if path == "." && errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}

View file

@ -3,13 +3,15 @@ package matrix
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"io"
"testing"
"github.com/Snider/Borg/pkg/datanode"
)
func TestNew(t *testing.T) {
func TestNew_Good(t *testing.T) {
m, err := New()
if err != nil {
t.Fatalf("New() returned an error: %v", err)
@ -23,9 +25,14 @@ func TestNew(t *testing.T) {
if m.RootFS == nil {
t.Error("New() returned a matrix with a nil RootFS")
}
// Verify the config is valid JSON
if !json.Valid(m.Config) {
t.Error("New() returned a matrix with invalid JSON config")
}
}
func TestFromDataNode(t *testing.T) {
func TestFromDataNode_Good(t *testing.T) {
dn := datanode.New()
dn.AddData("test.txt", []byte("hello world"))
m, err := FromDataNode(dn)
@ -38,9 +45,22 @@ func TestFromDataNode(t *testing.T) {
if m.RootFS != dn {
t.Error("FromDataNode() did not set the RootFS correctly")
}
if m.Config == nil {
t.Error("FromDataNode() did not create a default config")
}
}
func TestToTar(t *testing.T) {
func TestFromDataNode_Bad(t *testing.T) {
_, err := FromDataNode(nil)
if err == nil {
t.Fatal("expected error when passing a nil datanode, but got nil")
}
if !errors.Is(err, ErrDataNodeRequired) {
t.Errorf("expected ErrDataNodeRequired, got %v", err)
}
}
func TestToTar_Good(t *testing.T) {
m, err := New()
if err != nil {
t.Fatalf("New() returned an error: %v", err)
@ -55,35 +75,65 @@ func TestToTar(t *testing.T) {
}
tr := tar.NewReader(bytes.NewReader(tarball))
foundConfig := false
foundRootFS := false
foundTestFile := false
found := make(map[string]bool)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("failed to read tar header: %v", err)
}
found[header.Name] = true
}
expectedFiles := []string{"config.json", "rootfs/", "rootfs/test.txt"}
for _, f := range expectedFiles {
if !found[f] {
t.Errorf("%s not found in matrix tarball", f)
}
}
}
func TestToTar_Ugly(t *testing.T) {
t.Run("Empty RootFS", func(t *testing.T) {
m, _ := New()
tarball, err := m.ToTar()
if err != nil {
t.Fatalf("ToTar() with empty rootfs returned an error: %v", err)
}
tr := tar.NewReader(bytes.NewReader(tarball))
found := make(map[string]bool)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
t.Fatalf("failed to read tar header: %v", err)
if err != nil {
t.Fatalf("failed to read tar header: %v", err)
}
found[header.Name] = true
}
switch header.Name {
case "config.json":
foundConfig = true
case "rootfs/":
foundRootFS = true
case "rootfs/test.txt":
foundTestFile = true
if !found["config.json"] {
t.Error("config.json not found in matrix")
}
}
if !found["rootfs/"] {
t.Error("rootfs/ directory not found in matrix")
}
if len(found) != 2 {
t.Errorf("expected 2 files in tar, but found %d", len(found))
}
})
if !foundConfig {
t.Error("config.json not found in matrix")
}
if !foundRootFS {
t.Error("rootfs/ not found in matrix")
}
if !foundTestFile {
t.Error("rootfs/test.txt not found in matrix")
}
t.Run("Nil Config", func(t *testing.T) {
m, _ := New()
m.Config = nil // This should not happen in practice
_, err := m.ToTar()
if err == nil {
t.Fatal("expected error when Config is nil, but got nil")
}
if !errors.Is(err, ErrConfigIsNil) {
t.Errorf("expected ErrConfigIsNil, got %v", err)
}
})
}

View file

@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"github.com/Snider/Borg/pkg/datanode"
"github.com/schollz/progressbar/v3"
@ -36,6 +37,10 @@ func (p *pwaClient) FindManifest(pwaURL string) (string, error) {
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("failed to fetch PWA page: status code %d", resp.StatusCode)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return "", err
@ -81,6 +86,9 @@ func (p *pwaClient) FindManifest(pwaURL string) (string, error) {
// DownloadAndPackagePWA downloads and packages a PWA into a DataNode.
func (p *pwaClient) DownloadAndPackagePWA(pwaURL, manifestURL string, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
dn := datanode.New()
var wg sync.WaitGroup
var errs []error
var mu sync.Mutex
type Manifest struct {
StartURL string `json:"start_url"`
@ -89,82 +97,98 @@ func (p *pwaClient) DownloadAndPackagePWA(pwaURL, manifestURL string, bar *progr
} `json:"icons"`
}
downloadAndAdd := func(assetURL string) error {
downloadAndAdd := func(assetURL string) {
defer wg.Done()
if bar != nil {
bar.Add(1)
}
resp, err := p.client.Get(assetURL)
if err != nil {
return fmt.Errorf("failed to download %s: %w", assetURL, err)
mu.Lock()
errs = append(errs, fmt.Errorf("failed to download %s: %w", assetURL, err))
mu.Unlock()
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("failed to download %s: status code %d", assetURL, resp.StatusCode)
mu.Lock()
errs = append(errs, fmt.Errorf("failed to download %s: status code %d", assetURL, resp.StatusCode))
mu.Unlock()
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read body of %s: %w", assetURL, err)
mu.Lock()
errs = append(errs, fmt.Errorf("failed to read body of %s: %w", assetURL, err))
mu.Unlock()
return
}
u, err := url.Parse(assetURL)
if err != nil {
return fmt.Errorf("failed to parse asset URL %s: %w", assetURL, err)
mu.Lock()
errs = append(errs, fmt.Errorf("failed to parse asset URL %s: %w", assetURL, err))
mu.Unlock()
return
}
dn.AddData(strings.TrimPrefix(u.Path, "/"), body)
return nil
}
// Download manifest
if err := downloadAndAdd(manifestURL); err != nil {
return nil, err
}
// Parse manifest and download assets
var manifestPath string
u, parseErr := url.Parse(manifestURL)
if parseErr != nil {
manifestPath = "manifest.json"
} else {
manifestPath = strings.TrimPrefix(u.Path, "/")
}
manifestFile, err := dn.Open(manifestPath)
// Download manifest first, synchronously.
resp, err := p.client.Get(manifestURL)
if err != nil {
return nil, fmt.Errorf("failed to open manifest from datanode: %w", err)
return nil, fmt.Errorf("failed to download manifest: %w", err)
}
defer manifestFile.Close()
defer resp.Body.Close()
manifestData, err := io.ReadAll(manifestFile)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("failed to download manifest: status code %d", resp.StatusCode)
}
manifestData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read manifest from datanode: %w", err)
return nil, fmt.Errorf("failed to read manifest body: %w", err)
}
u, _ := url.Parse(manifestURL)
dn.AddData(strings.TrimPrefix(u.Path, "/"), manifestData)
// Parse manifest and download assets concurrently.
var manifest Manifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
// Download start_url
startURL, err := p.resolveURL(manifestURL, manifest.StartURL)
if err != nil {
return nil, fmt.Errorf("failed to resolve start_url: %w", err)
assetsToDownload := []string{}
if manifest.StartURL != "" {
startURL, err := p.resolveURL(manifestURL, manifest.StartURL)
if err == nil {
assetsToDownload = append(assetsToDownload, startURL.String())
}
}
if err := downloadAndAdd(startURL.String()); err != nil {
return nil, err
for _, icon := range manifest.Icons {
if icon.Src != "" {
iconURL, err := p.resolveURL(manifestURL, icon.Src)
if err == nil {
assetsToDownload = append(assetsToDownload, iconURL.String())
}
}
}
// Download icons
for _, icon := range manifest.Icons {
iconURL, err := p.resolveURL(manifestURL, icon.Src)
if err != nil {
// Skip icons with bad URLs
continue
}
if err := downloadAndAdd(iconURL.String()); err != nil {
return nil, err
wg.Add(len(assetsToDownload))
for _, asset := range assetsToDownload {
go downloadAndAdd(asset)
}
wg.Wait()
if len(errs) > 0 {
var errStrings []string
for _, e := range errs {
errStrings = append(errStrings, e.Error())
}
return dn, fmt.Errorf("%s", strings.Join(errStrings, "; "))
}
return dn, nil

View file

@ -1,47 +0,0 @@
package pwa
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/schollz/progressbar/v3"
)
func TestDownloadAndPackagePWA_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/manifest.json" {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"start_url": "index.html"}`))
} else {
http.NotFound(w, r)
}
}))
defer server.Close()
client := newTestPWAClient()
// Test with a server that returns a 404 for the start_url
bar := progressbar.New(1)
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar)
if err == nil {
t.Fatal("Expected an error when the start_url returns a 404, but got nil")
}
// Test with a bad manifest URL
_, err = client.DownloadAndPackagePWA(server.URL, "http://bad.url/manifest.json", bar)
if err == nil {
t.Fatal("Expected an error when the manifest URL is bad, but got nil")
}
// Test with a manifest that is not valid JSON
server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "this is not json")
}))
defer server2.Close()
_, err = client.DownloadAndPackagePWA(server2.URL, server2.URL, bar)
if err == nil {
t.Fatal("Expected an error when the manifest is not valid JSON, but got nil")
}
}

View file

@ -1,92 +1,91 @@
package pwa
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/schollz/progressbar/v3"
)
func newTestPWAClient() PWAClient {
return NewPWAClient()
}
// --- Test Cases for FindManifest ---
func TestFindManifest(t *testing.T) {
func TestFindManifest_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Test PWA</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Hello, PWA!</h1>
</body>
</html>
`))
fmt.Fprint(w, `<html><head><link rel="manifest" href="manifest.json"></head></html>`)
}))
defer server.Close()
client := newTestPWAClient()
client := NewPWAClient()
expectedURL := server.URL + "/manifest.json"
actualURL, err := client.FindManifest(server.URL)
if err != nil {
t.Fatalf("FindManifest failed: %v", err)
}
if actualURL != expectedURL {
t.Errorf("Expected manifest URL %s, but got %s", expectedURL, actualURL)
}
}
func TestDownloadAndPackagePWA(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
func TestFindManifest_Bad(t *testing.T) {
t.Run("No Manifest Link", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Test PWA</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Hello, PWA!</h1>
</body>
</html>
`))
case "/manifest.json":
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{
"name": "Test PWA",
"short_name": "TestPWA",
"start_url": "index.html",
"icons": [
{
"src": "icon.png",
"sizes": "192x192",
"type": "image/png"
}
]
}`))
case "/index.html":
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<h1>Hello, PWA!</h1>`))
case "/icon.png":
w.Header().Set("Content-Type", "image/png")
w.Write([]byte("fake image data"))
default:
http.NotFound(w, r)
fmt.Fprint(w, `<html><head></head></html>`)
}))
defer server.Close()
client := NewPWAClient()
_, err := client.FindManifest(server.URL)
if err == nil {
t.Fatal("expected an error, but got none")
}
}))
})
t.Run("Server Error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}))
defer server.Close()
client := NewPWAClient()
_, err := client.FindManifest(server.URL)
if err == nil {
t.Fatal("expected an error for server error, but got none")
}
})
}
func TestFindManifest_Ugly(t *testing.T) {
t.Run("Multiple Manifest Links", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><head><link rel="manifest" href="first.json"><link rel="manifest" href="second.json"></head></html>`)
}))
defer server.Close()
client := NewPWAClient()
// Should find the first one
expectedURL := server.URL + "/first.json"
actualURL, err := client.FindManifest(server.URL)
if err != nil {
t.Fatalf("FindManifest failed: %v", err)
}
if actualURL != expectedURL {
t.Errorf("Expected manifest URL %s, but got %s", expectedURL, actualURL)
}
})
}
// --- Test Cases for DownloadAndPackagePWA ---
func TestDownloadAndPackagePWA_Good(t *testing.T) {
server := newPWATestServer()
defer server.Close()
client := newTestPWAClient()
bar := progressbar.New(1)
client := NewPWAClient()
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar)
if err != nil {
t.Fatalf("DownloadAndPackagePWA failed: %v", err)
@ -94,18 +93,70 @@ func TestDownloadAndPackagePWA(t *testing.T) {
expectedFiles := []string{"manifest.json", "index.html", "icon.png"}
for _, file := range expectedFiles {
// The path in the datanode is relative to the root of the domain, so we need to remove the leading slash.
exists, err := dn.Exists(file)
if err != nil {
t.Fatalf("Exists failed for %s: %v", file, err)
}
exists, _ := dn.Exists(file)
if !exists {
t.Errorf("Expected to find file %s in DataNode, but it was not found", file)
}
}
}
func TestResolveURL(t *testing.T) {
func TestDownloadAndPackagePWA_Bad(t *testing.T) {
t.Run("Bad Manifest URL", func(t *testing.T) {
server := newPWATestServer()
defer server.Close()
client := NewPWAClient()
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/nonexistent-manifest.json", nil)
if err == nil {
t.Fatal("expected an error for bad manifest url, but got none")
}
})
t.Run("Asset 404", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/manifest.json" {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"start_url": "nonexistent.html"}`)
} else {
http.NotFound(w, r)
}
}))
defer server.Close()
client := NewPWAClient()
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
if err == nil {
t.Fatal("expected an error for asset 404, but got none")
}
// The current implementation aggregates errors.
if !strings.Contains(err.Error(), "status code 404") {
t.Errorf("expected error to contain 'status code 404', but got: %v", err)
}
})
}
func TestDownloadAndPackagePWA_Ugly(t *testing.T) {
t.Run("Manifest with no assets", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{ "name": "Test PWA" }`) // valid json, but no assets
}))
defer server.Close()
client := NewPWAClient()
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
if err != nil {
t.Fatalf("unexpected error for manifest with no assets: %v", err)
}
// Should still contain the manifest itself
exists, _ := dn.Exists("manifest.json")
if !exists {
t.Error("expected manifest.json to be in the datanode")
}
})
}
// --- Test Cases for resolveURL ---
func TestResolveURL_Good(t *testing.T) {
client := NewPWAClient().(*pwaClient)
tests := []struct {
base string
@ -114,10 +165,8 @@ func TestResolveURL(t *testing.T) {
}{
{"http://example.com/", "foo.html", "http://example.com/foo.html"},
{"http://example.com/foo/", "bar.html", "http://example.com/foo/bar.html"},
{"http://example.com/foo", "bar.html", "http://example.com/bar.html"},
{"http://example.com/foo/", "/bar.html", "http://example.com/bar.html"},
{"http://example.com/foo", "/bar.html", "http://example.com/bar.html"},
{"http://example.com/", "http://example.com/foo/bar.html", "http://example.com/foo/bar.html"},
{"http://example.com/", "http://othersite.com/bar.html", "http://othersite.com/bar.html"},
}
for _, tt := range tests {
@ -132,34 +181,38 @@ func TestResolveURL(t *testing.T) {
}
}
func TestPWA_Bad(t *testing.T) {
client := NewPWAClient()
// Test FindManifest with no manifest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Test PWA</title>
</head>
<body>
<h1>Hello, PWA!</h1>
</body>
</html>
`))
}))
defer server.Close()
_, err := client.FindManifest(server.URL)
func TestResolveURL_Bad(t *testing.T) {
client := NewPWAClient().(*pwaClient)
_, err := client.resolveURL("http://^invalid.com", "foo.html")
if err == nil {
t.Fatalf("expected an error, but got none")
}
// Test DownloadAndPackagePWA with bad manifest
_, err = client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
if err == nil {
t.Fatalf("expected an error, but got none")
t.Error("expected error for malformed base URL, but got nil")
}
}
// --- Helpers ---
// newPWATestServer creates a test server for a simple PWA.
func newPWATestServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><head><link rel="manifest" href="manifest.json"></head></html>`)
case "/manifest.json":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{
"name": "Test PWA",
"start_url": "index.html",
"icons": [{"src": "icon.png"}]
}`)
case "/index.html":
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<h1>Hello, PWA!</h1>`)
case "/icon.png":
w.Header().Set("Content-Type", "image/png")
fmt.Fprint(w, "fake image data")
default:
http.NotFound(w, r)
}
}))
}

View file

@ -39,6 +39,9 @@ func (g *gitCloner) CloneGitRepository(repoURL string, progress io.Writer) (*dat
_, err = git.PlainClone(tempPath, false, cloneOptions)
if err != nil {
if err.Error() == "remote repository is empty" {
return datanode.New(), nil
}
return nil, err
}
@ -47,6 +50,10 @@ func (g *gitCloner) CloneGitRepository(repoURL string, progress io.Writer) (*dat
if err != nil {
return err
}
// Skip the .git directory
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
if !info.IsDir() {
content, err := os.ReadFile(path)
if err != nil {

View file

@ -1,81 +1,77 @@
package vcs
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestCloneGitRepository(t *testing.T) {
// Create a temporary directory for the bare repository
// setupTestRepo creates a bare git repository with a single commit.
func setupTestRepo(t *testing.T) (repoPath string) {
t.Helper()
// Create a temporary directory for the bare repository.
bareRepoPath, err := os.MkdirTemp("", "bare-repo-")
if err != nil {
t.Fatalf("Failed to create temp dir for bare repo: %v", err)
}
defer os.RemoveAll(bareRepoPath)
// Initialize a bare git repository
cmd := exec.Command("git", "init", "--bare")
cmd.Dir = bareRepoPath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to init bare repo: %v", err)
}
// Initialize the bare git repository.
runCmd(t, bareRepoPath, "git", "init", "--bare")
// Clone the bare repository to a temporary directory to add a commit
// Clone the bare repository to a temporary directory to add a commit.
clonePath, err := os.MkdirTemp("", "clone-")
if err != nil {
t.Fatalf("Failed to create temp dir for clone: %v", err)
}
defer os.RemoveAll(clonePath)
cmd = exec.Command("git", "clone", bareRepoPath, clonePath)
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to clone bare repo: %v", err)
}
runCmd(t, clonePath, "git", "clone", bareRepoPath, ".")
// Create a file and commit it
// Create a file and commit it.
filePath := filepath.Join(clonePath, "foo.txt")
if err := os.WriteFile(filePath, []byte("foo"), 0644); err != nil {
t.Fatalf("Failed to write file: %v", err)
}
cmd = exec.Command("git", "add", "foo.txt")
cmd.Dir = clonePath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to git add: %v", err)
}
runCmd(t, clonePath, "git", "add", "foo.txt")
runCmd(t, clonePath, "git", "config", "user.email", "test@example.com")
runCmd(t, clonePath, "git", "config", "user.name", "Test User")
runCmd(t, clonePath, "git", "commit", "-m", "Initial commit")
runCmd(t, clonePath, "git", "push", "origin", "master")
cmd = exec.Command("git", "config", "user.email", "test@example.com")
cmd.Dir = clonePath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to set git user.email: %v", err)
}
return bareRepoPath
}
cmd = exec.Command("git", "config", "user.name", "Test User")
cmd.Dir = clonePath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to set git user.name: %v", err)
// runCmd executes a command and fails the test if it fails.
func runCmd(t *testing.T, dir, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
if testing.Verbose() {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Run(); err != nil {
t.Fatalf("Command %q failed: %v", strings.Join(append([]string{name}, args...), " "), err)
}
}
cmd = exec.Command("git", "commit", "-m", "Initial commit")
cmd.Dir = clonePath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to git commit: %v", err)
}
cmd = exec.Command("git", "push", "origin", "master")
cmd.Dir = clonePath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to git push: %v", err)
}
func TestCloneGitRepository_Good(t *testing.T) {
repoPath := setupTestRepo(t)
defer os.RemoveAll(repoPath)
// Clone the repository using the function we're testing
cloner := NewGitCloner()
dn, err := cloner.CloneGitRepository("file://"+bareRepoPath, os.Stdout)
var out bytes.Buffer
dn, err := cloner.CloneGitRepository("file://"+repoPath, &out)
if err != nil {
t.Fatalf("CloneGitRepository failed: %v", err)
t.Fatalf("CloneGitRepository failed: %v\nOutput: %s", err, out.String())
}
// Verify the DataNode contains the correct file
// Verify the DataNode contains the correct file.
exists, err := dn.Exists("foo.txt")
if err != nil {
t.Fatalf("Exists failed: %v", err)
@ -84,3 +80,45 @@ func TestCloneGitRepository(t *testing.T) {
t.Errorf("Expected to find file foo.txt in DataNode, but it was not found")
}
}
func TestCloneGitRepository_Bad(t *testing.T) {
t.Run("Non-existent repository", func(t *testing.T) {
cloner := NewGitCloner()
_, err := cloner.CloneGitRepository("file:///non-existent-repo", io.Discard)
if err == nil {
t.Fatal("Expected an error for a non-existent repository, but got nil")
}
if !strings.Contains(err.Error(), "repository not found") {
t.Errorf("Expected error to be about 'repository not found', but got: %v", err)
}
})
t.Run("Invalid URL", func(t *testing.T) {
cloner := NewGitCloner()
_, err := cloner.CloneGitRepository("not-a-valid-url", io.Discard)
if err == nil {
t.Fatal("Expected an error for an invalid URL, but got nil")
}
})
}
func TestCloneGitRepository_Ugly(t *testing.T) {
t.Run("Empty repository", func(t *testing.T) {
bareRepoPath, err := os.MkdirTemp("", "empty-bare-repo-")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(bareRepoPath)
runCmd(t, bareRepoPath, "git", "init", "--bare")
cloner := NewGitCloner()
dn, err := cloner.CloneGitRepository("file://"+bareRepoPath, io.Discard)
if err != nil {
t.Fatalf("CloneGitRepository failed on empty repo: %v", err)
}
if dn == nil {
t.Fatal("Expected a non-nil datanode for an empty repo")
}
// You might want to check if the datanode is empty, but for now, just checking for no error is enough.
})
}

View file

@ -168,7 +168,11 @@ func (d *Downloader) getRelativePath(pageURL string) string {
if err != nil {
return ""
}
return strings.TrimPrefix(u.Path, "/")
path := strings.TrimPrefix(u.Path, "/")
if path == "" {
return "index.html"
}
return path
}
func (d *Downloader) resolveURL(base, ref string) (string, error) {

View file

@ -1,78 +1,31 @@
package website
import (
"fmt"
"io"
"io/fs"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/schollz/progressbar/v3"
)
func TestDownloadAndPackageWebsite(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Test Website</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Website!</h1>
<a href="/page2.html">Page 2</a>
<img src="image.png">
</body>
</html>
`))
case "/style.css":
w.Header().Set("Content-Type", "text/css")
w.Write([]byte(`body { color: red; }`))
case "/image.png":
w.Header().Set("Content-Type", "image/png")
w.Write([]byte("fake image data"))
case "/page2.html":
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Page 2</title>
</head>
<body>
<h1>Page 2</h1>
<a href="/page3.html">Page 3</a>
</body>
</html>
`))
case "/page3.html":
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<title>Page 3</title>
</head>
<body>
<h1>Page 3</h1>
</body>
</html>
`))
default:
http.NotFound(w, r)
}
}))
// --- Test Cases ---
func TestDownloadAndPackageWebsite_Good(t *testing.T) {
server := newWebsiteTestServer()
defer server.Close()
bar := progressbar.New(1)
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
dn, err := DownloadAndPackageWebsite(server.URL, 2, bar)
if err != nil {
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
}
expectedFiles := []string{"", "style.css", "image.png", "page2.html", "page3.html"}
expectedFiles := []string{"index.html", "style.css", "image.png", "page2.html", "page3.html"}
for _, file := range expectedFiles {
exists, err := dn.Exists(file)
if err != nil {
@ -82,4 +35,172 @@ func TestDownloadAndPackageWebsite(t *testing.T) {
t.Errorf("Expected to find file %s in DataNode, but it was not found", file)
}
}
// Check content of one file
file, err := dn.Open("style.css")
if err != nil {
t.Fatalf("Failed to open style.css: %v", err)
}
content, err := io.ReadAll(file)
if err != nil {
t.Fatalf("Failed to read style.css: %v", err)
}
if string(content) != `body { color: red; }` {
t.Errorf("Unexpected content for style.css: %s", content)
}
}
func TestDownloadAndPackageWebsite_Bad(t *testing.T) {
t.Run("Invalid Start URL", func(t *testing.T) {
_, err := DownloadAndPackageWebsite("http://invalid-url", 1, nil)
if err == nil {
t.Fatal("Expected an error for an invalid start URL, but got nil")
}
})
t.Run("Server Error on Start URL", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}))
defer server.Close()
_, err := DownloadAndPackageWebsite(server.URL, 1, nil)
if err == nil {
t.Fatal("Expected an error for a server error on the start URL, but got nil")
}
})
t.Run("Broken Link", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<a href="/broken.html">Broken</a>`)
} else {
http.NotFound(w, r)
}
}))
defer server.Close()
// We expect an error because the link is broken.
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil)
if err == nil {
t.Fatal("Expected an error for a broken link, but got nil")
}
if !strings.Contains(err.Error(), "404 Not Found") {
t.Errorf("Expected error to contain '404 Not Found', but got: %v", err)
}
if dn != nil {
t.Error("DataNode should be nil on error")
}
})
}
func TestDownloadAndPackageWebsite_Ugly(t *testing.T) {
t.Run("Exceed Max Depth", func(t *testing.T) {
server := newWebsiteTestServer()
defer server.Close()
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
dn, err := DownloadAndPackageWebsite(server.URL, 1, bar) // Max depth of 1
if err != nil {
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
}
// page3.html is at depth 2, so it should not be present.
exists, _ := dn.Exists("page3.html")
if exists {
t.Error("page3.html should not have been downloaded due to max depth")
}
// page2.html is at depth 1, so it should be present.
exists, _ = dn.Exists("page2.html")
if !exists {
t.Error("page2.html should have been downloaded")
}
})
t.Run("External Links", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<a href="http://externalsite.com/page.html">External</a>`)
}))
defer server.Close()
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil)
if err != nil {
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
}
if dn == nil {
t.Fatal("DataNode should not be nil")
}
// We can't easily check if the external link was visited, but we can ensure
// it didn't cause an error and didn't add any unexpected files.
var fileCount int
dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() {
fileCount++
}
return nil
})
if fileCount != 1 { // Should only contain the root page
t.Errorf("expected 1 file in datanode, but found %d", fileCount)
}
})
t.Run("Timeout", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond)
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<h1>Hello</h1>`)
}))
defer server.Close()
// This test is tricky as it depends on timing.
// The current implementation uses the default http client with no timeout.
// A proper implementation would allow configuring a timeout.
// For now, we'll just test that it doesn't hang forever.
done := make(chan bool)
go func() {
_, err := DownloadAndPackageWebsite(server.URL, 1, nil)
if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") {
// We expect a timeout error, but other errors are failures.
t.Errorf("unexpected error: %v", err)
}
done <- true
}()
select {
case <-done:
// test finished
case <-time.After(5 * time.Second):
t.Fatal("Test timed out")
}
})
}
// --- Helpers ---
func newWebsiteTestServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `
<!DOCTYPE html>
<html><body>
<a href="/page2.html">Page 2</a>
<link rel="stylesheet" href="style.css">
<img src="image.png">
</body></html>
`)
case "/style.css":
w.Header().Set("Content-Type", "text/css")
fmt.Fprint(w, `body { color: red; }`)
case "/image.png":
w.Header().Set("Content-Type", "image/png")
fmt.Fprint(w, "fake image data")
case "/page2.html":
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><body><a href="/page3.html">Page 3</a></body></html>`)
case "/page3.html":
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><body><h1>Page 3</h1></body></html>`)
default:
http.NotFound(w, r)
}
}))
}