wraith/internal/ai/oauth_test.go
Vantz Stockwell 7ee5321d69
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
feat: AI copilot backend — OAuth PKCE, Claude API streaming, 16 tools, conversations
- OAuth PKCE flow for Max subscription auth (no API key needed)
- Claude API client with SSE streaming (Messages API v1)
- 16 tool definitions: terminal, SFTP, RDP, session management
- Tool dispatch router mapping to existing Wraith services
- Conversation manager with SQLite persistence
- Terminal output ring buffer for AI context
- RDP screenshot encoder (RGBA → JPEG with downscaling)
- Wired into Wails app as AIService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:09:23 -04:00

92 lines
2.3 KiB
Go

package ai
import (
"crypto/sha256"
"encoding/base64"
"path/filepath"
"testing"
"github.com/vstockwell/wraith/internal/db"
"github.com/vstockwell/wraith/internal/settings"
"github.com/vstockwell/wraith/internal/vault"
)
func TestGenerateCodeVerifier(t *testing.T) {
v, err := generateCodeVerifier()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 32 bytes -> 43 base64url chars (no padding)
if len(v) != 43 {
t.Errorf("expected verifier length 43, got %d", len(v))
}
if !isBase64URL(v) {
t.Errorf("verifier contains non-base64url characters: %s", v)
}
// Should be different each time
v2, _ := generateCodeVerifier()
if v == v2 {
t.Error("two generated verifiers should not be identical")
}
}
func TestGenerateCodeChallenge(t *testing.T) {
verifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
challenge := generateCodeChallenge(verifier)
// Verify it matches a manually computed S256 hash
h := sha256.Sum256([]byte(verifier))
expected := base64.RawURLEncoding.EncodeToString(h[:])
if challenge != expected {
t.Errorf("expected challenge %s, got %s", expected, challenge)
}
// Same verifier should produce the same challenge (deterministic)
challenge2 := generateCodeChallenge(verifier)
if challenge != challenge2 {
t.Error("challenge should be deterministic for the same verifier")
}
}
func TestGenerateState(t *testing.T) {
s, err := generateState()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 32 bytes -> 43 base64url chars (no padding)
if len(s) != 43 {
t.Errorf("expected state length 43, got %d", len(s))
}
if !isBase64URL(s) {
t.Errorf("state contains non-base64url characters: %s", s)
}
}
func TestIsAuthenticatedWhenNoTokens(t *testing.T) {
database, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.Migrate(database); err != nil {
t.Fatalf("migrate: %v", err)
}
t.Cleanup(func() { database.Close() })
settingsSvc := settings.NewSettingsService(database)
key := vault.DeriveKey("test-password", []byte("test-salt-1234567890123456789012"))
vaultSvc := vault.NewVaultService(key)
oauth := NewOAuthManager(settingsSvc, vaultSvc)
if oauth.IsAuthenticated() {
t.Error("expected IsAuthenticated to return false when no tokens are stored")
}
}