Skip to content

Commit 6cbdd71

Browse files
committed
test: add screener command tests + fix box ID parsing
- 12 tests covering list (empty/populated), approve, deny, spam, feed, trail, and missing-args validation for all subcommands - httptest server mocks the /clearances HTML and PATCH endpoints - Fixed feed/trail box ID regex to use (?s) for cross-line matching - Updated surface snapshot for new screener commands
1 parent d3e1ce3 commit 6cbdd71

3 files changed

Lines changed: 333 additions & 11 deletions

File tree

.surface

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ hey recordings --starts-on
6666
hey reply
6767
hey reply --from
6868
hey reply --message
69+
hey screener
70+
hey screener approve
71+
hey screener deny
72+
hey screener feed
73+
hey screener spam
74+
hey screener trail
6975
hey seen
7076
hey setup
7177
hey skill

internal/cmd/screener.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -233,28 +233,29 @@ func fetchScreenerItems(ctx context.Context) ([]screenerItem, string, string, er
233233
}
234234
}
235235

236-
// Extract feed/trail box IDs from hidden form inputs
237-
feedRe := regexp.MustCompile(`data-clearances-target="feedboxButton"[^<]*<input[^>]*name="designation_box_id"[^>]*value="(\d+)"`)
236+
// Extract feed/trail box IDs from hidden form inputs.
237+
// The HTML has forms where designation_box_id appears before the feedbox/trailbox button,
238+
// so we use (?s) to match across newlines.
239+
feedRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*feedboxButton`)
238240
if m := feedRe.FindStringSubmatch(body); m != nil {
239241
feedBoxID = m[1]
240242
}
241-
// Alternative: find feedbox button form
243+
// Alternative pattern: button target appears after the input in the same form
242244
if feedBoxID == "" {
243-
// Look for the pattern: feedboxButton form has designation_box_id
244-
feedForms := regexp.MustCompile(`feedboxButton.*?designation_box_id.*?value="(\d+)"`).FindStringSubmatch(body)
245-
if feedForms != nil {
246-
feedBoxID = feedForms[1]
245+
altFeedRe := regexp.MustCompile(`(?s)designation_box_id"[^>]*value="(\d+)".*?feedboxButton`)
246+
if m := altFeedRe.FindStringSubmatch(body); m != nil {
247+
feedBoxID = m[1]
247248
}
248249
}
249250

250-
trailRe := regexp.MustCompile(`data-clearances-target="trailboxButton"[^<]*<input[^>]*name="designation_box_id"[^>]*value="(\d+)"`)
251+
trailRe := regexp.MustCompile(`(?s)designation_box_id[^>]*value="(\d+)"[^<]*</input>?[^<]*<button[^>]*trailboxButton`)
251252
if m := trailRe.FindStringSubmatch(body); m != nil {
252253
trailBoxID = m[1]
253254
}
254255
if trailBoxID == "" {
255-
trailForms := regexp.MustCompile(`trailboxButton.*?designation_box_id.*?value="(\d+)"`).FindStringSubmatch(body)
256-
if trailForms != nil {
257-
trailBoxID = trailForms[1]
256+
altTrailRe := regexp.MustCompile(`(?s)designation_box_id"[^>]*value="(\d+)".*?trailboxButton`)
257+
if m := altTrailRe.FindStringSubmatch(body); m != nil {
258+
trailBoxID = m[1]
258259
}
259260
}
260261

internal/cmd/screener_test.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
12+
"github.com/basecamp/hey-cli/internal/output"
13+
)
14+
15+
const clearancesHTMLTemplate = `<!DOCTYPE html>
16+
<html>
17+
<head><title>The Screener</title></head>
18+
<body>
19+
<div data-controller="clearances">
20+
%s
21+
</div>
22+
</body>
23+
</html>`
24+
25+
func clearanceItemHTML(postingID, senderEmail, subject, feedBoxID, trailBoxID string) string {
26+
return fmt.Sprintf(`
27+
<div data-clearances-target="clearance" data-clearance-id="%[1]s">
28+
<span>%[3]s</span>
29+
<span>%[2]s</span>
30+
<form action="/clearances/%[1]s" method="post">
31+
<input type="hidden" name="_method" value="patch">
32+
<input type="hidden" name="status" value="approved">
33+
<button data-clearances-target="screenInButton">Screen in</button>
34+
</form>
35+
<form action="/clearances/%[1]s" method="post">
36+
<input type="hidden" name="_method" value="patch">
37+
<input type="hidden" name="status" value="approved">
38+
<input type="hidden" name="designation_box_id" value="%[4]s">
39+
<button data-clearances-target="feedboxButton">Screen in to Feed</button>
40+
</form>
41+
<form action="/clearances/%[1]s" method="post">
42+
<input type="hidden" name="_method" value="patch">
43+
<input type="hidden" name="status" value="approved">
44+
<input type="hidden" name="designation_box_id" value="%[5]s">
45+
<button data-clearances-target="trailboxButton">Screen in to Paper Trail</button>
46+
</form>
47+
<form action="/clearances/%[1]s" method="post">
48+
<input type="hidden" name="_method" value="patch">
49+
<input type="hidden" name="status" value="denied">
50+
<button data-clearances-target="screenOutButton">No</button>
51+
</form>
52+
<form action="/clearances/%[1]s" method="post">
53+
<input type="hidden" name="_method" value="patch">
54+
<input type="hidden" name="status" value="denied">
55+
<input type="hidden" name="spam" value="true">
56+
<button data-clearances-target="spamButton">Spam</button>
57+
</form>
58+
</div>`,
59+
postingID, senderEmail, subject, feedBoxID, trailBoxID)
60+
}
61+
62+
type clearanceTestItem struct {
63+
PostingID string
64+
Sender string
65+
Subject string
66+
}
67+
68+
func screenerServer(t *testing.T, items []clearanceTestItem) *httptest.Server {
69+
t.Helper()
70+
71+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72+
switch {
73+
case r.Method == "GET" && r.URL.Path == "/clearances.json":
74+
w.Header().Set("Content-Type", "application/json")
75+
fmt.Fprintf(w, `{"pending_clearances_count":%d}`, len(items))
76+
77+
case r.Method == "GET" && r.URL.Path == "/clearances":
78+
var itemsHTML string
79+
for _, item := range items {
80+
itemsHTML += clearanceItemHTML(item.PostingID, item.Sender, item.Subject, "4848561", "4848564")
81+
}
82+
w.Header().Set("Content-Type", "text/html")
83+
fmt.Fprintf(w, clearancesHTMLTemplate, itemsHTML)
84+
85+
case r.Method == "PATCH" && strings.HasPrefix(r.URL.Path, "/clearances/"):
86+
_ = r.ParseForm()
87+
w.Header().Set("Location", "/clearances")
88+
w.WriteHeader(302)
89+
90+
case r.Method == "GET" && r.URL.Path == "/me.json":
91+
w.Header().Set("Content-Type", "application/json")
92+
_, _ = w.Write([]byte(`{"id": 1}`))
93+
94+
default:
95+
w.WriteHeader(404)
96+
}
97+
}))
98+
}
99+
100+
func runScreener(t *testing.T, server *httptest.Server, args ...string) (output.Response, error) {
101+
t.Helper()
102+
t.Setenv("HEY_TOKEN", "test-token")
103+
t.Setenv("HEY_NO_KEYRING", "1")
104+
t.Setenv("HEY_BASE_URL", "")
105+
tmpDir := t.TempDir()
106+
t.Setenv("XDG_CONFIG_HOME", tmpDir)
107+
t.Setenv("XDG_STATE_HOME", tmpDir)
108+
t.Setenv("XDG_CACHE_HOME", tmpDir)
109+
110+
root := newRootCmd()
111+
var buf bytes.Buffer
112+
root.SetOut(&buf)
113+
root.SetErr(&buf)
114+
root.SetArgs(append([]string{"screener", "--json", "--base-url", server.URL}, args...))
115+
116+
err := root.Execute()
117+
var resp output.Response
118+
if buf.Len() > 0 {
119+
_ = json.Unmarshal(buf.Bytes(), &resp)
120+
}
121+
return resp, err
122+
}
123+
124+
func TestScreenerListEmpty(t *testing.T) {
125+
server := screenerServer(t, nil)
126+
defer server.Close()
127+
128+
resp, err := runScreener(t, server)
129+
if err != nil {
130+
t.Fatalf("execute: %v", err)
131+
}
132+
if resp.Summary != "0 pending" {
133+
t.Errorf("summary = %q, want %q", resp.Summary, "0 pending")
134+
}
135+
}
136+
137+
func TestScreenerListWithItems(t *testing.T) {
138+
items := []clearanceTestItem{
139+
{PostingID: "123456", Sender: "promo@example.com", Subject: "Your order is ready"},
140+
{PostingID: "789012", Sender: "noreply@shop.com", Subject: "Welcome to our store"},
141+
}
142+
server := screenerServer(t, items)
143+
defer server.Close()
144+
145+
resp, err := runScreener(t, server)
146+
if err != nil {
147+
t.Fatalf("execute: %v", err)
148+
}
149+
if resp.Summary != "2 pending" {
150+
t.Errorf("summary = %q, want %q", resp.Summary, "2 pending")
151+
}
152+
if resp.Data == nil {
153+
t.Fatal("expected data, got nil")
154+
}
155+
dataSlice, ok := resp.Data.([]any)
156+
if !ok {
157+
t.Fatalf("expected []any, got %T", resp.Data)
158+
}
159+
if len(dataSlice) != 2 {
160+
t.Errorf("expected 2 items, got %d", len(dataSlice))
161+
}
162+
}
163+
164+
func TestScreenerApprove(t *testing.T) {
165+
items := []clearanceTestItem{
166+
{PostingID: "123456", Sender: "promo@example.com", Subject: "Your order"},
167+
}
168+
server := screenerServer(t, items)
169+
defer server.Close()
170+
171+
resp, err := runScreener(t, server, "approve", "123456")
172+
if err != nil {
173+
t.Fatalf("execute: %v", err)
174+
}
175+
data, ok := resp.Data.(map[string]any)
176+
if !ok {
177+
t.Fatalf("expected map, got %T", resp.Data)
178+
}
179+
if data["posting_id"] != "123456" {
180+
t.Errorf("posting_id = %v, want %q", data["posting_id"], "123456")
181+
}
182+
if action, _ := data["action"].(string); action != "Screened in to Imbox" {
183+
t.Errorf("action = %q, want %q", action, "Screened in to Imbox")
184+
}
185+
}
186+
187+
func TestScreenerDeny(t *testing.T) {
188+
items := []clearanceTestItem{
189+
{PostingID: "123456", Sender: "spam@bad.com", Subject: "You won!"},
190+
}
191+
server := screenerServer(t, items)
192+
defer server.Close()
193+
194+
resp, err := runScreener(t, server, "deny", "123456")
195+
if err != nil {
196+
t.Fatalf("execute: %v", err)
197+
}
198+
data, ok := resp.Data.(map[string]any)
199+
if !ok {
200+
t.Fatalf("expected map, got %T", resp.Data)
201+
}
202+
if action, _ := data["action"].(string); action != "Screened out" {
203+
t.Errorf("action = %q, want %q", action, "Screened out")
204+
}
205+
}
206+
207+
func TestScreenerSpam(t *testing.T) {
208+
items := []clearanceTestItem{
209+
{PostingID: "123456", Sender: "spam@bad.com", Subject: "Buy now!"},
210+
}
211+
server := screenerServer(t, items)
212+
defer server.Close()
213+
214+
resp, err := runScreener(t, server, "spam", "123456")
215+
if err != nil {
216+
t.Fatalf("execute: %v", err)
217+
}
218+
data, ok := resp.Data.(map[string]any)
219+
if !ok {
220+
t.Fatalf("expected map, got %T", resp.Data)
221+
}
222+
if action, _ := data["action"].(string); action != "Marked as spam" {
223+
t.Errorf("action = %q, want %q", action, "Marked as spam")
224+
}
225+
}
226+
227+
func TestScreenerFeed(t *testing.T) {
228+
items := []clearanceTestItem{
229+
{PostingID: "123456", Sender: "newsletter@blog.com", Subject: "Weekly digest"},
230+
}
231+
server := screenerServer(t, items)
232+
defer server.Close()
233+
234+
resp, err := runScreener(t, server, "feed", "123456")
235+
if err != nil {
236+
t.Fatalf("execute: %v", err)
237+
}
238+
data, ok := resp.Data.(map[string]any)
239+
if !ok {
240+
t.Fatalf("expected map, got %T", resp.Data)
241+
}
242+
if action, _ := data["action"].(string); action != "Screened in to Feed" {
243+
t.Errorf("action = %q, want %q", action, "Screened in to Feed")
244+
}
245+
}
246+
247+
func TestScreenerTrail(t *testing.T) {
248+
items := []clearanceTestItem{
249+
{PostingID: "123456", Sender: "receipts@store.com", Subject: "Your receipt"},
250+
}
251+
server := screenerServer(t, items)
252+
defer server.Close()
253+
254+
resp, err := runScreener(t, server, "trail", "123456")
255+
if err != nil {
256+
t.Fatalf("execute: %v", err)
257+
}
258+
data, ok := resp.Data.(map[string]any)
259+
if !ok {
260+
t.Fatalf("expected map, got %T", resp.Data)
261+
}
262+
if action, _ := data["action"].(string); action != "Screened in to Paper Trail" {
263+
t.Errorf("action = %q, want %q", action, "Screened in to Paper Trail")
264+
}
265+
}
266+
267+
func TestScreenerApproveNoArgs(t *testing.T) {
268+
server := screenerServer(t, nil)
269+
defer server.Close()
270+
271+
_, err := runScreener(t, server, "approve")
272+
if err == nil {
273+
t.Fatal("expected error for missing posting ID")
274+
}
275+
}
276+
277+
func TestScreenerDenyNoArgs(t *testing.T) {
278+
server := screenerServer(t, nil)
279+
defer server.Close()
280+
281+
_, err := runScreener(t, server, "deny")
282+
if err == nil {
283+
t.Fatal("expected error for missing posting ID")
284+
}
285+
}
286+
287+
func TestScreenerSpamNoArgs(t *testing.T) {
288+
server := screenerServer(t, nil)
289+
defer server.Close()
290+
291+
_, err := runScreener(t, server, "spam")
292+
if err == nil {
293+
t.Fatal("expected error for missing posting ID")
294+
}
295+
}
296+
297+
func TestScreenerFeedNoArgs(t *testing.T) {
298+
server := screenerServer(t, nil)
299+
defer server.Close()
300+
301+
_, err := runScreener(t, server, "feed")
302+
if err == nil {
303+
t.Fatal("expected error for missing posting ID")
304+
}
305+
}
306+
307+
func TestScreenerTrailNoArgs(t *testing.T) {
308+
server := screenerServer(t, nil)
309+
defer server.Close()
310+
311+
_, err := runScreener(t, server, "trail")
312+
if err == nil {
313+
t.Fatal("expected error for missing posting ID")
314+
}
315+
}

0 commit comments

Comments
 (0)