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"))
}