From 592f4fed243683e2013aecceed6ef8e0eca954bd Mon Sep 17 00:00:00 2001 From: Jonatas Sas Date: Sun, 8 Jun 2025 14:22:00 -0300 Subject: [PATCH 1/3] Tests --- .goreleaser.yml | 1 + internal/downloader/downloader_test.go | 151 +++++++++++++------------ internal/extractor/extractor_test.go | 107 +++++++++++------- 3 files changed, 143 insertions(+), 116 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 91d9db0..2843c91 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -25,6 +25,7 @@ checksum: algorithm: sha256 release: + name_template: "phpMyAdmin Updater {{ .Tag }}" github: owner: jsas4coding name: pma-up diff --git a/internal/downloader/downloader_test.go b/internal/downloader/downloader_test.go index 165c49e..1c6d1eb 100644 --- a/internal/downloader/downloader_test.go +++ b/internal/downloader/downloader_test.go @@ -5,13 +5,13 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" ) -func TestDownloadPhpMyAdmin(t *testing.T) { +func TestDownloadPhpMyAdmin_Success(t *testing.T) { mockZipContent := []byte("PK\x03\x04 dummy zip content") - // Setup mock HTTP server to simulate phpMyAdmin download endpoint server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/zip") if _, err := w.Write(mockZipContent); err != nil { @@ -22,97 +22,100 @@ func TestDownloadPhpMyAdmin(t *testing.T) { tempDir := t.TempDir() version := "5.2.2" - mockDownloadURL := fmt.Sprintf("%s/phpMyAdmin-%s-all-languages.zip", server.URL, version) - // Execute the download function - downloadedFilePath, err := DownloadPhpMyAdmin(mockDownloadURL, tempDir, version) + filePath, err := DownloadPhpMyAdmin(mockDownloadURL, tempDir, version) if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Fatalf("unexpected error: %v", err) } - // Verify the file exists - if _, statErr := os.Stat(downloadedFilePath); statErr != nil { - t.Fatalf("expected file to exist, but got error: %v", statErr) + if _, err := os.Stat(filePath); err != nil { + t.Fatalf("expected file, got stat error: %v", err) } - // Verify the file content matches mock data - data, readErr := os.ReadFile(downloadedFilePath) - if readErr != nil { - t.Fatalf("failed to read downloaded file: %v", readErr) + data, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) } if string(data) != string(mockZipContent) { - t.Errorf("downloaded file content does not match expected mock content") + t.Errorf("file content mismatch") } } -func TestDownloadPhpMyAdmin_FailureScenarios(t *testing.T) { +func TestDownloadPhpMyAdmin_InputValidation(t *testing.T) { tests := []struct { - name string - serverFunc func() *httptest.Server - setupDir func() (string, error) - expectErr bool + name string + url string + dest string + ver string + experror string }{ - { - name: "http 500 error", - serverFunc: func() *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "internal server error", http.StatusInternalServerError) - })) - }, - setupDir: func() (string, error) { - return t.TempDir(), nil - }, - expectErr: true, - }, - { - name: "directory not writable", - serverFunc: func() *httptest.Server { - content := []byte("PK\x03\x04 dummy zip content") - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/zip") - if _, err := w.Write(content); err != nil { - t.Fatalf("failed to write mock zip content: %v", err) - } - })) - }, - setupDir: func() (string, error) { - dir := t.TempDir() - if chmodErr := os.Chmod(dir, 0500); chmodErr != nil { - return "", chmodErr - } - return dir, nil - }, - expectErr: true, - }, + {"empty url", "", "/tmp", "5.2.2", "empty download URL"}, + {"empty dest", "http://example.com/file.zip", "", "5.2.2", "empty destination directory"}, + {"empty version", "http://example.com/file.zip", "/tmp", "", "empty version string"}, } - for _, tt := range tests { - tt := tt // capture range variable - t.Run(tt.name, func(t *testing.T) { - server := tt.serverFunc() - defer server.Close() - - tempDir, dirErr := tt.setupDir() - if dirErr != nil { - t.Fatalf("failed to setup dir: %v", dirErr) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := DownloadPhpMyAdmin(tc.url, tc.dest, tc.ver) + if err == nil || !strings.Contains(err.Error(), tc.experror) { + t.Errorf("expected error '%s', got '%v'", tc.experror, err) } - defer func() { - // restore permission so tempdir can be cleaned up - _ = os.Chmod(tempDir, 0700) - }() + }) + } +} - version := "5.2.2" - mockDownloadURL := fmt.Sprintf("%s/phpMyAdmin-%s-all-languages.zip", server.URL, version) +func TestDownloadPhpMyAdmin_RequestCreationFailure(t *testing.T) { + tempDir := t.TempDir() + _, err := DownloadPhpMyAdmin(":/invalid-url", tempDir, "5.2.2") + if err == nil || !strings.Contains(err.Error(), "failed to create HTTP request") { + t.Errorf("expected HTTP request creation error, got %v", err) + } +} - _, downloadErr := DownloadPhpMyAdmin(mockDownloadURL, tempDir, version) - if tt.expectErr && downloadErr == nil { - t.Errorf("expected error but got none") - } - if !tt.expectErr && downloadErr != nil { - t.Errorf("unexpected error: %v", downloadErr) - } - }) +func TestDownloadPhpMyAdmin_ClientFailure(t *testing.T) { + tempDir := t.TempDir() + _, err := DownloadPhpMyAdmin("http://nonexistent.invalid/file.zip", tempDir, "5.2.2") + if err == nil || !strings.Contains(err.Error(), "failed to perform HTTP request") { + t.Errorf("expected client failure, got %v", err) + } +} + +func TestDownloadPhpMyAdmin_ServerReturns500(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "internal error", http.StatusInternalServerError) + })) + defer server.Close() + + tempDir := t.TempDir() + version := "5.2.2" + mockDownloadURL := fmt.Sprintf("%s/phpMyAdmin-%s-all-languages.zip", server.URL, version) + + _, err := DownloadPhpMyAdmin(mockDownloadURL, tempDir, version) + if err == nil || !strings.Contains(err.Error(), "unexpected HTTP status") { + t.Errorf("expected HTTP status error, got %v", err) + } +} + +func TestDownloadPhpMyAdmin_DirectoryNotWritable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/zip") + w.Write([]byte("PK\x03\x04 dummy zip content")) + })) + defer server.Close() + + tempDir := t.TempDir() + if err := os.Chmod(tempDir, 0500); err != nil { + t.Fatalf("failed to chmod: %v", err) + } + defer os.Chmod(tempDir, 0700) + + version := "5.2.2" + mockDownloadURL := fmt.Sprintf("%s/phpMyAdmin-%s-all-languages.zip", server.URL, version) + + _, err := DownloadPhpMyAdmin(mockDownloadURL, tempDir, version) + if err == nil { + t.Errorf("expected permission error, got none") } } diff --git a/internal/extractor/extractor_test.go b/internal/extractor/extractor_test.go index 9e5915d..75bb474 100644 --- a/internal/extractor/extractor_test.go +++ b/internal/extractor/extractor_test.go @@ -4,40 +4,35 @@ import ( "archive/zip" "os" "path/filepath" + "strings" "testing" ) -func TestExtractZip(t *testing.T) { +func TestExtractZip_Success(t *testing.T) { tempDir := t.TempDir() - - // Create test zip file zipPath := filepath.Join(tempDir, "test.zip") testFiles := map[string]string{ "folder1/file1.txt": "content1", "folder2/file2.txt": "content2", } - createErr := createTestZip(t, zipPath, testFiles) - if createErr != nil { - t.Fatalf("failed to create test zip: %v", createErr) + if err := createTestZip(t, zipPath, testFiles); err != nil { + t.Fatalf("failed to create test zip: %v", err) } extractDir := filepath.Join(tempDir, "extracted") - - extractErr := ExtractZip(zipPath, extractDir) - if extractErr != nil { - t.Fatalf("ExtractZip failed: %v", extractErr) + if err := ExtractZip(zipPath, extractDir); err != nil { + t.Fatalf("ExtractZip failed: %v", err) } for name, content := range testFiles { - extractedPath := filepath.Join(extractDir, name) - data, readErr := os.ReadFile(extractedPath) - if readErr != nil { - t.Errorf("failed to read extracted file %s: %v", name, readErr) - continue + path := filepath.Join(extractDir, name) + data, err := os.ReadFile(path) + if err != nil { + t.Errorf("failed to read extracted file %s: %v", name, err) } if string(data) != content { - t.Errorf("file content mismatch for %s: expected '%s', got '%s'", name, content, string(data)) + t.Errorf("content mismatch for %s", name) } } } @@ -46,58 +41,86 @@ func TestExtractZip_FailureScenarios(t *testing.T) { tempDir := t.TempDir() t.Run("file not found", func(t *testing.T) { - invalidPath := filepath.Join(tempDir, "nonexistent.zip") extractDir := filepath.Join(tempDir, "extracted1") - - err := ExtractZip(invalidPath, extractDir) + err := ExtractZip(filepath.Join(tempDir, "nonexistent.zip"), extractDir) if err == nil { - t.Errorf("expected error when opening nonexistent file, got nil") + t.Errorf("expected error for nonexistent file") } }) t.Run("corrupted zip file", func(t *testing.T) { badZipPath := filepath.Join(tempDir, "bad.zip") - writeErr := os.WriteFile(badZipPath, []byte("not a real zip content"), 0644) - if writeErr != nil { - t.Fatalf("failed to write corrupted zip: %v", writeErr) - } - + os.WriteFile(badZipPath, []byte("not a real zip content"), 0644) extractDir := filepath.Join(tempDir, "extracted2") err := ExtractZip(badZipPath, extractDir) if err == nil { - t.Errorf("expected error when extracting corrupted zip, got nil") + t.Errorf("expected error for corrupted zip") + } + }) + + t.Run("empty zip path", func(t *testing.T) { + extractDir := filepath.Join(tempDir, "extracted3") + err := ExtractZip("", extractDir) + if err == nil || !strings.Contains(err.Error(), "empty zip path") { + t.Errorf("expected empty zip path error") + } + }) + + t.Run("empty destination", func(t *testing.T) { + zipPath := filepath.Join(tempDir, "test.zip") + testFiles := map[string]string{"file.txt": "data"} + createTestZip(t, zipPath, testFiles) + err := ExtractZip(zipPath, "") + if err == nil || !strings.Contains(err.Error(), "empty destination path") { + t.Errorf("expected empty destination path error") + } + }) + + t.Run("permission denied on destination", func(t *testing.T) { + zipPath := filepath.Join(tempDir, "test2.zip") + testFiles := map[string]string{"file.txt": "data"} + createTestZip(t, zipPath, testFiles) + + extractDir := filepath.Join(tempDir, "extracted3") + os.MkdirAll(extractDir, 0500) + defer os.Chmod(extractDir, 0700) + + err := ExtractZip(zipPath, extractDir) + if err == nil { + t.Errorf("expected permission error") + } + }) + + t.Run("invalid path traversal", func(t *testing.T) { + zipPath := filepath.Join(tempDir, "evil.zip") + createTestZip(t, zipPath, map[string]string{"../evil.txt": "attack"}) + extractDir := filepath.Join(tempDir, "extracted4") + err := ExtractZip(zipPath, extractDir) + if err == nil || !strings.Contains(err.Error(), "invalid file path detected") { + t.Errorf("expected invalid path detection") } }) } -// Hardening helper - fully linter safe func createTestZip(t *testing.T, zipPath string, files map[string]string) error { zipFile, err := os.Create(zipPath) if err != nil { - return err + t.Fatalf("failed to create zip file: %v", err) } - defer func() { - if cerr := zipFile.Close(); cerr != nil { - t.Errorf("failed to close zipFile: %v", cerr) - } - }() + defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) - defer func() { - if cerr := zipWriter.Close(); cerr != nil { - t.Errorf("failed to close zipWriter: %v", cerr) - } - }() + defer zipWriter.Close() for name, content := range files { writer, err := zipWriter.Create(name) if err != nil { - return err + t.Fatalf("failed to create entry in zip: %v", err) } - if _, err := writer.Write([]byte(content)); err != nil { - return err + _, err = writer.Write([]byte(content)) + if err != nil { + t.Fatalf("failed to write zip content: %v", err) } } - return nil } From 089250aca69ad49c20af1e5d9927ab92ba52d6f9 Mon Sep 17 00:00:00 2001 From: Jonatas Sas Date: Sun, 8 Jun 2025 14:24:34 -0300 Subject: [PATCH 2/3] Update internal/extractor/extractor_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/extractor/extractor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/extractor/extractor_test.go b/internal/extractor/extractor_test.go index 75bb474..57b5127 100644 --- a/internal/extractor/extractor_test.go +++ b/internal/extractor/extractor_test.go @@ -32,7 +32,7 @@ func TestExtractZip_Success(t *testing.T) { t.Errorf("failed to read extracted file %s: %v", name, err) } if string(data) != content { - t.Errorf("content mismatch for %s", name) + t.Errorf("content mismatch for %s: expected %q, got %q", name, content, string(data)) } } } From 8e230339bd7ac14024e13cdefa71e91c4654d8cc Mon Sep 17 00:00:00 2001 From: Jonatas Sas Date: Sun, 8 Jun 2025 14:28:05 -0300 Subject: [PATCH 3/3] Fixes --- internal/downloader/downloader_test.go | 10 ++++- internal/extractor/extractor_test.go | 52 +++++++++++++++++--------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/internal/downloader/downloader_test.go b/internal/downloader/downloader_test.go index 1c6d1eb..766a148 100644 --- a/internal/downloader/downloader_test.go +++ b/internal/downloader/downloader_test.go @@ -101,7 +101,9 @@ func TestDownloadPhpMyAdmin_ServerReturns500(t *testing.T) { func TestDownloadPhpMyAdmin_DirectoryNotWritable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/zip") - w.Write([]byte("PK\x03\x04 dummy zip content")) + if _, err := w.Write([]byte("PK\x03\x04 dummy zip content")); err != nil { + t.Fatalf("failed to write mock zip content: %v", err) + } })) defer server.Close() @@ -109,7 +111,11 @@ func TestDownloadPhpMyAdmin_DirectoryNotWritable(t *testing.T) { if err := os.Chmod(tempDir, 0500); err != nil { t.Fatalf("failed to chmod: %v", err) } - defer os.Chmod(tempDir, 0700) + defer func() { + if err := os.Chmod(tempDir, 0700); err != nil { + t.Errorf("failed to restore permissions: %v", err) + } + }() version := "5.2.2" mockDownloadURL := fmt.Sprintf("%s/phpMyAdmin-%s-all-languages.zip", server.URL, version) diff --git a/internal/extractor/extractor_test.go b/internal/extractor/extractor_test.go index 57b5127..4967b3f 100644 --- a/internal/extractor/extractor_test.go +++ b/internal/extractor/extractor_test.go @@ -32,7 +32,7 @@ func TestExtractZip_Success(t *testing.T) { t.Errorf("failed to read extracted file %s: %v", name, err) } if string(data) != content { - t.Errorf("content mismatch for %s: expected %q, got %q", name, content, string(data)) + t.Errorf("content mismatch for %s", name) } } } @@ -50,10 +50,11 @@ func TestExtractZip_FailureScenarios(t *testing.T) { t.Run("corrupted zip file", func(t *testing.T) { badZipPath := filepath.Join(tempDir, "bad.zip") - os.WriteFile(badZipPath, []byte("not a real zip content"), 0644) + if err := os.WriteFile(badZipPath, []byte("not a real zip content"), 0644); err != nil { + t.Fatalf("failed to write corrupted zip: %v", err) + } extractDir := filepath.Join(tempDir, "extracted2") - err := ExtractZip(badZipPath, extractDir) - if err == nil { + if err := ExtractZip(badZipPath, extractDir); err == nil { t.Errorf("expected error for corrupted zip") } }) @@ -69,7 +70,9 @@ func TestExtractZip_FailureScenarios(t *testing.T) { t.Run("empty destination", func(t *testing.T) { zipPath := filepath.Join(tempDir, "test.zip") testFiles := map[string]string{"file.txt": "data"} - createTestZip(t, zipPath, testFiles) + if err := createTestZip(t, zipPath, testFiles); err != nil { + t.Fatalf("failed to create test zip: %v", err) + } err := ExtractZip(zipPath, "") if err == nil || !strings.Contains(err.Error(), "empty destination path") { t.Errorf("expected empty destination path error") @@ -79,24 +82,32 @@ func TestExtractZip_FailureScenarios(t *testing.T) { t.Run("permission denied on destination", func(t *testing.T) { zipPath := filepath.Join(tempDir, "test2.zip") testFiles := map[string]string{"file.txt": "data"} - createTestZip(t, zipPath, testFiles) + if err := createTestZip(t, zipPath, testFiles); err != nil { + t.Fatalf("failed to create test zip: %v", err) + } extractDir := filepath.Join(tempDir, "extracted3") - os.MkdirAll(extractDir, 0500) - defer os.Chmod(extractDir, 0700) + if err := os.MkdirAll(extractDir, 0500); err != nil { + t.Fatalf("failed to mkdir: %v", err) + } + defer func() { + if err := os.Chmod(extractDir, 0700); err != nil { + t.Errorf("failed to restore permissions: %v", err) + } + }() - err := ExtractZip(zipPath, extractDir) - if err == nil { + if err := ExtractZip(zipPath, extractDir); err == nil { t.Errorf("expected permission error") } }) t.Run("invalid path traversal", func(t *testing.T) { zipPath := filepath.Join(tempDir, "evil.zip") - createTestZip(t, zipPath, map[string]string{"../evil.txt": "attack"}) + if err := createTestZip(t, zipPath, map[string]string{"../evil.txt": "attack"}); err != nil { + t.Fatalf("failed to create evil zip: %v", err) + } extractDir := filepath.Join(tempDir, "extracted4") - err := ExtractZip(zipPath, extractDir) - if err == nil || !strings.Contains(err.Error(), "invalid file path detected") { + if err := ExtractZip(zipPath, extractDir); err == nil || !strings.Contains(err.Error(), "invalid file path detected") { t.Errorf("expected invalid path detection") } }) @@ -107,18 +118,25 @@ func createTestZip(t *testing.T, zipPath string, files map[string]string) error if err != nil { t.Fatalf("failed to create zip file: %v", err) } - defer zipFile.Close() + defer func() { + if cerr := zipFile.Close(); cerr != nil { + t.Errorf("failed to close zip file: %v", cerr) + } + }() zipWriter := zip.NewWriter(zipFile) - defer zipWriter.Close() + defer func() { + if cerr := zipWriter.Close(); cerr != nil { + t.Errorf("failed to close zip writer: %v", cerr) + } + }() for name, content := range files { writer, err := zipWriter.Create(name) if err != nil { t.Fatalf("failed to create entry in zip: %v", err) } - _, err = writer.Write([]byte(content)) - if err != nil { + if _, err := writer.Write([]byte(content)); err != nil { t.Fatalf("failed to write zip content: %v", err) } }