Files
at-container-registry/pkg/auth/oauth/store_test.go
T
2025-10-28 17:40:11 -05:00

632 lines
16 KiB
Go

package oauth
import (
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/bluesky-social/indigo/atproto/syntax"
)
func TestNewFileStore(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
if store == nil {
t.Fatal("Expected non-nil store")
}
if store.path != storePath {
t.Errorf("Expected path %q, got %q", storePath, store.path)
}
if store.sessions == nil {
t.Error("Expected sessions map to be initialized")
}
if store.requests == nil {
t.Error("Expected requests map to be initialized")
}
}
func TestFileStore_LoadNonExistent(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/nonexistent.json"
// Should succeed even if file doesn't exist
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() should succeed with non-existent file, got error: %v", err)
}
if store == nil {
t.Fatal("Expected non-nil store")
}
}
func TestFileStore_LoadCorruptedFile(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/corrupted.json"
// Create corrupted JSON file
if err := os.WriteFile(storePath, []byte("invalid json {{{"), 0600); err != nil {
t.Fatalf("Failed to create corrupted file: %v", err)
}
// Should fail to load corrupted file
_, err := NewFileStore(storePath)
if err == nil {
t.Error("Expected error when loading corrupted file")
}
}
func TestFileStore_GetSession_NotFound(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:test123")
sessionID := "session123"
// Should return error for non-existent session
session, err := store.GetSession(ctx, did, sessionID)
if err == nil {
t.Error("Expected error for non-existent session")
}
if session != nil {
t.Error("Expected nil session for non-existent entry")
}
}
func TestFileStore_SaveAndGetSession(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Create test session
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "test-session-123",
HostURL: "https://pds.example.com",
Scopes: []string{"atproto", "blob:read"},
}
// Save session
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// Retrieve session
retrieved, err := store.GetSession(ctx, did, "test-session-123")
if err != nil {
t.Fatalf("GetSession() error = %v", err)
}
if retrieved == nil {
t.Fatal("Expected non-nil session")
}
if retrieved.SessionID != sessionData.SessionID {
t.Errorf("Expected sessionID %q, got %q", sessionData.SessionID, retrieved.SessionID)
}
if retrieved.AccountDID.String() != did.String() {
t.Errorf("Expected DID %q, got %q", did.String(), retrieved.AccountDID.String())
}
if retrieved.HostURL != sessionData.HostURL {
t.Errorf("Expected hostURL %q, got %q", sessionData.HostURL, retrieved.HostURL)
}
}
func TestFileStore_UpdateSession(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Save initial session
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "test-session-123",
HostURL: "https://pds.example.com",
Scopes: []string{"atproto"},
}
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// Update session with new scopes
sessionData.Scopes = []string{"atproto", "blob:read", "blob:write"}
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() (update) error = %v", err)
}
// Retrieve updated session
retrieved, err := store.GetSession(ctx, did, "test-session-123")
if err != nil {
t.Fatalf("GetSession() error = %v", err)
}
if len(retrieved.Scopes) != 3 {
t.Errorf("Expected 3 scopes, got %d", len(retrieved.Scopes))
}
}
func TestFileStore_DeleteSession(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Save session
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "test-session-123",
HostURL: "https://pds.example.com",
}
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// Verify it exists
if _, err := store.GetSession(ctx, did, "test-session-123"); err != nil {
t.Fatalf("GetSession() should succeed before delete, got error: %v", err)
}
// Delete session
if err := store.DeleteSession(ctx, did, "test-session-123"); err != nil {
t.Fatalf("DeleteSession() error = %v", err)
}
// Verify it's gone
_, err = store.GetSession(ctx, did, "test-session-123")
if err == nil {
t.Error("Expected error after deleting session")
}
}
func TestFileStore_DeleteNonExistentSession(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Delete non-existent session should not error
if err := store.DeleteSession(ctx, did, "nonexistent"); err != nil {
t.Errorf("DeleteSession() on non-existent session should not error, got: %v", err)
}
}
func TestFileStore_SaveAndGetAuthRequestInfo(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
// Create test auth request
did, _ := syntax.ParseDID("did:plc:alice123")
authRequest := oauth.AuthRequestData{
State: "test-state-123",
AuthServerURL: "https://pds.example.com",
AccountDID: &did,
Scopes: []string{"atproto", "blob:read"},
RequestURI: "urn:ietf:params:oauth:request_uri:test123",
AuthServerTokenEndpoint: "https://pds.example.com/oauth/token",
}
// Save auth request
if err := store.SaveAuthRequestInfo(ctx, authRequest); err != nil {
t.Fatalf("SaveAuthRequestInfo() error = %v", err)
}
// Retrieve auth request
retrieved, err := store.GetAuthRequestInfo(ctx, "test-state-123")
if err != nil {
t.Fatalf("GetAuthRequestInfo() error = %v", err)
}
if retrieved == nil {
t.Fatal("Expected non-nil auth request")
}
if retrieved.State != authRequest.State {
t.Errorf("Expected state %q, got %q", authRequest.State, retrieved.State)
}
if retrieved.AuthServerURL != authRequest.AuthServerURL {
t.Errorf("Expected authServerURL %q, got %q", authRequest.AuthServerURL, retrieved.AuthServerURL)
}
}
func TestFileStore_GetAuthRequestInfo_NotFound(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
// Should return error for non-existent request
_, err = store.GetAuthRequestInfo(ctx, "nonexistent-state")
if err == nil {
t.Error("Expected error for non-existent auth request")
}
}
func TestFileStore_DeleteAuthRequestInfo(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
// Save auth request
authRequest := oauth.AuthRequestData{
State: "test-state-123",
AuthServerURL: "https://pds.example.com",
}
if err := store.SaveAuthRequestInfo(ctx, authRequest); err != nil {
t.Fatalf("SaveAuthRequestInfo() error = %v", err)
}
// Verify it exists
if _, err := store.GetAuthRequestInfo(ctx, "test-state-123"); err != nil {
t.Fatalf("GetAuthRequestInfo() should succeed before delete, got error: %v", err)
}
// Delete auth request
if err := store.DeleteAuthRequestInfo(ctx, "test-state-123"); err != nil {
t.Fatalf("DeleteAuthRequestInfo() error = %v", err)
}
// Verify it's gone
_, err = store.GetAuthRequestInfo(ctx, "test-state-123")
if err == nil {
t.Error("Expected error after deleting auth request")
}
}
func TestFileStore_ListSessions(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
// Initially empty
sessions := store.ListSessions()
if len(sessions) != 0 {
t.Errorf("Expected 0 sessions, got %d", len(sessions))
}
// Add multiple sessions
did1, _ := syntax.ParseDID("did:plc:alice123")
did2, _ := syntax.ParseDID("did:plc:bob456")
session1 := oauth.ClientSessionData{
AccountDID: did1,
SessionID: "session-1",
HostURL: "https://pds1.example.com",
}
session2 := oauth.ClientSessionData{
AccountDID: did2,
SessionID: "session-2",
HostURL: "https://pds2.example.com",
}
if err := store.SaveSession(ctx, session1); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
if err := store.SaveSession(ctx, session2); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// List sessions
sessions = store.ListSessions()
if len(sessions) != 2 {
t.Errorf("Expected 2 sessions, got %d", len(sessions))
}
// Verify we got both sessions
key1 := makeSessionKey(did1.String(), "session-1")
key2 := makeSessionKey(did2.String(), "session-2")
if sessions[key1] == nil {
t.Error("Expected session1 in list")
}
if sessions[key2] == nil {
t.Error("Expected session2 in list")
}
}
func TestFileStore_Persistence_Across_Instances(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Create first store and save data
store1, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "persistent-session",
HostURL: "https://pds.example.com",
}
if err := store1.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
authRequest := oauth.AuthRequestData{
State: "persistent-state",
AuthServerURL: "https://pds.example.com",
}
if err := store1.SaveAuthRequestInfo(ctx, authRequest); err != nil {
t.Fatalf("SaveAuthRequestInfo() error = %v", err)
}
// Create second store from same file
store2, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("Second NewFileStore() error = %v", err)
}
// Verify session persisted
retrievedSession, err := store2.GetSession(ctx, did, "persistent-session")
if err != nil {
t.Fatalf("GetSession() from second store error = %v", err)
}
if retrievedSession.SessionID != "persistent-session" {
t.Errorf("Expected persistent session ID, got %q", retrievedSession.SessionID)
}
// Verify auth request persisted
retrievedAuth, err := store2.GetAuthRequestInfo(ctx, "persistent-state")
if err != nil {
t.Fatalf("GetAuthRequestInfo() from second store error = %v", err)
}
if retrievedAuth.State != "persistent-state" {
t.Errorf("Expected persistent state, got %q", retrievedAuth.State)
}
}
func TestFileStore_FileSecurity(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Save some data to trigger file creation
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "test-session",
HostURL: "https://pds.example.com",
}
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// Check file permissions (should be 0600)
info, err := os.Stat(storePath)
if err != nil {
t.Fatalf("Failed to stat file: %v", err)
}
mode := info.Mode()
if mode.Perm() != 0600 {
t.Errorf("Expected file permissions 0600, got %o", mode.Perm())
}
}
func TestFileStore_JSONFormat(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
did, _ := syntax.ParseDID("did:plc:alice123")
// Save data
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "test-session",
HostURL: "https://pds.example.com",
}
if err := store.SaveSession(ctx, sessionData); err != nil {
t.Fatalf("SaveSession() error = %v", err)
}
// Read and verify JSON format
data, err := os.ReadFile(storePath)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
var storeData FileStoreData
if err := json.Unmarshal(data, &storeData); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if storeData.Sessions == nil {
t.Error("Expected sessions in JSON")
}
if storeData.Requests == nil {
t.Error("Expected requests in JSON")
}
}
func TestFileStore_CleanupExpired(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
// CleanupExpired should not error even with no data
if err := store.CleanupExpired(); err != nil {
t.Errorf("CleanupExpired() error = %v", err)
}
// Note: Current implementation doesn't actually clean anything
// since AuthRequestData and ClientSessionData don't have expiry timestamps
// This test verifies the method doesn't panic
}
func TestGetDefaultStorePath(t *testing.T) {
path, err := GetDefaultStorePath()
if err != nil {
t.Fatalf("GetDefaultStorePath() error = %v", err)
}
if path == "" {
t.Fatal("Expected non-empty path")
}
// Path should either be /var/lib/atcr or ~/.atcr
// We can't assert exact path since it depends on permissions
t.Logf("Default store path: %s", path)
}
func TestMakeSessionKey(t *testing.T) {
did := "did:plc:alice123"
sessionID := "session-456"
key := makeSessionKey(did, sessionID)
expected := "did:plc:alice123:session-456"
if key != expected {
t.Errorf("Expected key %q, got %q", expected, key)
}
}
func TestFileStore_ConcurrentAccess(t *testing.T) {
tmpDir := t.TempDir()
storePath := tmpDir + "/oauth-test.json"
store, err := NewFileStore(storePath)
if err != nil {
t.Fatalf("NewFileStore() error = %v", err)
}
ctx := context.Background()
// Run concurrent operations
done := make(chan bool)
// Writer goroutine
go func() {
for i := 0; i < 10; i++ {
did, _ := syntax.ParseDID("did:plc:alice123")
sessionData := oauth.ClientSessionData{
AccountDID: did,
SessionID: "session-1",
HostURL: "https://pds.example.com",
}
store.SaveSession(ctx, sessionData)
time.Sleep(1 * time.Millisecond)
}
done <- true
}()
// Reader goroutine
go func() {
for i := 0; i < 10; i++ {
did, _ := syntax.ParseDID("did:plc:alice123")
store.GetSession(ctx, did, "session-1")
time.Sleep(1 * time.Millisecond)
}
done <- true
}()
// Wait for both goroutines
<-done
<-done
// If we got here without panicking, the locking works
t.Log("Concurrent access test passed")
}