(#10) GitHub Actions workflow and refactor build and test infrastructure

This commit is contained in:
Snider 2025-10-30 16:23:00 +00:00 committed by GitHub
parent 2af6c4ad3e
commit 35024677c2
6 changed files with 297 additions and 25 deletions

35
.github/workflows/coverage.yml vendored Normal file
View file

@ -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

View file

@ -8,8 +8,11 @@ tasks:
test: test:
desc: "Run all Go tests recursively for the entire project." desc: "Run all Go tests recursively for the entire project."
cmds: cmds:
- clear # Clear the terminal for better readability - cmd: clear
- go test ./... platforms: [linux, darwin]
- cmd: cls
platforms: [windows]
- cmd: go test ./...
review: review:
desc: "Run CodeRabbit review to get feedback on the current changes." desc: "Run CodeRabbit review to get feedback on the current changes."

View file

@ -18,19 +18,22 @@ tasks:
summary: Builds and runs the core executable summary: Builds and runs the core executable
cmds: cmds:
- task: build - task: build
- chmod +x {{.TASKFILE_DIR}}/bin/core - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core
platforms: [linux, darwin]
- "{{.TASKFILE_DIR}}/bin/core {{.CLI_ARGS}}" - "{{.TASKFILE_DIR}}/bin/core {{.CLI_ARGS}}"
sync: sync:
summary: Updates the public API Go files summary: Updates the public API Go files
deps: [build] deps: [build]
cmds: cmds:
- chmod +x {{.TASKFILE_DIR}}/bin/core - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core
platforms: [linux, darwin]
- "{{.TASKFILE_DIR}}/bin/core dev sync" - "{{.TASKFILE_DIR}}/bin/core dev sync"
test-gen: test-gen:
summary: Generates tests for the public API summary: Generates tests for the public API
deps: [build] deps: [build]
cmds: cmds:
- chmod +x {{.TASKFILE_DIR}}/bin/core - cmd: chmod +x {{.TASKFILE_DIR}}/bin/core
platforms: [linux, darwin]
- "{{.TASKFILE_DIR}}/bin/core dev test-gen" - "{{.TASKFILE_DIR}}/bin/core dev test-gen"

View file

@ -1,7 +1,11 @@
package help package help
import ( import (
"testing"
"github.com/Snider/Core/pkg/core" "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. // 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 { type MockCore struct {
Core *core.Core Core *core.Core
ActionCalled bool 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.ActionCalled = true
m.ActionMsg = msg
return nil 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)
}

View file

@ -2,13 +2,28 @@ package sftp
import ( import (
"os" "os"
"path/filepath"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "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) { func TestNew(t *testing.T) {
setupTest(t)
// Provide a dummy ConnectionConfig for testing. // Provide a dummy ConnectionConfig for testing.
// Since we are not setting up a real SFTP server, we expect an error during connection. // Since we are not setting up a real SFTP server, we expect an error during connection.
cfg := ConnectionConfig{ cfg := ConnectionConfig{
@ -25,6 +40,7 @@ func TestNew(t *testing.T) {
} }
func TestNew_InvalidHost(t *testing.T) { func TestNew_InvalidHost(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "non-resolvable-host.domain.invalid", Host: "non-resolvable-host.domain.invalid",
Port: "22", Port: "22",
@ -39,6 +55,7 @@ func TestNew_InvalidHost(t *testing.T) {
} }
func TestNew_InvalidPort(t *testing.T) { func TestNew_InvalidPort(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "localhost", Host: "localhost",
Port: "99999", // Invalid port number Port: "99999", // Invalid port number
@ -53,6 +70,7 @@ func TestNew_InvalidPort(t *testing.T) {
} }
func TestNew_ConnectionTimeout(t *testing.T) { func TestNew_ConnectionTimeout(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "192.0.2.0", // Non-routable IP to simulate timeout Host: "192.0.2.0", // Non-routable IP to simulate timeout
Port: "22", Port: "22",
@ -68,6 +86,7 @@ func TestNew_ConnectionTimeout(t *testing.T) {
} }
func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) { func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "localhost", Host: "localhost",
Port: "22", Port: "22",
@ -82,14 +101,23 @@ func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) {
} }
func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) { func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) {
setupTest(t)
// Create a temporary file with invalid key content // Create a temporary file with invalid key content
tmpFile, err := os.CreateTemp("", "invalid_key") tmpFile, err := os.CreateTemp("", "invalid_key")
assert.NoError(t, err) 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") _, err = tmpFile.WriteString("not a valid ssh key")
assert.NoError(t, err) assert.NoError(t, err)
tmpFile.Close() err = tmpFile.Close()
if err != nil {
return
}
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "localhost", Host: "localhost",
@ -105,14 +133,23 @@ func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) {
} }
func TestNew_MultipleAuthMethods(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 // Create a temporary file with invalid key content to ensure key-based auth is attempted
tmpFile, err := os.CreateTemp("", "dummy_key") tmpFile, err := os.CreateTemp("", "dummy_key")
assert.NoError(t, err) 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") _, err = tmpFile.WriteString("not a valid ssh key")
assert.NoError(t, err) assert.NoError(t, err)
tmpFile.Close() err = tmpFile.Close()
if err != nil {
return
}
cfg := ConnectionConfig{ cfg := ConnectionConfig{
Host: "localhost", Host: "localhost",

View file

@ -1,29 +1,155 @@
package webdav package webdav
import ( import (
"fmt"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNew(t *testing.T) { // mockWebDAVServer creates a test HTTP server that mimics a WebDAV server.
// Provide a dummy ConnectionConfig for testing. func mockWebDAVServer() *httptest.Server {
// Since we are not setting up a real WebDAV server, we expect an error during connection. 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, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<D:multistatus xmlns:D=\"DAV:\">
<D:response>
<D:href>/test.txt</D:href>
<D:propstat>
<D:prop>
<D:resourcetype/>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`)
return
}
if r.URL.Path == "/testdir/" {
w.WriteHeader(http.StatusMultiStatus)
fmt.Fprint(w, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<D:multistatus xmlns:D=\"DAV:\">
<D:response>
<D:href>/testdir/</D:href>
<D:propstat>
<D:prop>
<D:resourcetype><D:collection/></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`)
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{ cfg := ConnectionConfig{
URL: "http://192.0.2.1:1/webdav", // Non-routable address URL: server.URL,
User: "testuser", User: "user",
Password: "testpassword", Password: "password",
} }
service, err := New(cfg) medium, err := New(cfg)
assert.Error(t, err) assert.NoError(t, err)
assert.Nil(t, service, "New() should return a nil service instance on connection error") assert.NotNil(t, medium)
assert.Contains(t, err.Error(), "timeout", "Expected connection error message")
} }
// Functional tests for WebDAV operations (Read, Write, EnsureDir, IsFile, etc.) func TestRead(t *testing.T) {
// would require a running WebDAV server or a sophisticated mock. server := mockWebDAVServer()
// These are typically integration tests rather than unit tests. defer server.Close()
func TestWebDAVFunctional(t *testing.T) {
t.Skip("Skipping WebDAV functional tests as they require a WebDAV server setup.") 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"))
} }