Harden CLI local vault spool durability
This commit is contained in:
+22
-2
@@ -1051,6 +1051,12 @@ func (a *app) localVaultSpool(args []string) error {
|
|||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(*spoolFile) == "" {
|
||||||
|
return errors.New("--spool-file is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(*file) == "" {
|
||||||
|
return errors.New("--file is required")
|
||||||
|
}
|
||||||
raw, err := os.ReadFile(*file)
|
raw, err := os.ReadFile(*file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1066,12 +1072,26 @@ func (a *app) localVaultSpool(args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
|
||||||
canonical, err := attesto.CanonicalJSON(obj)
|
canonical, err := attesto.CanonicalJSON(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = fh.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := fh.Write(append(canonical, '\n')); err != nil {
|
line := append(canonical, '\n')
|
||||||
|
written, err := fh.Write(line)
|
||||||
|
if err != nil {
|
||||||
|
_ = fh.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if written != len(line) {
|
||||||
|
_ = fh.Close()
|
||||||
|
return fmt.Errorf("short spool write: wrote %d of %d bytes", written, len(line))
|
||||||
|
}
|
||||||
|
if err := fh.Sync(); err != nil {
|
||||||
|
_ = fh.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fh.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return a.write(map[string]any{"ok": true, "spoolFile": *spoolFile})
|
return a.write(map[string]any{"ok": true, "spoolFile": *spoolFile})
|
||||||
|
|||||||
@@ -158,6 +158,54 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalVaultSpoolAndStatus(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
eventFile := filepath.Join(dir, "event.json")
|
||||||
|
spoolFile := filepath.Join(dir, "spool", "events.jsonl")
|
||||||
|
if err := os.WriteFile(eventFile, []byte(`{"z":2,"a":1}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"--json", "local-vault", "spool", "--spool-file", spoolFile, "--file", eventFile}, &stdout, &stderr, testEnv(t, nil))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(spoolFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(raw) != "{\"a\":1,\"z\":2}\n" {
|
||||||
|
t.Fatalf("spool must write canonical JSONL, got %q", raw)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(spoolFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm() != 0o600 {
|
||||||
|
t.Fatalf("spool mode = %o, want 600", info.Mode().Perm())
|
||||||
|
}
|
||||||
|
stdout.Reset()
|
||||||
|
stderr.Reset()
|
||||||
|
code = run([]string{"--json", "local-vault", "status", "--spool-file", spoolFile}, &stdout, &stderr, testEnv(t, nil))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), `"events": 1`) || !strings.Contains(stdout.String(), `"ok": true`) {
|
||||||
|
t.Fatalf("unexpected status output: %s", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalVaultSpoolRequiresPaths(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"--json", "local-vault", "spool", "--spool-file", filepath.Join(t.TempDir(), "events.jsonl")}, &stdout, &stderr, testEnv(t, nil))
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatal("missing --file must fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "--file is required") {
|
||||||
|
t.Fatalf("missing required path error: %s", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStreamsCreateCallsAPI(t *testing.T) {
|
func TestStreamsCreateCallsAPI(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost {
|
if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost {
|
||||||
|
|||||||
Reference in New Issue
Block a user