package api import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" gin "github.com/gin-gonic/gin" proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) func newTestServer(t *testing.T) *Server { t.Helper() gin.SetMode(gin.TestMode) tmpDir := t.TempDir() authDir := filepath.Join(tmpDir, "auth") if err := os.MkdirAll(authDir, 0o700); err != nil { t.Fatalf("failed to create auth dir: %v", err) } cfg := &proxyconfig.Config{ SDKConfig: sdkconfig.SDKConfig{ APIKeys: []string{"test-key"}, }, Port: 0, AuthDir: authDir, Debug: true, LoggingToFile: false, UsageStatisticsEnabled: false, } authManager := auth.NewManager(nil, nil, nil) accessManager := sdkaccess.NewManager() configPath := filepath.Join(tmpDir, "config.yaml") return NewServer(cfg, authManager, accessManager, configPath) } func TestAmpProviderModelRoutes(t *testing.T) { testCases := []struct { name string path string wantStatus int wantContains string }{ { name: "openai root models", path: "/api/provider/openai/models", wantStatus: http.StatusOK, wantContains: `"object":"list"`, }, { name: "groq root models", path: "/api/provider/groq/models", wantStatus: http.StatusOK, wantContains: `"object":"list"`, }, { name: "openai models", path: "/api/provider/openai/v1/models", wantStatus: http.StatusOK, wantContains: `"object":"list"`, }, { name: "anthropic models", path: "/api/provider/anthropic/v1/models", wantStatus: http.StatusOK, wantContains: `"data"`, }, { name: "google models v1", path: "/api/provider/google/v1/models", wantStatus: http.StatusOK, wantContains: `"models"`, }, { name: "google models v1beta", path: "/api/provider/google/v1beta/models", wantStatus: http.StatusOK, wantContains: `"models"`, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { server := newTestServer(t) req := httptest.NewRequest(http.MethodGet, tc.path, nil) req.Header.Set("Authorization", "Bearer test-key") rr := httptest.NewRecorder() server.engine.ServeHTTP(rr, req) if rr.Code != tc.wantStatus { t.Fatalf("unexpected status code for %s: got %d want %d; body=%s", tc.path, rr.Code, tc.wantStatus, rr.Body.String()) } if body := rr.Body.String(); !strings.Contains(body, tc.wantContains) { t.Fatalf("response body for %s missing %q: %s", tc.path, tc.wantContains, body) } }) } } func TestDefaultRequestLoggerFactory_UsesResolvedLogDirectory(t *testing.T) { t.Setenv("WRITABLE_PATH", "") t.Setenv("writable_path", "") originalWD, errGetwd := os.Getwd() if errGetwd != nil { t.Fatalf("failed to get current working directory: %v", errGetwd) } tmpDir := t.TempDir() if errChdir := os.Chdir(tmpDir); errChdir != nil { t.Fatalf("failed to switch working directory: %v", errChdir) } defer func() { if errChdirBack := os.Chdir(originalWD); errChdirBack != nil { t.Fatalf("failed to restore working directory: %v", errChdirBack) } }() // Force ResolveLogDirectory to fallback to auth-dir/logs by making ./logs not a writable directory. if errWriteFile := os.WriteFile(filepath.Join(tmpDir, "logs"), []byte("not-a-directory"), 0o644); errWriteFile != nil { t.Fatalf("failed to create blocking logs file: %v", errWriteFile) } configDir := filepath.Join(tmpDir, "config") if errMkdirConfig := os.MkdirAll(configDir, 0o755); errMkdirConfig != nil { t.Fatalf("failed to create config dir: %v", errMkdirConfig) } configPath := filepath.Join(configDir, "config.yaml") authDir := filepath.Join(tmpDir, "auth") if errMkdirAuth := os.MkdirAll(authDir, 0o700); errMkdirAuth != nil { t.Fatalf("failed to create auth dir: %v", errMkdirAuth) } cfg := &proxyconfig.Config{ SDKConfig: proxyconfig.SDKConfig{ RequestLog: false, }, AuthDir: authDir, ErrorLogsMaxFiles: 10, } logger := defaultRequestLoggerFactory(cfg, configPath) fileLogger, ok := logger.(*internallogging.FileRequestLogger) if !ok { t.Fatalf("expected *FileRequestLogger, got %T", logger) } errLog := fileLogger.LogRequestWithOptions( "/v1/chat/completions", http.MethodPost, map[string][]string{"Content-Type": []string{"application/json"}}, []byte(`{"input":"hello"}`), http.StatusBadGateway, map[string][]string{"Content-Type": []string{"application/json"}}, []byte(`{"error":"upstream failure"}`), nil, nil, nil, true, "issue-1711", time.Now(), time.Now(), ) if errLog != nil { t.Fatalf("failed to write forced error request log: %v", errLog) } authLogsDir := filepath.Join(authDir, "logs") authEntries, errReadAuthDir := os.ReadDir(authLogsDir) if errReadAuthDir != nil { t.Fatalf("failed to read auth logs dir %s: %v", authLogsDir, errReadAuthDir) } foundErrorLogInAuthDir := false for _, entry := range authEntries { if strings.HasPrefix(entry.Name(), "error-") && strings.HasSuffix(entry.Name(), ".log") { foundErrorLogInAuthDir = true break } } if !foundErrorLogInAuthDir { t.Fatalf("expected forced error log in auth fallback dir %s, got entries: %+v", authLogsDir, authEntries) } configLogsDir := filepath.Join(configDir, "logs") configEntries, errReadConfigDir := os.ReadDir(configLogsDir) if errReadConfigDir != nil && !os.IsNotExist(errReadConfigDir) { t.Fatalf("failed to inspect config logs dir %s: %v", configLogsDir, errReadConfigDir) } for _, entry := range configEntries { if strings.HasPrefix(entry.Name(), "error-") && strings.HasSuffix(entry.Name(), ".log") { t.Fatalf("unexpected forced error log in config dir %s", configLogsDir) } } }