Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions go/adk/pkg/auth/token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package auth

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestWithUserIDAndUserIDFromContext(t *testing.T) {
ctx := context.Background()
assert.Equal(t, "", UserIDFromContext(ctx))

ctx = WithUserID(ctx, "user-123")
assert.Equal(t, "user-123", UserIDFromContext(ctx))
}

func TestKAgentTokenService_GetToken_InitiallyEmpty(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
assert.Equal(t, "", svc.GetToken())
}

func TestKAgentTokenService_AddHeaders(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
svc.mu.Lock()
svc.token = "abc123"
svc.mu.Unlock()

req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
req = req.WithContext(WithUserID(req.Context(), "user-42"))

svc.AddHeaders(req)

assert.Equal(t, "my-agent", req.Header.Get("X-Agent-Name"))
assert.Equal(t, "Bearer abc123", req.Header.Get("Authorization"))
assert.Equal(t, "user-42", req.Header.Get("X-User-Id"))
}

func TestKAgentTokenService_AddHeaders_NoTokenNoUser(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)

svc.AddHeaders(req)

assert.Equal(t, "my-agent", req.Header.Get("X-Agent-Name"))
assert.Equal(t, "", req.Header.Get("Authorization"))
assert.Equal(t, "", req.Header.Get("X-User-Id"))
}

func TestKAgentTokenService_Stop_SafeMultipleCalls(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
assert.NotPanics(t, func() {
svc.Stop()
svc.Stop()
svc.Stop()
})
}

func TestKAgentTokenService_Start_MissingTokenFile(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
ctx, cancel := context.WithCancel(t.Context())
defer cancel()

err := svc.Start(ctx)
require.NoError(t, err)
assert.Equal(t, "", svc.GetToken())

svc.Stop()
}

func TestKAgentTokenService_ReadToken_MissingFile(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
token, err := svc.readToken()
assert.Error(t, err)
assert.Empty(t, token)
}

type stubRoundTripper struct {
req *http.Request
resp *http.Response
err error
}

func (s *stubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
s.req = req
if s.resp != nil {
return s.resp, s.err
}
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, s.err
}

func TestTokenRoundTripper_InjectsHeaders(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
svc.mu.Lock()
svc.token = "xyz"
svc.mu.Unlock()

base := &stubRoundTripper{}
rt := &TokenRoundTripper{base: base, tokenService: svc}

req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
resp, err := rt.RoundTrip(req)

require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, "Bearer xyz", base.req.Header.Get("Authorization"))
assert.Equal(t, "my-agent", base.req.Header.Get("X-Agent-Name"))
}

func TestTokenRoundTripper_NilTokenService(t *testing.T) {
base := &stubRoundTripper{}
rt := &TokenRoundTripper{base: base, tokenService: nil}

req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
_, err := rt.RoundTrip(req)

require.NoError(t, err)
assert.Equal(t, "", base.req.Header.Get("Authorization"))
}

func TestNewHTTPClientWithToken(t *testing.T) {
svc := NewKAgentTokenService("my-agent")
client := NewHTTPClientWithToken(svc)

require.NotNil(t, client)
rt, ok := client.Transport.(*TokenRoundTripper)
require.True(t, ok)
assert.Equal(t, svc, rt.tokenService)
assert.Equal(t, 30*time.Second, client.Timeout)
}
182 changes: 182 additions & 0 deletions go/adk/pkg/runner/adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package runner

import (
"context"
"embed"
"encoding/json"
"strings"
"testing"

"github.com/go-logr/logr"
"github.com/kagent-dev/kagent/go/adk/pkg/controllerclient"
"github.com/kagent-dev/kagent/go/api/adk"
"github.com/kagent-dev/mockllm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
adksession "google.golang.org/adk/v2/session"
)

//go:embed testdata
var testdata embed.FS

func startMock(t *testing.T, mockFile string) string {
t.Helper()
cfg, err := mockllm.LoadConfigFromFile(mockFile, testdata)
require.NoError(t, err)
server := mockllm.NewServer(cfg)
baseURL, err := server.Start(t.Context())
require.NoError(t, err)
t.Cleanup(func() { server.Stop(context.Background()) }) //nolint:errcheck
return baseURL
}

func loadConfig(t *testing.T, path string, baseURL string) *adk.AgentConfig {
t.Helper()
data, err := testdata.ReadFile(path)
require.NoError(t, err)

raw := strings.ReplaceAll(string(data), "{{BASE_URL}}", baseURL)

var cfg adk.AgentConfig
require.NoError(t, json.Unmarshal([]byte(raw), &cfg))
return &cfg
}

// Pure helper functions.

func TestAgentNameFromAppName(t *testing.T) {
tests := []struct {
name string
appName string
want string
}{
{"no namespace marker returns as-is", "myagent", "myagent"},
{"namespace marker strips prefix", "default__NS__myagent", "myagent"},
{"multiple markers uses last occurrence", "a__NS__b__NS__c", "c"},
{"empty string", "", ""},
{"marker at end returns empty suffix", "default__NS__", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, agentNameFromAppName(tt.appName))
})
}
}

func TestSplitCSV(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{"empty string returns nil", "", nil},
{"single value", "foo", []string{"foo"}},
{"multiple values", "foo,bar,baz", []string{"foo", "bar", "baz"}},
{"trims whitespace around values", " foo , bar ,baz ", []string{"foo", "bar", "baz"}},
{"skips empty entries", "foo,,bar,", []string{"foo", "bar"}},
{"all empty entries returns nil", " , , ", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, splitCSV(tt.in))
})
}
}

// buildTokenPropagationPlugin: env-var driven branching.

func TestBuildTokenPropagationPlugin_DisabledByDefault(t *testing.T) {
t.Setenv("KAGENT_PROPAGATE_TOKEN", "")
t.Setenv("STS_WELL_KNOWN_URI", "")

plugin, err := buildTokenPropagationPlugin(context.Background(), logr.Discard())
require.NoError(t, err)
assert.Nil(t, plugin, "plugin should be disabled when neither env var is set")
}

func TestBuildTokenPropagationPlugin_PropagateOnlyMode(t *testing.T) {
t.Setenv("KAGENT_PROPAGATE_TOKEN", "true")
t.Setenv("STS_WELL_KNOWN_URI", "")

plugin, err := buildTokenPropagationPlugin(context.Background(), logr.Discard())
require.NoError(t, err)
require.NotNil(t, plugin, "plugin should be enabled in propagate-only mode without STS exchange")
}

func TestBuildTokenPropagationPlugin_PropagateFlagCaseInsensitive(t *testing.T) {
t.Setenv("KAGENT_PROPAGATE_TOKEN", "TRUE")
t.Setenv("STS_WELL_KNOWN_URI", "")

plugin, err := buildTokenPropagationPlugin(context.Background(), logr.Discard())
require.NoError(t, err)
require.NotNil(t, plugin)
}

// CreateRunnerConfig: full path via mockllm.

func TestCreateRunnerConfig_MinimalOpenAI(t *testing.T) {
baseURL := startMock(t, "testdata/mock_openai.json")
t.Setenv("OPENAI_API_KEY", "test-key")

cfg := loadConfig(t, "testdata/config_openai.json", baseURL)
sessionService := adksession.InMemoryService()

runnerCfg, err := CreateRunnerConfig(context.Background(), cfg, sessionService, "myapp", nil, nil)
require.NoError(t, err)
assert.Equal(t, "myapp", runnerCfg.AppName)
assert.NotNil(t, runnerCfg.Agent)
assert.Equal(t, sessionService, runnerCfg.SessionService)
}

func TestCreateRunnerConfig_DefaultsAppNameWhenEmpty(t *testing.T) {
baseURL := startMock(t, "testdata/mock_openai.json")
t.Setenv("OPENAI_API_KEY", "test-key")

cfg := loadConfig(t, "testdata/config_openai.json", baseURL)

runnerCfg, err := CreateRunnerConfig(context.Background(), cfg, nil, "", nil, nil)
require.NoError(t, err)
assert.Equal(t, "kagent-app", runnerCfg.AppName)
assert.NotNil(t, runnerCfg.SessionService, "should fall back to an in-memory session service")
}

func TestCreateRunnerConfig_ShareToolsRequiresControllerClient(t *testing.T) {
baseURL := startMock(t, "testdata/mock_openai.json")
t.Setenv("OPENAI_API_KEY", "test-key")

cfg := loadConfig(t, "testdata/config_openai.json", baseURL)
shareTools := true
cfg.ShareTools = &shareTools

// controllerClient is nil, so share tools should be silently skipped
// rather than causing an error (per the `controllerClient != nil` guard).
runnerCfg, err := CreateRunnerConfig(context.Background(), cfg, nil, "myapp", nil, nil)
require.NoError(t, err)
assert.NotNil(t, runnerCfg.Agent)
}

func TestCreateRunnerConfig_ShareToolsWithControllerClient(t *testing.T) {
baseURL := startMock(t, "testdata/mock_openai.json")
t.Setenv("OPENAI_API_KEY", "test-key")

cfg := loadConfig(t, "testdata/config_openai.json", baseURL)
shareTools := true
cfg.ShareTools = &shareTools

controllerClient := &controllerclient.Client{}

runnerCfg, err := CreateRunnerConfig(context.Background(), cfg, nil, "myapp", nil, controllerClient)
require.NoError(t, err)
assert.NotNil(t, runnerCfg.Agent, "agent should build successfully with share tools wired in")
}

func TestCreateRunnerConfig_MissingModelFails(t *testing.T) {
cfg := &adk.AgentConfig{
Description: "test",
Instruction: "test",
// Model deliberately omitted.
}

_, err := CreateRunnerConfig(context.Background(), cfg, nil, "myapp", nil, nil)
require.Error(t, err)
}
9 changes: 9 additions & 0 deletions go/adk/pkg/runner/testdata/config_openai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"model": {
"type": "openai",
"model": "gpt-4.1-mini",
"base_url": "{{BASE_URL}}/v1"
},
"description": "test",
"instruction": "You are helpful. Answer concisely."
}
35 changes: 35 additions & 0 deletions go/adk/pkg/runner/testdata/mock_openai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"openai": [
{
"name": "catch_all",
"match": {
"match_type": "contains",
"message": {
"content": "2+2",
"role": "user"
}
},
"response": {
"id": "chatcmpl-mock-1",
"object": "chat.completion",
"created": 1700000000,
"model": "mock-model",
"choices": [
{
"index": 0,
"message": {
"content": "The answer is 4.",
"role": "assistant"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
}
}
]
}
Loading
Loading