nightsky_test.gno
7.69 Kb · 271 lines
1package nightsky
2
3import (
4 "testing"
5 "time"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10)
11
12const (
13 owner = "g1owner"
14 guest = "g1guest"
15)
16
17func newTestRealm() *TelescopeRealm {
18 cfg := NewTelescopeConfig("Test Scope", "Seestar S50", 49.0, 1.0, owner, "/r/test/telescope")
19 return NewTelescopeRealm(&cfg)
20}
21
22func TestNewTelescopeConfig(t *testing.T) {
23 cfg := NewTelescopeConfig("Scope", "Model", 1.5, 2.5, owner, "/r/x/y")
24 uassert.Equal(t, "Scope", cfg.Name)
25 uassert.Equal(t, "offline", cfg.Status) // starts offline
26 uassert.Equal(t, owner, cfg.Owner)
27
28 cfg.UpdateStatus("online")
29 uassert.Equal(t, "online", cfg.Status)
30}
31
32func TestValidateCommand(cur realm, t *testing.T) {
33 cfg := NewTelescopeConfig("Scope", "Model", 0, 0, owner, "/r/x/y")
34
35 // Valid capture and stop should not panic.
36 uassert.NotPanics(t, cur, func() {
37 ValidateCommand(TelescopeCommand{CommandType: "capture", TargetRA: 5.5, TargetDec: 22.5, Exposure: 60}, cfg)
38 })
39 uassert.NotPanics(t, cur, func() {
40 // stop ignores coordinates entirely.
41 ValidateCommand(TelescopeCommand{CommandType: "stop", TargetRA: 999, TargetDec: 999, Exposure: 0}, cfg)
42 })
43
44 cases := []struct {
45 name string
46 cmd TelescopeCommand
47 msg string
48 }{
49 {"bad type", TelescopeCommand{CommandType: "explode"}, "invalid command type"},
50 {"RA low", TelescopeCommand{CommandType: "capture", TargetRA: -1, Exposure: 10}, "invalid RA"},
51 {"RA high", TelescopeCommand{CommandType: "capture", TargetRA: 24, Exposure: 10}, "invalid RA"},
52 {"Dec low", TelescopeCommand{CommandType: "capture", TargetRA: 1, TargetDec: -91, Exposure: 10}, "invalid Dec"},
53 {"Dec high", TelescopeCommand{CommandType: "capture", TargetRA: 1, TargetDec: 91, Exposure: 10}, "invalid Dec"},
54 {"exp low", TelescopeCommand{CommandType: "capture", TargetRA: 1, Exposure: 0}, "invalid exposure"},
55 {"exp high", TelescopeCommand{CommandType: "capture", TargetRA: 1, Exposure: 301}, "invalid exposure"},
56 }
57 for _, tc := range cases {
58 cmd := tc.cmd
59 uassert.PanicsContains(t, cur, tc.msg, func() { ValidateCommand(cmd, cfg) }, tc.name)
60 }
61}
62
63func TestIsAccessAllowed(t *testing.T) {
64 rules := avl.NewTree()
65
66 // Unknown address: denied.
67 uassert.False(t, IsAccessAllowed(guest, rules))
68
69 // Never-expires rule (zero time): allowed.
70 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Time{}})
71 uassert.True(t, IsAccessAllowed(guest, rules))
72
73 // Future expiry: allowed.
74 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Now().Add(time.Hour)})
75 uassert.True(t, IsAccessAllowed(guest, rules))
76
77 // Past expiry: denied.
78 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Now().Add(-time.Hour)})
79 uassert.False(t, IsAccessAllowed(guest, rules))
80}
81
82func TestAccessFlow(cur realm, t *testing.T) {
83 r := newTestRealm()
84
85 // Owner can always submit.
86 urequire.NotPanics(t, cur, func() {
87 r.SubmitCommand(owner, "capture", 5, 10, 30)
88 })
89
90 // Guest without a grant is denied.
91 uassert.PanicsContains(t, cur, "access denied", func() {
92 r.SubmitCommand(guest, "capture", 5, 10, 30)
93 })
94
95 // Grant, then guest can submit.
96 r.GrantAccess(owner, guest, 0) // 0 = never expires
97 uassert.Equal(t, 1, r.GetAccessRuleCount())
98 urequire.NotPanics(t, cur, func() {
99 r.SubmitCommand(guest, "capture", 5, 10, 30)
100 })
101
102 // Re-granting the same address does not duplicate the rule.
103 r.GrantAccess(owner, guest, 5)
104 uassert.Equal(t, 1, r.GetAccessRuleCount())
105
106 // Revoke, then guest is denied again.
107 r.RevokeAccess(owner, guest)
108 uassert.Equal(t, 0, r.GetAccessRuleCount())
109 uassert.PanicsContains(t, cur, "access denied", func() {
110 r.SubmitCommand(guest, "capture", 5, 10, 30)
111 })
112
113 // Non-owner cannot grant access.
114 uassert.PanicsContains(t, cur, "owner only", func() {
115 r.GrantAccess(guest, "g1evil", 0)
116 })
117}
118
119func TestCommandQueueFIFO(cur realm, t *testing.T) {
120 r := newTestRealm()
121
122 r.SubmitCommand(owner, "capture", 1, 1, 10)
123 r.SubmitCommand(owner, "capture", 2, 2, 20)
124 r.SubmitCommand(owner, "stop", 0, 0, 0)
125 uassert.Equal(t, 3, r.GetCommandCount())
126
127 // FIFO order.
128 c1 := r.GetNextCommand(owner)
129 uassert.Equal(t, "capture", c1.CommandType)
130 uassert.Equal(t, int64(10), int64(c1.Exposure))
131
132 c2 := r.GetNextCommand(owner)
133 uassert.Equal(t, int64(20), int64(c2.Exposure))
134
135 c3 := r.GetNextCommand(owner)
136 uassert.Equal(t, "stop", c3.CommandType)
137
138 uassert.Equal(t, 0, r.GetCommandCount())
139
140 // Draining an empty queue panics.
141 uassert.PanicsContains(t, cur, "no commands", func() { r.GetNextCommand(owner) })
142
143 // Only the owner may pull commands.
144 r.SubmitCommand(owner, "stop", 0, 0, 0)
145 uassert.PanicsContains(t, cur, "owner only", func() { r.GetNextCommand(guest) })
146
147 // Emergency stop clears everything and sets status online.
148 r.ClearCommandQueue(owner)
149 uassert.Equal(t, 0, r.GetCommandCount())
150 uassert.Equal(t, "online", r.Config.Status)
151}
152
153func TestRecordCaptureUpdatesStatus(cur realm, t *testing.T) {
154 r := newTestRealm()
155
156 // Submitting a command flips status to busy.
157 r.SubmitCommand(owner, "capture", 5, 10, 30)
158 uassert.Equal(t, "busy", r.Config.Status)
159
160 // Recording a capture flips it back to online and stores the capture.
161 r.RecordCapture(owner, "https://img/1.png", 5, 10, 30, owner)
162 uassert.Equal(t, "online", r.Config.Status)
163 uassert.Equal(t, 1, r.GetCaptureCount())
164
165 // Empty image URL is rejected.
166 uassert.PanicsContains(t, cur, "image URL", func() {
167 r.RecordCapture(owner, "", 5, 10, 30, owner)
168 })
169
170 // Non-owner cannot record.
171 uassert.PanicsContains(t, cur, "owner only", func() {
172 r.RecordCapture(guest, "https://img/2.png", 5, 10, 30, guest)
173 })
174}
175
176// --- CaptureFeed tests ------------------------------------------------------
177
178func mkCap(url string) CaptureResult {
179 return CaptureResult{ImageURL: url}
180}
181
182func TestCaptureFeedOrderAndLatest(t *testing.T) {
183 f := NewCaptureFeed(10)
184 uassert.Equal(t, 0, f.Len())
185
186 _, ok := f.Latest()
187 uassert.False(t, ok)
188
189 f.Add(mkCap("a"))
190 f.Add(mkCap("b"))
191 f.Add(mkCap("c"))
192 uassert.Equal(t, 3, f.Len())
193
194 latest, ok := f.Latest()
195 urequire.True(t, ok)
196 uassert.Equal(t, "c", latest.ImageURL)
197
198 // Newest-first iteration order.
199 var got []string
200 f.IterateNewest(func(c CaptureResult) bool {
201 got = append(got, c.ImageURL)
202 return false
203 })
204 urequire.Equal(t, 3, len(got))
205 uassert.Equal(t, "c", got[0])
206 uassert.Equal(t, "b", got[1])
207 uassert.Equal(t, "a", got[2])
208
209 // Early stop.
210 count := 0
211 f.IterateNewest(func(c CaptureResult) bool {
212 count++
213 return true // stop after first
214 })
215 uassert.Equal(t, 1, count)
216}
217
218func TestCaptureFeedTrim(t *testing.T) {
219 f := NewCaptureFeed(3)
220 f.Add(mkCap("1"))
221 f.Add(mkCap("2"))
222 f.Add(mkCap("3"))
223 f.Add(mkCap("4")) // evicts "1"
224 f.Add(mkCap("5")) // evicts "2"
225
226 uassert.Equal(t, 3, f.Len())
227
228 var got []string
229 f.IterateNewest(func(c CaptureResult) bool {
230 got = append(got, c.ImageURL)
231 return false
232 })
233 // Newest-first: 5, 4, 3 — oldest two evicted.
234 uassert.Equal(t, "5", got[0])
235 uassert.Equal(t, "4", got[1])
236 uassert.Equal(t, "3", got[2])
237}
238
239func TestCaptureFeedRemoveAt(t *testing.T) {
240 f := NewCaptureFeed(10)
241 f.Add(mkCap("a"))
242 f.Add(mkCap("b"))
243 f.Add(mkCap("c")) // newest-first: c(0), b(1), a(2)
244
245 // Out-of-range is a no-op returning false.
246 uassert.False(t, f.RemoveAt(-1))
247 uassert.False(t, f.RemoveAt(3))
248 uassert.Equal(t, 3, f.Len())
249
250 // Remove the middle (b).
251 uassert.True(t, f.RemoveAt(1))
252 uassert.Equal(t, 2, f.Len())
253
254 var got []string
255 f.IterateNewest(func(c CaptureResult) bool {
256 got = append(got, c.ImageURL)
257 return false
258 })
259 uassert.Equal(t, "c", got[0])
260 uassert.Equal(t, "a", got[1])
261
262 // Removing newest (c).
263 uassert.True(t, f.RemoveAt(0))
264 latest, ok := f.Latest()
265 urequire.True(t, ok)
266 uassert.Equal(t, "a", latest.ImageURL)
267}
268
269func TestNewCaptureFeedRejectsNonPositive(cur realm, t *testing.T) {
270 uassert.PanicsContains(t, cur, "must be positive", func() { NewCaptureFeed(0) })
271}