(#10) GitHub Actions workflow and refactor build and test infrastructure
This commit is contained in:
parent
2af6c4ad3e
commit
35024677c2
6 changed files with 297 additions and 25 deletions
35
.github/workflows/coverage.yml
vendored
Normal file
35
.github/workflows/coverage.yml
vendored
Normal 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
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue