e60b10256a
Address two gaps in the first DingTalk-session UI: - add an explicit admin-only '+ 新增钉钉知识库' action on the data-source panel; it accepts an alidocs space URL, resolves the live name/root through the authenticated sidecar, writes the inventory root atomically, upserts a deterministic source connection, and enqueues its first catalog refresh - read DingTalk's real QR canvas pixels rather than taking a DOM screenshot; select by dark-pixel content, composite onto a white quiet zone, and render at 320x320 so the dialog contains a large complete code, not a page thumbnail The live refresh path resolves registered roots from the connection locator, while existing name-based DingTalk sources remain compatible.
95 lines
3.6 KiB
Go
95 lines
3.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDingtalkSessionProxyRelaysSidecarStates(t *testing.T) {
|
|
sidecar := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/sessions":
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": "abc123", "state": "qr", "created_at": 1, "expires_at": 2})
|
|
case r.Method == http.MethodGet && r.URL.Path == "/sessions/abc123":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": "abc123", "state": "success"})
|
|
case r.Method == http.MethodGet && r.URL.Path == "/sessions/missing":
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = w.Write([]byte(`{"error":"session_not_found"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer sidecar.Close()
|
|
t.Setenv("DINGTALK_LOGIN_SIDECAR_URL", sidecar.URL)
|
|
|
|
start := httptest.NewRequest(http.MethodPost, "/api/v1/dingtalk-session", strings.NewReader(`{}`))
|
|
start = start.WithContext(context.WithValue(start.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: roleAdmin}))
|
|
rec := httptest.NewRecorder()
|
|
startDingtalkSession(rec, start)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("start proxy status = %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var created map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created["state"] != "qr" {
|
|
t.Fatalf("relayed state = %v", created["state"])
|
|
}
|
|
|
|
poll := httptest.NewRequest(http.MethodGet, "/api/v1/dingtalk-session/abc123", nil)
|
|
poll = poll.WithContext(context.WithValue(poll.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: roleAdmin}))
|
|
poll.SetPathValue("id", "abc123")
|
|
rec = httptest.NewRecorder()
|
|
getDingtalkSession(rec, poll)
|
|
if !strings.Contains(rec.Body.String(), `"success"`) {
|
|
t.Fatalf("poll body = %s", rec.Body.String())
|
|
}
|
|
|
|
missing := httptest.NewRequest(http.MethodGet, "/api/v1/dingtalk-session/missing", nil)
|
|
missing = missing.WithContext(context.WithValue(missing.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: roleAdmin}))
|
|
missing.SetPathValue("id", "missing")
|
|
rec = httptest.NewRecorder()
|
|
getDingtalkSession(rec, missing)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("missing status = %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestDingtalkSessionRejectsUnauthenticatedAndHostileIDs(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
startDingtalkSession(rec, httptest.NewRequest(http.MethodPost, "/api/v1/dingtalk-session", nil))
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated start = %d, want 401", rec.Code)
|
|
}
|
|
if got := safeSessionID("../etc/passwd"); got != "" {
|
|
t.Fatalf("hostile id accepted: %q", got)
|
|
}
|
|
for _, test := range []struct{ raw, want string }{
|
|
{"https://alidocs.dingtalk.com/i/spaces/9JOGOQneRgkneX4Q/overview?rnd=1", "9JOGOQneRgkneX4Q"},
|
|
{"9JOGOQneRgkneX4Q", "9JOGOQneRgkneX4Q"},
|
|
{"https://example.com/not-a-space", ""},
|
|
} {
|
|
if got := extractDingtalkSpaceID(test.raw); got != test.want {
|
|
t.Fatalf("extractDingtalkSpaceID(%q) = %q, want %q", test.raw, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDingtalkSessionUnavailableWithoutSidecar(t *testing.T) {
|
|
t.Setenv("DINGTALK_LOGIN_SIDECAR_URL", "")
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dingtalk-session/abc", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: roleAdmin}))
|
|
req.SetPathValue("id", "abc")
|
|
rec := httptest.NewRecorder()
|
|
getDingtalkSession(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("sidecar down status = %d, want 503", rec.Code)
|
|
}
|
|
}
|