OpenSpeak/internal/auth/token_manager_test.go
Alexis Bruneteau dc59df9336 🎉 Complete OpenSpeak v0.1.0 Implementation - Server, CLI Client, and Web GUI
## Summary
OpenSpeak is a fully functional open-source voice communication platform built in Go with gRPC and Protocol Buffers. This release includes a production-ready server, interactive CLI client, and a modern web-based GUI.

## Components Implemented

### Server (cmd/openspeak-server)
- Complete gRPC server with 4 services and 20+ RPC methods
- Token-based authentication system with permission management
- Channel management with CRUD operations and member tracking
- Real-time presence tracking with idle detection (5-min timeout)
- Voice packet routing infrastructure with multi-subscriber support
- Graceful shutdown and signal handling
- Configurable logging and monitoring

### Core Systems (internal/)
- **auth/**: Token generation, validation, and management
- **channel/**: Channel CRUD, member management, capacity enforcement
- **presence/**: Session management, status tracking, mute control
- **voice/**: Packet routing with subscriber pattern
- **grpc/**: Service handlers with proper error handling
- **logger/**: Structured logging with configurable levels

### CLI Client (cmd/openspeak-client)
- Interactive REPL with 8 commands
- Token-based login and authentication
- Channel listing, selection, and joining
- Member viewing and status management
- Microphone mute control
- Beautiful formatted output with emoji indicators

### Web GUI (cmd/openspeak-gui) [NEW]
- Modern web-based interface replacing terminal CLI
- Responsive design for desktop, tablet, and mobile
- HTTP server with embedded HTML5/CSS3/JavaScript
- 8 RESTful API endpoints bridging web to gRPC
- Real-time updates with 2-second polling
- Beautiful UI with gradient background and color-coded buttons
- Zero external dependencies (pure vanilla JavaScript)

## Key Features
 4 production-ready gRPC services
 20+ RPC methods with proper error handling
 57+ unit tests, all passing
 Zero race conditions detected
 100+ concurrent user support
 Real-time presence and voice infrastructure
 Token-based authentication
 Channel management with member tracking
 Interactive CLI and web GUI clients
 Comprehensive documentation

## Testing Results
-  All 57+ tests passing
-  Zero race conditions (tested with -race flag)
-  Concurrent operation testing (100+ ops)
-  Integration tests verified
-  End-to-end scenarios validated

## Documentation
- README.md: Project overview and quick start
- IMPLEMENTATION_SUMMARY.md: Comprehensive project details
- GRPC_IMPLEMENTATION.md: Service and method documentation
- CLI_CLIENT.md: CLI usage guide with examples
- WEB_GUI.md: Web GUI usage and API documentation
- GUI_IMPLEMENTATION_SUMMARY.md: Web GUI implementation details
- TEST_SCENARIO.md: End-to-end testing guide
- OpenSpec: Complete specification documents

## Technology Stack
- Language: Go 1.24.11
- Framework: gRPC v1.77.0
- Serialization: Protocol Buffers v1.36.10
- UUID: github.com/google/uuid v1.6.0

## Build Information
- openspeak-server: 16MB (complete server)
- openspeak-client: 2.2MB (CLI interface)
- openspeak-gui: 18MB (web interface)
- Build time: <30 seconds
- Test runtime: <5 seconds

## Getting Started
1. Build: make build
2. Server: ./bin/openspeak-server -port 50051 -log-level info
3. Client: ./bin/openspeak-client -host localhost -port 50051
4. Web GUI: ./bin/openspeak-gui -port 9090
5. Browser: http://localhost:9090

## Production Readiness
-  Error handling and recovery
-  Graceful shutdown
-  Concurrent connection handling
-  Resource cleanup
-  Race condition free
-  Comprehensive logging
-  Proper timeout handling

## Next Steps (Future Phases)
- Phase 2: Voice streaming, event subscriptions, GUI enhancements
- Phase 3: Docker/Kubernetes, database persistence, web dashboard
- Phase 4: Advanced features (video, encryption, mobile apps)

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 17:32:47 +01:00

311 lines
6.9 KiB
Go

package auth
import (
"testing"
"time"
)
func TestGenerateToken(t *testing.T) {
tests := []struct {
name string
wantErr bool
}{
{
name: "generate valid token",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, err := GenerateToken()
if (err != nil) != tt.wantErr {
t.Errorf("GenerateToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Verify token is 64 characters (32 bytes hex encoded)
if len(token) != 64 {
t.Errorf("GenerateToken() returned token of length %d, expected 64", len(token))
}
// Verify tokens are unique
token2, _ := GenerateToken()
if token == token2 {
t.Error("GenerateToken() generated duplicate tokens")
}
})
}
}
func TestAddToken(t *testing.T) {
tests := []struct {
name string
token string
userID string
permissions []string
wantErr bool
}{
{
name: "add valid token",
token: "test-token-123",
userID: "user-1",
permissions: []string{"admin"},
wantErr: false,
},
{
name: "add user token",
token: "user-token-456",
userID: "user-2",
permissions: []string{"channels:create", "channels:join"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tm := NewTokenManager()
tm.AddToken(tt.token, tt.userID, tt.permissions)
// Verify token was added
info, err := tm.ValidateToken(tt.token)
if err != nil {
t.Errorf("ValidateToken() error = %v", err)
return
}
if info.UserID != tt.userID {
t.Errorf("Expected userID %s, got %s", tt.userID, info.UserID)
}
if len(info.Permissions) != len(tt.permissions) {
t.Errorf("Expected %d permissions, got %d", len(tt.permissions), len(info.Permissions))
}
})
}
}
func TestValidateToken(t *testing.T) {
tm := NewTokenManager()
tests := []struct {
name string
token string
setup func()
wantErr bool
errType error
}{
{
name: "valid token",
token: "valid-token",
setup: func() {
tm.AddToken("valid-token", "user-1", []string{"admin"})
},
wantErr: false,
},
{
name: "invalid token",
token: "nonexistent-token",
setup: func() {},
wantErr: true,
errType: ErrInvalidToken,
},
{
name: "revoked token",
token: "revoked-token",
setup: func() {
tm.AddToken("revoked-token", "user-2", []string{"user"})
tm.RevokeToken("revoked-token")
},
wantErr: true,
errType: ErrInvalidToken,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setup()
info, err := tm.ValidateToken(tt.token)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != tt.errType {
t.Errorf("ValidateToken() error = %v, expected %v", err, tt.errType)
}
if !tt.wantErr && info == nil {
t.Error("ValidateToken() returned nil info for valid token")
}
})
}
}
func TestRevokeToken(t *testing.T) {
tm := NewTokenManager()
token := "revoke-test-token"
// Add token
tm.AddToken(token, "user-1", []string{"admin"})
// Verify it's valid
_, err := tm.ValidateToken(token)
if err != nil {
t.Errorf("ValidateToken() before revoke error = %v", err)
return
}
// Revoke token
err = tm.RevokeToken(token)
if err != nil {
t.Errorf("RevokeToken() error = %v", err)
return
}
// Verify it's now invalid
_, err = tm.ValidateToken(token)
if err != ErrInvalidToken {
t.Errorf("ValidateToken() after revoke error = %v, expected %v", err, ErrInvalidToken)
}
// Test revoking nonexistent token
err = tm.RevokeToken("nonexistent-token")
if err != ErrInvalidToken {
t.Errorf("RevokeToken() nonexistent error = %v, expected %v", err, ErrInvalidToken)
}
}
func TestListTokens(t *testing.T) {
tm := NewTokenManager()
// Add multiple tokens
tm.AddToken("token-1", "user-1", []string{"admin"})
tm.AddToken("token-2", "user-2", []string{"user"})
tm.AddToken("token-3", "user-3", []string{"user"})
tokens := tm.ListTokens()
if len(tokens) != 3 {
t.Errorf("Expected 3 tokens, got %d", len(tokens))
}
}
func TestHasPermission(t *testing.T) {
tm := NewTokenManager()
tests := []struct {
name string
token string
permission string
setup func()
want bool
wantErr bool
}{
{
name: "admin has all permissions",
token: "admin-token",
permission: "channels:create",
setup: func() {
tm.AddToken("admin-token", "user-1", []string{"admin"})
},
want: true,
wantErr: false,
},
{
name: "user has specific permission",
token: "user-token",
permission: "channels:join",
setup: func() {
tm.AddToken("user-token", "user-2", []string{"channels:join", "channels:create"})
},
want: true,
wantErr: false,
},
{
name: "user missing permission",
token: "limited-token",
permission: "users:manage",
setup: func() {
tm.AddToken("limited-token", "user-3", []string{"channels:join"})
},
want: false,
wantErr: false,
},
{
name: "invalid token",
token: "bad-token",
permission: "channels:create",
setup: func() {},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tm := NewTokenManager() // Fresh manager for each test
tt.setup = func() {
switch tt.name {
case "admin has all permissions":
tm.AddToken("admin-token", "user-1", []string{"admin"})
case "user has specific permission":
tm.AddToken("user-token", "user-2", []string{"channels:join", "channels:create"})
case "user missing permission":
tm.AddToken("limited-token", "user-3", []string{"channels:join"})
}
}
tt.setup()
got, err := tm.HasPermission(tt.token, tt.permission)
if (err != nil) != tt.wantErr {
t.Errorf("HasPermission() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestTokenExpiration(t *testing.T) {
tm := NewTokenManager()
token := "expiring-token"
// Create token with expiration in the past
tm.AddToken(token, "user-1", []string{"admin"})
tokenInfo, _ := tm.ValidateToken(token)
// Manually set expiration to past
pastTime := time.Now().Add(-1 * time.Hour)
tokenInfo.ExpiresAt = &pastTime
// Should be expired now
_, err := tm.ValidateToken(token)
if err != ErrTokenExpired {
t.Errorf("Expected ErrTokenExpired, got %v", err)
}
}
func TestTokenConcurrency(t *testing.T) {
tm := NewTokenManager()
// Create tokens concurrently
for i := 0; i < 100; i++ {
go func(index int) {
token, _ := GenerateToken()
tm.AddToken(token, "user-1", []string{"admin"})
}(i)
}
// Give goroutines time to complete
time.Sleep(100 * time.Millisecond)
tokens := tm.ListTokens()
if len(tokens) != 100 {
t.Errorf("Expected 100 tokens, got %d", len(tokens))
}
}