check / check (push) Waiting to run
secret list, secret version list, secret unlocker list and secret info printed times in the machine's time zone without naming it, so one vault showed different times on two machines. Entry() now sets time.Local to UTC before anything else, as AGENTS.md policy 6 asks, so every time the program prints or writes is in UTC: --json times end in Z, and a new version's name takes the UTC date. The README says so. A test runs the built secret list with TZ=America/New_York and checks the table and the JSON against the UTC time set on the secret's current file. Rule suppressed: gosmopolitan, which flags any use of time.Local. Model: opus-5-5
216 lines
6.8 KiB
Go
216 lines
6.8 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/secret/internal/cli"
|
|
"sneak.berlin/go/secret/internal/secret"
|
|
"sneak.berlin/go/secret/internal/vault"
|
|
)
|
|
|
|
const (
|
|
// listClockStep is how long TestListShowsWhenSecretWasWritten waits
|
|
// between its steps, so that a time read in one step cannot be the
|
|
// time read in the next.
|
|
listClockStep = 200 * time.Millisecond
|
|
|
|
// fileTimeSlack allows for the clock the kernel stamps files with,
|
|
// which advances only every few milliseconds: a file's modification
|
|
// time can be slightly earlier than a time.Now() read before the file
|
|
// was written.
|
|
fileTimeSlack = 100 * time.Millisecond
|
|
)
|
|
|
|
// TestListShowsWhenSecretWasWritten is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/142, where `secret list` gave
|
|
// the time of the listing as every secret's creation and last-update time.
|
|
// Two listings with a clock step between must be the same, and give the
|
|
// time of the add; after a forced add, the creation time is unchanged and
|
|
// the last update is the time of that add. It runs on a real filesystem,
|
|
// where writing a file into a directory also changes the directory's time.
|
|
func TestListShowsWhenSecretWasWritten(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = testMnemonicBuffer(t)
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
|
|
beforeAdd := time.Now()
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("one")), false)
|
|
require.NoError(t, err)
|
|
|
|
afterAdd := time.Now()
|
|
|
|
firstListing := listSecrets(t, c, true)
|
|
|
|
time.Sleep(listClockStep)
|
|
require.Equal(t, firstListing, listSecrets(t, c, true))
|
|
|
|
added := listedTimes(t, firstListing)
|
|
assert.WithinRange(t, added.CreatedAt, beforeAdd.Add(-fileTimeSlack), afterAdd)
|
|
assert.WithinRange(t, added.UpdatedAt, beforeAdd.Add(-fileTimeSlack), afterAdd)
|
|
|
|
time.Sleep(listClockStep)
|
|
|
|
beforeUpdate := time.Now()
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("two")), true)
|
|
require.NoError(t, err)
|
|
|
|
afterUpdate := time.Now()
|
|
|
|
updated := listedTimes(t, listSecrets(t, c, true))
|
|
assert.True(t, added.CreatedAt.Equal(updated.CreatedAt),
|
|
"created %s, then %s", added.CreatedAt, updated.CreatedAt)
|
|
assert.WithinRange(t, updated.UpdatedAt,
|
|
beforeUpdate.Add(-fileTimeSlack), afterUpdate)
|
|
|
|
// The table's LAST UPDATED column gives the same time, to the minute
|
|
assert.Contains(t, listSecrets(t, c, false),
|
|
updated.UpdatedAt.Format("2006-01-02 15:04"))
|
|
}
|
|
|
|
// TestMovedSecretKeepsItsTimes checks that a secret moved to another vault
|
|
// is listed there with the times it was listed with before the move, and
|
|
// not with the time of the move.
|
|
func TestMovedSecretKeepsItsTimes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
|
|
_, err := vault.CreateVault(fs, stateDir, "other", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
// "default" is created last, so it is the current vault
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
// Two versions written apart, so that the creation and the last update
|
|
// are different times
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("one")), false)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(listClockStep)
|
|
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("two")), true)
|
|
require.NoError(t, err)
|
|
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
c.Mnemonic = testMnemonicBuffer(t)
|
|
beforeMove := listedTimes(t, listSecrets(t, c, true))
|
|
|
|
time.Sleep(listClockStep)
|
|
require.NoError(t, c.MoveSecret(&cobra.Command{}, "default:x", "other", false))
|
|
require.NoError(t, vault.SelectVault(fs, stateDir, "other"))
|
|
|
|
afterMove := listedTimes(t, listSecrets(t, c, true))
|
|
assert.True(t, beforeMove.CreatedAt.Equal(afterMove.CreatedAt),
|
|
"created %s, then %s", beforeMove.CreatedAt, afterMove.CreatedAt)
|
|
assert.True(t, beforeMove.UpdatedAt.Equal(afterMove.UpdatedAt),
|
|
"updated %s, then %s", beforeMove.UpdatedAt, afterMove.UpdatedAt)
|
|
}
|
|
|
|
// TestListedTimesAreUTC is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/169, where times were printed
|
|
// in the local time zone. It runs `secret list` on the built binary with
|
|
// TZ set to New York, where the secret's last update, 03:04:05 UTC on 2
|
|
// January, is 22:04:05 the day before.
|
|
func TestListedTimesAreUTC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
vlt, err := vault.CreateVault(
|
|
afero.NewOsFs(), stateDir, "default", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = testMnemonicBuffer(t)
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("one")), false)
|
|
require.NoError(t, err)
|
|
|
|
updated := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC)
|
|
current := filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "x",
|
|
"current")
|
|
require.NoError(t, os.Chtimes(current, updated, updated))
|
|
|
|
tests := []struct {
|
|
flags []string
|
|
want string
|
|
}{
|
|
{flags: nil, want: "2026-01-02 03:04"},
|
|
{
|
|
flags: []string{"--json"},
|
|
want: `"updated_at": "2026-01-02T03:04:05Z"`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
args := append([]string{"list"}, tt.flags...)
|
|
|
|
//nolint:gosec // G204: test executes the freshly built secret binary
|
|
cmd := exec.CommandContext(t.Context(), secretBinaryPath(t), args...)
|
|
cmd.Env = []string{
|
|
secret.EnvStateDir + "=" + stateDir,
|
|
"TZ=America/New_York",
|
|
"PATH=" + os.Getenv("PATH"),
|
|
}
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "%v: %s", args, output)
|
|
assert.Contains(t, string(output), tt.want, "%v", args)
|
|
}
|
|
}
|
|
|
|
// listSecrets returns the output of `secret list`, or of
|
|
// `secret list --json` when jsonOutput is set.
|
|
func listSecrets(t *testing.T, c *cli.Instance, jsonOutput bool) string {
|
|
t.Helper()
|
|
|
|
var out bytes.Buffer
|
|
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
require.NoError(t, c.ListSecrets(cmd, jsonOutput, false, ""))
|
|
|
|
return out.String()
|
|
}
|
|
|
|
// listedTimes returns the times that the output of `secret list --json`
|
|
// gives for the one secret in the vault.
|
|
func listedTimes(t *testing.T, listing string) secret.Metadata {
|
|
t.Helper()
|
|
|
|
var parsed struct {
|
|
Secrets []map[string]string `json:"secrets"`
|
|
}
|
|
|
|
require.NoError(t, json.Unmarshal([]byte(listing), &parsed))
|
|
require.Len(t, parsed.Secrets, 1)
|
|
|
|
createdAt, err := time.Parse(time.RFC3339Nano, parsed.Secrets[0]["created_at"])
|
|
require.NoError(t, err)
|
|
|
|
updatedAt, err := time.Parse(time.RFC3339Nano, parsed.Secrets[0]["updated_at"])
|
|
require.NoError(t, err)
|
|
|
|
return secret.Metadata{CreatedAt: createdAt, UpdatedAt: updatedAt}
|
|
}
|