Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 5.0 KiB
1package audit
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10)
11
12func writeLog(t *testing.T, path string, records []Record) {
13 t.Helper()
14 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
15 if err != nil {
16 t.Fatal(err)
17 }
18 defer f.Close()
19 at := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
20 for i, r := range records {
21 line := Format(r, at.Add(time.Duration(i)*time.Second))
22 if _, err := f.WriteString(line); err != nil {
23 t.Fatal(err)
24 }
25 }
26}
27
28func records(n int) []Record {
29 out := make([]Record, 0, n)
30 for i := 0; i < n; i++ {
31 out = append(out, Record{
32 Action: fmt.Sprintf("action.%d", i),
33 Actor: fmt.Sprintf("actor-%d", i),
34 Subject: "demo",
35 Detail: fmt.Sprintf("detail %d", i),
36 })
37 }
38 return out
39}
40
41func TestParseRoundTripsFormat(t *testing.T) {
42 at := time.Date(2026, 10, 27, 12, 0, 0, 0, time.UTC)
43 line := Format(Record{Action: "acl.update", Actor: "keenan", Subject: "vole", Detail: "role=admin"}, at)
44 e, err := Parse(strings.TrimSuffix(line, "\n"))
45 if err != nil {
46 t.Fatal(err)
47 }
48 if !e.Time.Equal(at) || e.Action != "acl.update" || e.Actor != "keenan" ||
49 e.Subject != "vole" || e.Detail != "role=admin" {
50 t.Fatalf("Parse() = %+v", e)
51 }
52}
53
54func TestParseRejectsBrokenLines(t *testing.T) {
55 for _, line := range []string{
56 "",
57 "2026-01-01T00:00:00Z\tonly.three\tcolumns",
58 "not-a-time\taction\t-\t-\tdetail",
59 } {
60 if _, err := Parse(line); err == nil {
61 t.Errorf("Parse(%q) succeeded, want an error", line)
62 }
63 }
64}
65
66func TestTailPagesNewestFirst(t *testing.T) {
67 path := filepath.Join(t.TempDir(), "audit.log")
68 writeLog(t, path, records(5))
69
70 cases := []struct {
71 offset, limit int
72 wantActions []string
73 wantMore bool
74 }{
75 {0, 2, []string{"action.4", "action.3"}, true},
76 {2, 2, []string{"action.2", "action.1"}, true},
77 {4, 2, []string{"action.0"}, false},
78 {5, 2, nil, false},
79 {0, 10, []string{"action.4", "action.3", "action.2", "action.1", "action.0"}, false},
80 }
81 for _, c := range cases {
82 got, more, err := Tail(path, c.offset, c.limit)
83 if err != nil {
84 t.Fatalf("Tail(%d, %d): %v", c.offset, c.limit, err)
85 }
86 if more != c.wantMore {
87 t.Errorf("Tail(%d, %d) more = %v, want %v", c.offset, c.limit, more, c.wantMore)
88 }
89 var actions []string
90 for _, e := range got {
91 actions = append(actions, e.Action)
92 }
93 if strings.Join(actions, ",") != strings.Join(c.wantActions, ",") {
94 t.Errorf("Tail(%d, %d) = %v, want %v", c.offset, c.limit, actions, c.wantActions)
95 }
96 }
97}
98
99// A line the reader cannot parse must not hide the records around it.
100func TestTailSkipsMalformedLines(t *testing.T) {
101 path := filepath.Join(t.TempDir(), "audit.log")
102 writeLog(t, path, records(3))
103 f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o600)
104 if err != nil {
105 t.Fatal(err)
106 }
107 if _, err := f.WriteString("garbage without columns\n"); err != nil {
108 t.Fatal(err)
109 }
110 f.Close()
111
112 got, _, err := Tail(path, 0, 10)
113 if err != nil {
114 t.Fatal(err)
115 }
116 if len(got) != 3 || got[0].Action != "action.2" || got[2].Action != "action.0" {
117 t.Fatalf("Tail() = %+v", got)
118 }
119}
120
121// The writer always terminates a line, but a hand-edited or legacy file may not.
122func TestTailReadsAnUnterminatedLastLine(t *testing.T) {
123 path := filepath.Join(t.TempDir(), "audit.log")
124 at := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
125 line := strings.TrimSuffix(Format(Record{Action: "push.force", Actor: "keenan", Subject: "demo"}, at), "\n")
126 if err := os.WriteFile(path, []byte(line), 0o600); err != nil {
127 t.Fatal(err)
128 }
129 got, more, err := Tail(path, 0, 10)
130 if err != nil || more {
131 t.Fatalf("Tail() = %v, %v", more, err)
132 }
133 if len(got) != 1 || got[0].Action != "push.force" {
134 t.Fatalf("Tail() = %+v", got)
135 }
136}
137
138func TestTailMissingFileIsEmpty(t *testing.T) {
139 got, more, err := Tail(filepath.Join(t.TempDir(), "missing.log"), 0, 10)
140 if err != nil || more || len(got) != 0 {
141 t.Fatalf("Tail() = %+v, %v, %v", got, more, err)
142 }
143}
144
145// A record larger than one read chunk exercises the pending-line path, and a
146// deep page proves the arithmetic holds across many chunks.
147func TestTailPagesAcrossChunks(t *testing.T) {
148 path := filepath.Join(t.TempDir(), "audit.log")
149 const n = 2000
150 writeLog(t, path, records(n))
151
152 got, more, err := Tail(path, n-3, 2)
153 if err != nil {
154 t.Fatal(err)
155 }
156 if !more {
157 t.Fatal("a page before the last record should report an older page")
158 }
159 if len(got) != 2 || got[0].Action != "action.2" || got[1].Action != "action.1" {
160 t.Fatalf("deep page = %+v", got)
161 }
162
163 last, more, err := Tail(path, n-1, 5)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if more || len(last) != 1 || last[0].Action != "action.0" {
168 t.Fatalf("last page = %+v, more=%v, err=%v", last, more, err)
169 }
170}
171
172func TestTailRefusesASymlink(t *testing.T) {
173 dir := t.TempDir()
174 target := filepath.Join(dir, "real.log")
175 writeLog(t, target, records(1))
176 link := filepath.Join(dir, "audit.log")
177 if err := os.Symlink(target, link); err != nil {
178 t.Skipf("symlink unavailable: %v", err)
179 }
180 if _, _, err := Tail(link, 0, 10); err == nil {
181 t.Fatal("Tail() followed a symlink")
182 }
183}
184