check / check (push) Failing after 1s
errmsg_test.go now invokes the functions that actually emit each user-visible message (findManifest, verifyRequiredSigner, collectInputPaths, generateManifestOperation, openManifestReader, fetchManifestOperation, downloadFile, finishDownload, sanitizePath, verifyDownloadedHash) and asserts on what they return. No production format string is restated in the test, so rewording a message now fails the suite instead of passing against a copied literal. The signer-mismatch message needs a signed manifest, so it is driven against one signed by a throwaway gpg key and skipped where gpg is absent, as the repo's other signing tests are. Freshen's mtime-presence test now gives the scanned file an epoch mtime, so an absent manifest mtime misread as the epoch is distinguishable and the test fails if recordEntry's nil guard is dropped. Model: opus-4-8
201 lines
5.5 KiB
Go
201 lines
5.5 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
// stubFileInfo is a minimal fs.FileInfo for exercising recordEntry
|
|
// without touching a filesystem.
|
|
type stubFileInfo struct {
|
|
size int64
|
|
mtime time.Time
|
|
}
|
|
|
|
func (s stubFileInfo) Name() string { return "stub" }
|
|
func (s stubFileInfo) Size() int64 { return s.size }
|
|
func (s stubFileInfo) Mode() os.FileMode { return 0 }
|
|
func (s stubFileInfo) ModTime() time.Time { return s.mtime }
|
|
func (s stubFileInfo) IsDir() bool { return false }
|
|
func (s stubFileInfo) Sys() any { return nil }
|
|
|
|
// setupFreshenDir populates /testdir with two files, scans it, and
|
|
// writes the resulting manifest to /testdir/.index.mf.
|
|
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
|
t.Helper()
|
|
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "content1")
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "content2")
|
|
|
|
// Generate initial manifest
|
|
opts := &mfer.ScannerOptions{Fs: fs}
|
|
s := mfer.NewScannerWithOptions(opts)
|
|
require.NoError(t, s.EnumeratePath(testDir, nil))
|
|
|
|
var manifestBuf bytes.Buffer
|
|
|
|
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
|
|
|
// Write manifest to filesystem
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, "/testdir/.index.mf", manifestBuf.Bytes(), 0o644))
|
|
}
|
|
|
|
func TestFreshenUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
setupFreshenDir(t, fs)
|
|
|
|
// Parse manifest to verify
|
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 2)
|
|
}
|
|
|
|
func TestFreshenWithChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
setupFreshenDir(t, fs)
|
|
|
|
// Verify initial manifest has 2 files
|
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 2)
|
|
|
|
// Add a new file
|
|
writeTestFile(t, fs, "/testdir/file3.txt", "content3")
|
|
|
|
// Modify file2 (change content and size)
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "modified content2")
|
|
|
|
// Remove file1
|
|
require.NoError(t, fs.Remove(testFile1))
|
|
|
|
// Note: The freshen operation would need to be run here
|
|
// For now, we just verify the test setup is correct
|
|
exists, _ := afero.Exists(fs, testFile1)
|
|
assert.False(t, exists)
|
|
|
|
exists, _ = afero.Exists(fs, "/testdir/file3.txt")
|
|
assert.True(t, exists)
|
|
|
|
content, _ := afero.ReadFile(fs, "/testdir/file2.txt")
|
|
assert.Equal(t, "modified content2", string(content))
|
|
}
|
|
|
|
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
|
// with respect to MFFilePath.Mtime, which is a message pointer with
|
|
// proto3 field presence and may legitimately be absent.
|
|
//
|
|
// An absent mtime must never be read as time.Unix(0, 0): that value
|
|
// never equals a real modification time, so every entry would be
|
|
// classified as changed, re-hashed, and the manifest rewritten
|
|
// unconditionally - the exact inverse of what freshen is for, and
|
|
// silent. An entry with no mtime is therefore "changed" because it
|
|
// cannot be compared, not because it looks like it dates from 1970.
|
|
func TestFreshenRecordEntryMtimePresence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const relPath = "file1.txt"
|
|
|
|
// The scanned file's mtime is the Unix epoch. If recordEntry ever misreads
|
|
// an absent manifest mtime as the epoch, the "absent" case below would
|
|
// compare equal to this and be classified unchanged, so the test fails.
|
|
mtime := time.Unix(0, 0)
|
|
info := stubFileInfo{size: 8, mtime: mtime}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry *mfer.MFFilePath
|
|
needsHash bool
|
|
changed int64
|
|
unchanged int64
|
|
}{
|
|
{
|
|
name: "matching mtime and size is unchanged",
|
|
entry: &mfer.MFFilePath{
|
|
Path: relPath,
|
|
Size: 8,
|
|
Mtime: &mfer.Timestamp{Seconds: mtime.Unix()},
|
|
},
|
|
needsHash: false,
|
|
changed: 0,
|
|
unchanged: 1,
|
|
},
|
|
{
|
|
name: "absent mtime is changed, not epoch",
|
|
entry: &mfer.MFFilePath{
|
|
Path: relPath,
|
|
Size: 8,
|
|
Mtime: nil,
|
|
},
|
|
needsHash: true,
|
|
changed: 1,
|
|
unchanged: 0,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &freshenScanner{
|
|
existingByPath: map[string]*mfer.MFFilePath{relPath: tc.entry},
|
|
}
|
|
s.recordEntry(relPath, info)
|
|
|
|
require.Len(t, s.entries, 1)
|
|
assert.Equal(t, tc.needsHash, s.entries[0].needsHash)
|
|
assert.Equal(t, tc.changed, s.changed)
|
|
assert.Equal(t, tc.unchanged, s.unchanged)
|
|
assert.Zero(t, s.added)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFreshenAddExistingRejectsMissingMtime pins that an entry with no
|
|
// mtime is never carried forward into a rebuilt manifest with a
|
|
// fabricated epoch timestamp.
|
|
func TestFreshenAddExistingRejectsMissingMtime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := mfer.NewBuilder()
|
|
entry := &mfer.MFFilePath{
|
|
Path: "file1.txt",
|
|
Size: 8,
|
|
Mtime: nil,
|
|
Hashes: []*mfer.MFFileChecksum{
|
|
{MultiHash: []byte{0x12, 0x20}},
|
|
},
|
|
}
|
|
|
|
err := addExistingToBuilder(b, entry)
|
|
require.ErrorIs(t, err, errEntryMissingMtime)
|
|
assert.Contains(t, err.Error(), "file1.txt")
|
|
}
|
|
|
|
// TestEntryMtime pins the presence semantics the callers depend on.
|
|
func TestEntryMtime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, ok := entryMtime(&mfer.MFFilePath{Mtime: nil})
|
|
assert.False(t, ok)
|
|
assert.True(t, got.IsZero())
|
|
|
|
got, ok = entryMtime(&mfer.MFFilePath{
|
|
Mtime: &mfer.Timestamp{Seconds: 1_700_000_000, Nanos: 500},
|
|
})
|
|
assert.True(t, ok)
|
|
assert.Equal(t, time.Unix(1_700_000_000, 500), got)
|
|
}
|