mirror of
https://github.com/tiennm99/claude-code-routine-cron.git
synced 2026-05-19 17:28:39 +00:00
175 lines
4.6 KiB
Go
175 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
func captureLogs() (*slog.Logger, *bytes.Buffer) {
|
|
var buf bytes.Buffer
|
|
h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
|
|
return slog.New(h), &buf
|
|
}
|
|
|
|
func mustTemplate(t *testing.T, raw string) *template.Template {
|
|
t.Helper()
|
|
tpl, err := template.New("t").Parse(raw)
|
|
if err != nil {
|
|
t.Fatalf("template parse: %v", err)
|
|
}
|
|
return tpl
|
|
}
|
|
|
|
func TestFire_HappyPath(t *testing.T) {
|
|
var gotMethod, gotAuth, gotVersion, gotBeta, gotContentType string
|
|
var gotBody []byte
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotMethod = r.Method
|
|
gotAuth = r.Header.Get("Authorization")
|
|
gotVersion = r.Header.Get("anthropic-version")
|
|
gotBeta = r.Header.Get("anthropic-beta")
|
|
gotContentType = r.Header.Get("Content-Type")
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
_, _ = w.Write([]byte(`{"type":"routine_fire","claude_code_session_id":"sess_1","claude_code_session_url":"https://claude.ai/code/sess_1"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
logger, logbuf := captureLogs()
|
|
c := &FireClient{
|
|
URL: srv.URL,
|
|
Token: "tok",
|
|
HTTP: &http.Client{Timeout: 5 * time.Second},
|
|
Template: mustTemplate(t, "fire {{.Cron}}"),
|
|
TZ: time.UTC,
|
|
Log: logger,
|
|
}
|
|
|
|
if err := c.Fire(context.Background(), "0 9 * * *"); err != nil {
|
|
t.Fatalf("Fire returned error: %v", err)
|
|
}
|
|
|
|
if gotMethod != http.MethodPost {
|
|
t.Errorf("method = %q", gotMethod)
|
|
}
|
|
if gotAuth != "Bearer tok" {
|
|
t.Errorf("auth = %q", gotAuth)
|
|
}
|
|
if gotVersion != headerVersion {
|
|
t.Errorf("anthropic-version = %q", gotVersion)
|
|
}
|
|
if gotBeta != headerBeta {
|
|
t.Errorf("anthropic-beta = %q", gotBeta)
|
|
}
|
|
if gotContentType != "application/json" {
|
|
t.Errorf("content-type = %q", gotContentType)
|
|
}
|
|
|
|
var body fireRequest
|
|
if err := json.Unmarshal(gotBody, &body); err != nil {
|
|
t.Fatalf("body unmarshal: %v", err)
|
|
}
|
|
if body.Text != "fire 0 9 * * *" {
|
|
t.Errorf("body.text = %q", body.Text)
|
|
}
|
|
|
|
if !strings.Contains(logbuf.String(), "fire ok") {
|
|
t.Errorf("expected 'fire ok' log, got: %s", logbuf.String())
|
|
}
|
|
if !strings.Contains(logbuf.String(), "https://claude.ai/code/sess_1") {
|
|
t.Errorf("expected session url in log, got: %s", logbuf.String())
|
|
}
|
|
}
|
|
|
|
func TestFire_NonOK(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
logger, logbuf := captureLogs()
|
|
c := &FireClient{
|
|
URL: srv.URL,
|
|
Token: "tok",
|
|
HTTP: &http.Client{Timeout: 5 * time.Second},
|
|
Template: mustTemplate(t, "x"),
|
|
TZ: time.UTC,
|
|
Log: logger,
|
|
}
|
|
|
|
if err := c.Fire(context.Background(), "0 9 * * *"); err != nil {
|
|
t.Fatalf("Fire should swallow non-2xx, got error: %v", err)
|
|
}
|
|
out := logbuf.String()
|
|
if !strings.Contains(out, "non-2xx") {
|
|
t.Errorf("expected non-2xx log, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, "401") {
|
|
t.Errorf("expected status 401 in log, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestFire_NetworkError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close() // close immediately so requests fail
|
|
|
|
logger, logbuf := captureLogs()
|
|
c := &FireClient{
|
|
URL: srv.URL,
|
|
Token: "tok",
|
|
HTTP: &http.Client{Timeout: 1 * time.Second},
|
|
Template: mustTemplate(t, "x"),
|
|
TZ: time.UTC,
|
|
Log: logger,
|
|
}
|
|
|
|
if err := c.Fire(context.Background(), "0 9 * * *"); err != nil {
|
|
t.Fatalf("Fire should swallow network error, got: %v", err)
|
|
}
|
|
if !strings.Contains(logbuf.String(), "fire request failed") {
|
|
t.Errorf("expected request-failed log, got: %s", logbuf.String())
|
|
}
|
|
}
|
|
|
|
func TestFire_TemplateUsesNowFunc(t *testing.T) {
|
|
var gotBody []byte
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
fixed := time.Date(2026, 5, 8, 23, 30, 0, 0, time.UTC)
|
|
logger, _ := captureLogs()
|
|
c := &FireClient{
|
|
URL: srv.URL,
|
|
Token: "tok",
|
|
HTTP: &http.Client{Timeout: 5 * time.Second},
|
|
Template: mustTemplate(t, "{{.LocalTime}}"),
|
|
TZ: time.UTC,
|
|
Log: logger,
|
|
NowFunc: func() time.Time { return fixed },
|
|
}
|
|
|
|
if err := c.Fire(context.Background(), "0 9 * * *"); err != nil {
|
|
t.Fatalf("Fire: %v", err)
|
|
}
|
|
var body fireRequest
|
|
if err := json.Unmarshal(gotBody, &body); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if body.Text != "2026-05-08 23:30 UTC" {
|
|
t.Errorf("body.text = %q", body.Text)
|
|
}
|
|
}
|