forked from lei.chen/myxce
Complete the natural-language scheduler agent's Phase 0 read-only pipeline: Task #2 (API Key auth mechanism): - New store/apikey.go: APIKey store (hash, validate, expiry) - controllers/apikey.go: GenerateAPIKey finally persists + registers RBAC role - controllers/auth.go: GetUsernameFromToken recognizes xce_ak_ prefix - middleware/validatehandler.go: isValidToken handles API Key - config/rbac.yaml: mcp_readonly role (view-only permissions) - 6 new test files covering unit + integration full-chain Task #3 (schedulerclient Go library): - types.go: URM types (URMJob/URMQueue/URMNode/JobStatus/Metrics) - client.go: read-only Client interface (6 methods) - httpclient.go: resty-based HTTP implementation - statusmap.go + _test.go: TDD status mapping (33 tests) Task #4 (mcp-server binary): - cmd/main.go: entry, reads XCE_API_URL/XCE_API_KEY, registers 6 MCP tools - internal/app.go: tool handlers wiring schedulerclient to MCP stdio - Uses github.com/mark3labs/mcp-go@v0.57.0 for stdio transport All: go vet clean, go build clean, schedulerclient 33/33 tests pass. mcp-server binary ~11MB arm64, CGO_ENABLED=0. Architecture: Claude Code --stdio--> mcp-server --HTTP--> apiserver REST API --cgo--> C backend Co-Authored-By: Claude <noreply@anthropic.com>
94 lines
2.2 KiB
Go
94 lines
2.2 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func resetAPIKeyState() {
|
|
InitStores()
|
|
}
|
|
|
|
func TestHashAPIKey_Deterministic(t *testing.T) {
|
|
h1 := HashAPIKey("xce_ak_abc123")
|
|
h2 := HashAPIKey("xce_ak_abc123")
|
|
if h1 != h2 {
|
|
t.Errorf("HashAPIKey should be deterministic: %q != %q", h1, h2)
|
|
}
|
|
h3 := HashAPIKey("xce_ak_different")
|
|
if h1 == h3 {
|
|
t.Error("HashAPIKey should differ for different inputs")
|
|
}
|
|
}
|
|
|
|
func TestHashAPIKey_NeverPlaintext(t *testing.T) {
|
|
raw := "xce_ak_secret-value"
|
|
hashed := HashAPIKey(raw)
|
|
if hashed == raw {
|
|
t.Error("HashAPIKey must not return the raw key unchanged")
|
|
}
|
|
}
|
|
|
|
func TestAddAPIKey_ValidateAPIKey_RoundTrip(t *testing.T) {
|
|
resetAPIKeyState()
|
|
|
|
raw := "xce_ak_roundtrip"
|
|
entry := APIKeyEntry{
|
|
Username: "mcp-bot",
|
|
RoleName: "mcp_readonly",
|
|
CreateTime: time.Now(),
|
|
ExpiryTime: time.Now().Add(24 * time.Hour),
|
|
}
|
|
AddAPIKey(raw, entry)
|
|
|
|
got, ok := ValidateAPIKey(raw)
|
|
if !ok {
|
|
t.Fatal("expected ValidateAPIKey to succeed for a freshly-added key")
|
|
}
|
|
if got.Username != "mcp-bot" {
|
|
t.Errorf("Username = %q, want mcp-bot", got.Username)
|
|
}
|
|
if got.RoleName != "mcp_readonly" {
|
|
t.Errorf("RoleName = %q, want mcp_readonly", got.RoleName)
|
|
}
|
|
}
|
|
|
|
func TestValidateAPIKey_Unknown(t *testing.T) {
|
|
resetAPIKeyState()
|
|
|
|
_, ok := ValidateAPIKey("xce_ak_never-issued")
|
|
if ok {
|
|
t.Error("expected ValidateAPIKey to fail for a key that was never added")
|
|
}
|
|
}
|
|
|
|
func TestValidateAPIKey_Expired(t *testing.T) {
|
|
resetAPIKeyState()
|
|
|
|
raw := "xce_ak_expired"
|
|
AddAPIKey(raw, APIKeyEntry{
|
|
Username: "mcp-bot",
|
|
RoleName: "mcp_readonly",
|
|
CreateTime: time.Now().Add(-48 * time.Hour),
|
|
ExpiryTime: time.Now().Add(-1 * time.Hour), // already expired
|
|
})
|
|
|
|
_, ok := ValidateAPIKey(raw)
|
|
if ok {
|
|
t.Error("expected ValidateAPIKey to fail for an expired key")
|
|
}
|
|
}
|
|
|
|
func TestValidateAPIKey_StoredByHashNotPlaintext(t *testing.T) {
|
|
resetAPIKeyState()
|
|
|
|
raw := "xce_ak_hashed-storage"
|
|
AddAPIKey(raw, APIKeyEntry{Username: "mcp-bot", ExpiryTime: time.Now().Add(time.Hour)})
|
|
|
|
if APIKeys.Has(raw) {
|
|
t.Error("store must not contain the raw key as a map key")
|
|
}
|
|
if !APIKeys.Has(HashAPIKey(raw)) {
|
|
t.Error("store should contain the hashed key")
|
|
}
|
|
}
|