mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 03:03:32 +01:00
fix: cross-platform CI test fixes and parse bug fix
- Add .gitattributes to force LF in mock files (Windows autocrlf) - Fix parse.go: detect line endings from content instead of runtime.GOOS - Add fail-fast: false to CI matrix; trigger on all branch pushes - Skip chmod-based tests on Windows (permissions work differently) - Use filepath.Join for expected paths in Windows path tests - Use platform-appropriate invalid paths in error tests - Add Windows absolute path test case for ValidateSheetName - Skip Unix-specific integration tests on Windows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Force LF line endings for mock/test data files to ensure consistent
|
||||||
|
# behavior across platforms (Windows git autocrlf converts to CRLF otherwise)
|
||||||
|
mocks/** text eol=lf
|
||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -3,9 +3,6 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -26,6 +23,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -12,6 +13,10 @@ import (
|
|||||||
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
||||||
// path traversal attempts when invoked as a subprocess.
|
// path traversal attempts when invoked as a subprocess.
|
||||||
func TestPathTraversalIntegration(t *testing.T) {
|
func TestPathTraversalIntegration(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env and tools")
|
||||||
|
}
|
||||||
|
|
||||||
// Build the cheat binary
|
// Build the cheat binary
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||||
@@ -146,6 +151,10 @@ cheatpaths:
|
|||||||
|
|
||||||
// TestPathTraversalRealWorld tests with more realistic scenarios
|
// TestPathTraversalRealWorld tests with more realistic scenarios
|
||||||
func TestPathTraversalRealWorld(t *testing.T) {
|
func TestPathTraversalRealWorld(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env and tools")
|
||||||
|
}
|
||||||
|
|
||||||
// This test ensures our protection works with actual file operations
|
// This test ensures our protection works with actual file operations
|
||||||
|
|
||||||
// Build cheat
|
// Build cheat
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cheatpath
|
package cheatpath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -53,9 +54,15 @@ func TestValidateSheetName(t *testing.T) {
|
|||||||
errMsg: "'..'",
|
errMsg: "'..'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "absolute path",
|
name: "absolute path unix",
|
||||||
input: "/etc/passwd",
|
input: "/etc/passwd",
|
||||||
wantErr: true,
|
wantErr: runtime.GOOS != "windows", // /etc/passwd is not absolute on Windows
|
||||||
|
errMsg: "absolute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absolute path windows",
|
||||||
|
input: `C:\evil`,
|
||||||
|
wantErr: runtime.GOOS == "windows", // C:\evil is not absolute on Unix
|
||||||
errMsg: "absolute",
|
errMsg: "absolute",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/internal/mock"
|
||||||
@@ -226,6 +227,10 @@ cheatpaths:
|
|||||||
|
|
||||||
// TestConfigGetCwdError tests error handling when os.Getwd fails
|
// TestConfigGetCwdError tests error handling when os.Getwd fails
|
||||||
func TestConfigGetCwdError(t *testing.T) {
|
func TestConfigGetCwdError(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Windows does not allow removing the current directory")
|
||||||
|
}
|
||||||
|
|
||||||
// This is difficult to test without being able to break os.Getwd
|
// This is difficult to test without being able to break os.Getwd
|
||||||
// We'll create a scenario where the current directory is removed
|
// We'll create a scenario where the current directory is removed
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -78,6 +79,9 @@ func TestConfigFailure(t *testing.T) {
|
|||||||
// TestEmptyEditor asserts that envvars are respected if an editor is not
|
// TestEmptyEditor asserts that envvars are respected if an editor is not
|
||||||
// specified in the configs
|
// specified in the configs
|
||||||
func TestEmptyEditor(t *testing.T) {
|
func TestEmptyEditor(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Editor() returns notepad on Windows before checking env vars")
|
||||||
|
}
|
||||||
|
|
||||||
// clear the environment variables
|
// clear the environment variables
|
||||||
os.Setenv("VISUAL", "")
|
os.Setenv("VISUAL", "")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -74,12 +75,18 @@ func TestInitCreateDirectory(t *testing.T) {
|
|||||||
// TestInitWriteError tests error handling when file write fails
|
// TestInitWriteError tests error handling when file write fails
|
||||||
func TestInitWriteError(t *testing.T) {
|
func TestInitWriteError(t *testing.T) {
|
||||||
// Skip this test if running as root (can write anywhere)
|
// Skip this test if running as root (can write anywhere)
|
||||||
if os.Getuid() == 0 {
|
if runtime.GOOS != "windows" && os.Getuid() == 0 {
|
||||||
t.Skip("Cannot test write errors as root")
|
t.Skip("Cannot test write errors as root")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use a platform-appropriate invalid path
|
||||||
|
invalidPath := "/dev/null/impossible/path/conf.yml"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
invalidPath = `NUL\impossible\path\conf.yml`
|
||||||
|
}
|
||||||
|
|
||||||
// Try to write to a read-only directory
|
// Try to write to a read-only directory
|
||||||
err := Init("/dev/null/impossible/path/conf.yml", "test")
|
err := Init(invalidPath, "test")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error when writing to invalid path, got nil")
|
t.Error("expected error when writing to invalid path, got nil")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -10,6 +12,9 @@ import (
|
|||||||
// TestValidatePathsNix asserts that the proper config paths are returned on
|
// TestValidatePathsNix asserts that the proper config paths are returned on
|
||||||
// *nix platforms
|
// *nix platforms
|
||||||
func TestValidatePathsNix(t *testing.T) {
|
func TestValidatePathsNix(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("filepath.Join uses backslashes on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
// mock the user's home directory
|
// mock the user's home directory
|
||||||
home := "/home/foo"
|
home := "/home/foo"
|
||||||
@@ -57,6 +62,9 @@ func TestValidatePathsNix(t *testing.T) {
|
|||||||
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned
|
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned
|
||||||
// on *nix platforms when `XDG_CONFIG_HOME is not set
|
// on *nix platforms when `XDG_CONFIG_HOME is not set
|
||||||
func TestValidatePathsNixNoXDG(t *testing.T) {
|
func TestValidatePathsNixNoXDG(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("filepath.Join uses backslashes on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
// mock the user's home directory
|
// mock the user's home directory
|
||||||
home := "/home/foo"
|
home := "/home/foo"
|
||||||
@@ -106,8 +114,8 @@ func TestValidatePathsWindows(t *testing.T) {
|
|||||||
|
|
||||||
// mock some envvars
|
// mock some envvars
|
||||||
envvars := map[string]string{
|
envvars := map[string]string{
|
||||||
"APPDATA": "/apps",
|
"APPDATA": filepath.Join("C:", "apps"),
|
||||||
"PROGRAMDATA": "/programs",
|
"PROGRAMDATA": filepath.Join("C:", "programs"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the paths for the platform
|
// get the paths for the platform
|
||||||
@@ -118,8 +126,8 @@ func TestValidatePathsWindows(t *testing.T) {
|
|||||||
|
|
||||||
// specify the expected output
|
// specify the expected output
|
||||||
want := []string{
|
want := []string{
|
||||||
"/apps/cheat/conf.yml",
|
filepath.Join("C:", "apps", "cheat", "conf.yml"),
|
||||||
"/programs/cheat/conf.yml",
|
filepath.Join("C:", "programs", "cheat", "conf.yml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// assert that output matches expectations
|
// assert that output matches expectations
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -71,21 +72,29 @@ cheatpaths:
|
|||||||
{
|
{
|
||||||
name: "invalid config path",
|
name: "invalid config path",
|
||||||
configs: "test",
|
configs: "test",
|
||||||
confpath: "/nonexistent/path/conf.yml",
|
// /dev/null/... is truly uncreatable on Unix;
|
||||||
|
// NUL\... is uncreatable on Windows
|
||||||
|
confpath: func() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return `NUL\impossible\conf.yml`
|
||||||
|
}
|
||||||
|
return "/dev/null/impossible/conf.yml"
|
||||||
|
}(),
|
||||||
userInput: "n\n",
|
userInput: "n\n",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantInErr: "failed to create config file",
|
wantInErr: "failed to create",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-create a non-empty community dir so PlainClone fails reliably
|
// Pre-create a .git dir inside the community path so go-git's PlainClone
|
||||||
// (otherwise, on CI runners with network access, the clone succeeds)
|
// returns ErrRepositoryAlreadyExists (otherwise, on CI runners with
|
||||||
cloneBlocker := filepath.Join(tempDir, "conf2", "cheatsheets", "community")
|
// network access, the real clone succeeds and the test fails)
|
||||||
if err := os.MkdirAll(cloneBlocker, 0755); err != nil {
|
fakeGitDir := filepath.Join(tempDir, "conf2", "cheatsheets", "community", ".git")
|
||||||
t.Fatalf("failed to create clone blocker dir: %v", err)
|
if err := os.MkdirAll(fakeGitDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create fake .git dir: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(cloneBlocker, ".gitkeep"), []byte(""), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(fakeGitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil {
|
||||||
t.Fatalf("failed to write clone blocker file: %v", err)
|
t.Fatalf("failed to write fake HEAD: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ func TestClone(t *testing.T) {
|
|||||||
// that don't require actual cloning
|
// that don't require actual cloning
|
||||||
|
|
||||||
t.Run("clone to read-only directory", func(t *testing.T) {
|
t.Run("clone to read-only directory", func(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("chmod does not restrict writes on Windows")
|
||||||
|
}
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
t.Skip("Cannot test read-only directory as root")
|
t.Skip("Cannot test read-only directory as root")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sheet
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,6 +131,10 @@ func TestCopyIOError(t *testing.T) {
|
|||||||
|
|
||||||
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
||||||
func TestCopyCleanupOnError(t *testing.T) {
|
func TestCopyCleanupOnError(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("chmod does not restrict reads on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
// Create a source file that we'll make unreadable after opening
|
// Create a source file that we'll make unreadable after opening
|
||||||
src, err := os.CreateTemp("", "copy-test-cleanup-*")
|
src, err := os.CreateTemp("", "copy-test-cleanup-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package sheet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -11,9 +10,9 @@ import (
|
|||||||
// Parse parses cheatsheet frontmatter
|
// Parse parses cheatsheet frontmatter
|
||||||
func parse(markdown string) (frontmatter, string, error) {
|
func parse(markdown string) (frontmatter, string, error) {
|
||||||
|
|
||||||
// determine the appropriate line-break for the platform
|
// detect the line-break style used in the content
|
||||||
linebreak := "\n"
|
linebreak := "\n"
|
||||||
if runtime.GOOS == "windows" {
|
if strings.Contains(markdown, "\r\n") {
|
||||||
linebreak = "\r\n"
|
linebreak = "\r\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
||||||
func TestParseWindowsLineEndings(t *testing.T) {
|
func TestParseWindowsLineEndings(t *testing.T) {
|
||||||
// Only test Windows line endings on Windows
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Skipping Windows line ending test on non-Windows platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
// stub our cheatsheet content with Windows line endings
|
// stub our cheatsheet content with Windows line endings
|
||||||
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user