docs: add maintainer guidance and API contract

This commit is contained in:
zhoujun0601
2026-08-13 10:36:00 -04:00
parent 0a17b6b1c9
commit b5b36674d3
17 changed files with 649 additions and 11 deletions
+10
View File
@@ -19,6 +19,10 @@ import (
func (a *App) api(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/api/v1/")
if p == "system/status" {
if r.Method != http.MethodGet {
jsonOut(w, http.StatusMethodNotAllowed, map[string]string{"error": "方法不允许"})
return
}
connected := false
a.mu.RLock()
for _, rt := range a.runtimes {
@@ -97,6 +101,12 @@ func (a *App) api(w http.ResponseWriter, r *http.Request) {
rt.syncing.Lock()
d := rt.dash
rt.syncing.Unlock()
if d.Limits == nil {
d.Limits = []LimitBucket{}
}
if d.Usage == nil {
d.Usage = []UsagePoint{}
}
jsonOut(w, 200, d)
}
case p == "sync" && r.Method == "POST":
+27 -11
View File
@@ -96,22 +96,38 @@ func (a *App) smtpTest(w http.ResponseWriter, r *http.Request) {
}
jsonOut(w, 200, map[string]bool{"ok": true})
}
const smtpTimeout = 35 * time.Second
func sendSMTP(s SMTPSettings, subject, textBody, htmlBody string) error {
return sendSMTPWithTimeout(s, subject, textBody, htmlBody, smtpTimeout)
}
func sendSMTPWithTimeout(s SMTPSettings, subject, textBody, htmlBody string, timeout time.Duration) error {
addr := net.JoinHostPort(s.Host, strconv.Itoa(s.Port))
var c *smtp.Client
var e error
if s.Security == "tls" {
conn, x := tls.Dial("tcp", addr, &tls.Config{ServerName: s.Host, MinVersion: tls.VersionTLS12})
if x != nil {
return x
}
c, e = smtp.NewClient(conn, s.Host)
} else {
c, e = smtp.Dial(addr)
}
dialer := net.Dialer{Timeout: timeout}
conn, e := dialer.Dial("tcp", addr)
if e != nil {
return e
}
if e = conn.SetDeadline(time.Now().Add(timeout)); e != nil {
_ = conn.Close()
return e
}
var c *smtp.Client
if s.Security == "tls" {
tlsConn := tls.Client(conn, &tls.Config{ServerName: s.Host, MinVersion: tls.VersionTLS12})
if e = tlsConn.Handshake(); e != nil {
_ = conn.Close()
return e
}
conn = tlsConn
}
c, e = smtp.NewClient(conn, s.Host)
if e != nil {
_ = conn.Close()
return e
}
defer c.Close()
if s.Security == "starttls" {
if e = c.StartTLS(&tls.Config{ServerName: s.Host, MinVersion: tls.VersionTLS12}); e != nil {
+37
View File
@@ -1,6 +1,7 @@
package app
import (
"net"
"strconv"
"strings"
"testing"
@@ -9,6 +10,42 @@ import (
"codex-helper/internal/store"
)
func TestSendSMTPStopsWhenServerDoesNotRespond(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan net.Conn, 1)
go func() {
conn, acceptErr := listener.Accept()
if acceptErr == nil {
accepted <- conn
}
}()
host, portRaw, err := net.SplitHostPort(listener.Addr().String())
if err != nil {
t.Fatal(err)
}
port, err := strconv.Atoi(portRaw)
if err != nil {
t.Fatal(err)
}
started := time.Now()
err = sendSMTPWithTimeout(SMTPSettings{Host: host, Port: port, From: "from@example.com", To: "to@example.com"}, "subject", "text", "html", 100*time.Millisecond)
if err == nil {
t.Fatal("sendSMTPWithTimeout unexpectedly succeeded")
}
if elapsed := time.Since(started); elapsed > time.Second {
t.Fatalf("SMTP timeout took %s", elapsed)
}
select {
case conn := <-accepted:
_ = conn.Close()
default:
}
}
func newReminderTestApp(t *testing.T) *App {
t.Helper()
s, err := store.Open(t.TempDir())
+41
View File
@@ -2,12 +2,53 @@ package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"codex-helper/internal/security"
)
func TestSystemStatusRejectsNonGETMethods(t *testing.T) {
a := newReminderTestApp(t)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/v1/system/status", nil)
a.api(recorder, request)
if recorder.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
}
func TestDashboardSerializesNilListsAsEmptyArrays(t *testing.T) {
a := newReminderTestApp(t)
a.runtimes[1] = &accountRuntime{}
_, err := a.store.DB.Exec("INSERT INTO sessions(token_hash,expires_at,created_at) VALUES(?,?,?)", security.HashToken("test-session"), time.Now().Add(time.Hour).Unix(), time.Now().Unix())
if err != nil {
t.Fatal(err)
}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboard?accountId=1", nil)
request.AddCookie(&http.Cookie{Name: "session", Value: "test-session"})
a.api(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
var body struct {
Limits []LimitBucket `json:"limits"`
Usage []UsagePoint `json:"usage"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Limits == nil || body.Usage == nil {
t.Fatalf("nil lists in response: %s", recorder.Body.String())
}
}
type fakeCodexClient struct {
mu sync.Mutex
connected bool