mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 08:16:57 +00:00
Completes the swap 0033 set up. layers and manifest_references move onto manifest_key and manifests.id is gone, which removes the last node-allocated identifier in the AppView schema. Statement order in 0034 is load-bearing. With foreign keys on, DROP TABLE performs an implicit DELETE FROM, so dropping manifests while layers still holds an ON DELETE CASCADE reference deletes every layer row. Migration 0009 did exactly that; it went unnoticed because the Jetstream backfill rebuilds layers from PDS records, so the damage healed itself. PRAGMA foreign_keys is no help: it is a no-op inside a transaction and migrations run in one. So the new children are built pointing at manifests_new, the old children are dropped first, and only then is the old manifests table dropped, by which point nothing references it. Verified both behaviors before relying on them. manifest_key is declared NOT NULL as well as PRIMARY KEY, because in SQLite a PRIMARY KEY column still accepts NULL unless it is INTEGER PRIMARY KEY. That constraint immediately caught four test helpers inserting manifests without one. Five queries used MAX(id) as "the newest manifest in this repo", which I had previously reported as absent after grepping only for ORDER BY. A derived key has no ordering, so recency now comes from created_at with manifest_key as a deterministic tiebreak. This is a real behavior change, and a fix: the two disagree whenever a manifest is indexed out of order, which the backfill does routinely, and created_at is the push time these queries always wanted. Both directions are tested, including that ties resolve the same way every run. InsertManifest and BatchInsertManifests no longer read anything back. The key is derived from (did, repository, digest), so the writer knows it before the statement runs: the select-back, its per-DID IN list, and the "manifest missing id after batch insert" branch all go away, along with the UNIQUE-conflict fallback that existed only to recover a rowid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
309 lines
8.7 KiB
Go
309 lines
8.7 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDeleteUserDataFull_DeletesAllData(t *testing.T) {
|
|
db, err := InitDB(":memory:", LibsqlConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to init database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create test user
|
|
testUser := &User{
|
|
DID: "did:plc:test123",
|
|
Handle: "test.bsky.social",
|
|
PDSEndpoint: "https://bsky.social",
|
|
LastSeen: time.Now(),
|
|
}
|
|
if err := UpsertUser(db, testUser); err != nil {
|
|
t.Fatalf("Failed to create user: %v", err)
|
|
}
|
|
|
|
// Create manifest
|
|
_, err = db.Exec(`
|
|
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, ManifestKey(testUser.DID, "myapp", "sha256:abc123"),
|
|
testUser.DID, "myapp", "sha256:abc123", "did:web:hold.example.com", 2,
|
|
"application/vnd.oci.image.manifest.v1+json", time.Now())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create manifest: %v", err)
|
|
}
|
|
|
|
// Create tag
|
|
_, err = db.Exec(`
|
|
INSERT INTO tags (did, repository, tag, digest, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, testUser.DID, "myapp", "latest", "sha256:abc123", time.Now())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create tag: %v", err)
|
|
}
|
|
|
|
// Create hold membership data (non-cascading)
|
|
_, err = db.Exec(`
|
|
INSERT INTO hold_crew_approvals (hold_did, user_did, approved_at, expires_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`, "did:web:hold.example.com", testUser.DID, time.Now(), time.Now().Add(24*time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create crew approval: %v", err)
|
|
}
|
|
|
|
_, err = db.Exec(`
|
|
INSERT INTO hold_crew_members (hold_did, member_did, rkey, permissions)
|
|
VALUES (?, ?, ?, ?)
|
|
`, "did:web:hold.example.com", testUser.DID, "member1", `["blob:read","blob:write"]`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create crew member: %v", err)
|
|
}
|
|
|
|
// Create OAuth store
|
|
oauthStore := NewOAuthStore(db)
|
|
|
|
// Delete all user data
|
|
err = DeleteUserDataFull(db, oauthStore, testUser.DID)
|
|
if err != nil {
|
|
t.Fatalf("DeleteUserDataFull failed: %v", err)
|
|
}
|
|
|
|
// Verify user was deleted
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query users: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected user to be deleted")
|
|
}
|
|
|
|
// Verify manifests were cascade deleted
|
|
err = db.QueryRow("SELECT COUNT(*) FROM manifests WHERE did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query manifests: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected manifests to be cascade deleted")
|
|
}
|
|
|
|
// Verify tags were cascade deleted
|
|
err = db.QueryRow("SELECT COUNT(*) FROM tags WHERE did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query tags: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected tags to be cascade deleted")
|
|
}
|
|
|
|
// Verify hold membership data was deleted
|
|
err = db.QueryRow("SELECT COUNT(*) FROM hold_crew_approvals WHERE user_did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query crew approvals: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected crew approvals to be deleted")
|
|
}
|
|
|
|
err = db.QueryRow("SELECT COUNT(*) FROM hold_crew_members WHERE member_did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query crew members: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected crew members to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestDeleteUserDataFull_DoesNotAffectOtherUsers(t *testing.T) {
|
|
db, err := InitDB(":memory:", LibsqlConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to init database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create two users
|
|
user1 := &User{
|
|
DID: "did:plc:user1",
|
|
Handle: "user1.bsky.social",
|
|
PDSEndpoint: "https://bsky.social",
|
|
LastSeen: time.Now(),
|
|
}
|
|
user2 := &User{
|
|
DID: "did:plc:user2",
|
|
Handle: "user2.bsky.social",
|
|
PDSEndpoint: "https://bsky.social",
|
|
LastSeen: time.Now(),
|
|
}
|
|
if err := UpsertUser(db, user1); err != nil {
|
|
t.Fatalf("Failed to create user1: %v", err)
|
|
}
|
|
if err := UpsertUser(db, user2); err != nil {
|
|
t.Fatalf("Failed to create user2: %v", err)
|
|
}
|
|
|
|
// Create manifests for both users
|
|
for _, user := range []*User{user1, user2} {
|
|
_, err = db.Exec(`
|
|
INSERT INTO manifests (manifest_key, did, repository, digest, hold_endpoint, schema_version, media_type, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, ManifestKey(user.DID, "myapp", "sha256:"+user.DID),
|
|
user.DID, "myapp", "sha256:"+user.DID, "did:web:hold.example.com", 2,
|
|
"application/vnd.oci.image.manifest.v1+json", time.Now())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create manifest for %s: %v", user.Handle, err)
|
|
}
|
|
}
|
|
|
|
// Create hold membership data for both users
|
|
for i, user := range []*User{user1, user2} {
|
|
_, err = db.Exec(`
|
|
INSERT INTO hold_crew_members (hold_did, member_did, rkey, permissions)
|
|
VALUES (?, ?, ?, ?)
|
|
`, "did:web:hold.example.com", user.DID, fmt.Sprintf("member%d", i+1), `["blob:read"]`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create crew member for %s: %v", user.Handle, err)
|
|
}
|
|
}
|
|
|
|
oauthStore := NewOAuthStore(db)
|
|
|
|
// Delete only user1's data
|
|
err = DeleteUserDataFull(db, oauthStore, user1.DID)
|
|
if err != nil {
|
|
t.Fatalf("DeleteUserDataFull failed: %v", err)
|
|
}
|
|
|
|
// Verify user1 was deleted
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE did = ?", user1.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query users: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected user1 to be deleted")
|
|
}
|
|
|
|
// Verify user2 still exists
|
|
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE did = ?", user2.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query users: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Error("Expected user2 to still exist")
|
|
}
|
|
|
|
// Verify user2's manifests still exist
|
|
err = db.QueryRow("SELECT COUNT(*) FROM manifests WHERE did = ?", user2.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query manifests: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Error("Expected user2's manifest to still exist")
|
|
}
|
|
|
|
// Verify user2's crew membership still exists
|
|
err = db.QueryRow("SELECT COUNT(*) FROM hold_crew_members WHERE member_did = ?", user2.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query crew members: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Error("Expected user2's crew membership to still exist")
|
|
}
|
|
}
|
|
|
|
func TestDeleteUserDataFull_HandlesNonExistentUser(t *testing.T) {
|
|
db, err := InitDB(":memory:", LibsqlConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to init database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
oauthStore := NewOAuthStore(db)
|
|
|
|
// Try to delete non-existent user - should not error
|
|
err = DeleteUserDataFull(db, oauthStore, "did:plc:nonexistent")
|
|
if err != nil {
|
|
t.Errorf("Expected no error for non-existent user, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteUserDataFull_WithNilOAuthStore(t *testing.T) {
|
|
db, err := InitDB(":memory:", LibsqlConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to init database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
testUser := &User{
|
|
DID: "did:plc:test123",
|
|
Handle: "test.bsky.social",
|
|
PDSEndpoint: "https://bsky.social",
|
|
LastSeen: time.Now(),
|
|
}
|
|
if err := UpsertUser(db, testUser); err != nil {
|
|
t.Fatalf("Failed to create user: %v", err)
|
|
}
|
|
|
|
// Delete with nil OAuth store - should still work
|
|
err = DeleteUserDataFull(db, nil, testUser.DID)
|
|
if err != nil {
|
|
t.Errorf("Expected no error with nil OAuth store, got: %v", err)
|
|
}
|
|
|
|
// Verify user was deleted
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query users: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected user to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestDeleteUserDataFull_DeletesDenials(t *testing.T) {
|
|
db, err := InitDB(":memory:", LibsqlConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to init database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
testUser := &User{
|
|
DID: "did:plc:test123",
|
|
Handle: "test.bsky.social",
|
|
PDSEndpoint: "https://bsky.social",
|
|
LastSeen: time.Now(),
|
|
}
|
|
if err := UpsertUser(db, testUser); err != nil {
|
|
t.Fatalf("Failed to create user: %v", err)
|
|
}
|
|
|
|
// Create denial record
|
|
_, err = db.Exec(`
|
|
INSERT INTO hold_crew_denials (hold_did, user_did, denial_count, next_retry_at, last_denied_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, "did:web:hold.example.com", testUser.DID, 1, time.Now().Add(24*time.Hour), time.Now())
|
|
if err != nil {
|
|
t.Fatalf("Failed to create crew denial: %v", err)
|
|
}
|
|
|
|
oauthStore := NewOAuthStore(db)
|
|
|
|
err = DeleteUserDataFull(db, oauthStore, testUser.DID)
|
|
if err != nil {
|
|
t.Fatalf("DeleteUserDataFull failed: %v", err)
|
|
}
|
|
|
|
// Verify denial was deleted
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM hold_crew_denials WHERE user_did = ?", testUser.DID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query crew denials: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Expected crew denials to be deleted")
|
|
}
|
|
}
|