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. GetSecretObject no longer loads the times, so secret get reads no more files than before and 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
119 lines
3.7 KiB
Go
119 lines
3.7 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"))
|
|
}
|
|
|
|
// 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}
|
|
}
|