mirror of
https://github.com/ollama/ollama.git
synced 2026-01-18 12:28:35 -05:00
Compare commits
5 Commits
usage
...
parth/sign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b978dc28d2 | ||
|
|
106640b978 | ||
|
|
f4e5978e4f | ||
|
|
528de77b4b | ||
|
|
eed58a312d |
86
auth/auth.go
86
auth/auth.go
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,11 +13,22 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const defaultPrivateKey = "id_ed25519"
|
||||
const (
|
||||
defaultPrivateKey = "id_ed25519"
|
||||
signInStateFile = "signin.json"
|
||||
)
|
||||
|
||||
// SignInState represents the locally cached sign-in state
|
||||
type SignInState struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
CachedAt time.Time `json:"cached_at"`
|
||||
}
|
||||
|
||||
func GetPublicKey() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -83,3 +95,75 @@ func Sign(ctx context.Context, bts []byte) (string, error) {
|
||||
// signature is <pubkey>:<signature>
|
||||
return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil
|
||||
}
|
||||
|
||||
// GetSignInState reads the locally cached sign-in state from ~/.ollama/signin.json
|
||||
func GetSignInState() (*SignInState, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statePath := filepath.Join(home, ".ollama", signInStateFile)
|
||||
data, err := os.ReadFile(statePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var state SignInState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// SetSignInState atomically writes the sign-in state to ~/.ollama/signin.json
|
||||
func SetSignInState(state *SignInState) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ollamaDir := filepath.Join(home, ".ollama")
|
||||
statePath := filepath.Join(ollamaDir, signInStateFile)
|
||||
tmpPath := statePath + ".tmp"
|
||||
|
||||
state.CachedAt = time.Now()
|
||||
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to temp file first
|
||||
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
return os.Rename(tmpPath, statePath)
|
||||
}
|
||||
|
||||
// ClearSignInState removes the locally cached sign-in state
|
||||
func ClearSignInState() error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statePath := filepath.Join(home, ".ollama", signInStateFile)
|
||||
err = os.Remove(statePath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil // Already cleared
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IsSignedIn returns true if there is a valid locally cached sign-in state
|
||||
func IsSignedIn() bool {
|
||||
state, err := GetSignInState()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return state.Name != ""
|
||||
}
|
||||
|
||||
294
auth/signin_flow_test.go
Normal file
294
auth/signin_flow_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWhoamiHandlerFlow simulates the WhoamiHandler logic flow
|
||||
func TestWhoamiHandlerFlow(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Scenario 1: No local cache - should indicate need for network call
|
||||
t.Run("NoCache_RequiresNetwork", func(t *testing.T) {
|
||||
state, err := GetSignInState()
|
||||
if err == nil && state != nil && state.Name != "" {
|
||||
t.Error("should not have cached state initially")
|
||||
}
|
||||
// In real WhoamiHandler, this would trigger a network call
|
||||
})
|
||||
|
||||
// Scenario 2: Simulate successful sign-in from network
|
||||
t.Run("CacheAfterNetworkSuccess", func(t *testing.T) {
|
||||
// Simulate receiving user from ollama.com
|
||||
networkUser := &SignInState{
|
||||
Name: "networkuser",
|
||||
Email: "network@example.com",
|
||||
}
|
||||
|
||||
// Cache the result (as WhoamiHandler would)
|
||||
if err := SetSignInState(networkUser); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's cached
|
||||
if !IsSignedIn() {
|
||||
t.Error("should be signed in after caching")
|
||||
}
|
||||
})
|
||||
|
||||
// Scenario 3: Subsequent calls use cache (no network)
|
||||
t.Run("SubsequentCalls_UseCache", func(t *testing.T) {
|
||||
state, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
if state.Name != "networkuser" {
|
||||
t.Errorf("expected cached name 'networkuser', got '%s'", state.Name)
|
||||
}
|
||||
|
||||
// In real WhoamiHandler, this would skip the network call and return cached data
|
||||
})
|
||||
|
||||
// Scenario 4: Sign-out clears cache
|
||||
t.Run("SignOut_ClearsCache", func(t *testing.T) {
|
||||
// Simulate SignoutHandler clearing cache
|
||||
if err := ClearSignInState(); err != nil {
|
||||
t.Fatalf("ClearSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
if IsSignedIn() {
|
||||
t.Error("should not be signed in after sign-out")
|
||||
}
|
||||
})
|
||||
|
||||
// Scenario 5: After sign-out, next call requires network
|
||||
t.Run("AfterSignOut_RequiresNetwork", func(t *testing.T) {
|
||||
state, err := GetSignInState()
|
||||
if err == nil && state != nil && state.Name != "" {
|
||||
t.Error("should require network after sign-out")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOfflineScenarios tests behavior when offline
|
||||
func TestOfflineScenarios(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
t.Run("Offline_WithCache_Works", func(t *testing.T) {
|
||||
// Pre-populate cache (simulate previous sign-in)
|
||||
state := &SignInState{Name: "cacheduser", Email: "cached@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Simulate offline check - should work with cache
|
||||
cached, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("should work offline with cache: %v", err)
|
||||
}
|
||||
|
||||
if cached.Name != "cacheduser" {
|
||||
t.Errorf("expected 'cacheduser', got '%s'", cached.Name)
|
||||
}
|
||||
|
||||
if !IsSignedIn() {
|
||||
t.Error("should report signed in with cache")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Offline_WithoutCache_Fails", func(t *testing.T) {
|
||||
// Clear cache first
|
||||
ClearSignInState()
|
||||
|
||||
// Offline without cache should indicate not signed in
|
||||
if IsSignedIn() {
|
||||
t.Error("should not be signed in offline without cache")
|
||||
}
|
||||
|
||||
_, err := GetSignInState()
|
||||
if err == nil {
|
||||
t.Error("should get error when no cache exists")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMultipleSessions tests overwriting sessions
|
||||
func TestMultipleSessions(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
t.Run("NewSignIn_OverwritesOld", func(t *testing.T) {
|
||||
// First user signs in
|
||||
user1 := &SignInState{Name: "user1", Email: "user1@example.com"}
|
||||
SetSignInState(user1)
|
||||
|
||||
// Different user signs in (should overwrite)
|
||||
user2 := &SignInState{Name: "user2", Email: "user2@example.com"}
|
||||
SetSignInState(user2)
|
||||
|
||||
// Should have user2's data
|
||||
state, _ := GetSignInState()
|
||||
if state.Name != "user2" {
|
||||
t.Errorf("expected 'user2', got '%s'", state.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests various edge cases
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
t.Run("EmptyName_NotSignedIn", func(t *testing.T) {
|
||||
// User with empty name should not count as signed in
|
||||
state := &SignInState{Name: "", Email: "noname@example.com"}
|
||||
SetSignInState(state)
|
||||
|
||||
if IsSignedIn() {
|
||||
t.Error("empty name should not count as signed in")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SpecialCharactersInName", func(t *testing.T) {
|
||||
state := &SignInState{
|
||||
Name: "user with spaces & symbols!@#$%",
|
||||
Email: "special@example.com",
|
||||
}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("failed to set state with special chars: %v", err)
|
||||
}
|
||||
|
||||
read, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state: %v", err)
|
||||
}
|
||||
|
||||
if read.Name != state.Name {
|
||||
t.Errorf("name mismatch with special chars")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UnicodeInName", func(t *testing.T) {
|
||||
state := &SignInState{
|
||||
Name: "用户名 🎉 émojis",
|
||||
Email: "unicode@example.com",
|
||||
}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("failed to set state with unicode: %v", err)
|
||||
}
|
||||
|
||||
read, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state: %v", err)
|
||||
}
|
||||
|
||||
if read.Name != state.Name {
|
||||
t.Errorf("name mismatch with unicode")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("VeryLongEmail", func(t *testing.T) {
|
||||
longEmail := ""
|
||||
for range 1000 {
|
||||
longEmail += "a"
|
||||
}
|
||||
longEmail += "@example.com"
|
||||
|
||||
state := &SignInState{Name: "user", Email: longEmail}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("failed to set state with long email: %v", err)
|
||||
}
|
||||
|
||||
read, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state: %v", err)
|
||||
}
|
||||
|
||||
if read.Email != longEmail {
|
||||
t.Error("email mismatch with long value")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcurrentAccess tests race conditions
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
t.Run("ConcurrentReads", func(t *testing.T) {
|
||||
// Set up initial state
|
||||
state := &SignInState{Name: "concurrent", Email: "concurrent@example.com"}
|
||||
SetSignInState(state)
|
||||
|
||||
// Multiple concurrent reads should all succeed
|
||||
done := make(chan bool, 10)
|
||||
for range 10 {
|
||||
go func() {
|
||||
read, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Errorf("concurrent read failed: %v", err)
|
||||
}
|
||||
if read.Name != "concurrent" {
|
||||
t.Errorf("wrong name in concurrent read")
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for range 10 {
|
||||
<-done
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConcurrentWrites", func(t *testing.T) {
|
||||
// Multiple concurrent writes - last one should win
|
||||
done := make(chan bool, 10)
|
||||
for range 10 {
|
||||
go func() {
|
||||
state := &SignInState{
|
||||
Name: "user",
|
||||
Email: "user@example.com",
|
||||
}
|
||||
SetSignInState(state)
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for range 10 {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Should have some valid state
|
||||
if !IsSignedIn() {
|
||||
t.Error("should be signed in after concurrent writes")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFileSystemEdgeCases tests filesystem-related edge cases
|
||||
func TestFileSystemEdgeCases(t *testing.T) {
|
||||
t.Run("DirectoryIsFile", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a file where .ollama directory should be
|
||||
ollamaPath := filepath.Join(tmpDir, ".ollama")
|
||||
if err := os.WriteFile(ollamaPath, []byte("not a directory"), 0o600); err != nil {
|
||||
t.Fatalf("failed to create blocking file: %v", err)
|
||||
}
|
||||
|
||||
// Override home directory
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
} else {
|
||||
t.Setenv("HOME", tmpDir)
|
||||
}
|
||||
|
||||
// Try to write - should fail because .ollama is a file, not a directory
|
||||
state := &SignInState{Name: "newuser", Email: "new@example.com"}
|
||||
err := SetSignInState(state)
|
||||
if err == nil {
|
||||
t.Error("should fail when .ollama is a file instead of directory")
|
||||
}
|
||||
})
|
||||
}
|
||||
347
auth/signin_state_test.go
Normal file
347
auth/signin_state_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setupTestDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create .ollama subdirectory
|
||||
ollamaDir := filepath.Join(tmpDir, ".ollama")
|
||||
if err := os.MkdirAll(ollamaDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create .ollama dir: %v", err)
|
||||
}
|
||||
|
||||
// Override home directory for tests (platform-specific)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
} else {
|
||||
t.Setenv("HOME", tmpDir)
|
||||
}
|
||||
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
func TestSetSignInState(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
state := &SignInState{
|
||||
Name: "testuser",
|
||||
Email: "test@example.com",
|
||||
}
|
||||
|
||||
err := SetSignInState(state)
|
||||
if err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file was created
|
||||
home, _ := os.UserHomeDir()
|
||||
statePath := filepath.Join(home, ".ollama", signInStateFile)
|
||||
|
||||
data, err := os.ReadFile(statePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state file: %v", err)
|
||||
}
|
||||
|
||||
var savedState SignInState
|
||||
if err := json.Unmarshal(data, &savedState); err != nil {
|
||||
t.Fatalf("failed to unmarshal state: %v", err)
|
||||
}
|
||||
|
||||
if savedState.Name != "testuser" {
|
||||
t.Errorf("expected name 'testuser', got '%s'", savedState.Name)
|
||||
}
|
||||
|
||||
if savedState.Email != "test@example.com" {
|
||||
t.Errorf("expected email 'test@example.com', got '%s'", savedState.Email)
|
||||
}
|
||||
|
||||
if savedState.CachedAt.IsZero() {
|
||||
t.Error("expected CachedAt to be set, got zero time")
|
||||
}
|
||||
|
||||
// Verify CachedAt is recent (within last minute)
|
||||
if time.Since(savedState.CachedAt) > time.Minute {
|
||||
t.Errorf("CachedAt is too old: %v", savedState.CachedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSignInState_Overwrites(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Set initial state
|
||||
state1 := &SignInState{Name: "user1", Email: "user1@example.com"}
|
||||
if err := SetSignInState(state1); err != nil {
|
||||
t.Fatalf("first SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Overwrite with new state
|
||||
state2 := &SignInState{Name: "user2", Email: "user2@example.com"}
|
||||
if err := SetSignInState(state2); err != nil {
|
||||
t.Fatalf("second SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify only new state exists
|
||||
readState, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
if readState.Name != "user2" {
|
||||
t.Errorf("expected name 'user2', got '%s'", readState.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSignInState(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// First set a state
|
||||
originalState := &SignInState{
|
||||
Name: "testuser",
|
||||
Email: "test@example.com",
|
||||
}
|
||||
if err := SetSignInState(originalState); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Now read it back
|
||||
readState, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
if readState.Name != originalState.Name {
|
||||
t.Errorf("expected name '%s', got '%s'", originalState.Name, readState.Name)
|
||||
}
|
||||
|
||||
if readState.Email != originalState.Email {
|
||||
t.Errorf("expected email '%s', got '%s'", originalState.Email, readState.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSignInState_NoFile(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Try to read without any file existing
|
||||
state, err := GetSignInState()
|
||||
if err == nil {
|
||||
t.Error("expected error when file doesn't exist, got nil")
|
||||
}
|
||||
if state != nil {
|
||||
t.Errorf("expected nil state, got %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSignInState_InvalidJSON(t *testing.T) {
|
||||
tmpDir := setupTestDir(t)
|
||||
|
||||
// Write invalid JSON to the state file
|
||||
statePath := filepath.Join(tmpDir, ".ollama", signInStateFile)
|
||||
if err := os.WriteFile(statePath, []byte("not valid json"), 0o600); err != nil {
|
||||
t.Fatalf("failed to write invalid json: %v", err)
|
||||
}
|
||||
|
||||
state, err := GetSignInState()
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid JSON, got nil")
|
||||
}
|
||||
if state != nil {
|
||||
t.Errorf("expected nil state for invalid JSON, got %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearSignInState(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// First set a state
|
||||
state := &SignInState{Name: "testuser", Email: "test@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
home, _ := os.UserHomeDir()
|
||||
statePath := filepath.Join(home, ".ollama", signInStateFile)
|
||||
if _, err := os.Stat(statePath); os.IsNotExist(err) {
|
||||
t.Fatal("state file should exist before clearing")
|
||||
}
|
||||
|
||||
// Clear the state
|
||||
if err := ClearSignInState(); err != nil {
|
||||
t.Fatalf("ClearSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file is gone
|
||||
if _, err := os.Stat(statePath); !os.IsNotExist(err) {
|
||||
t.Error("state file should be deleted after clearing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearSignInState_NoFile(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Clear when no file exists should not error
|
||||
err := ClearSignInState()
|
||||
if err != nil {
|
||||
t.Errorf("ClearSignInState should not error when file doesn't exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearSignInState_Idempotent(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Set a state first
|
||||
state := &SignInState{Name: "testuser", Email: "test@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Clear multiple times should not error
|
||||
for i := range 3 {
|
||||
if err := ClearSignInState(); err != nil {
|
||||
t.Errorf("ClearSignInState iteration %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSignedIn(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Initially not signed in
|
||||
if IsSignedIn() {
|
||||
t.Error("should not be signed in initially")
|
||||
}
|
||||
|
||||
// Set a state with a name
|
||||
state := &SignInState{Name: "testuser", Email: "test@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Now should be signed in
|
||||
if !IsSignedIn() {
|
||||
t.Error("should be signed in after setting state")
|
||||
}
|
||||
|
||||
// Clear the state
|
||||
if err := ClearSignInState(); err != nil {
|
||||
t.Fatalf("ClearSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Should not be signed in after clearing
|
||||
if IsSignedIn() {
|
||||
t.Error("should not be signed in after clearing state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSignedIn_EmptyName(t *testing.T) {
|
||||
tmpDir := setupTestDir(t)
|
||||
|
||||
// Write a state with empty name directly
|
||||
state := SignInState{
|
||||
Name: "",
|
||||
Email: "test@example.com",
|
||||
CachedAt: time.Now(),
|
||||
}
|
||||
data, _ := json.Marshal(state)
|
||||
statePath := filepath.Join(tmpDir, ".ollama", signInStateFile)
|
||||
if err := os.WriteFile(statePath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write state: %v", err)
|
||||
}
|
||||
|
||||
// Should not be signed in with empty name
|
||||
if IsSignedIn() {
|
||||
t.Error("should not be signed in with empty name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSignInState_AtomicWrite(t *testing.T) {
|
||||
tmpDir := setupTestDir(t)
|
||||
|
||||
state := &SignInState{Name: "testuser", Email: "test@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify temp file is cleaned up
|
||||
tmpPath := filepath.Join(tmpDir, ".ollama", signInStateFile+".tmp")
|
||||
if _, err := os.Stat(tmpPath); !os.IsNotExist(err) {
|
||||
t.Error("temp file should be cleaned up after atomic write")
|
||||
}
|
||||
|
||||
// Verify final file exists
|
||||
statePath := filepath.Join(tmpDir, ".ollama", signInStateFile)
|
||||
if _, err := os.Stat(statePath); os.IsNotExist(err) {
|
||||
t.Error("final state file should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSignInState_FilePermissions(t *testing.T) {
|
||||
tmpDir := setupTestDir(t)
|
||||
|
||||
state := &SignInState{Name: "testuser", Email: "test@example.com"}
|
||||
if err := SetSignInState(state); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
statePath := filepath.Join(tmpDir, ".ollama", signInStateFile)
|
||||
info, err := os.Stat(statePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat state file: %v", err)
|
||||
}
|
||||
|
||||
// Check file permissions (should be 0600 - owner read/write only)
|
||||
perm := info.Mode().Perm()
|
||||
if perm != 0o600 {
|
||||
t.Errorf("expected permissions 0600, got %04o", perm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
_ = setupTestDir(t)
|
||||
|
||||
// Test full round trip: set -> get -> clear -> get
|
||||
original := &SignInState{
|
||||
Name: "roundtrip_user",
|
||||
Email: "roundtrip@example.com",
|
||||
}
|
||||
|
||||
// Set
|
||||
if err := SetSignInState(original); err != nil {
|
||||
t.Fatalf("SetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Get and verify
|
||||
retrieved, err := GetSignInState()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != original.Name {
|
||||
t.Errorf("name mismatch: expected '%s', got '%s'", original.Name, retrieved.Name)
|
||||
}
|
||||
if retrieved.Email != original.Email {
|
||||
t.Errorf("email mismatch: expected '%s', got '%s'", original.Email, retrieved.Email)
|
||||
}
|
||||
|
||||
// Clear
|
||||
if err := ClearSignInState(); err != nil {
|
||||
t.Fatalf("ClearSignInState failed: %v", err)
|
||||
}
|
||||
|
||||
// Get should fail now
|
||||
_, err = GetSignInState()
|
||||
if err == nil {
|
||||
t.Error("GetSignInState should fail after clear")
|
||||
}
|
||||
}
|
||||
@@ -1745,6 +1745,14 @@ func streamResponse(c *gin.Context, ch chan any) {
|
||||
}
|
||||
|
||||
func (s *Server) WhoamiHandler(c *gin.Context) {
|
||||
// Check local cache first
|
||||
state, err := auth.GetSignInState()
|
||||
if err == nil && state.Name != "" {
|
||||
c.JSON(http.StatusOK, &api.UserResponse{Name: state.Name, Email: state.Email})
|
||||
return
|
||||
}
|
||||
|
||||
// No local cache - try network
|
||||
// todo allow other hosts
|
||||
u, err := url.Parse("https://ollama.com")
|
||||
if err != nil {
|
||||
@@ -1772,6 +1780,13 @@ func (s *Server) WhoamiHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cache successful result locally
|
||||
if user != nil && user.Name != "" {
|
||||
if err := auth.SetSignInState(&auth.SignInState{Name: user.Name, Email: user.Email}); err != nil {
|
||||
slog.Warn("failed to cache sign-in state", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
@@ -1805,6 +1820,11 @@ func (s *Server) SignoutHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear local sign-in cache
|
||||
if err := auth.ClearSignInState(); err != nil {
|
||||
slog.Warn("failed to clear local sign-in state", "error", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user