check / check (push) Waiting to run
secret list and secret list --json gave the time of the listing as every secret's creation and last-update time, because Secret.LoadMetadata set both to the current time. It now reads file modification times, so listing still needs no passphrase: the last update is when the secret's current file was written, and the creation is when its oldest version's value.age was written. A move to another vault gives the copied files the times of the originals. GetSecretObject no longer loads the times, so secret get keeps its error for a missing current file; secret list calls LoadMetadata itself. Judgement call: the creation time comes from value.age, not from the oldest version's directory, whose time changes when the next add rewrites that version's metadata.age. Model: opus-5-5
162 lines
5.2 KiB
Go
162 lines
5.2 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"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)
|
|
}
|
|
|
|
// 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}
|
|
}
|