package database import ( "os" "path/filepath" "testing" "time" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" "github.com/peterqiu0516/sub-store/internal/model" ) func testDB(t *testing.T) *sqlx.DB { t.Helper() dir := t.TempDir() dbPath := filepath.Join(dir, "test.db") db, err := sqlx.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=foreign_keys(on)") if err != nil { t.Fatalf("failed to open db: %v", err) } if err := RunMigrations(db); err != nil { t.Fatalf("failed to run migrations: %v", err) } return db } func TestSourceCRUD(t *testing.T) { db := testDB(t) defer db.Close() repo := NewSourceRepo(db) // Create src := model.SourceRecord{ ID: "test-src", Name: "Test Source", Type: "local", Content: "ss://pass@host:port#name", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } created, err := repo.Upsert(src) if err != nil { t.Fatalf("create failed: %v", err) } if created.ID != "test-src" { t.Errorf("expected id test-src, got %s", created.ID) } // Read got, err := repo.Get("test-src") if err != nil { t.Fatalf("get failed: %v", err) } if got.Name != "Test Source" { t.Errorf("expected name Test Source, got %s", got.Name) } // List list, err := repo.List() if err != nil { t.Fatalf("list failed: %v", err) } if len(list) != 1 { t.Errorf("expected 1 source, got %d", len(list)) } // Update src.Name = "Updated" src.URL = "https://example.com/sub" src.Type = "remote" updated, err := repo.Upsert(src) if err != nil { t.Fatalf("update failed: %v", err) } if updated.Name != "Updated" { t.Errorf("expected name Updated, got %s", updated.Name) } // Delete — use RecycleRepo for archive + delete recycleRepo := NewRecycleRepo(db) _, err = recycleRepo.ArchiveAndDelete("source", "test-src", map[string]any{"id": "test-src"}, "DELETE FROM sources WHERE id = ?", "test-src") if err != nil { t.Fatalf("delete failed: %v", err) } got, err = repo.Get("test-src") if got != nil { t.Error("expected nil after delete") } } func TestCollectionCRUD(t *testing.T) { db := testDB(t) defer db.Close() // Create a source first srcRepo := NewSourceRepo(db) srcRepo.Upsert(model.SourceRecord{ ID: "src1", Name: "Src1", Type: "local", Content: "ss://pass@host:port#name", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, }) repo := NewCollectionRepo(db) col := model.CollectionRecord{ ID: "test-col", Name: "Test Collection", SourceIds: []string{"src1"}, RenameEnabled: true, TemplateId: "default", IgnoreFailed: true, Enabled: true, Meta: map[string]any{}, } created, err := repo.Upsert(col) if err != nil { t.Fatalf("create failed: %v", err) } if created.ID != "test-col" { t.Errorf("expected id test-col, got %s", created.ID) } got, err := repo.Get("test-col") if err != nil { t.Fatalf("get failed: %v", err) } if len(got.SourceIds) != 1 || got.SourceIds[0] != "src1" { t.Errorf("expected sourceIds [src1], got %v", got.SourceIds) } } func TestTemplateCRUD(t *testing.T) { db := testDB(t) defer db.Close() repo := NewTemplateRepo(db) tmpl := model.TemplateRecord{ ID: "test-tmpl", Name: "Test Template", Target: "mihomo", Config: map[string]any{ "proxy-groups": []any{ map[string]any{"name": "PROXY", "type": "select"}, }, }, } created, err := repo.Upsert(tmpl) if err != nil { t.Fatalf("create failed: %v", err) } if created.ID != "test-tmpl" { t.Errorf("expected id test-tmpl, got %s", created.ID) } got, err := repo.Get("test-tmpl") if err != nil { t.Fatalf("get failed: %v", err) } if got.Target != "mihomo" { t.Errorf("expected target mihomo, got %s", got.Target) } } func TestSettingsCRUD(t *testing.T) { db := testDB(t) defer db.Close() repo := NewSettingsRepo(db) // Get default (empty) settings, err := repo.Get() if err != nil { t.Fatalf("get failed: %v", err) } if len(settings) != 0 { t.Errorf("expected empty settings initially, got %d keys", len(settings)) } // Update updated, err := repo.Update(map[string]any{ "theme": map[string]any{"name": "dark"}, "appName": "MyStore", }) if err != nil { t.Fatalf("update failed: %v", err) } if updated["appName"] != "MyStore" { t.Errorf("expected appName MyStore, got %v", updated["appName"]) } // Get again settings, err = repo.Get() if err != nil { t.Fatalf("get after update failed: %v", err) } if settings["appName"] != "MyStore" { t.Errorf("expected appName MyStore, got %v", settings["appName"]) } } func TestCacheCRUD(t *testing.T) { db := testDB(t) defer db.Close() repo := NewCacheRepo(db) // Put repo.SafePut("cache-key-1", "cached content", map[string]any{"etag": "abc"}, 300) // Get entry, ok := repo.SafeGet("cache-key-1") if !ok { t.Fatal("expected cache hit") } if entry.Content != "cached content" { t.Errorf("expected content 'cached content', got %s", entry.Content) } // Test expiry — put with TTL 1 and wait for it to expire repo.SafePut("expired-key", "old", map[string]any{}, 1) time.Sleep(2 * time.Second) _, ok = repo.SafeGet("expired-key") if ok { t.Error("expected cache miss for expired key") } } func TestRecycleCRUD(t *testing.T) { db := testDB(t) defer db.Close() repo := NewRecycleRepo(db) // Archive — ArchiveAndDelete returns (id, error) snapshot := map[string]any{"id": "test-src", "name": "Test"} entryID, err := repo.ArchiveAndDelete("source", "test-src", snapshot, "SELECT 1", nil) if err != nil { t.Fatalf("archive failed: %v", err) } if entryID == "" { t.Fatal("expected non-empty entry id") } // List list, err := repo.List() if err != nil { t.Fatalf("list failed: %v", err) } if len(list) != 1 { t.Errorf("expected 1 entry, got %d", len(list)) } // Get got, err := repo.Get(entryID) if err != nil { t.Fatalf("get failed: %v", err) } if got["resourceType"] != "source" { t.Errorf("expected resourceType source, got %v", got["resourceType"]) } // Delete err = repo.Delete(entryID) if err != nil { t.Fatalf("delete failed: %v", err) } list, _ = repo.List() if len(list) != 0 { t.Errorf("expected 0 entries after delete, got %d", len(list)) } } func TestSortSources(t *testing.T) { db := testDB(t) defer db.Close() repo := NewSourceRepo(db) repo.Upsert(model.SourceRecord{ID: "a", Name: "A", Type: "local", Content: "", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}}) repo.Upsert(model.SourceRecord{ID: "b", Name: "B", Type: "local", Content: "", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}}) repo.Upsert(model.SourceRecord{ID: "c", Name: "C", Type: "local", Content: "", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}}) // Sort reverse err := repo.Sort([]string{"c", "b", "a"}) if err != nil { t.Fatalf("sort failed: %v", err) } list, _ := repo.List() if list[0].ID != "c" || list[1].ID != "b" || list[2].ID != "a" { t.Errorf("sort order wrong: %s, %s, %s", list[0].ID, list[1].ID, list[2].ID) } } func TestMain(m *testing.M) { os.Exit(m.Run()) }