mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-12 15:28:12 +00:00
9e339957c6
The DisableDockerUpdateActions setting was being saved to disk but not updated in h.config, causing the UI toggle to appear to revert on page refresh since the API returned the stale runtime value. Related to #1023
729 lines
18 KiB
Go
729 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSessionHash(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
token1 string
|
|
token2 string
|
|
same bool
|
|
}{
|
|
{
|
|
name: "same token produces same hash",
|
|
token1: "test-token-123",
|
|
token2: "test-token-123",
|
|
same: true,
|
|
},
|
|
{
|
|
name: "different tokens produce different hashes",
|
|
token1: "token-a",
|
|
token2: "token-b",
|
|
same: false,
|
|
},
|
|
{
|
|
name: "empty token produces consistent hash",
|
|
token1: "",
|
|
token2: "",
|
|
same: true,
|
|
},
|
|
{
|
|
name: "empty vs non-empty token",
|
|
token1: "",
|
|
token2: "something",
|
|
same: false,
|
|
},
|
|
{
|
|
name: "case sensitive",
|
|
token1: "Token",
|
|
token2: "token",
|
|
same: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
hash1 := sessionHash(tc.token1)
|
|
hash2 := sessionHash(tc.token2)
|
|
|
|
if tc.same && hash1 != hash2 {
|
|
t.Errorf("sessionHash(%q) = %q, sessionHash(%q) = %q; want same", tc.token1, hash1, tc.token2, hash2)
|
|
}
|
|
if !tc.same && hash1 == hash2 {
|
|
t.Errorf("sessionHash(%q) and sessionHash(%q) produced same hash %q; want different", tc.token1, tc.token2, hash1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionHash_Format(t *testing.T) {
|
|
hash := sessionHash("test-token")
|
|
|
|
// SHA256 produces 64 hex characters
|
|
if len(hash) != 64 {
|
|
t.Errorf("sessionHash length = %d, want 64", len(hash))
|
|
}
|
|
|
|
// Should only contain hex characters
|
|
for _, c := range hash {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
|
t.Errorf("sessionHash contains non-hex character: %c", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionHash_Deterministic(t *testing.T) {
|
|
token := "reproducible-token-value"
|
|
|
|
// Call multiple times
|
|
results := make([]string, 100)
|
|
for i := range results {
|
|
results[i] = sessionHash(token)
|
|
}
|
|
|
|
// All results should be identical
|
|
for i := 1; i < len(results); i++ {
|
|
if results[i] != results[0] {
|
|
t.Errorf("sessionHash call %d returned %q, want %q", i, results[i], results[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionData_Fields(t *testing.T) {
|
|
now := time.Now()
|
|
duration := 24 * time.Hour
|
|
|
|
data := SessionData{
|
|
ExpiresAt: now.Add(duration),
|
|
CreatedAt: now,
|
|
UserAgent: "Mozilla/5.0",
|
|
IP: "192.168.1.100",
|
|
OriginalDuration: duration,
|
|
}
|
|
|
|
if data.ExpiresAt.Before(data.CreatedAt) {
|
|
t.Error("ExpiresAt should be after CreatedAt")
|
|
}
|
|
if data.UserAgent != "Mozilla/5.0" {
|
|
t.Errorf("UserAgent = %q, want Mozilla/5.0", data.UserAgent)
|
|
}
|
|
if data.IP != "192.168.1.100" {
|
|
t.Errorf("IP = %q, want 192.168.1.100", data.IP)
|
|
}
|
|
if data.OriginalDuration != duration {
|
|
t.Errorf("OriginalDuration = %v, want %v", data.OriginalDuration, duration)
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_CreateAndValidate(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "test-session-token"
|
|
duration := time.Hour
|
|
|
|
// Create session
|
|
store.CreateSession(token, duration, "TestAgent", "127.0.0.1")
|
|
|
|
// Validate session
|
|
if !store.ValidateSession(token) {
|
|
t.Error("ValidateSession returned false for valid session")
|
|
}
|
|
|
|
// Verify internal state
|
|
store.mu.RLock()
|
|
key := sessionHash(token)
|
|
session, exists := store.sessions[key]
|
|
store.mu.RUnlock()
|
|
|
|
if !exists {
|
|
t.Fatal("Session not found in store")
|
|
}
|
|
if session.UserAgent != "TestAgent" {
|
|
t.Errorf("UserAgent = %q, want TestAgent", session.UserAgent)
|
|
}
|
|
if session.IP != "127.0.0.1" {
|
|
t.Errorf("IP = %q, want 127.0.0.1", session.IP)
|
|
}
|
|
if session.OriginalDuration != duration {
|
|
t.Errorf("OriginalDuration = %v, want %v", session.OriginalDuration, duration)
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateSession_NonExistent(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
if store.ValidateSession("nonexistent-token") {
|
|
t.Error("ValidateSession returned true for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateSession_Expired(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "expired-token"
|
|
key := sessionHash(token)
|
|
|
|
// Add an already-expired session
|
|
store.mu.Lock()
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(-time.Hour), // Expired an hour ago
|
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
store.mu.Unlock()
|
|
|
|
if store.ValidateSession(token) {
|
|
t.Error("ValidateSession returned true for expired session")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_DeleteSession(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "delete-me-token"
|
|
|
|
// Create session
|
|
store.CreateSession(token, time.Hour, "", "")
|
|
|
|
// Verify exists
|
|
if !store.ValidateSession(token) {
|
|
t.Fatal("Session should exist before deletion")
|
|
}
|
|
|
|
// Delete session
|
|
store.DeleteSession(token)
|
|
|
|
// Verify deleted
|
|
if store.ValidateSession(token) {
|
|
t.Error("Session should not exist after deletion")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateAndExtendSession(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "extend-me-token"
|
|
duration := time.Hour
|
|
|
|
// Create session
|
|
store.CreateSession(token, duration, "", "")
|
|
|
|
// Get original expiry
|
|
store.mu.RLock()
|
|
key := sessionHash(token)
|
|
originalExpiry := store.sessions[key].ExpiresAt
|
|
store.mu.RUnlock()
|
|
|
|
// Wait a moment
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Validate and extend
|
|
if !store.ValidateAndExtendSession(token) {
|
|
t.Error("ValidateAndExtendSession returned false for valid session")
|
|
}
|
|
|
|
// Verify expiry was extended
|
|
store.mu.RLock()
|
|
newExpiry := store.sessions[key].ExpiresAt
|
|
store.mu.RUnlock()
|
|
|
|
if !newExpiry.After(originalExpiry) {
|
|
t.Error("Session expiry was not extended")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateAndExtendSession_NonExistent(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
if store.ValidateAndExtendSession("nonexistent-token") {
|
|
t.Error("ValidateAndExtendSession returned true for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateAndExtendSession_Expired(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "expired-extend-token"
|
|
key := sessionHash(token)
|
|
|
|
// Add an already-expired session
|
|
store.mu.Lock()
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(-time.Hour),
|
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
OriginalDuration: time.Hour,
|
|
}
|
|
store.mu.Unlock()
|
|
|
|
if store.ValidateAndExtendSession(token) {
|
|
t.Error("ValidateAndExtendSession returned true for expired session")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_ValidateAndExtendSession_ZeroDuration(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "zero-duration-token"
|
|
key := sessionHash(token)
|
|
|
|
// Add session with zero original duration
|
|
store.mu.Lock()
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
OriginalDuration: 0, // Zero duration - should not extend
|
|
}
|
|
store.mu.Unlock()
|
|
|
|
originalExpiry := store.sessions[key].ExpiresAt
|
|
|
|
// Validate and extend
|
|
if !store.ValidateAndExtendSession(token) {
|
|
t.Error("ValidateAndExtendSession returned false for valid session")
|
|
}
|
|
|
|
// Expiry should not change (zero duration means no sliding)
|
|
store.mu.RLock()
|
|
newExpiry := store.sessions[key].ExpiresAt
|
|
store.mu.RUnlock()
|
|
|
|
if !newExpiry.Equal(originalExpiry) {
|
|
t.Error("Session with zero OriginalDuration should not have expiry extended")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_MultipleSessions(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
tokens := []string{"token-1", "token-2", "token-3"}
|
|
|
|
// Create multiple sessions
|
|
for _, token := range tokens {
|
|
store.CreateSession(token, time.Hour, "Agent-"+token, "")
|
|
}
|
|
|
|
// All should be valid
|
|
for _, token := range tokens {
|
|
if !store.ValidateSession(token) {
|
|
t.Errorf("ValidateSession(%q) = false, want true", token)
|
|
}
|
|
}
|
|
|
|
// Delete middle one
|
|
store.DeleteSession("token-2")
|
|
|
|
// First and last should still be valid
|
|
if !store.ValidateSession("token-1") {
|
|
t.Error("token-1 should still be valid")
|
|
}
|
|
if store.ValidateSession("token-2") {
|
|
t.Error("token-2 should be deleted")
|
|
}
|
|
if !store.ValidateSession("token-3") {
|
|
t.Error("token-3 should still be valid")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_Persistence(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create store and add session
|
|
store1 := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "persistent-token"
|
|
store1.CreateSession(token, time.Hour, "PersistAgent", "10.0.0.1")
|
|
|
|
// Verify file was created
|
|
sessionsFile := filepath.Join(tmpDir, "sessions.json")
|
|
if _, err := os.Stat(sessionsFile); os.IsNotExist(err) {
|
|
t.Fatal("sessions.json was not created")
|
|
}
|
|
|
|
// Create new store that should load from disk
|
|
store2 := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
store2.load()
|
|
|
|
// Session should be loaded
|
|
if !store2.ValidateSession(token) {
|
|
t.Error("Session was not persisted and loaded correctly")
|
|
}
|
|
|
|
// Verify metadata was preserved
|
|
store2.mu.RLock()
|
|
key := sessionHash(token)
|
|
session := store2.sessions[key]
|
|
store2.mu.RUnlock()
|
|
|
|
if session == nil {
|
|
t.Fatal("Session not found after load")
|
|
}
|
|
if session.UserAgent != "PersistAgent" {
|
|
t.Errorf("UserAgent = %q, want PersistAgent", session.UserAgent)
|
|
}
|
|
if session.IP != "10.0.0.1" {
|
|
t.Errorf("IP = %q, want 10.0.0.1", session.IP)
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_LoadExpiredSessions(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Manually create a sessions file with an expired session
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
token := "already-expired"
|
|
key := sessionHash(token)
|
|
|
|
// Add expired session
|
|
store.mu.Lock()
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(-time.Hour),
|
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
store.mu.Unlock()
|
|
store.save()
|
|
|
|
// Create new store and load
|
|
store2 := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
store2.load()
|
|
|
|
// Expired session should not be loaded
|
|
store2.mu.RLock()
|
|
_, exists := store2.sessions[key]
|
|
store2.mu.RUnlock()
|
|
|
|
if exists {
|
|
t.Error("Expired session should not be loaded from disk")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_Cleanup(t *testing.T) {
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: t.TempDir(),
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Add a valid and an expired session
|
|
validKey := sessionHash("valid-session")
|
|
expiredKey := sessionHash("expired-session")
|
|
|
|
store.mu.Lock()
|
|
store.sessions[validKey] = &SessionData{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
store.sessions[expiredKey] = &SessionData{
|
|
ExpiresAt: time.Now().Add(-time.Hour),
|
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
store.mu.Unlock()
|
|
|
|
// Run cleanup
|
|
store.cleanup()
|
|
|
|
// Valid session should remain
|
|
store.mu.RLock()
|
|
_, validExists := store.sessions[validKey]
|
|
_, expiredExists := store.sessions[expiredKey]
|
|
store.mu.RUnlock()
|
|
|
|
if !validExists {
|
|
t.Error("Valid session was incorrectly removed during cleanup")
|
|
}
|
|
if expiredExists {
|
|
t.Error("Expired session was not removed during cleanup")
|
|
}
|
|
}
|
|
|
|
func TestSessionPersistedStruct_Fields(t *testing.T) {
|
|
now := time.Now()
|
|
duration := 2 * time.Hour
|
|
|
|
persisted := sessionPersisted{
|
|
Key: "abc123",
|
|
ExpiresAt: now.Add(duration),
|
|
CreatedAt: now,
|
|
UserAgent: "TestUA",
|
|
IP: "1.2.3.4",
|
|
OriginalDuration: duration,
|
|
}
|
|
|
|
if persisted.Key != "abc123" {
|
|
t.Errorf("Key = %q, want abc123", persisted.Key)
|
|
}
|
|
if persisted.UserAgent != "TestUA" {
|
|
t.Errorf("UserAgent = %q, want TestUA", persisted.UserAgent)
|
|
}
|
|
if persisted.IP != "1.2.3.4" {
|
|
t.Errorf("IP = %q, want 1.2.3.4", persisted.IP)
|
|
}
|
|
if persisted.OriginalDuration != duration {
|
|
t.Errorf("OriginalDuration = %v, want %v", persisted.OriginalDuration, duration)
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_SaveUnsafe_MkdirAllError(t *testing.T) {
|
|
// Create a file where the directory should be - MkdirAll will fail
|
|
tmpDir := t.TempDir()
|
|
blockedPath := filepath.Join(tmpDir, "blocked")
|
|
|
|
// Create a file at the path where dataPath should be
|
|
if err := os.WriteFile(blockedPath, []byte("blocking file"), 0644); err != nil {
|
|
t.Fatalf("failed to create blocking file: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: blockedPath, // This is a file, not a directory
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Add a session to save
|
|
key := sessionHash("test-token")
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// saveUnsafe should handle error gracefully (logs but doesn't panic)
|
|
store.saveUnsafe()
|
|
|
|
// Verify the blocking file still exists (wasn't overwritten)
|
|
data, err := os.ReadFile(blockedPath)
|
|
if err != nil {
|
|
t.Fatalf("blocking file was removed: %v", err)
|
|
}
|
|
if string(data) != "blocking file" {
|
|
t.Error("blocking file was modified")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_SaveUnsafe_WriteFileError(t *testing.T) {
|
|
if os.Geteuid() == 0 {
|
|
t.Skip("Skipping permission test running as root")
|
|
}
|
|
// Create a read-only directory to prevent file creation
|
|
tmpDir := t.TempDir()
|
|
readOnlyDir := filepath.Join(tmpDir, "readonly")
|
|
|
|
if err := os.Mkdir(readOnlyDir, 0755); err != nil {
|
|
t.Fatalf("failed to create readonly dir: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: readOnlyDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Add a session
|
|
key := sessionHash("test-token")
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Make directory read-only after it exists (MkdirAll succeeds, WriteFile fails)
|
|
if err := os.Chmod(readOnlyDir, 0555); err != nil {
|
|
t.Fatalf("failed to make dir readonly: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
os.Chmod(readOnlyDir, 0755) // Restore for cleanup
|
|
})
|
|
|
|
// saveUnsafe should handle error gracefully (logs but doesn't panic)
|
|
store.saveUnsafe()
|
|
|
|
// Verify no file was created
|
|
sessionsFile := filepath.Join(readOnlyDir, "sessions.json")
|
|
if _, err := os.Stat(sessionsFile); err == nil {
|
|
t.Error("sessions file should not exist after write failure")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_SaveUnsafe_RenameError(t *testing.T) {
|
|
// Create a directory at the target path to cause rename error
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a directory at the exact path where sessions.json should go
|
|
sessionsFilePath := filepath.Join(tmpDir, "sessions.json")
|
|
if err := os.Mkdir(sessionsFilePath, 0755); err != nil {
|
|
t.Fatalf("failed to create blocking directory: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Add a session
|
|
key := sessionHash("test-token")
|
|
store.sessions[key] = &SessionData{
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// saveUnsafe should handle rename error gracefully
|
|
store.saveUnsafe()
|
|
|
|
// The blocking directory should still exist (rename failed)
|
|
info, err := os.Stat(sessionsFilePath)
|
|
if err != nil {
|
|
t.Fatalf("blocking directory was removed: %v", err)
|
|
}
|
|
if !info.IsDir() {
|
|
t.Error("blocking directory was replaced with a file")
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_Load_ReadError(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a directory where the sessions file should be - reading it will fail
|
|
sessionsPath := filepath.Join(tmpDir, "sessions.json")
|
|
if err := os.Mkdir(sessionsPath, 0755); err != nil {
|
|
t.Fatalf("failed to create directory: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Should not panic and should log error
|
|
store.load()
|
|
|
|
if len(store.sessions) != 0 {
|
|
t.Errorf("store should be empty after read error, got %d sessions", len(store.sessions))
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_Load_InvalidJSON(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Write invalid JSON that won't match either format
|
|
sessionsFile := filepath.Join(tmpDir, "sessions.json")
|
|
if err := os.WriteFile(sessionsFile, []byte("not valid json at all"), 0600); err != nil {
|
|
t.Fatalf("failed to write invalid JSON: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
// Should not panic
|
|
store.load()
|
|
|
|
if len(store.sessions) != 0 {
|
|
t.Errorf("store should be empty after loading invalid JSON, got %d sessions", len(store.sessions))
|
|
}
|
|
}
|
|
|
|
func TestSessionStore_Load_LegacyFormat(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create legacy format JSON (map[token]sessionData) with snake_case fields
|
|
// The legacy format uses raw tokens as keys
|
|
legacyJSON := `{
|
|
"raw-token-1": {"expires_at": "2099-12-31T23:59:59Z", "created_at": "2024-01-01T00:00:00Z", "user_agent": "Agent1", "ip": "1.1.1.1"},
|
|
"raw-token-2": {"expires_at": "2099-12-31T23:59:59Z", "created_at": "2024-01-01T00:00:00Z", "user_agent": "Agent2", "ip": "2.2.2.2"},
|
|
"raw-token-expired": {"expires_at": "2020-01-01T00:00:00Z", "created_at": "2019-01-01T00:00:00Z", "user_agent": "ExpiredAgent", "ip": "3.3.3.3"}
|
|
}`
|
|
sessionsFile := filepath.Join(tmpDir, "sessions.json")
|
|
if err := os.WriteFile(sessionsFile, []byte(legacyJSON), 0600); err != nil {
|
|
t.Fatalf("failed to write legacy format JSON: %v", err)
|
|
}
|
|
|
|
store := &SessionStore{
|
|
sessions: make(map[string]*SessionData),
|
|
dataPath: tmpDir,
|
|
stopChan: make(chan bool),
|
|
}
|
|
|
|
store.load()
|
|
|
|
// Should load the two non-expired sessions (raw-token-1 and raw-token-2)
|
|
// The expired one (raw-token-expired) should be skipped
|
|
if len(store.sessions) != 2 {
|
|
t.Errorf("expected 2 sessions from legacy format, got %d", len(store.sessions))
|
|
}
|
|
|
|
// Verify sessions are hashed and can be validated
|
|
// The legacy format stores raw tokens as keys, which get hashed during migration
|
|
if !store.ValidateSession("raw-token-1") {
|
|
t.Error("should validate session for raw-token-1 after legacy migration")
|
|
}
|
|
if !store.ValidateSession("raw-token-2") {
|
|
t.Error("should validate session for raw-token-2 after legacy migration")
|
|
}
|
|
// Expired token should not be valid
|
|
if store.ValidateSession("raw-token-expired") {
|
|
t.Error("expired session should not be loaded from legacy format")
|
|
}
|
|
}
|