mirror of
https://github.com/Jguer/yay.git
synced 2025-12-25 11:22:10 +01:00
* 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
553 lines
13 KiB
Go
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")
|
|
}
|