1
0
mirror of https://github.com/Jguer/yay.git synced 2025-12-25 11:22:10 +01:00
yay/pkg/completion/completion_test.go
Jo aecd78040c
chore(completion): refactor and increase coverage (#2722)
* refactor(completion): separate cache validation from update logic

- Add NeedsUpdate() to check if completion cache is stale
- Rename Update() to UpdateCache() and make it unconditional
- Move caching decision to call sites (Show and sync.Run)
- Improve error handling with proper defer for file close

* increase completion coverage

* launch goroutine if update is needed

* remove user dependent test
2025-12-06 13:20:13 +01:00

553 lines
13 KiB
Go

//go:build !integration
// +build !integration
package completion
import (
"bytes"
"compress/gzip"
"context"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/Jguer/yay/v12/pkg/db"
"github.com/Jguer/yay/v12/pkg/db/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const samplePackageResp = `
# AUR package list, generated on Fri, 24 Jul 2020 22:05:22 GMT
cytadela
bitefusion
globs-svn
ri-li
globs-benchmarks-svn
dunelegacy
lumina
eternallands-sound
`
const expectPackageCompletion = `cytadela AUR
bitefusion AUR
globs-svn AUR
ri-li AUR
globs-benchmarks-svn AUR
dunelegacy AUR
lumina AUR
eternallands-sound AUR
`
type mockDoer struct {
t *testing.T
returnBody []byte
returnStatusCode int
returnErr error
wantURL string
}
func (m *mockDoer) Get(url string) (*http.Response, error) {
assert.Equal(m.t, m.wantURL, url)
return &http.Response{
StatusCode: m.returnStatusCode,
Body: io.NopCloser(bytes.NewReader(m.returnBody)),
}, m.returnErr
}
func gzipString(s string) []byte {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
gz.Write([]byte(s))
gz.Close()
return buf.Bytes()
}
func Test_createAURList(t *testing.T) {
t.Parallel()
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: []byte(samplePackageResp),
returnErr: nil,
}
out := &bytes.Buffer{}
err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
assert.NoError(t, err)
gotOut := out.String()
assert.Equal(t, expectPackageCompletion, gotOut)
}
func Test_createAURListGzip(t *testing.T) {
t.Parallel()
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: gzipString(samplePackageResp),
returnErr: nil,
}
out := &bytes.Buffer{}
err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
assert.NoError(t, err)
gotOut := out.String()
assert.Equal(t, expectPackageCompletion, gotOut)
}
func Test_createAURListHTTPError(t *testing.T) {
t.Parallel()
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: []byte(samplePackageResp),
returnErr: errors.New("Not available"),
}
out := &bytes.Buffer{}
err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
assert.EqualError(t, err, "Not available")
}
func Test_createAURListStatusError(t *testing.T) {
t.Parallel()
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 503,
returnBody: []byte(samplePackageResp),
returnErr: nil,
}
out := &bytes.Buffer{}
err := createAURList(context.Background(), doer, "https://aur.archlinux.org", out, nil)
assert.EqualError(t, err, "invalid status code: 503")
}
func TestNeedsUpdate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setupFile func(t *testing.T, path string)
interval int
force bool
expectedResult bool
}{
{
name: "force returns true",
setupFile: nil,
interval: 7,
force: true,
expectedResult: true,
},
{
name: "file does not exist returns true",
setupFile: nil,
interval: 7,
force: false,
expectedResult: true,
},
{
name: "fresh file returns false",
setupFile: func(t *testing.T, path string) {
t.Helper()
err := os.WriteFile(path, []byte("test"), 0o600)
require.NoError(t, err)
},
interval: 7,
force: false,
expectedResult: false,
},
{
name: "interval -1 never updates",
setupFile: func(t *testing.T, path string) {
t.Helper()
err := os.WriteFile(path, []byte("test"), 0o600)
require.NoError(t, err)
// Set file time to 30 days ago
oldTime := time.Now().Add(-30 * 24 * time.Hour)
err = os.Chtimes(path, oldTime, oldTime)
require.NoError(t, err)
},
interval: -1,
force: false,
expectedResult: false,
},
{
name: "old file returns true",
setupFile: func(t *testing.T, path string) {
t.Helper()
err := os.WriteFile(path, []byte("test"), 0o600)
require.NoError(t, err)
// Set file time to 10 days ago
oldTime := time.Now().Add(-10 * 24 * time.Hour)
err = os.Chtimes(path, oldTime, oldTime)
require.NoError(t, err)
},
interval: 7,
force: false,
expectedResult: true,
},
{
name: "file within interval returns false",
setupFile: func(t *testing.T, path string) {
t.Helper()
err := os.WriteFile(path, []byte("test"), 0o600)
require.NoError(t, err)
// Set file time to 3 days ago
oldTime := time.Now().Add(-3 * 24 * time.Hour)
err = os.Chtimes(path, oldTime, oldTime)
require.NoError(t, err)
},
interval: 7,
force: false,
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
completionPath := filepath.Join(tmpDir, "completion")
if tt.setupFile != nil {
tt.setupFile(t, completionPath)
}
result := NeedsUpdate(completionPath, tt.interval, tt.force)
assert.Equal(t, tt.expectedResult, result)
})
}
}
// mockPkgSynchronizer implements PkgSynchronizer for testing.
type mockPkgSynchronizer struct {
packages []db.IPackage
}
func (m *mockPkgSynchronizer) SyncPackages(...string) []db.IPackage {
return m.packages
}
func Test_createRepoList(t *testing.T) {
t.Parallel()
tests := []struct {
name string
packages []db.IPackage
expectedOutput string
expectedError error
}{
{
name: "empty package list",
packages: []db.IPackage{},
expectedOutput: "",
expectedError: nil,
},
{
name: "single package",
packages: []db.IPackage{
&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
},
expectedOutput: "vim\textra\n",
expectedError: nil,
},
{
name: "multiple packages",
packages: []db.IPackage{
&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
&mock.Package{PName: "git", PDB: mock.NewDB("extra")},
&mock.Package{PName: "linux", PDB: mock.NewDB("core")},
},
expectedOutput: "vim\textra\ngit\textra\nlinux\tcore\n",
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dbExecutor := &mockPkgSynchronizer{packages: tt.packages}
out := &bytes.Buffer{}
err := createRepoList(dbExecutor, out)
if tt.expectedError != nil {
assert.EqualError(t, err, tt.expectedError.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedOutput, out.String())
})
}
}
// errorWriter is a writer that always returns an error.
type errorWriter struct{}
func (e *errorWriter) Write(p []byte) (n int, err error) {
return 0, errors.New("write error")
}
func Test_createRepoListWriteError(t *testing.T) {
t.Parallel()
dbExecutor := &mockPkgSynchronizer{
packages: []db.IPackage{
&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
},
}
err := createRepoList(dbExecutor, &errorWriter{})
assert.EqualError(t, err, "write error")
}
func TestUpdateCache(t *testing.T) {
t.Parallel()
tests := []struct {
name string
doer *mockDoer
packages []db.IPackage
expectedOutput string
expectError bool
}{
{
name: "successful update",
doer: &mockDoer{
returnStatusCode: 200,
returnBody: []byte("# Comment\npkg1\npkg2\n"),
returnErr: nil,
},
packages: []db.IPackage{
&mock.Package{PName: "vim", PDB: mock.NewDB("extra")},
},
expectedOutput: "pkg1\tAUR\npkg2\tAUR\nvim\textra\n",
expectError: false,
},
{
name: "AUR fetch error removes file",
doer: &mockDoer{
returnStatusCode: 500,
returnBody: []byte{},
returnErr: nil,
},
packages: []db.IPackage{},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
completionPath := filepath.Join(tmpDir, "subdir", "completion")
tt.doer.t = t
tt.doer.wantURL = "https://aur.archlinux.org/packages.gz"
dbExecutor := &mockPkgSynchronizer{packages: tt.packages}
err := UpdateCache(context.Background(), tt.doer, dbExecutor, "https://aur.archlinux.org", completionPath, nil)
if tt.expectError {
assert.Error(t, err)
// File should be removed on error
_, statErr := os.Stat(completionPath)
assert.True(t, os.IsNotExist(statErr))
} else {
require.NoError(t, err)
content, readErr := os.ReadFile(completionPath)
require.NoError(t, readErr)
assert.Equal(t, tt.expectedOutput, string(content))
}
})
}
}
func TestShow(t *testing.T) {
// Note: Not running in parallel because we need to capture os.Stdout
tests := []struct {
name string
setupFile func(t *testing.T, path string)
doer *mockDoer
packages []db.IPackage
interval int
force bool
expectError bool
}{
{
name: "existing fresh file",
setupFile: func(t *testing.T, path string) {
t.Helper()
err := os.WriteFile(path, []byte("cached\tdata\n"), 0o600)
require.NoError(t, err)
},
doer: nil, // Should not be called
packages: nil,
interval: 7,
force: false,
expectError: false,
},
{
name: "file needs update",
setupFile: nil,
doer: &mockDoer{
returnStatusCode: 200,
returnBody: []byte("# Comment\naur-pkg\n"),
returnErr: nil,
},
packages: []db.IPackage{
&mock.Package{PName: "repo-pkg", PDB: mock.NewDB("core")},
},
interval: 7,
force: false,
expectError: false,
},
{
name: "force update",
setupFile: nil,
doer: &mockDoer{
returnStatusCode: 200,
returnBody: []byte("# Comment\nforced-pkg\n"),
returnErr: nil,
},
packages: []db.IPackage{},
interval: 7,
force: true,
expectError: false,
},
{
name: "update cache error",
setupFile: nil,
doer: &mockDoer{
returnStatusCode: 500,
returnBody: []byte{},
returnErr: nil,
},
packages: []db.IPackage{},
interval: 7,
force: false,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Not running in parallel because we capture os.Stdout
tmpDir := t.TempDir()
completionPath := filepath.Join(tmpDir, "completion")
if tt.setupFile != nil {
tt.setupFile(t, completionPath)
}
if tt.doer != nil {
tt.doer.t = t
tt.doer.wantURL = "https://aur.archlinux.org/packages.gz"
}
dbExecutor := &mockPkgSynchronizer{packages: tt.packages}
// Capture stdout using a pipe
oldStdout := os.Stdout
r, w, pipeErr := os.Pipe()
require.NoError(t, pipeErr)
os.Stdout = w
err := Show(context.Background(), tt.doer, dbExecutor, "https://aur.archlinux.org", completionPath, tt.interval, tt.force, nil)
// Close writer first, then restore stdout, then read
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, copyErr := io.Copy(&buf, r)
r.Close()
if tt.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
require.NoError(t, copyErr)
// Verify file exists and has content
content, readErr := os.ReadFile(completionPath)
require.NoError(t, readErr)
assert.NotEmpty(t, content)
}
})
}
}
func TestShowFileOpenError(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
// Use a path that can't be created (directory as file)
completionPath := filepath.Join(tmpDir, "completion")
// Create a directory where we expect a file - this will cause OpenFile to fail
err := os.MkdirAll(completionPath, 0o755)
require.NoError(t, err)
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: []byte("# Comment\npkg\n"),
returnErr: nil,
}
dbExecutor := &mockPkgSynchronizer{packages: []db.IPackage{}}
err = Show(context.Background(), doer, dbExecutor, "https://aur.archlinux.org", completionPath, 7, true, nil)
assert.Error(t, err)
}
func TestUpdateCacheMkdirError(t *testing.T) {
t.Parallel()
// Create a file where we expect a directory - this will cause MkdirAll to fail
tmpDir := t.TempDir()
blockingFile := filepath.Join(tmpDir, "blocking")
err := os.WriteFile(blockingFile, []byte("block"), 0o600)
require.NoError(t, err)
completionPath := filepath.Join(blockingFile, "subdir", "completion")
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: []byte("# Comment\npkg\n"),
returnErr: nil,
}
dbExecutor := &mockPkgSynchronizer{packages: []db.IPackage{}}
err = UpdateCache(context.Background(), doer, dbExecutor, "https://aur.archlinux.org", completionPath, nil)
assert.Error(t, err)
}
func Test_createAURListWriteError(t *testing.T) {
t.Parallel()
doer := &mockDoer{
t: t,
wantURL: "https://aur.archlinux.org/packages.gz",
returnStatusCode: 200,
returnBody: []byte("# Comment\npkg1\npkg2\n"),
returnErr: nil,
}
err := createAURList(context.Background(), doer, "https://aur.archlinux.org", &errorWriter{}, nil)
assert.EqualError(t, err, "write error")
}