mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
remove unused filestore. replace it with memstore for tests
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Hold Service XRPC Endpoints
|
||||
|
||||
This document lists all XRPC endpoints implemented in the Hold service (`pkg/hold/`).
|
||||
|
||||
## PDS Endpoints (`pkg/hold/pds/xrpc.go`)
|
||||
|
||||
### Public (No Auth Required)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/xrpc/_health` | GET | Health check |
|
||||
| `/xrpc/com.atproto.server.describeServer` | GET | Server metadata |
|
||||
| `/xrpc/com.atproto.repo.describeRepo` | GET | Repository information |
|
||||
| `/xrpc/com.atproto.repo.getRecord` | GET | Retrieve a single record |
|
||||
| `/xrpc/com.atproto.repo.listRecords` | GET | List records in a collection (paginated) |
|
||||
| `/xrpc/com.atproto.sync.listRepos` | GET | List all repositories |
|
||||
| `/xrpc/com.atproto.sync.getRecord` | GET | Get record as CAR file |
|
||||
| `/xrpc/com.atproto.sync.getRepo` | GET | Full repository as CAR file |
|
||||
| `/xrpc/com.atproto.sync.getRepoStatus` | GET | Repository hosting status |
|
||||
| `/xrpc/com.atproto.sync.subscribeRepos` | GET | WebSocket firehose |
|
||||
| `/xrpc/com.atproto.identity.resolveHandle` | GET | Resolve handle to DID |
|
||||
| `/xrpc/app.bsky.actor.getProfile` | GET | Get actor profile |
|
||||
| `/xrpc/app.bsky.actor.getProfiles` | GET | Get multiple profiles |
|
||||
| `/.well-known/did.json` | GET | DID document |
|
||||
| `/.well-known/atproto-did` | GET | DID for handle resolution |
|
||||
|
||||
### Conditional Auth (based on captain.public)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/xrpc/com.atproto.sync.getBlob` | GET/HEAD | Get blob (routes OCI vs ATProto) |
|
||||
|
||||
### Owner/Crew Admin Required
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/xrpc/com.atproto.repo.deleteRecord` | POST | Delete a record |
|
||||
| `/xrpc/com.atproto.repo.uploadBlob` | POST | Upload ATProto blob |
|
||||
|
||||
### DPoP Auth Required
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/xrpc/io.atcr.hold.requestCrew` | POST | Request crew membership |
|
||||
|
||||
---
|
||||
|
||||
## OCI Multipart Upload Endpoints (`pkg/hold/oci/xrpc.go`)
|
||||
|
||||
All require `blob:write` permission via service token:
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/xrpc/io.atcr.hold.initiateUpload` | POST | Start multipart upload |
|
||||
| `/xrpc/io.atcr.hold.getPartUploadUrl` | POST | Get presigned URL for part |
|
||||
| `/xrpc/io.atcr.hold.uploadPart` | PUT | Direct buffered part upload |
|
||||
| `/xrpc/io.atcr.hold.completeUpload` | POST | Finalize multipart upload |
|
||||
| `/xrpc/io.atcr.hold.abortUpload` | POST | Cancel multipart upload |
|
||||
| `/xrpc/io.atcr.hold.notifyManifest` | POST | Notify manifest push (creates layer records + optional Bluesky post) |
|
||||
|
||||
---
|
||||
|
||||
## Standard ATProto Endpoints (excluding io.atcr.hold.*)
|
||||
|
||||
| Endpoint |
|
||||
|----------|
|
||||
| /xrpc/_health |
|
||||
| /xrpc/com.atproto.server.describeServer |
|
||||
| /xrpc/com.atproto.repo.describeRepo |
|
||||
| /xrpc/com.atproto.repo.getRecord |
|
||||
| /xrpc/com.atproto.repo.listRecords |
|
||||
| /xrpc/com.atproto.repo.deleteRecord |
|
||||
| /xrpc/com.atproto.repo.uploadBlob |
|
||||
| /xrpc/com.atproto.sync.listRepos |
|
||||
| /xrpc/com.atproto.sync.getRecord |
|
||||
| /xrpc/com.atproto.sync.getRepo |
|
||||
| /xrpc/com.atproto.sync.getRepoStatus |
|
||||
| /xrpc/com.atproto.sync.getBlob |
|
||||
| /xrpc/com.atproto.sync.subscribeRepos |
|
||||
| /xrpc/com.atproto.identity.resolveHandle |
|
||||
| /xrpc/app.bsky.actor.getProfile |
|
||||
| /xrpc/app.bsky.actor.getProfiles |
|
||||
| /.well-known/did.json |
|
||||
| /.well-known/atproto-did |
|
||||
@@ -460,9 +460,9 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name
|
||||
Repository: repositoryName,
|
||||
ServiceToken: serviceToken, // Cached service token from puller's PDS
|
||||
ATProtoClient: atprotoClient,
|
||||
AuthMethod: authMethod, // Auth method from JWT token
|
||||
PullerDID: pullerDID, // Authenticated user making the request
|
||||
PullerPDSEndpoint: pullerPDSEndpoint, // Puller's PDS for service token refresh
|
||||
AuthMethod: authMethod, // Auth method from JWT token
|
||||
PullerDID: pullerDID, // Authenticated user making the request
|
||||
PullerPDSEndpoint: pullerPDSEndpoint, // Puller's PDS for service token refresh
|
||||
Database: nr.database,
|
||||
Authorizer: nr.authorizer,
|
||||
Refresher: nr.refresher,
|
||||
|
||||
@@ -20,16 +20,16 @@ type RegistryContext struct {
|
||||
// Per-request identity and routing information
|
||||
// Owner = the user whose repository is being accessed
|
||||
// Puller = the authenticated user making the request (from JWT Subject)
|
||||
DID string // Owner's DID - whose repo is being accessed (e.g., "did:plc:abc123")
|
||||
Handle string // Owner's handle (e.g., "alice.bsky.social")
|
||||
HoldDID string // Hold service DID (e.g., "did:web:hold01.atcr.io")
|
||||
PDSEndpoint string // Owner's PDS endpoint URL
|
||||
Repository string // Image repository name (e.g., "debian")
|
||||
ServiceToken string // Service token for hold authentication (from puller's PDS)
|
||||
ATProtoClient *atproto.Client // Authenticated ATProto client for the owner
|
||||
AuthMethod string // Auth method used ("oauth" or "app_password")
|
||||
PullerDID string // Puller's DID - who is making the request (from JWT Subject)
|
||||
PullerPDSEndpoint string // Puller's PDS endpoint URL
|
||||
DID string // Owner's DID - whose repo is being accessed (e.g., "did:plc:abc123")
|
||||
Handle string // Owner's handle (e.g., "alice.bsky.social")
|
||||
HoldDID string // Hold service DID (e.g., "did:web:hold01.atcr.io")
|
||||
PDSEndpoint string // Owner's PDS endpoint URL
|
||||
Repository string // Image repository name (e.g., "debian")
|
||||
ServiceToken string // Service token for hold authentication (from puller's PDS)
|
||||
ATProtoClient *atproto.Client // Authenticated ATProto client for the owner
|
||||
AuthMethod string // Auth method used ("oauth" or "app_password")
|
||||
PullerDID string // Puller's DID - who is making the request (from JWT Subject)
|
||||
PullerPDSEndpoint string // Puller's PDS endpoint URL
|
||||
|
||||
// Shared services (same for all requests)
|
||||
Database DatabaseMetrics // Metrics tracking database
|
||||
|
||||
@@ -310,7 +310,6 @@ type HoldRecord struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
|
||||
// SailorProfileRecord represents a user's profile with registry preferences
|
||||
// Stored in the user's PDS to configure default hold and other settings
|
||||
type SailorProfileRecord struct {
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClientApp(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
keyPath := tmpDir + "/oauth-key.bin"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
keyPath := t.TempDir() + "/oauth-key.bin"
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
baseURL := "http://localhost:5000"
|
||||
scopes := GetDefaultScopes("*")
|
||||
@@ -32,14 +27,8 @@ func TestNewClientApp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewClientAppWithCustomScopes(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
keyPath := tmpDir + "/oauth-key.bin"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
keyPath := t.TempDir() + "/oauth-key.bin"
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
baseURL := "http://localhost:5000"
|
||||
scopes := []string{"atproto", "custom:scope"}
|
||||
@@ -128,13 +117,7 @@ func TestScopesMatch(t *testing.T) {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestNewRefresher(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -153,13 +136,7 @@ func TestNewRefresher(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefresher_SetUISessionStore(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
|
||||
@@ -26,11 +26,7 @@ func InteractiveFlowWithCallback(
|
||||
registerCallback func(handler http.HandlerFunc) error,
|
||||
displayAuthURL func(string) error,
|
||||
) (*InteractiveResult, error) {
|
||||
// Create temporary file store for this flow
|
||||
store, err := NewFileStore("/tmp/atcr-oauth-temp.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OAuth store: %w", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
// Create OAuth client app with custom scopes (or defaults if nil)
|
||||
// Interactive flows are typically for production use (credential helper, etc.)
|
||||
|
||||
@@ -2,6 +2,7 @@ package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -11,13 +12,7 @@ import (
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
// Create a basic OAuth app for testing
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -36,13 +31,7 @@ func TestNewServer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_SetRefresher(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -60,13 +49,7 @@ func TestServer_SetRefresher(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_SetPostAuthCallback(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -87,13 +70,7 @@ func TestServer_SetPostAuthCallback(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_SetUISessionStore(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -151,13 +128,7 @@ func (m *mockRefresher) InvalidateSession(did string) {
|
||||
// ServeAuthorize tests
|
||||
|
||||
func TestServer_ServeAuthorize_MissingHandle(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -179,13 +150,7 @@ func TestServer_ServeAuthorize_MissingHandle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_ServeAuthorize_InvalidMethod(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -209,13 +174,7 @@ func TestServer_ServeAuthorize_InvalidMethod(t *testing.T) {
|
||||
// ServeCallback tests
|
||||
|
||||
func TestServer_ServeCallback_InvalidMethod(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -237,13 +196,7 @@ func TestServer_ServeCallback_InvalidMethod(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_ServeCallback_OAuthError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -270,13 +223,7 @@ func TestServer_ServeCallback_OAuthError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_ServeCallback_WithPostAuthCallback(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -315,13 +262,7 @@ func TestServer_ServeCallback_UIFlow_SessionCreationLogic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -345,13 +286,7 @@ func TestServer_ServeCallback_UIFlow_SessionCreationLogic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_RenderError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
@@ -380,13 +315,7 @@ func TestServer_RenderError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_RenderRedirectToSettings(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
storePath := tmpDir + "/oauth-test.json"
|
||||
|
||||
store, err := NewFileStore(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileStore() error = %v", err)
|
||||
}
|
||||
store := oauth.NewMemStore()
|
||||
|
||||
scopes := GetDefaultScopes("*")
|
||||
clientApp, err := NewClientApp("http://localhost:5000", store, scopes, "", "AT Container Registry")
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
)
|
||||
|
||||
// FileStore implements oauth.ClientAuthStore with file-based persistence
|
||||
type FileStore struct {
|
||||
path string
|
||||
sessions map[string]*oauth.ClientSessionData // Key: "did:sessionID"
|
||||
requests map[string]*oauth.AuthRequestData // Key: state
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// FileStoreData represents the JSON structure stored on disk
|
||||
type FileStoreData struct {
|
||||
Sessions map[string]*oauth.ClientSessionData `json:"sessions"`
|
||||
Requests map[string]*oauth.AuthRequestData `json:"requests"`
|
||||
}
|
||||
|
||||
// NewFileStore creates a new file-based OAuth store
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
store := &FileStore{
|
||||
path: path,
|
||||
sessions: make(map[string]*oauth.ClientSessionData),
|
||||
requests: make(map[string]*oauth.AuthRequestData),
|
||||
}
|
||||
|
||||
// Load existing data if file exists
|
||||
if err := store.load(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to load store: %w", err)
|
||||
}
|
||||
// File doesn't exist yet, that's ok
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// GetDefaultStorePath returns the default storage path for OAuth data
|
||||
func GetDefaultStorePath() (string, error) {
|
||||
// For AppView: /var/lib/atcr/oauth-sessions.json
|
||||
// For CLI tools: ~/.atcr/oauth-sessions.json
|
||||
|
||||
// Check if running as a service (has write access to /var/lib)
|
||||
servicePath := "/var/lib/atcr/oauth-sessions.json"
|
||||
if err := os.MkdirAll(filepath.Dir(servicePath), 0700); err == nil {
|
||||
// Can write to /var/lib, use service path
|
||||
return servicePath, nil
|
||||
}
|
||||
|
||||
// Fall back to user home directory
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
|
||||
atcrDir := filepath.Join(homeDir, ".atcr")
|
||||
if err := os.MkdirAll(atcrDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create .atcr directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(atcrDir, "oauth-sessions.json"), nil
|
||||
}
|
||||
|
||||
// GetSession retrieves a session by DID and session ID
|
||||
func (s *FileStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
key := makeSessionKey(did.String(), sessionID)
|
||||
session, ok := s.sessions[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("session not found: %s/%s", did, sessionID)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// SaveSession saves or updates a session (upsert)
|
||||
func (s *FileStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := makeSessionKey(sess.AccountDID.String(), sess.SessionID)
|
||||
s.sessions[key] = &sess
|
||||
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// DeleteSession removes a session
|
||||
func (s *FileStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := makeSessionKey(did.String(), sessionID)
|
||||
delete(s.sessions, key)
|
||||
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// GetAuthRequestInfo retrieves authentication request data by state
|
||||
func (s *FileStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
request, ok := s.requests[state]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("auth request not found: %s", state)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// SaveAuthRequestInfo saves authentication request data
|
||||
func (s *FileStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.requests[info.State] = &info
|
||||
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// DeleteAuthRequestInfo removes authentication request data
|
||||
func (s *FileStore) DeleteAuthRequestInfo(ctx context.Context, state string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.requests, state)
|
||||
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// CleanupExpired removes expired sessions and auth requests
|
||||
// Should be called periodically (e.g., every hour)
|
||||
func (s *FileStore) CleanupExpired() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
modified := false
|
||||
|
||||
// Clean up auth requests older than 10 minutes
|
||||
// (OAuth flows should complete quickly)
|
||||
for state := range s.requests {
|
||||
// Note: AuthRequestData doesn't have a timestamp in indigo's implementation
|
||||
// For now, we'll rely on the OAuth server's cleanup routine
|
||||
// or we could extend AuthRequestData with metadata
|
||||
_ = state // Placeholder for future expiration logic
|
||||
}
|
||||
|
||||
// Sessions don't have expiry in the data structure
|
||||
// Cleanup would need to be token-based (check token expiry)
|
||||
// For now, manual cleanup via DeleteSession
|
||||
_ = now
|
||||
|
||||
if modified {
|
||||
return s.save()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSessions returns all stored sessions for debugging/management
|
||||
func (s *FileStore) ListSessions() map[string]*oauth.ClientSessionData {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make(map[string]*oauth.ClientSessionData)
|
||||
maps.Copy(result, s.sessions)
|
||||
return result
|
||||
}
|
||||
|
||||
// load reads data from disk
|
||||
func (s *FileStore) load() error {
|
||||
data, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var storeData FileStoreData
|
||||
if err := json.Unmarshal(data, &storeData); err != nil {
|
||||
return fmt.Errorf("failed to parse store: %w", err)
|
||||
}
|
||||
|
||||
if storeData.Sessions != nil {
|
||||
s.sessions = storeData.Sessions
|
||||
}
|
||||
if storeData.Requests != nil {
|
||||
s.requests = storeData.Requests
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save writes data to disk
|
||||
func (s *FileStore) save() error {
|
||||
storeData := FileStoreData{
|
||||
Sessions: s.sessions,
|
||||
Requests: s.requests,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(storeData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal store: %w", err)
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Write with restrictive permissions
|
||||
if err := os.WriteFile(s.path, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write store: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeSessionKey creates a composite key for session storage
|
||||
func makeSessionKey(did, sessionID string) string {
|
||||
return fmt.Sprintf("%s:%s", did, sessionID)
|
||||
}
|
||||
@@ -1,631 +0,0 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user