Skip to content

Commit a99db0a

Browse files
author
jxu3
committed
Implement HTTP server with graceful shutdown and worker pool
- Added internal server implementation with HTTP server and worker pool for handling requests. - Introduced new request/response structures for OpenAI compatibility in transform package. - Updated integration tests to validate API endpoints, ensuring server is running before tests. - Refactored test utilities to support new server structure and configuration. - Created unit tests for authentication, configuration loading, and logger initialization. - Enhanced error handling and logging throughout the application.
1 parent 010531d commit a99db0a

File tree

15 files changed

+666
-203
lines changed

15 files changed

+666
-203
lines changed

cmd/github-copilot-svcs/main.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@ package main
33
import (
44
"os"
55

6-
"github.com/privapps/github-copilot-svcs/internal/cli"
7-
"github.com/privapps/github-copilot-svcs/internal/logger"
6+
"github.com/privapps/github-copilot-svcs/internal"
87
)
98

109
// version will be set by the build process
1110
var version = "dev"
1211

1312
func main() {
1413
// Initialize logger early
15-
logger.Init()
14+
internal.Init()
1615

1716
const minArgsRequired = 2
1817
if len(os.Args) < minArgsRequired {
19-
cli.PrintUsage()
18+
internal.PrintUsage()
2019
return
2120
}
2221

23-
if err := cli.RunCommand(os.Args[1], os.Args[2:], version); err != nil {
24-
logger.Error("Command failed", "error", err)
22+
if err := internal.RunCommand(os.Args[1], os.Args[2:], version); err != nil {
23+
internal.Error("Command failed", err)
2524
os.Exit(1)
2625
}
2726
}
Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package auth
1+
package internal
22

33
import (
44
"encoding/json"
@@ -7,9 +7,6 @@ import (
77
"net/http"
88
"strings"
99
"time"
10-
11-
"github.com/privapps/github-copilot-svcs/internal/config"
12-
"github.com/privapps/github-copilot-svcs/internal/logger"
1310
)
1411

1512
const (
@@ -48,19 +45,19 @@ type copilotTokenResponse struct {
4845
}
4946

5047
// Service provides authentication operations
51-
type Service struct {
48+
type AuthService struct {
5249
httpClient *http.Client
5350
}
5451

55-
// NewService creates a new auth service
56-
func NewService(httpClient *http.Client) *Service {
57-
return &Service{
52+
// NewAuthService creates a new auth service
53+
func NewAuthService(httpClient *http.Client) *AuthService {
54+
return &AuthService{
5855
httpClient: httpClient,
5956
}
6057
}
6158

6259
// Authenticate performs the full GitHub Copilot authentication flow
63-
func (s *Service) Authenticate(cfg *config.Config) error {
60+
func (s *AuthService) Authenticate(cfg *Config) error {
6461
now := time.Now().Unix()
6562
if cfg.CopilotToken != "" && cfg.ExpiresAt > now+60 {
6663
logger.Info("Token still valid", "expires_in", cfg.ExpiresAt-now)
@@ -98,7 +95,7 @@ func (s *Service) Authenticate(cfg *config.Config) error {
9895
cfg.ExpiresAt = expiresAt
9996
cfg.RefreshIn = refreshIn
10097

101-
if err := cfg.Save(); err != nil {
98+
if err := cfg.SaveConfig(); err != nil {
10299
return fmt.Errorf("failed to save config: %w", err)
103100
}
104101

@@ -107,7 +104,7 @@ func (s *Service) Authenticate(cfg *config.Config) error {
107104
}
108105

109106
// RefreshToken refreshes the Copilot token using the stored GitHub token
110-
func (s *Service) RefreshToken(cfg *config.Config) error {
107+
func (s *AuthService) RefreshToken(cfg *Config) error {
111108
if cfg.GitHubToken == "" {
112109
logger.Warn("Cannot refresh token: no GitHub token available")
113110
return errors.New("no GitHub token available for refresh")
@@ -136,14 +133,14 @@ func (s *Service) RefreshToken(cfg *config.Config) error {
136133
cfg.ExpiresAt = expiresAt
137134
cfg.RefreshIn = refreshIn
138135

139-
return cfg.Save()
136+
return cfg.SaveConfig()
140137
}
141138

142139
return errors.New("maximum retry attempts exceeded")
143140
}
144141

145142
// EnsureValidToken ensures we have a valid token, refreshing if necessary
146-
func (s *Service) EnsureValidToken(cfg *config.Config) error {
143+
func (s *AuthService) EnsureValidToken(cfg *Config) error {
147144
now := time.Now().Unix()
148145
if cfg.CopilotToken == "" {
149146
return errors.New("no token available - authentication required")
@@ -157,7 +154,7 @@ func (s *Service) EnsureValidToken(cfg *config.Config) error {
157154
return nil
158155
}
159156

160-
func (s *Service) getDeviceCode(cfg *config.Config) (*deviceCodeResponse, error) {
157+
func (s *AuthService) getDeviceCode(cfg *Config) (*deviceCodeResponse, error) {
161158
body := fmt.Sprintf(`{"client_id":%q,"scope":%q}`, copilotClientID, copilotScope)
162159
req, err := http.NewRequest("POST", copilotDeviceCodeURL, strings.NewReader(body))
163160
if err != nil {
@@ -181,7 +178,7 @@ func (s *Service) getDeviceCode(cfg *config.Config) (*deviceCodeResponse, error)
181178
return &dc, nil
182179
}
183180

184-
func (s *Service) pollForGitHubToken(cfg *config.Config, deviceCode string, interval int) (string, error) {
181+
func (s *AuthService) pollForGitHubToken(cfg *Config, deviceCode string, interval int) (string, error) {
185182
for i := 0; i < 120; i++ { // Poll for 2 minutes max
186183
time.Sleep(time.Duration(interval) * time.Second)
187184

@@ -222,7 +219,7 @@ func (s *Service) pollForGitHubToken(cfg *config.Config, deviceCode string, inte
222219
return "", fmt.Errorf("authentication timed out")
223220
}
224221

225-
func (s *Service) getCopilotToken(cfg *config.Config, githubToken string) (token string, expiresAt, refreshIn int64, err error) {
222+
func (s *AuthService) getCopilotToken(cfg *Config, githubToken string) (token string, expiresAt, refreshIn int64, err error) {
226223
req, err := http.NewRequest("GET", copilotAPIKeyURL, http.NoBody)
227224
if err != nil {
228225
return "", 0, 0, err
Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
package cli
1+
package internal
22

33
import (
44
"encoding/json"
55
"flag"
66
"fmt"
77
"os"
88
"time"
9-
10-
"github.com/privapps/github-copilot-svcs/internal/auth"
11-
"github.com/privapps/github-copilot-svcs/internal/config"
12-
"github.com/privapps/github-copilot-svcs/internal/logger"
13-
"github.com/privapps/github-copilot-svcs/internal/models"
14-
"github.com/privapps/github-copilot-svcs/internal/server"
159
)
1610

1711
// Command constants to avoid goconst errors
@@ -100,14 +94,14 @@ func RunCommand(command string, args []string, version string) error {
10094
}
10195

10296
func handleAuth() error {
103-
cfg, err := config.Load()
97+
cfg, err := LoadConfig()
10498
if err != nil {
10599
return fmt.Errorf("failed to load config: %v", err)
106100
}
107101

108102
// Create HTTP client with timeouts
109-
httpClient := server.CreateHTTPClient(cfg)
110-
authService := auth.NewService(httpClient)
103+
httpClient := CreateHTTPClient(cfg)
104+
authService := NewAuthService(httpClient)
111105

112106
fmt.Println("Starting GitHub Copilot authentication...")
113107
if err := authService.Authenticate(cfg); err != nil {
@@ -119,7 +113,7 @@ func handleAuth() error {
119113
}
120114

121115
func handleStatusWithFormat(jsonOutput bool) error {
122-
cfg, err := config.Load()
116+
cfg, err := LoadConfig()
123117
if err != nil {
124118
return fmt.Errorf("failed to load config: %v", err)
125119
}
@@ -130,8 +124,8 @@ func handleStatusWithFormat(jsonOutput bool) error {
130124
return printStatusText(cfg)
131125
}
132126

133-
func printStatusJSON(cfg *config.Config) error {
134-
path, _ := config.GetConfigPath()
127+
func printStatusJSON(cfg *Config) error {
128+
path, _ := GetConfigPath()
135129
now := getCurrentTime()
136130

137131
status := map[string]interface{}{
@@ -171,8 +165,8 @@ func printStatusJSON(cfg *config.Config) error {
171165
return nil
172166
}
173167

174-
func printStatusText(cfg *config.Config) error {
175-
path, _ := config.GetConfigPath()
168+
func printStatusText(cfg *Config) error {
169+
path, _ := GetConfigPath()
176170
fmt.Printf("Configuration file: %s\n", path)
177171
fmt.Printf("Port: %d\n", cfg.Port)
178172

@@ -216,12 +210,12 @@ func printStatusText(cfg *config.Config) error {
216210
}
217211

218212
func handleConfig() error {
219-
cfg, err := config.Load()
213+
cfg, err := LoadConfig()
220214
if err != nil {
221215
return fmt.Errorf("failed to load config: %v", err)
222216
}
223217

224-
path, _ := config.GetConfigPath()
218+
path, _ := GetConfigPath()
225219
fmt.Printf("Configuration file: %s\n", path)
226220
fmt.Printf("Port: %d\n", cfg.Port)
227221
fmt.Printf("Has GitHub token: %t\n", cfg.GitHubToken != "")
@@ -246,46 +240,46 @@ func getCurrentTime() int64 {
246240
}
247241

248242
func handleRun() error {
249-
cfg, err := config.Load()
243+
cfg, err := LoadConfig()
250244
if err != nil {
251245
return fmt.Errorf("failed to load config: %v", err)
252246
}
253247

254248
// Create HTTP client and auth service
255-
httpClient := server.CreateHTTPClient(cfg)
256-
authService := auth.NewService(httpClient)
249+
httpClient := CreateHTTPClient(cfg)
250+
authService := NewAuthService(httpClient)
257251

258252
// Ensure we're authenticated
259253
if err := authService.EnsureValidToken(cfg); err != nil {
260254
return fmt.Errorf("authentication failed: %v", err)
261255
}
262256

263257
// Create and start server
264-
srv := server.New(cfg, httpClient)
258+
srv := NewServer(cfg, httpClient)
265259
return srv.Start()
266260
}
267261

268262
func handleModels() error {
269-
cfg, err := config.Load()
263+
cfg, err := LoadConfig()
270264
if err != nil {
271265
return fmt.Errorf("failed to load config: %v", err)
272266
}
273267

274268
// Create HTTP client and auth service
275-
httpClient := server.CreateHTTPClient(cfg)
276-
authService := auth.NewService(httpClient)
269+
httpClient := CreateHTTPClient(cfg)
270+
authService := NewAuthService(httpClient)
277271

278272
// Ensure we're authenticated
279273
if authErr := authService.EnsureValidToken(cfg); authErr != nil {
280274
return fmt.Errorf("authentication failed: %v", authErr)
281275
}
282276

283277
// Fetch models
284-
modelList, err := models.FetchFromModelsDev()
278+
modelList, err := FetchFromModelsDev()
285279
if err != nil {
286280
fmt.Printf("Failed to fetch models from models.dev: %v\n", err)
287281
fmt.Println("Using default models:")
288-
defaultModels := models.GetDefault()
282+
defaultModels := GetDefault()
289283
for _, model := range defaultModels {
290284
fmt.Printf(" - %s (%s)\n", model.ID, model.OwnedBy)
291285
}
@@ -301,7 +295,7 @@ func handleModels() error {
301295
}
302296

303297
func handleRefresh() error {
304-
cfg, err := config.Load()
298+
cfg, err := LoadConfig()
305299
if err != nil {
306300
return fmt.Errorf("failed to load config: %v", err)
307301
}
@@ -311,8 +305,8 @@ func handleRefresh() error {
311305
}
312306

313307
// Create HTTP client and auth service
314-
httpClient := server.CreateHTTPClient(cfg)
315-
authService := auth.NewService(httpClient)
308+
httpClient := CreateHTTPClient(cfg)
309+
authService := NewAuthService(httpClient)
316310

317311
fmt.Println("Forcing token refresh...")
318312
if err := authService.RefreshToken(cfg); err != nil {
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package config
1+
package internal
22

33
import (
44
"encoding/json"
@@ -7,8 +7,6 @@ import (
77
"os/user"
88
"path/filepath"
99
"strconv"
10-
11-
"github.com/privapps/github-copilot-svcs/internal/logger"
1210
)
1311

1412
// Constants for configuration
@@ -94,8 +92,8 @@ func GetConfigPath() (string, error) {
9492
return filepath.Join(dir, configFileName), nil
9593
}
9694

97-
// Load loads the configuration from file and environment variables
98-
func Load() (*Config, error) {
95+
// LoadConfig loads the configuration from file and environment variables
96+
func LoadConfig() (*Config, error) {
9997
path, err := GetConfigPath()
10098
if err != nil {
10199
return nil, err
@@ -111,7 +109,7 @@ func Load() (*Config, error) {
111109
if err == nil {
112110
defer func() {
113111
if closeErr := file.Close(); closeErr != nil {
114-
logger.Error("Failed to close config file", "error", closeErr)
112+
Error("Failed to close config file", "error", closeErr)
115113
}
116114
}()
117115
if err := json.NewDecoder(file).Decode(cfg); err != nil {
@@ -230,8 +228,8 @@ func (c *Config) Validate() error {
230228
return nil
231229
}
232230

233-
// Save saves the configuration to file
234-
func (c *Config) Save() error {
231+
// SaveConfig saves the configuration to file
232+
func (c *Config) SaveConfig() error {
235233
path, err := GetConfigPath()
236234
if err != nil {
237235
return err
@@ -242,7 +240,7 @@ func (c *Config) Save() error {
242240
}
243241
defer func() {
244242
if closeErr := f.Close(); closeErr != nil {
245-
logger.Error("Failed to close config file", "error", closeErr)
243+
Error("Failed to close config file", "error", closeErr)
246244
}
247245
}()
248246
return json.NewEncoder(f).Encode(c)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package logger
1+
package internal
22

33
import (
44
"log/slog"

0 commit comments

Comments
 (0)