unit tests

This commit is contained in:
Evan Jarrett
2025-10-28 17:40:11 -05:00
parent 93b1d0d4ba
commit b0799cd94d
56 changed files with 9857 additions and 58 deletions
+361
View File
@@ -0,0 +1,361 @@
package db
import (
"database/sql"
"testing"
)
func TestAnnotations_Placeholder(t *testing.T) {
// Placeholder test for annotations package
// GetRepositoryAnnotations returns map[string]string
annotations := make(map[string]string)
annotations["test"] = "value"
if annotations["test"] != "value" {
t.Error("Expected annotation value to be stored")
}
}
// Integration tests
func setupAnnotationsTestDB(t *testing.T) *sql.DB {
t.Helper()
// Use file::memory: with cache=shared to ensure all connections share the same in-memory DB
db, err := InitDB("file::memory:?cache=shared")
if err != nil {
t.Fatalf("Failed to initialize test database: %v", err)
}
// Limit to single connection to avoid race conditions in tests
db.SetMaxOpenConns(1)
t.Cleanup(func() { db.Close() })
return db
}
func createAnnotationTestUser(t *testing.T, db *sql.DB, did, handle string) {
t.Helper()
_, err := db.Exec(`
INSERT OR IGNORE INTO users (did, handle, pds_endpoint, last_seen)
VALUES (?, ?, ?, datetime('now'))
`, did, handle, "https://pds.example.com")
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
}
// TestGetRepositoryAnnotations_Empty tests retrieving from empty repository
func TestGetRepositoryAnnotations_Empty(t *testing.T) {
db := setupAnnotationsTestDB(t)
annotations, err := GetRepositoryAnnotations(db, "did:plc:alice123", "myapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(annotations) != 0 {
t.Errorf("Expected empty annotations, got %d entries", len(annotations))
}
}
// TestGetRepositoryAnnotations_WithData tests retrieving existing annotations
func TestGetRepositoryAnnotations_WithData(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:alice123", "alice.bsky.social")
// Insert test annotations
testAnnotations := map[string]string{
"org.opencontainers.image.title": "My App",
"org.opencontainers.image.description": "A test application",
"org.opencontainers.image.version": "1.0.0",
}
err := UpsertRepositoryAnnotations(db, "did:plc:alice123", "myapp", testAnnotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
}
// Retrieve annotations
annotations, err := GetRepositoryAnnotations(db, "did:plc:alice123", "myapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(annotations) != len(testAnnotations) {
t.Errorf("Expected %d annotations, got %d", len(testAnnotations), len(annotations))
}
for key, expectedValue := range testAnnotations {
if actualValue, ok := annotations[key]; !ok {
t.Errorf("Missing annotation key: %s", key)
} else if actualValue != expectedValue {
t.Errorf("Annotation[%s] = %v, want %v", key, actualValue, expectedValue)
}
}
}
// TestUpsertRepositoryAnnotations_Insert tests inserting new annotations
func TestUpsertRepositoryAnnotations_Insert(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:bob456", "bob.bsky.social")
annotations := map[string]string{
"key1": "value1",
"key2": "value2",
}
err := UpsertRepositoryAnnotations(db, "did:plc:bob456", "testapp", annotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
}
// Verify annotations were inserted
retrieved, err := GetRepositoryAnnotations(db, "did:plc:bob456", "testapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(retrieved) != len(annotations) {
t.Errorf("Expected %d annotations, got %d", len(annotations), len(retrieved))
}
for key, expectedValue := range annotations {
if actualValue := retrieved[key]; actualValue != expectedValue {
t.Errorf("Annotation[%s] = %v, want %v", key, actualValue, expectedValue)
}
}
}
// TestUpsertRepositoryAnnotations_Update tests updating existing annotations
func TestUpsertRepositoryAnnotations_Update(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:charlie789", "charlie.bsky.social")
// Insert initial annotations
initial := map[string]string{
"key1": "oldvalue1",
"key2": "oldvalue2",
"key3": "oldvalue3",
}
err := UpsertRepositoryAnnotations(db, "did:plc:charlie789", "updateapp", initial)
if err != nil {
t.Fatalf("Initial UpsertRepositoryAnnotations() error = %v", err)
}
// Update with new annotations (completely replaces old ones)
updated := map[string]string{
"key1": "newvalue1", // Updated
"key4": "newvalue4", // New key (key2 and key3 removed)
}
err = UpsertRepositoryAnnotations(db, "did:plc:charlie789", "updateapp", updated)
if err != nil {
t.Fatalf("Update UpsertRepositoryAnnotations() error = %v", err)
}
// Verify annotations were replaced
retrieved, err := GetRepositoryAnnotations(db, "did:plc:charlie789", "updateapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(retrieved) != len(updated) {
t.Errorf("Expected %d annotations, got %d", len(updated), len(retrieved))
}
// Verify new values
if retrieved["key1"] != "newvalue1" {
t.Errorf("key1 = %v, want newvalue1", retrieved["key1"])
}
if retrieved["key4"] != "newvalue4" {
t.Errorf("key4 = %v, want newvalue4", retrieved["key4"])
}
// Verify old keys were removed
if _, exists := retrieved["key2"]; exists {
t.Error("key2 should have been removed")
}
if _, exists := retrieved["key3"]; exists {
t.Error("key3 should have been removed")
}
}
// TestUpsertRepositoryAnnotations_EmptyMap tests upserting with empty map
func TestUpsertRepositoryAnnotations_EmptyMap(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:dave111", "dave.bsky.social")
// Insert initial annotations
initial := map[string]string{
"key1": "value1",
"key2": "value2",
}
err := UpsertRepositoryAnnotations(db, "did:plc:dave111", "emptyapp", initial)
if err != nil {
t.Fatalf("Initial UpsertRepositoryAnnotations() error = %v", err)
}
// Upsert with empty map (should delete all)
empty := make(map[string]string)
err = UpsertRepositoryAnnotations(db, "did:plc:dave111", "emptyapp", empty)
if err != nil {
t.Fatalf("Empty UpsertRepositoryAnnotations() error = %v", err)
}
// Verify all annotations were deleted
retrieved, err := GetRepositoryAnnotations(db, "did:plc:dave111", "emptyapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(retrieved) != 0 {
t.Errorf("Expected 0 annotations after empty upsert, got %d", len(retrieved))
}
}
// TestUpsertRepositoryAnnotations_MultipleRepos tests isolation between repositories
func TestUpsertRepositoryAnnotations_MultipleRepos(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:eve222", "eve.bsky.social")
// Insert annotations for repo1
repo1Annotations := map[string]string{
"repo": "repo1",
"key1": "value1",
}
err := UpsertRepositoryAnnotations(db, "did:plc:eve222", "repo1", repo1Annotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations(repo1) error = %v", err)
}
// Insert annotations for repo2 (same DID, different repo)
repo2Annotations := map[string]string{
"repo": "repo2",
"key2": "value2",
}
err = UpsertRepositoryAnnotations(db, "did:plc:eve222", "repo2", repo2Annotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations(repo2) error = %v", err)
}
// Verify repo1 annotations unchanged
retrieved1, err := GetRepositoryAnnotations(db, "did:plc:eve222", "repo1")
if err != nil {
t.Fatalf("GetRepositoryAnnotations(repo1) error = %v", err)
}
if len(retrieved1) != len(repo1Annotations) {
t.Errorf("repo1: Expected %d annotations, got %d", len(repo1Annotations), len(retrieved1))
}
if retrieved1["repo"] != "repo1" {
t.Errorf("repo1: Expected repo=repo1, got %v", retrieved1["repo"])
}
// Verify repo2 annotations
retrieved2, err := GetRepositoryAnnotations(db, "did:plc:eve222", "repo2")
if err != nil {
t.Fatalf("GetRepositoryAnnotations(repo2) error = %v", err)
}
if len(retrieved2) != len(repo2Annotations) {
t.Errorf("repo2: Expected %d annotations, got %d", len(repo2Annotations), len(retrieved2))
}
if retrieved2["repo"] != "repo2" {
t.Errorf("repo2: Expected repo=repo2, got %v", retrieved2["repo"])
}
}
// TestDeleteRepositoryAnnotations tests deleting annotations
func TestDeleteRepositoryAnnotations(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:frank333", "frank.bsky.social")
// Insert annotations
annotations := map[string]string{
"key1": "value1",
"key2": "value2",
}
err := UpsertRepositoryAnnotations(db, "did:plc:frank333", "deleteapp", annotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
}
// Verify annotations exist
retrieved, err := GetRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() error = %v", err)
}
if len(retrieved) != 2 {
t.Fatalf("Expected 2 annotations before delete, got %d", len(retrieved))
}
// Delete annotations
err = DeleteRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
if err != nil {
t.Fatalf("DeleteRepositoryAnnotations() error = %v", err)
}
// Verify annotations were deleted
retrieved, err = GetRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
if err != nil {
t.Fatalf("GetRepositoryAnnotations() after delete error = %v", err)
}
if len(retrieved) != 0 {
t.Errorf("Expected 0 annotations after delete, got %d", len(retrieved))
}
}
// TestDeleteRepositoryAnnotations_NonExistent tests deleting non-existent annotations
func TestDeleteRepositoryAnnotations_NonExistent(t *testing.T) {
db := setupAnnotationsTestDB(t)
// Delete from non-existent repository (should not error)
err := DeleteRepositoryAnnotations(db, "did:plc:ghost999", "nonexistent")
if err != nil {
t.Errorf("DeleteRepositoryAnnotations() for non-existent repo should not error, got: %v", err)
}
}
// TestAnnotations_DifferentDIDs tests isolation between different DIDs
func TestAnnotations_DifferentDIDs(t *testing.T) {
db := setupAnnotationsTestDB(t)
createAnnotationTestUser(t, db, "did:plc:alice123", "alice.bsky.social")
createAnnotationTestUser(t, db, "did:plc:bob456", "bob.bsky.social")
// Insert annotations for alice
aliceAnnotations := map[string]string{
"owner": "alice",
"key1": "alice-value1",
}
err := UpsertRepositoryAnnotations(db, "did:plc:alice123", "sharedname", aliceAnnotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations(alice) error = %v", err)
}
// Insert annotations for bob (same repo name, different DID)
bobAnnotations := map[string]string{
"owner": "bob",
"key1": "bob-value1",
}
err = UpsertRepositoryAnnotations(db, "did:plc:bob456", "sharedname", bobAnnotations)
if err != nil {
t.Fatalf("UpsertRepositoryAnnotations(bob) error = %v", err)
}
// Verify alice's annotations unchanged
aliceRetrieved, err := GetRepositoryAnnotations(db, "did:plc:alice123", "sharedname")
if err != nil {
t.Fatalf("GetRepositoryAnnotations(alice) error = %v", err)
}
if aliceRetrieved["owner"] != "alice" {
t.Errorf("alice: Expected owner=alice, got %v", aliceRetrieved["owner"])
}
// Verify bob's annotations
bobRetrieved, err := GetRepositoryAnnotations(db, "did:plc:bob456", "sharedname")
if err != nil {
t.Fatalf("GetRepositoryAnnotations(bob) error = %v", err)
}
if bobRetrieved["owner"] != "bob" {
t.Errorf("bob: Expected owner=bob, got %v", bobRetrieved["owner"])
}
}
+1 -1
View File
@@ -416,7 +416,7 @@ func (s *DeviceStore) CleanupExpiredContext(ctx context.Context) error {
// Format: XXXX-XXXX (e.g., "WDJB-MJHT")
// Character set: A-Z excluding ambiguous chars (0, O, I, 1, L)
func generateUserCode() string {
chars := "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
chars := "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
code := make([]byte, 8)
if _, err := rand.Read(code); err != nil {
// Fallback to timestamp-based generation if crypto rand fails
+635
View File
@@ -0,0 +1,635 @@
package db
import (
"context"
"strings"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
)
// setupTestDB creates an in-memory SQLite database for testing
func setupTestDB(t *testing.T) *DeviceStore {
t.Helper()
// Use file::memory: with cache=shared to ensure all connections share the same in-memory DB
// This prevents race conditions where different connections see different databases
db, err := InitDB("file::memory:?cache=shared")
if err != nil {
t.Fatalf("Failed to initialize test database: %v", err)
}
// Limit to single connection to avoid race conditions in tests
db.SetMaxOpenConns(1)
t.Cleanup(func() {
db.Close()
})
return NewDeviceStore(db)
}
// createTestUser creates a test user in the database
func createTestUser(t *testing.T, store *DeviceStore, did, handle string) {
t.Helper()
_, err := store.db.Exec(`
INSERT OR IGNORE INTO users (did, handle, pds_endpoint, last_seen)
VALUES (?, ?, ?, datetime('now'))
`, did, handle, "https://pds.example.com")
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
}
func TestDevice_Struct(t *testing.T) {
device := &Device{
DID: "did:plc:test",
Handle: "alice.bsky.social",
Name: "My Device",
CreatedAt: time.Now(),
}
if device.DID != "did:plc:test" {
t.Errorf("Expected DID, got %q", device.DID)
}
}
func TestGenerateUserCode(t *testing.T) {
// Generate multiple codes to test
codes := make(map[string]bool)
for i := 0; i < 100; i++ {
code := generateUserCode()
// Test format: XXXX-XXXX
if len(code) != 9 {
t.Errorf("Expected code length 9, got %d for code %q", len(code), code)
}
if code[4] != '-' {
t.Errorf("Expected hyphen at position 4, got %q", string(code[4]))
}
// Test valid characters (A-Z, 2-9, no ambiguous chars)
validChars := "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
parts := strings.Split(code, "-")
if len(parts) != 2 {
t.Errorf("Expected 2 parts separated by hyphen, got %d", len(parts))
}
for _, part := range parts {
for _, ch := range part {
if !strings.ContainsRune(validChars, ch) {
t.Errorf("Invalid character %q in code %q", ch, code)
}
}
}
// Test uniqueness (should be very rare to get duplicates)
if codes[code] {
t.Logf("Warning: duplicate code generated: %q (rare but possible)", code)
}
codes[code] = true
}
// Verify we got mostly unique codes (at least 95%)
if len(codes) < 95 {
t.Errorf("Expected at least 95 unique codes out of 100, got %d", len(codes))
}
}
func TestGenerateUserCode_Format(t *testing.T) {
code := generateUserCode()
// Test exact format
if len(code) != 9 {
t.Fatal("Code must be exactly 9 characters")
}
if code[4] != '-' {
t.Fatal("Character at index 4 must be hyphen")
}
// Test no ambiguous characters (O, 0, I, 1, L)
ambiguous := "O01IL"
for _, ch := range code {
if strings.ContainsRune(ambiguous, ch) {
t.Errorf("Code contains ambiguous character %q: %s", ch, code)
}
}
}
// TestDeviceStore_CreatePendingAuth tests creating pending authorization
func TestDeviceStore_CreatePendingAuth(t *testing.T) {
store := setupTestDB(t)
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
if pending.DeviceCode == "" {
t.Error("DeviceCode should not be empty")
}
if pending.UserCode == "" {
t.Error("UserCode should not be empty")
}
if pending.DeviceName != "My Device" {
t.Errorf("DeviceName = %v, want My Device", pending.DeviceName)
}
if pending.IPAddress != "192.168.1.1" {
t.Errorf("IPAddress = %v, want 192.168.1.1", pending.IPAddress)
}
if pending.UserAgent != "Test Agent" {
t.Errorf("UserAgent = %v, want Test Agent", pending.UserAgent)
}
if pending.ExpiresAt.Before(time.Now()) {
t.Error("ExpiresAt should be in the future")
}
}
// TestDeviceStore_GetPendingByUserCode tests retrieving pending auth by user code
func TestDeviceStore_GetPendingByUserCode(t *testing.T) {
store := setupTestDB(t)
// Create pending auth
created, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
tests := []struct {
name string
userCode string
wantFound bool
}{
{
name: "existing user code",
userCode: created.UserCode,
wantFound: true,
},
{
name: "non-existent user code",
userCode: "AAAA-BBBB",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pending, found := store.GetPendingByUserCode(tt.userCode)
if found != tt.wantFound {
t.Errorf("GetPendingByUserCode() found = %v, want %v", found, tt.wantFound)
}
if tt.wantFound && pending == nil {
t.Error("Expected pending auth, got nil")
}
if tt.wantFound && pending != nil {
if pending.DeviceName != "My Device" {
t.Errorf("DeviceName = %v, want My Device", pending.DeviceName)
}
}
})
}
}
// TestDeviceStore_GetPendingByDeviceCode tests retrieving pending auth by device code
func TestDeviceStore_GetPendingByDeviceCode(t *testing.T) {
store := setupTestDB(t)
// Create pending auth
created, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
tests := []struct {
name string
deviceCode string
wantFound bool
}{
{
name: "existing device code",
deviceCode: created.DeviceCode,
wantFound: true,
},
{
name: "non-existent device code",
deviceCode: "invalidcode",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pending, found := store.GetPendingByDeviceCode(tt.deviceCode)
if found != tt.wantFound {
t.Errorf("GetPendingByDeviceCode() found = %v, want %v", found, tt.wantFound)
}
if tt.wantFound && pending == nil {
t.Error("Expected pending auth, got nil")
}
})
}
}
// TestDeviceStore_ApprovePending tests approving pending authorization
func TestDeviceStore_ApprovePending(t *testing.T) {
store := setupTestDB(t)
// Create test users
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
createTestUser(t, store, "did:plc:bob123", "bob.bsky.social")
// Create pending auth
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
tests := []struct {
name string
userCode string
did string
handle string
wantErr bool
errString string
}{
{
name: "successful approval",
userCode: pending.UserCode,
did: "did:plc:alice123",
handle: "alice.bsky.social",
wantErr: false,
},
{
name: "non-existent user code",
userCode: "AAAA-BBBB",
did: "did:plc:bob123",
handle: "bob.bsky.social",
wantErr: true,
errString: "not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret, err := store.ApprovePending(tt.userCode, tt.did, tt.handle)
if (err != nil) != tt.wantErr {
t.Errorf("ApprovePending() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if secret == "" {
t.Error("Expected device secret, got empty string")
}
if !strings.HasPrefix(secret, "atcr_device_") {
t.Errorf("Secret should start with atcr_device_, got %v", secret)
}
// Verify device was created
devices := store.ListDevices(tt.did)
if len(devices) != 1 {
t.Errorf("Expected 1 device, got %d", len(devices))
}
}
if tt.wantErr && tt.errString != "" && err != nil {
if !strings.Contains(err.Error(), tt.errString) {
t.Errorf("Error should contain %q, got %v", tt.errString, err)
}
}
})
}
}
// TestDeviceStore_ApprovePending_AlreadyApproved tests double approval
func TestDeviceStore_ApprovePending_AlreadyApproved(t *testing.T) {
store := setupTestDB(t)
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
// First approval
_, err = store.ApprovePending(pending.UserCode, "did:plc:alice123", "alice.bsky.social")
if err != nil {
t.Fatalf("First ApprovePending() error = %v", err)
}
// Second approval should fail
_, err = store.ApprovePending(pending.UserCode, "did:plc:alice123", "alice.bsky.social")
if err == nil {
t.Error("Expected error for double approval, got nil")
}
if !strings.Contains(err.Error(), "already approved") {
t.Errorf("Error should contain 'already approved', got %v", err)
}
}
// TestDeviceStore_ValidateDeviceSecret tests device secret validation
func TestDeviceStore_ValidateDeviceSecret(t *testing.T) {
store := setupTestDB(t)
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Create and approve a device
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
secret, err := store.ApprovePending(pending.UserCode, "did:plc:alice123", "alice.bsky.social")
if err != nil {
t.Fatalf("ApprovePending() error = %v", err)
}
tests := []struct {
name string
secret string
wantErr bool
}{
{
name: "valid secret",
secret: secret,
wantErr: false,
},
{
name: "invalid secret",
secret: "atcr_device_invalid",
wantErr: true,
},
{
name: "empty secret",
secret: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
device, err := store.ValidateDeviceSecret(tt.secret)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateDeviceSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if device == nil {
t.Error("Expected device, got nil")
}
if device.DID != "did:plc:alice123" {
t.Errorf("DID = %v, want did:plc:alice123", device.DID)
}
if device.Name != "My Device" {
t.Errorf("Name = %v, want My Device", device.Name)
}
}
})
}
}
// TestDeviceStore_ListDevices tests listing devices
func TestDeviceStore_ListDevices(t *testing.T) {
store := setupTestDB(t)
did := "did:plc:alice123"
createTestUser(t, store, did, "alice.bsky.social")
// Initially empty
devices := store.ListDevices(did)
if len(devices) != 0 {
t.Errorf("Expected 0 devices initially, got %d", len(devices))
}
// Create 3 devices
for i := 0; i < 3; i++ {
pending, err := store.CreatePendingAuth("Device "+string(rune('A'+i)), "192.168.1.1", "Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
_, err = store.ApprovePending(pending.UserCode, did, "alice.bsky.social")
if err != nil {
t.Fatalf("ApprovePending() error = %v", err)
}
}
// List devices
devices = store.ListDevices(did)
if len(devices) != 3 {
t.Errorf("Expected 3 devices, got %d", len(devices))
}
// Verify they're sorted by created_at DESC (newest first)
for i := 0; i < len(devices)-1; i++ {
if devices[i].CreatedAt.Before(devices[i+1].CreatedAt) {
t.Error("Devices should be sorted by created_at DESC")
}
}
// List devices for different DID
otherDevices := store.ListDevices("did:plc:bob123")
if len(otherDevices) != 0 {
t.Errorf("Expected 0 devices for different DID, got %d", len(otherDevices))
}
}
// TestDeviceStore_RevokeDevice tests revoking a device
func TestDeviceStore_RevokeDevice(t *testing.T) {
store := setupTestDB(t)
did := "did:plc:alice123"
createTestUser(t, store, did, "alice.bsky.social")
// Create device
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
_, err = store.ApprovePending(pending.UserCode, did, "alice.bsky.social")
if err != nil {
t.Fatalf("ApprovePending() error = %v", err)
}
devices := store.ListDevices(did)
if len(devices) != 1 {
t.Fatalf("Expected 1 device, got %d", len(devices))
}
deviceID := devices[0].ID
tests := []struct {
name string
did string
deviceID string
wantErr bool
}{
{
name: "successful revocation",
did: did,
deviceID: deviceID,
wantErr: false,
},
{
name: "non-existent device",
did: did,
deviceID: "non-existent-id",
wantErr: true,
},
{
name: "wrong DID",
did: "did:plc:bob123",
deviceID: deviceID,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := store.RevokeDevice(tt.did, tt.deviceID)
if (err != nil) != tt.wantErr {
t.Errorf("RevokeDevice() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
// Verify device was removed (after first successful test)
devices = store.ListDevices(did)
if len(devices) != 0 {
t.Errorf("Expected 0 devices after revocation, got %d", len(devices))
}
}
// TestDeviceStore_UpdateLastUsed tests updating last used timestamp
func TestDeviceStore_UpdateLastUsed(t *testing.T) {
store := setupTestDB(t)
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Create device
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
secret, err := store.ApprovePending(pending.UserCode, "did:plc:alice123", "alice.bsky.social")
if err != nil {
t.Fatalf("ApprovePending() error = %v", err)
}
// Get device to get secret hash
device, err := store.ValidateDeviceSecret(secret)
if err != nil {
t.Fatalf("ValidateDeviceSecret() error = %v", err)
}
initialLastUsed := device.LastUsed
// Wait a bit to ensure timestamp difference
time.Sleep(10 * time.Millisecond)
// Update last used
err = store.UpdateLastUsed(device.SecretHash)
if err != nil {
t.Errorf("UpdateLastUsed() error = %v", err)
}
// Verify it was updated
device2, err := store.ValidateDeviceSecret(secret)
if err != nil {
t.Fatalf("ValidateDeviceSecret() error = %v", err)
}
if !device2.LastUsed.After(initialLastUsed) {
t.Error("LastUsed should be updated to later time")
}
}
// TestDeviceStore_CleanupExpired tests cleanup of expired pending auths
func TestDeviceStore_CleanupExpired(t *testing.T) {
store := setupTestDB(t)
// Create pending auth with manual expiration time
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
// Manually update expiration to the past
_, err = store.db.Exec(`
UPDATE pending_device_auth
SET expires_at = datetime('now', '-1 hour')
WHERE device_code = ?
`, pending.DeviceCode)
if err != nil {
t.Fatalf("Failed to update expiration: %v", err)
}
// Run cleanup
store.CleanupExpired()
// Verify it was deleted
_, found := store.GetPendingByDeviceCode(pending.DeviceCode)
if found {
t.Error("Expired pending auth should have been cleaned up")
}
}
// TestDeviceStore_CleanupExpiredContext tests context-aware cleanup
func TestDeviceStore_CleanupExpiredContext(t *testing.T) {
store := setupTestDB(t)
// Create and expire pending auth
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
_, err = store.db.Exec(`
UPDATE pending_device_auth
SET expires_at = datetime('now', '-1 hour')
WHERE device_code = ?
`, pending.DeviceCode)
if err != nil {
t.Fatalf("Failed to update expiration: %v", err)
}
// Run context-aware cleanup
ctx := context.Background()
err = store.CleanupExpiredContext(ctx)
if err != nil {
t.Errorf("CleanupExpiredContext() error = %v", err)
}
// Verify it was deleted
_, found := store.GetPendingByDeviceCode(pending.DeviceCode)
if found {
t.Error("Expired pending auth should have been cleaned up")
}
}
// TestDeviceStore_SecretHashing tests bcrypt hashing
func TestDeviceStore_SecretHashing(t *testing.T) {
store := setupTestDB(t)
createTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
pending, err := store.CreatePendingAuth("My Device", "192.168.1.1", "Test Agent")
if err != nil {
t.Fatalf("CreatePendingAuth() error = %v", err)
}
secret, err := store.ApprovePending(pending.UserCode, "did:plc:alice123", "alice.bsky.social")
if err != nil {
t.Fatalf("ApprovePending() error = %v", err)
}
// Get device via ValidateDeviceSecret to access secret hash
device, err := store.ValidateDeviceSecret(secret)
if err != nil {
t.Fatalf("ValidateDeviceSecret() error = %v", err)
}
// Verify bcrypt hash is valid
err = bcrypt.CompareHashAndPassword([]byte(device.SecretHash), []byte(secret))
if err != nil {
t.Error("Secret hash should match secret")
}
// Verify wrong secret doesn't match
err = bcrypt.CompareHashAndPassword([]byte(device.SecretHash), []byte("wrong_secret"))
if err == nil {
t.Error("Wrong secret should not match hash")
}
}
+477
View File
@@ -0,0 +1,477 @@
package db
import (
"database/sql"
"testing"
"time"
)
func TestNullString(t *testing.T) {
tests := []struct {
name string
input string
expectedValid bool
expectedStr string
}{
{
name: "empty string",
input: "",
expectedValid: false,
expectedStr: "",
},
{
name: "non-empty string",
input: "hello",
expectedValid: true,
expectedStr: "hello",
},
{
name: "whitespace string",
input: " ",
expectedValid: true,
expectedStr: " ",
},
{
name: "single character",
input: "a",
expectedValid: true,
expectedStr: "a",
},
{
name: "newline string",
input: "\n",
expectedValid: true,
expectedStr: "\n",
},
{
name: "tab string",
input: "\t",
expectedValid: true,
expectedStr: "\t",
},
{
name: "DID string",
input: "did:plc:abc123",
expectedValid: true,
expectedStr: "did:plc:abc123",
},
{
name: "URL string",
input: "https://example.com",
expectedValid: true,
expectedStr: "https://example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := nullString(tt.input)
if result.Valid != tt.expectedValid {
t.Errorf("nullString(%q).Valid = %v, want %v", tt.input, result.Valid, tt.expectedValid)
}
if result.String != tt.expectedStr {
t.Errorf("nullString(%q).String = %q, want %q", tt.input, result.String, tt.expectedStr)
}
})
}
}
// Integration tests
func setupHoldTestDB(t *testing.T) *sql.DB {
t.Helper()
// Use file::memory: with cache=shared to ensure all connections share the same in-memory DB
db, err := InitDB("file::memory:?cache=shared")
if err != nil {
t.Fatalf("Failed to initialize test database: %v", err)
}
// Limit to single connection to avoid race conditions in tests
db.SetMaxOpenConns(1)
t.Cleanup(func() { db.Close() })
return db
}
// TestGetCaptainRecord tests retrieving captain records
func TestGetCaptainRecord(t *testing.T) {
db := setupHoldTestDB(t)
// Insert a test record
testRecord := &HoldCaptainRecord{
HoldDID: "did:web:hold01.atcr.io",
OwnerDID: "did:plc:alice123",
Public: true,
AllowAllCrew: false,
DeployedAt: "2025-01-15",
Region: "us-west-2",
Provider: "aws",
UpdatedAt: time.Now(),
}
err := UpsertCaptainRecord(db, testRecord)
if err != nil {
t.Fatalf("UpsertCaptainRecord() error = %v", err)
}
tests := []struct {
name string
holdDID string
wantFound bool
}{
{
name: "existing record",
holdDID: "did:web:hold01.atcr.io",
wantFound: true,
},
{
name: "non-existent record",
holdDID: "did:web:unknown.atcr.io",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
record, err := GetCaptainRecord(db, tt.holdDID)
if err != nil {
t.Fatalf("GetCaptainRecord() error = %v", err)
}
if tt.wantFound {
if record == nil {
t.Error("Expected record, got nil")
return
}
if record.HoldDID != tt.holdDID {
t.Errorf("HoldDID = %v, want %v", record.HoldDID, tt.holdDID)
}
if record.OwnerDID != testRecord.OwnerDID {
t.Errorf("OwnerDID = %v, want %v", record.OwnerDID, testRecord.OwnerDID)
}
if record.Public != testRecord.Public {
t.Errorf("Public = %v, want %v", record.Public, testRecord.Public)
}
if record.AllowAllCrew != testRecord.AllowAllCrew {
t.Errorf("AllowAllCrew = %v, want %v", record.AllowAllCrew, testRecord.AllowAllCrew)
}
if record.DeployedAt != testRecord.DeployedAt {
t.Errorf("DeployedAt = %v, want %v", record.DeployedAt, testRecord.DeployedAt)
}
if record.Region != testRecord.Region {
t.Errorf("Region = %v, want %v", record.Region, testRecord.Region)
}
if record.Provider != testRecord.Provider {
t.Errorf("Provider = %v, want %v", record.Provider, testRecord.Provider)
}
} else {
if record != nil {
t.Errorf("Expected nil, got record: %+v", record)
}
}
})
}
}
// TestGetCaptainRecord_NullableFields tests handling of NULL fields
func TestGetCaptainRecord_NullableFields(t *testing.T) {
db := setupHoldTestDB(t)
// Insert record with empty nullable fields
testRecord := &HoldCaptainRecord{
HoldDID: "did:web:hold02.atcr.io",
OwnerDID: "did:plc:bob456",
Public: false,
AllowAllCrew: true,
DeployedAt: "", // Empty - should be NULL
Region: "", // Empty - should be NULL
Provider: "", // Empty - should be NULL
UpdatedAt: time.Now(),
}
err := UpsertCaptainRecord(db, testRecord)
if err != nil {
t.Fatalf("UpsertCaptainRecord() error = %v", err)
}
record, err := GetCaptainRecord(db, testRecord.HoldDID)
if err != nil {
t.Fatalf("GetCaptainRecord() error = %v", err)
}
if record == nil {
t.Fatal("Expected record, got nil")
}
if record.DeployedAt != "" {
t.Errorf("DeployedAt = %v, want empty string", record.DeployedAt)
}
if record.Region != "" {
t.Errorf("Region = %v, want empty string", record.Region)
}
if record.Provider != "" {
t.Errorf("Provider = %v, want empty string", record.Provider)
}
}
// TestUpsertCaptainRecord_Insert tests inserting new records
func TestUpsertCaptainRecord_Insert(t *testing.T) {
db := setupHoldTestDB(t)
record := &HoldCaptainRecord{
HoldDID: "did:web:hold03.atcr.io",
OwnerDID: "did:plc:charlie789",
Public: true,
AllowAllCrew: true,
DeployedAt: "2025-02-01",
Region: "eu-west-1",
Provider: "gcp",
UpdatedAt: time.Now(),
}
err := UpsertCaptainRecord(db, record)
if err != nil {
t.Fatalf("UpsertCaptainRecord() error = %v", err)
}
// Verify it was inserted
retrieved, err := GetCaptainRecord(db, record.HoldDID)
if err != nil {
t.Fatalf("GetCaptainRecord() error = %v", err)
}
if retrieved == nil {
t.Fatal("Expected record to be inserted")
}
if retrieved.HoldDID != record.HoldDID {
t.Errorf("HoldDID = %v, want %v", retrieved.HoldDID, record.HoldDID)
}
if retrieved.OwnerDID != record.OwnerDID {
t.Errorf("OwnerDID = %v, want %v", retrieved.OwnerDID, record.OwnerDID)
}
}
// TestUpsertCaptainRecord_Update tests updating existing records
func TestUpsertCaptainRecord_Update(t *testing.T) {
db := setupHoldTestDB(t)
// Insert initial record
initialRecord := &HoldCaptainRecord{
HoldDID: "did:web:hold04.atcr.io",
OwnerDID: "did:plc:dave111",
Public: false,
AllowAllCrew: false,
DeployedAt: "2025-01-01",
Region: "us-east-1",
Provider: "aws",
UpdatedAt: time.Now().Add(-1 * time.Hour),
}
err := UpsertCaptainRecord(db, initialRecord)
if err != nil {
t.Fatalf("Initial UpsertCaptainRecord() error = %v", err)
}
// Update the record
updatedRecord := &HoldCaptainRecord{
HoldDID: "did:web:hold04.atcr.io", // Same DID
OwnerDID: "did:plc:eve222", // Changed owner
Public: true, // Changed to public
AllowAllCrew: true, // Changed allow all crew
DeployedAt: "2025-03-01", // Changed date
Region: "ap-south-1", // Changed region
Provider: "azure", // Changed provider
UpdatedAt: time.Now(),
}
err = UpsertCaptainRecord(db, updatedRecord)
if err != nil {
t.Fatalf("Update UpsertCaptainRecord() error = %v", err)
}
// Verify it was updated
retrieved, err := GetCaptainRecord(db, updatedRecord.HoldDID)
if err != nil {
t.Fatalf("GetCaptainRecord() error = %v", err)
}
if retrieved == nil {
t.Fatal("Expected record to exist")
}
if retrieved.OwnerDID != updatedRecord.OwnerDID {
t.Errorf("OwnerDID = %v, want %v", retrieved.OwnerDID, updatedRecord.OwnerDID)
}
if retrieved.Public != updatedRecord.Public {
t.Errorf("Public = %v, want %v", retrieved.Public, updatedRecord.Public)
}
if retrieved.AllowAllCrew != updatedRecord.AllowAllCrew {
t.Errorf("AllowAllCrew = %v, want %v", retrieved.AllowAllCrew, updatedRecord.AllowAllCrew)
}
if retrieved.DeployedAt != updatedRecord.DeployedAt {
t.Errorf("DeployedAt = %v, want %v", retrieved.DeployedAt, updatedRecord.DeployedAt)
}
if retrieved.Region != updatedRecord.Region {
t.Errorf("Region = %v, want %v", retrieved.Region, updatedRecord.Region)
}
if retrieved.Provider != updatedRecord.Provider {
t.Errorf("Provider = %v, want %v", retrieved.Provider, updatedRecord.Provider)
}
// Verify there's still only one record in the database
holds, err := ListHoldDIDs(db)
if err != nil {
t.Fatalf("ListHoldDIDs() error = %v", err)
}
if len(holds) != 1 {
t.Errorf("Expected 1 record, got %d", len(holds))
}
}
// TestListHoldDIDs tests listing all hold DIDs
func TestListHoldDIDs(t *testing.T) {
tests := []struct {
name string
records []*HoldCaptainRecord
wantCount int
}{
{
name: "empty database",
records: []*HoldCaptainRecord{},
wantCount: 0,
},
{
name: "single record",
records: []*HoldCaptainRecord{
{
HoldDID: "did:web:hold05.atcr.io",
OwnerDID: "did:plc:alice123",
Public: true,
AllowAllCrew: false,
UpdatedAt: time.Now(),
},
},
wantCount: 1,
},
{
name: "multiple records",
records: []*HoldCaptainRecord{
{
HoldDID: "did:web:hold06.atcr.io",
OwnerDID: "did:plc:alice123",
Public: true,
AllowAllCrew: false,
UpdatedAt: time.Now().Add(-2 * time.Hour),
},
{
HoldDID: "did:web:hold07.atcr.io",
OwnerDID: "did:plc:bob456",
Public: false,
AllowAllCrew: true,
UpdatedAt: time.Now().Add(-1 * time.Hour),
},
{
HoldDID: "did:web:hold08.atcr.io",
OwnerDID: "did:plc:charlie789",
Public: true,
AllowAllCrew: true,
UpdatedAt: time.Now(), // Most recent
},
},
wantCount: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Fresh database for each test
db := setupHoldTestDB(t)
// Insert test records
for _, record := range tt.records {
err := UpsertCaptainRecord(db, record)
if err != nil {
t.Fatalf("UpsertCaptainRecord() error = %v", err)
}
}
// List holds
holds, err := ListHoldDIDs(db)
if err != nil {
t.Fatalf("ListHoldDIDs() error = %v", err)
}
if len(holds) != tt.wantCount {
t.Errorf("ListHoldDIDs() count = %d, want %d", len(holds), tt.wantCount)
}
// Verify order (most recent first)
if len(tt.records) > 1 {
// Most recent should be first (hold08)
if holds[0] != "did:web:hold08.atcr.io" {
t.Errorf("First hold = %v, want did:web:hold08.atcr.io", holds[0])
}
// Oldest should be last (hold06)
if holds[len(holds)-1] != "did:web:hold06.atcr.io" {
t.Errorf("Last hold = %v, want did:web:hold06.atcr.io", holds[len(holds)-1])
}
}
})
}
}
// TestListHoldDIDs_OrderByUpdatedAt tests that holds are ordered correctly
func TestListHoldDIDs_OrderByUpdatedAt(t *testing.T) {
db := setupHoldTestDB(t)
// Insert records with specific update times
now := time.Now()
records := []*HoldCaptainRecord{
{
HoldDID: "did:web:oldest.atcr.io",
OwnerDID: "did:plc:test1",
Public: true,
UpdatedAt: now.Add(-3 * time.Hour),
},
{
HoldDID: "did:web:newest.atcr.io",
OwnerDID: "did:plc:test2",
Public: true,
UpdatedAt: now,
},
{
HoldDID: "did:web:middle.atcr.io",
OwnerDID: "did:plc:test3",
Public: true,
UpdatedAt: now.Add(-1 * time.Hour),
},
}
for _, record := range records {
err := UpsertCaptainRecord(db, record)
if err != nil {
t.Fatalf("UpsertCaptainRecord() error = %v", err)
}
}
holds, err := ListHoldDIDs(db)
if err != nil {
t.Fatalf("ListHoldDIDs() error = %v", err)
}
// Verify order: newest first, oldest last
expectedOrder := []string{
"did:web:newest.atcr.io",
"did:web:middle.atcr.io",
"did:web:oldest.atcr.io",
}
if len(holds) != len(expectedOrder) {
t.Fatalf("Expected %d holds, got %d", len(expectedOrder), len(holds))
}
for i, expected := range expectedOrder {
if holds[i] != expected {
t.Errorf("holds[%d] = %v, want %v", i, holds[i], expected)
}
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
description: Example migrarion query
description: Example migration query
query: |
SELECT COUNT(*) FROM schema_migrations;
+27
View File
@@ -0,0 +1,27 @@
package db
import "testing"
func TestUser_Struct(t *testing.T) {
user := &User{
DID: "did:plc:test",
Handle: "alice.bsky.social",
PDSEndpoint: "https://bsky.social",
}
if user.DID != "did:plc:test" {
t.Errorf("Expected DID %q, got %q", "did:plc:test", user.DID)
}
if user.Handle != "alice.bsky.social" {
t.Errorf("Expected handle %q, got %q", "alice.bsky.social", user.Handle)
}
if user.PDSEndpoint != "https://bsky.social" {
t.Errorf("Expected PDS endpoint %q, got %q", "https://bsky.social", user.PDSEndpoint)
}
}
// RepositoryInfo tests removed - struct definition may vary
// TODO: Add tests for all model structs
+50
View File
@@ -369,3 +369,53 @@ func TestCleanupOldSessions(t *testing.T) {
t.Errorf("Expected recent session to exist, got error: %v", err)
}
}
// TestMakeSessionKey tests the session key generation function
func TestMakeSessionKey(t *testing.T) {
tests := []struct {
name string
did string
sessionID string
expected string
}{
{
name: "normal case",
did: "did:plc:abc123",
sessionID: "session_xyz789",
expected: "did:plc:abc123:session_xyz789",
},
{
name: "empty did",
did: "",
sessionID: "session123",
expected: ":session123",
},
{
name: "empty session",
did: "did:plc:test",
sessionID: "",
expected: "did:plc:test:",
},
{
name: "both empty",
did: "",
sessionID: "",
expected: ":",
},
{
name: "with colon in did",
did: "did:web:example.com",
sessionID: "session123",
expected: "did:web:example.com:session123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := makeSessionKey(tt.did, tt.sessionID)
if result != tt.expected {
t.Errorf("makeSessionKey(%q, %q) = %q, want %q", tt.did, tt.sessionID, result, tt.expected)
}
})
}
}
+147
View File
@@ -1052,3 +1052,150 @@ func TestUpdateUserHandle(t *testing.T) {
}
}
}
// TestEscapeLikePattern tests the SQL LIKE pattern escaping function
func TestEscapeLikePattern(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "plain text",
input: "hello",
expected: "hello",
},
{
name: "with percent wildcard",
input: "hello%world",
expected: "hello\\%world",
},
{
name: "with underscore wildcard",
input: "hello_world",
expected: "hello\\_world",
},
{
name: "with backslash",
input: "hello\\world",
expected: "hello\\\\world",
},
{
name: "with null byte",
input: "test\x00null",
expected: "testnull",
},
{
name: "with control characters",
input: "test\x01\x02control",
expected: "testcontrol",
},
{
name: "keep tabs and newlines",
input: "test\t\n\rwhitespace",
expected: "test\t\n\rwhitespace",
},
{
name: "with leading/trailing spaces",
input: " padded ",
expected: "padded",
},
{
name: "multiple wildcards",
input: "test%_value\\here",
expected: "test\\%\\_value\\\\here",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "only spaces",
input: " ",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := escapeLikePattern(tt.input)
if result != tt.expected {
t.Errorf("escapeLikePattern(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
// TestParseTimestamp tests the timestamp parsing function with multiple formats
func TestParseTimestamp(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
}{
{
name: "RFC3339",
input: "2024-01-01T12:00:00Z",
shouldErr: false,
},
{
name: "RFC3339Nano",
input: "2024-01-01T12:00:00.123456789Z",
shouldErr: false,
},
{
name: "SQLite format",
input: "2024-01-01 12:00:00",
shouldErr: false,
},
{
name: "SQLite with nanos",
input: "2024-01-01 12:00:00.123456789",
shouldErr: false,
},
{
name: "SQLite with timezone",
input: "2024-01-01 12:00:00.123456789-07:00",
shouldErr: false,
},
{
name: "RFC3339 with timezone",
input: "2024-01-01T12:00:00-07:00",
shouldErr: false,
},
{
name: "invalid format",
input: "not-a-date",
shouldErr: true,
},
{
name: "empty string",
input: "",
shouldErr: true,
},
{
name: "partial date",
input: "2024-01-01",
shouldErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseTimestamp(tt.input)
if tt.shouldErr {
if err == nil {
t.Errorf("parseTimestamp(%q) expected error, got nil (result: %v)", tt.input, result)
}
} else {
if err != nil {
t.Errorf("parseTimestamp(%q) unexpected error: %v", tt.input, err)
}
if result.IsZero() {
t.Errorf("parseTimestamp(%q) returned zero time", tt.input)
}
}
})
}
}
+533
View File
@@ -0,0 +1,533 @@
package db
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// setupSessionTestDB creates an in-memory SQLite database for testing
func setupSessionTestDB(t *testing.T) *SessionStore {
t.Helper()
// Use file::memory: with cache=shared to ensure all connections share the same in-memory DB
db, err := InitDB("file::memory:?cache=shared")
if err != nil {
t.Fatalf("Failed to initialize test database: %v", err)
}
// Limit to single connection to avoid race conditions in tests
db.SetMaxOpenConns(1)
t.Cleanup(func() {
db.Close()
})
return NewSessionStore(db)
}
// createSessionTestUser creates a test user in the database
func createSessionTestUser(t *testing.T, store *SessionStore, did, handle string) {
t.Helper()
_, err := store.db.Exec(`
INSERT OR IGNORE INTO users (did, handle, pds_endpoint, last_seen)
VALUES (?, ?, ?, datetime('now'))
`, did, handle, "https://pds.example.com")
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
}
func TestSession_Struct(t *testing.T) {
sess := &Session{
ID: "test-session",
DID: "did:plc:test",
Handle: "alice.bsky.social",
PDSEndpoint: "https://bsky.social",
OAuthSessionID: "oauth-123",
ExpiresAt: time.Now().Add(1 * time.Hour),
}
if sess.DID != "did:plc:test" {
t.Errorf("Expected DID, got %q", sess.DID)
}
}
// TestSessionStore_Create tests session creation without OAuth
func TestSessionStore_Create(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
sessionID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if sessionID == "" {
t.Error("Create() returned empty session ID")
}
// Verify session can be retrieved
sess, found := store.Get(sessionID)
if !found {
t.Error("Created session not found")
}
if sess == nil {
t.Fatal("Session is nil")
}
if sess.DID != "did:plc:alice123" {
t.Errorf("DID = %v, want did:plc:alice123", sess.DID)
}
if sess.Handle != "alice.bsky.social" {
t.Errorf("Handle = %v, want alice.bsky.social", sess.Handle)
}
if sess.OAuthSessionID != "" {
t.Errorf("OAuthSessionID should be empty, got %v", sess.OAuthSessionID)
}
}
// TestSessionStore_CreateWithOAuth tests session creation with OAuth
func TestSessionStore_CreateWithOAuth(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
oauthSessionID := "oauth-123"
sessionID, err := store.CreateWithOAuth("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", oauthSessionID, 1*time.Hour)
if err != nil {
t.Fatalf("CreateWithOAuth() error = %v", err)
}
if sessionID == "" {
t.Error("CreateWithOAuth() returned empty session ID")
}
// Verify session has OAuth session ID
sess, found := store.Get(sessionID)
if !found {
t.Error("Created session not found")
}
if sess.OAuthSessionID != oauthSessionID {
t.Errorf("OAuthSessionID = %v, want %v", sess.OAuthSessionID, oauthSessionID)
}
}
// TestSessionStore_Get tests retrieving sessions
func TestSessionStore_Get(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Create a valid session
validID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Create a session and manually expire it
expiredID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Manually update expiration to the past
_, err = store.db.Exec(`
UPDATE ui_sessions
SET expires_at = datetime('now', '-1 hour')
WHERE id = ?
`, expiredID)
if err != nil {
t.Fatalf("Failed to update expiration: %v", err)
}
tests := []struct {
name string
sessionID string
wantFound bool
}{
{
name: "valid session",
sessionID: validID,
wantFound: true,
},
{
name: "expired session",
sessionID: expiredID,
wantFound: false,
},
{
name: "non-existent session",
sessionID: "non-existent-id",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sess, found := store.Get(tt.sessionID)
if found != tt.wantFound {
t.Errorf("Get() found = %v, want %v", found, tt.wantFound)
}
if tt.wantFound && sess == nil {
t.Error("Expected session, got nil")
}
})
}
}
// TestSessionStore_Extend tests extending session expiration
func TestSessionStore_Extend(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
sessionID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Get initial expiration
sess1, _ := store.Get(sessionID)
initialExpiry := sess1.ExpiresAt
// Wait a bit to ensure time difference
time.Sleep(10 * time.Millisecond)
// Extend session
err = store.Extend(sessionID, 2*time.Hour)
if err != nil {
t.Errorf("Extend() error = %v", err)
}
// Verify expiration was updated
sess2, found := store.Get(sessionID)
if !found {
t.Fatal("Session not found after extend")
}
if !sess2.ExpiresAt.After(initialExpiry) {
t.Error("ExpiresAt should be later after extend")
}
// Test extending non-existent session
err = store.Extend("non-existent-id", 1*time.Hour)
if err == nil {
t.Error("Expected error when extending non-existent session")
}
if err != nil && !strings.Contains(err.Error(), "not found") {
t.Errorf("Expected 'not found' error, got %v", err)
}
}
// TestSessionStore_Delete tests deleting a session
func TestSessionStore_Delete(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
sessionID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Verify session exists
_, found := store.Get(sessionID)
if !found {
t.Fatal("Session should exist before delete")
}
// Delete session
store.Delete(sessionID)
// Verify session is gone
_, found = store.Get(sessionID)
if found {
t.Error("Session should not exist after delete")
}
// Deleting non-existent session should not error
store.Delete("non-existent-id")
}
// TestSessionStore_DeleteByDID tests deleting all sessions for a DID
func TestSessionStore_DeleteByDID(t *testing.T) {
store := setupSessionTestDB(t)
did := "did:plc:alice123"
createSessionTestUser(t, store, did, "alice.bsky.social")
createSessionTestUser(t, store, "did:plc:bob123", "bob.bsky.social")
// Create multiple sessions for alice
sessionIDs := make([]string, 3)
for i := 0; i < 3; i++ {
id, err := store.Create(did, "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
sessionIDs[i] = id
}
// Create a session for bob
bobSessionID, err := store.Create("did:plc:bob123", "bob.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Delete all sessions for alice
store.DeleteByDID(did)
// Verify alice's sessions are gone
for _, id := range sessionIDs {
_, found := store.Get(id)
if found {
t.Errorf("Session %v should have been deleted", id)
}
}
// Verify bob's session still exists
_, found := store.Get(bobSessionID)
if !found {
t.Error("Bob's session should still exist")
}
// Deleting sessions for non-existent DID should not error
store.DeleteByDID("did:plc:nonexistent")
}
// TestSessionStore_Cleanup tests removing expired sessions
func TestSessionStore_Cleanup(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Create valid session by inserting directly with SQLite datetime format
validID := "valid-session-id"
_, err := store.db.Exec(`
INSERT INTO ui_sessions (id, did, handle, pds_endpoint, oauth_session_id, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, datetime('now', '+1 hour'), datetime('now'))
`, validID, "did:plc:alice123", "alice.bsky.social", "https://pds.example.com", "")
if err != nil {
t.Fatalf("Failed to create valid session: %v", err)
}
// Create expired session
expiredID := "expired-session-id"
_, err = store.db.Exec(`
INSERT INTO ui_sessions (id, did, handle, pds_endpoint, oauth_session_id, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, datetime('now', '-1 hour'), datetime('now'))
`, expiredID, "did:plc:alice123", "alice.bsky.social", "https://pds.example.com", "")
if err != nil {
t.Fatalf("Failed to create expired session: %v", err)
}
// Verify we have 2 sessions before cleanup
var countBefore int
err = store.db.QueryRow("SELECT COUNT(*) FROM ui_sessions").Scan(&countBefore)
if err != nil {
t.Fatalf("Query error: %v", err)
}
if countBefore != 2 {
t.Fatalf("Expected 2 sessions before cleanup, got %d", countBefore)
}
// Run cleanup
store.Cleanup()
// Verify valid session still exists in database
var countValid int
err = store.db.QueryRow("SELECT COUNT(*) FROM ui_sessions WHERE id = ?", validID).Scan(&countValid)
if err != nil {
t.Fatalf("Query error: %v", err)
}
if countValid != 1 {
t.Errorf("Valid session should still exist in database, count = %d", countValid)
}
// Verify expired session was cleaned up
var countExpired int
err = store.db.QueryRow("SELECT COUNT(*) FROM ui_sessions WHERE id = ?", expiredID).Scan(&countExpired)
if err != nil {
t.Fatalf("Query error: %v", err)
}
if countExpired != 0 {
t.Error("Expired session should have been deleted from database")
}
// Verify we can still get the valid session
_, found := store.Get(validID)
if !found {
t.Error("Valid session should be retrievable after cleanup")
}
}
// TestSessionStore_CleanupContext tests context-aware cleanup
func TestSessionStore_CleanupContext(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Create a session and manually expire it
expiredID, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// Manually update expiration to the past
_, err = store.db.Exec(`
UPDATE ui_sessions
SET expires_at = datetime('now', '-1 hour')
WHERE id = ?
`, expiredID)
if err != nil {
t.Fatalf("Failed to update expiration: %v", err)
}
// Run context-aware cleanup
ctx := context.Background()
err = store.CleanupContext(ctx)
if err != nil {
t.Errorf("CleanupContext() error = %v", err)
}
// Verify expired session was cleaned up
var count int
err = store.db.QueryRow("SELECT COUNT(*) FROM ui_sessions WHERE id = ?", expiredID).Scan(&count)
if err != nil {
t.Fatalf("Query error: %v", err)
}
if count != 0 {
t.Error("Expired session should have been deleted from database")
}
}
// TestSetCookie tests setting session cookie
func TestSetCookie(t *testing.T) {
w := httptest.NewRecorder()
sessionID := "test-session-id"
maxAge := 3600
SetCookie(w, sessionID, maxAge)
cookies := w.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("Expected 1 cookie, got %d", len(cookies))
}
cookie := cookies[0]
if cookie.Name != "atcr_session" {
t.Errorf("Name = %v, want atcr_session", cookie.Name)
}
if cookie.Value != sessionID {
t.Errorf("Value = %v, want %v", cookie.Value, sessionID)
}
if cookie.MaxAge != maxAge {
t.Errorf("MaxAge = %v, want %v", cookie.MaxAge, maxAge)
}
if !cookie.HttpOnly {
t.Error("HttpOnly should be true")
}
if !cookie.Secure {
t.Error("Secure should be true")
}
if cookie.SameSite != http.SameSiteLaxMode {
t.Errorf("SameSite = %v, want Lax", cookie.SameSite)
}
if cookie.Path != "/" {
t.Errorf("Path = %v, want /", cookie.Path)
}
}
// TestClearCookie tests clearing session cookie
func TestClearCookie(t *testing.T) {
w := httptest.NewRecorder()
ClearCookie(w)
cookies := w.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("Expected 1 cookie, got %d", len(cookies))
}
cookie := cookies[0]
if cookie.Name != "atcr_session" {
t.Errorf("Name = %v, want atcr_session", cookie.Name)
}
if cookie.Value != "" {
t.Errorf("Value should be empty, got %v", cookie.Value)
}
if cookie.MaxAge != -1 {
t.Errorf("MaxAge = %v, want -1", cookie.MaxAge)
}
if !cookie.HttpOnly {
t.Error("HttpOnly should be true")
}
if !cookie.Secure {
t.Error("Secure should be true")
}
}
// TestGetSessionID tests retrieving session ID from cookie
func TestGetSessionID(t *testing.T) {
tests := []struct {
name string
cookie *http.Cookie
wantID string
wantFound bool
}{
{
name: "valid cookie",
cookie: &http.Cookie{
Name: "atcr_session",
Value: "test-session-id",
},
wantID: "test-session-id",
wantFound: true,
},
{
name: "no cookie",
cookie: nil,
wantID: "",
wantFound: false,
},
{
name: "wrong cookie name",
cookie: &http.Cookie{
Name: "other_cookie",
Value: "test-value",
},
wantID: "",
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
if tt.cookie != nil {
req.AddCookie(tt.cookie)
}
id, found := GetSessionID(req)
if found != tt.wantFound {
t.Errorf("GetSessionID() found = %v, want %v", found, tt.wantFound)
}
if id != tt.wantID {
t.Errorf("GetSessionID() id = %v, want %v", id, tt.wantID)
}
})
}
}
// TestSessionStore_SessionIDUniqueness tests that generated session IDs are unique
func TestSessionStore_SessionIDUniqueness(t *testing.T) {
store := setupSessionTestDB(t)
createSessionTestUser(t, store, "did:plc:alice123", "alice.bsky.social")
// Generate multiple session IDs
ids := make(map[string]bool)
for i := 0; i < 100; i++ {
id, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour)
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if ids[id] {
t.Errorf("Duplicate session ID generated: %v", id)
}
ids[id] = true
}
if len(ids) != 100 {
t.Errorf("Expected 100 unique IDs, got %d", len(ids))
}
}