From 35024677c2f4aef777306679990de581b998c585 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 30 Oct 2025 16:23:00 +0000 Subject: [PATCH] (#10) GitHub Actions workflow and refactor build and test infrastructure --- .github/workflows/coverage.yml | 35 ++++++++ Taskfile.yml | 7 +- cmd/core/Taskfile.yml | 9 +- pkg/help/help_test.go | 70 ++++++++++++++- pkg/io/sftp/sftp_test.go | 45 +++++++++- pkg/io/webdav/webdav_test.go | 156 +++++++++++++++++++++++++++++---- 6 files changed, 297 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..c14da98e --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,35 @@ +name: Go Test Coverage + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.work' + + - name: Setup Task + uses: arduino/setup-task@v1 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + + - name: Run coverage + run: task cov + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.txt diff --git a/Taskfile.yml b/Taskfile.yml index e9327750..693b3da3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -8,8 +8,11 @@ tasks: test: desc: "Run all Go tests recursively for the entire project." cmds: - - clear # Clear the terminal for better readability - - go test ./... + - cmd: clear + platforms: [linux, darwin] + - cmd: cls + platforms: [windows] + - cmd: go test ./... review: desc: "Run CodeRabbit review to get feedback on the current changes." diff --git a/cmd/core/Taskfile.yml b/cmd/core/Taskfile.yml index dcf19077..afb356d9 100644 --- a/cmd/core/Taskfile.yml +++ b/cmd/core/Taskfile.yml @@ -18,19 +18,22 @@ tasks: summary: Builds and runs the core executable cmds: - task: build - - chmod +x {{.TASKFILE_DIR}}/bin/core + - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core + platforms: [linux, darwin] - "{{.TASKFILE_DIR}}/bin/core {{.CLI_ARGS}}" sync: summary: Updates the public API Go files deps: [build] cmds: - - chmod +x {{.TASKFILE_DIR}}/bin/core + - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core + platforms: [linux, darwin] - "{{.TASKFILE_DIR}}/bin/core dev sync" test-gen: summary: Generates tests for the public API deps: [build] cmds: - - chmod +x {{.TASKFILE_DIR}}/bin/core + - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core + platforms: [linux, darwin] - "{{.TASKFILE_DIR}}/bin/core dev test-gen" diff --git a/pkg/help/help_test.go b/pkg/help/help_test.go index d0c6bc2f..2e9c7eab 100644 --- a/pkg/help/help_test.go +++ b/pkg/help/help_test.go @@ -1,7 +1,11 @@ package help import ( + "testing" + "github.com/Snider/Core/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/wailsapp/wails/v3/pkg/application" ) // MockDisplay is a mock implementation of the core.Display interface. @@ -27,9 +31,73 @@ func (m *MockDisplay) OpenWindow(opts ...core.WindowOption) error { return nil } type MockCore struct { Core *core.Core ActionCalled bool + ActionMsg core.Message } -func (m *MockCore) ACTION(msg core.Message) error { +// ACTION matches the signature required by RegisterAction. +func (m *MockCore) ACTION(c *core.Core, msg core.Message) error { m.ActionCalled = true + m.ActionMsg = msg return nil } + +func setupService(t *testing.T) (*Service, *MockCore, *MockDisplay) { + s, err := New() + assert.NoError(t, err) + + app := application.New(application.Options{}) + c, err := core.New(core.WithWails(app)) + assert.NoError(t, err) + mockCore := &MockCore{Core: c} + mockDisplay := &MockDisplay{} + + s.Runtime = core.NewRuntime(c, Options{}) + s.display = mockDisplay + // Register our mock handler. When the real s.Core().ACTION is called, + // our mock handler will be executed. + c.RegisterAction(mockCore.ACTION) + + return s, mockCore, mockDisplay +} + +func TestNew(t *testing.T) { + s, err := New() + assert.NoError(t, err) + assert.NotNil(t, s) +} + +func TestShow(t *testing.T) { + s, mockCore, _ := setupService(t) + + err := s.Show() + assert.NoError(t, err) + assert.True(t, mockCore.ActionCalled) + + msg, ok := mockCore.ActionMsg.(map[string]any) + assert.True(t, ok) + assert.Equal(t, "display.open_window", msg["action"]) + assert.Equal(t, "help", msg["name"]) +} + +func TestShowAt(t *testing.T) { + s, mockCore, _ := setupService(t) + + err := s.ShowAt("test-anchor") + assert.NoError(t, err) + assert.True(t, mockCore.ActionCalled) + + msg, ok := mockCore.ActionMsg.(map[string]any) + assert.True(t, ok) + assert.Equal(t, "display.open_window", msg["action"]) + assert.Equal(t, "help", msg["name"]) + + opts, ok := msg["options"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "/#test-anchor", opts["URL"]) +} + +func TestHandleIPCEvents_ServiceStartup(t *testing.T) { + s, _, _ := setupService(t) + err := s.HandleIPCEvents(s.Core(), core.ActionServiceStartup{}) + assert.NoError(t, err) +} diff --git a/pkg/io/sftp/sftp_test.go b/pkg/io/sftp/sftp_test.go index 4417d533..dda4a6ed 100644 --- a/pkg/io/sftp/sftp_test.go +++ b/pkg/io/sftp/sftp_test.go @@ -2,13 +2,28 @@ package sftp import ( "os" + "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" ) +// setupTest creates a temporary home directory and a dummy known_hosts file +// to prevent tests from failing in CI environments where the file doesn't exist. +func setupTest(t *testing.T) { + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + sshDir := filepath.Join(homeDir, ".ssh") + err := os.Mkdir(sshDir, 0700) + assert.NoError(t, err) + knownHostsFile := filepath.Join(sshDir, "known_hosts") + err = os.WriteFile(knownHostsFile, []byte{}, 0600) + assert.NoError(t, err) +} + func TestNew(t *testing.T) { + setupTest(t) // Provide a dummy ConnectionConfig for testing. // Since we are not setting up a real SFTP server, we expect an error during connection. cfg := ConnectionConfig{ @@ -25,6 +40,7 @@ func TestNew(t *testing.T) { } func TestNew_InvalidHost(t *testing.T) { + setupTest(t) cfg := ConnectionConfig{ Host: "non-resolvable-host.domain.invalid", Port: "22", @@ -39,6 +55,7 @@ func TestNew_InvalidHost(t *testing.T) { } func TestNew_InvalidPort(t *testing.T) { + setupTest(t) cfg := ConnectionConfig{ Host: "localhost", Port: "99999", // Invalid port number @@ -53,6 +70,7 @@ func TestNew_InvalidPort(t *testing.T) { } func TestNew_ConnectionTimeout(t *testing.T) { + setupTest(t) cfg := ConnectionConfig{ Host: "192.0.2.0", // Non-routable IP to simulate timeout Port: "22", @@ -68,6 +86,7 @@ func TestNew_ConnectionTimeout(t *testing.T) { } func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) { + setupTest(t) cfg := ConnectionConfig{ Host: "localhost", Port: "22", @@ -82,14 +101,23 @@ func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) { } func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) { + setupTest(t) // Create a temporary file with invalid key content tmpFile, err := os.CreateTemp("", "invalid_key") assert.NoError(t, err) - defer os.Remove(tmpFile.Name()) + defer func(name string) { + err := os.Remove(name) + if err != nil { + t.Logf("Failed to remove temporary file: %v", err) + } + }(tmpFile.Name()) _, err = tmpFile.WriteString("not a valid ssh key") assert.NoError(t, err) - tmpFile.Close() + err = tmpFile.Close() + if err != nil { + return + } cfg := ConnectionConfig{ Host: "localhost", @@ -105,14 +133,23 @@ func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) { } func TestNew_MultipleAuthMethods(t *testing.T) { + setupTest(t) // Create a temporary file with invalid key content to ensure key-based auth is attempted tmpFile, err := os.CreateTemp("", "dummy_key") assert.NoError(t, err) - defer os.Remove(tmpFile.Name()) + defer func(name string) { + err := os.Remove(name) + if err != nil { + t.Logf("Failed to remove temporary file: %v", err) + } + }(tmpFile.Name()) _, err = tmpFile.WriteString("not a valid ssh key") assert.NoError(t, err) - tmpFile.Close() + err = tmpFile.Close() + if err != nil { + return + } cfg := ConnectionConfig{ Host: "localhost", diff --git a/pkg/io/webdav/webdav_test.go b/pkg/io/webdav/webdav_test.go index 61343cf9..a0b2602f 100644 --- a/pkg/io/webdav/webdav_test.go +++ b/pkg/io/webdav/webdav_test.go @@ -1,29 +1,155 @@ package webdav import ( + "fmt" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) -func TestNew(t *testing.T) { - // Provide a dummy ConnectionConfig for testing. - // Since we are not setting up a real WebDAV server, we expect an error during connection. +// mockWebDAVServer creates a test HTTP server that mimics a WebDAV server. +func mockWebDAVServer() *httptest.Server { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "PROPFIND": + if r.URL.Path == "/" { + w.WriteHeader(http.StatusMultiStatus) + return + } + // For IsFile test + if r.URL.Path == "/test.txt" { + w.WriteHeader(http.StatusMultiStatus) + fmt.Fprint(w, ` + + + /test.txt + + + + + HTTP/1.1 200 OK + + +`) + return + } + if r.URL.Path == "/testdir/" { + w.WriteHeader(http.StatusMultiStatus) + fmt.Fprint(w, ` + + + /testdir/ + + + + + HTTP/1.1 200 OK + + +`) + return + } + http.NotFound(w, r) + case "GET": + if r.URL.Path == "/test.txt" { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "Hello, WebDAV!") + return + } + http.NotFound(w, r) + case "PUT": + if r.URL.Path == "/test.txt" { + w.WriteHeader(http.StatusCreated) + return + } + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + case "MKCOL": + if r.URL.Path == "/testdir/" { + w.WriteHeader(http.StatusCreated) + return + } + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + default: + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + } + }) + return httptest.NewServer(handler) +} + +func TestNew_Success(t *testing.T) { + server := mockWebDAVServer() + defer server.Close() + cfg := ConnectionConfig{ - URL: "http://192.0.2.1:1/webdav", // Non-routable address - User: "testuser", - Password: "testpassword", + URL: server.URL, + User: "user", + Password: "password", } - service, err := New(cfg) - assert.Error(t, err) - assert.Nil(t, service, "New() should return a nil service instance on connection error") - assert.Contains(t, err.Error(), "timeout", "Expected connection error message") + medium, err := New(cfg) + assert.NoError(t, err) + assert.NotNil(t, medium) } -// Functional tests for WebDAV operations (Read, Write, EnsureDir, IsFile, etc.) -// would require a running WebDAV server or a sophisticated mock. -// These are typically integration tests rather than unit tests. -func TestWebDAVFunctional(t *testing.T) { - t.Skip("Skipping WebDAV functional tests as they require a WebDAV server setup.") +func TestRead(t *testing.T) { + server := mockWebDAVServer() + defer server.Close() + + cfg := ConnectionConfig{ + URL: server.URL, + User: "user", + Password: "password", + } + medium, err := New(cfg) + assert.NoError(t, err) + content, err := medium.Read("test.txt") + assert.NoError(t, err) + assert.Equal(t, "Hello, WebDAV!", content) +} + +func TestWrite(t *testing.T) { + server := mockWebDAVServer() + defer server.Close() + + cfg := ConnectionConfig{ + URL: server.URL, + User: "user", + Password: "password", + } + medium, err := New(cfg) + assert.NoError(t, err) + err = medium.Write("test.txt", "Hello, WebDAV!") + assert.NoError(t, err) +} + +func TestEnsureDir(t *testing.T) { + server := mockWebDAVServer() + defer server.Close() + + cfg := ConnectionConfig{ + URL: server.URL, + User: "user", + Password: "password", + } + medium, err := New(cfg) + assert.NoError(t, err) + err = medium.EnsureDir("testdir") + assert.NoError(t, err) +} + +func TestIsFile(t *testing.T) { + server := mockWebDAVServer() + defer server.Close() + + cfg := ConnectionConfig{ + URL: server.URL, + User: "user", + Password: "password", + } + medium, err := New(cfg) + assert.NoError(t, err) + assert.True(t, medium.IsFile("test.txt")) + assert.False(t, medium.IsFile("testdir")) }