test: add comprehensive unit tests — coverage 20.2% → 91.8%
Add 12 test files covering all internal packages: - internal/util/util_test.go (97.2%) - internal/model/model_test.go (100.0%) - internal/config/config_test.go (88.1%) - internal/template/builtin_test.go (98.8%) - internal/middleware/middleware_test.go (98.7%) - internal/database/repo_extra_test.go (85.5%) - internal/rules/converter_test.go (99.1%) - internal/service/subscription_test.go (75.7%) - internal/handler/handler_test.go (90.6%) - internal/filter/filter_extra_test.go (91.9%) - internal/proxy/client_parser_test.go (96.1%) - internal/render/render_extra_test.go (99.2%) Overall: 91.8% (4033/4400 statements) — exceeds 85% acceptance threshold. All tests pass, go vet clean, go build clean.
This commit is contained in:
@@ -0,0 +1,763 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/peterqiu0516/sub-store/internal/model"
|
||||
)
|
||||
|
||||
// --- InitDB ---
|
||||
|
||||
func TestInitDB_CreatesDirectoryAndAppliesPragmas(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "subdir", "test.db")
|
||||
db, err := InitDB(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("InitDB failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Verify WAL journal mode
|
||||
var mode string
|
||||
if err := db.Get(&mode, "PRAGMA journal_mode"); err != nil {
|
||||
t.Fatalf("failed to read journal_mode: %v", err)
|
||||
}
|
||||
if mode != "wal" {
|
||||
t.Errorf("expected journal_mode wal, got %s", mode)
|
||||
}
|
||||
|
||||
// Verify foreign_keys
|
||||
var fk int
|
||||
if err := db.Get(&fk, "PRAGMA foreign_keys"); err != nil {
|
||||
t.Fatalf("failed to read foreign_keys: %v", err)
|
||||
}
|
||||
if fk != 1 {
|
||||
t.Errorf("expected foreign_keys 1, got %d", fk)
|
||||
}
|
||||
|
||||
// Verify busy_timeout
|
||||
var bt int
|
||||
if err := db.Get(&bt, "PRAGMA busy_timeout"); err != nil {
|
||||
t.Fatalf("failed to read busy_timeout: %v", err)
|
||||
}
|
||||
if bt != 5000 {
|
||||
t.Errorf("expected busy_timeout 5000, got %d", bt)
|
||||
}
|
||||
|
||||
// Verify synchronous
|
||||
var sync string
|
||||
if err := db.Get(&sync, "PRAGMA synchronous"); err != nil {
|
||||
t.Fatalf("failed to read synchronous: %v", err)
|
||||
}
|
||||
if sync != "1" { // NORMAL = 1
|
||||
t.Errorf("expected synchronous 1 (NORMAL), got %s", sync)
|
||||
}
|
||||
|
||||
// Verify MaxOpenConns
|
||||
if db.Stats().MaxOpenConnections != 1 {
|
||||
t.Errorf("expected MaxOpenConnections 1, got %d", db.Stats().MaxOpenConnections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitDB_RelativePath(t *testing.T) {
|
||||
db, err := InitDB(":memory:")
|
||||
if err != nil {
|
||||
// modernc sqlite may not support :memory: via file: DSN; try temp file instead
|
||||
t.Skipf("InitDB with :memory: not supported: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
}
|
||||
|
||||
func TestInitDB_ExistingDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "test.db")
|
||||
db, err := InitDB(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("InitDB failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
}
|
||||
|
||||
// --- StartCacheCleaner ---
|
||||
|
||||
func TestStartCacheCleaner_RemovesExpiredEntries(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCacheRepo(db)
|
||||
|
||||
// Insert an expired entry directly (cached_at in the past)
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO source_cache (cache_key, content, metadata, cached_at, ttl) VALUES (?, ?, ?, ?, ?)`,
|
||||
"expired-by-cleaner", "old content", "{}", time.Now().Unix()-100, 1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("insert expired entry: %v", err)
|
||||
}
|
||||
// Insert a valid entry
|
||||
repo.SafePut("valid-by-cleaner", "fresh", map[string]any{}, 3600)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
StartCacheCleaner(ctx, db, 100*time.Millisecond)
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
cancel()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Expired entry should be gone
|
||||
var count int
|
||||
db.Get(&count, "SELECT COUNT(*) FROM source_cache WHERE cache_key = ?", "expired-by-cleaner")
|
||||
if count != 0 {
|
||||
t.Errorf("expected expired entry to be cleaned, got count=%d", count)
|
||||
}
|
||||
// Valid entry should remain
|
||||
db.Get(&count, "SELECT COUNT(*) FROM source_cache WHERE cache_key = ?", "valid-by-cleaner")
|
||||
if count != 1 {
|
||||
t.Errorf("expected valid entry to remain, got count=%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// --- SettingsRepo.Replace ---
|
||||
|
||||
func TestSettingsReplace(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewSettingsRepo(db)
|
||||
|
||||
// Replace with new settings
|
||||
err := repo.Replace(map[string]any{"appName": "Replaced"})
|
||||
if err != nil {
|
||||
t.Fatalf("replace failed: %v", err)
|
||||
}
|
||||
settings, _ := repo.Get()
|
||||
if settings["appName"] != "Replaced" {
|
||||
t.Errorf("expected appName Replaced, got %v", settings["appName"])
|
||||
}
|
||||
|
||||
// Replace again (should overwrite, not merge)
|
||||
err = repo.Replace(map[string]any{"theme": "dark"})
|
||||
if err != nil {
|
||||
t.Fatalf("replace 2 failed: %v", err)
|
||||
}
|
||||
settings, _ = repo.Get()
|
||||
if _, exists := settings["appName"]; exists {
|
||||
t.Error("expected appName to be gone after replace (not merge)")
|
||||
}
|
||||
if settings["theme"] != "dark" {
|
||||
t.Errorf("expected theme dark, got %v", settings["theme"])
|
||||
}
|
||||
}
|
||||
|
||||
// --- CollectionRepo.Delete and Sort ---
|
||||
|
||||
func TestCollectionDelete(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCollectionRepo(db)
|
||||
repo.Upsert(model.CollectionRecord{
|
||||
ID: "col-del", Name: "Del", SourceIds: []string{}, Filters: []model.FilterRule{},
|
||||
TemplateId: "acl4ssr-mihomo", Meta: map[string]any{},
|
||||
})
|
||||
err := repo.Delete("col-del")
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get("col-del")
|
||||
if got != nil {
|
||||
t.Error("expected nil after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionSort(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCollectionRepo(db)
|
||||
for _, id := range []string{"a", "b", "c"} {
|
||||
repo.Upsert(model.CollectionRecord{
|
||||
ID: id, Name: id, SourceIds: []string{}, Filters: []model.FilterRule{},
|
||||
TemplateId: "acl4ssr-mihomo", Meta: map[string]any{},
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// --- TemplateRepo.Delete ---
|
||||
|
||||
func TestTemplateDelete(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewTemplateRepo(db)
|
||||
repo.Upsert(model.TemplateRecord{
|
||||
ID: "tmpl-del", Name: "Del", Target: "mihomo", Config: map[string]any{},
|
||||
})
|
||||
err := repo.Delete("tmpl-del")
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get("tmpl-del")
|
||||
if got != nil {
|
||||
t.Error("expected nil after delete")
|
||||
}
|
||||
}
|
||||
|
||||
// --- GrantRepo: Get, GetSnapshot, Update, Delete, RestoreFromSnapshot ---
|
||||
|
||||
func TestGrantGet(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
rec, _, _ := repo.Create("source", "src1", "mihomo", nil)
|
||||
|
||||
got, err := repo.Get(rec.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil grant")
|
||||
}
|
||||
if got.ResourceType != "source" {
|
||||
t.Errorf("expected resourceType source, got %s", got.ResourceType)
|
||||
}
|
||||
if got.ResourceId != "src1" {
|
||||
t.Errorf("expected resourceId src1, got %s", got.ResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantGet_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
got, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error for not found, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantGetSnapshot(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
rec, _, _ := repo.Create("source", "src1", "mihomo", nil)
|
||||
|
||||
snap, err := repo.GetSnapshot(rec.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("getSnapshot failed: %v", err)
|
||||
}
|
||||
if snap == nil {
|
||||
t.Fatal("expected non-nil snapshot")
|
||||
}
|
||||
if snap["id"] != rec.ID {
|
||||
t.Errorf("expected id %s, got %v", rec.ID, snap["id"])
|
||||
}
|
||||
if snap["resourceType"] != "source" {
|
||||
t.Errorf("expected resourceType source, got %v", snap["resourceType"])
|
||||
}
|
||||
if snap["enabled"] != true {
|
||||
t.Errorf("expected enabled true, got %v", snap["enabled"])
|
||||
}
|
||||
if snap["tokenHash"] == "" {
|
||||
t.Error("expected non-empty tokenHash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantGetSnapshot_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
snap, err := repo.GetSnapshot("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if snap != nil {
|
||||
t.Error("expected nil snapshot for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantUpdate(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
rec, _, _ := repo.Create("source", "src1", "mihomo", nil)
|
||||
|
||||
// Disable
|
||||
enabled := false
|
||||
updated, err := repo.Update(rec.ID, &enabled, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("update failed: %v", err)
|
||||
}
|
||||
if updated.Enabled {
|
||||
t.Error("expected enabled false after update")
|
||||
}
|
||||
|
||||
// Set expiry
|
||||
exp := int64(9999999999000)
|
||||
updated, err = repo.Update(rec.ID, nil, &exp)
|
||||
if err != nil {
|
||||
t.Fatalf("update expiry failed: %v", err)
|
||||
}
|
||||
if updated.ExpiresAt == nil || *updated.ExpiresAt != exp {
|
||||
t.Errorf("expected expiresAt %d, got %v", exp, updated.ExpiresAt)
|
||||
}
|
||||
|
||||
// Set expiry to 0 -> treated as nil (never expire)
|
||||
zero := int64(0)
|
||||
updated, err = repo.Update(rec.ID, nil, &zero)
|
||||
if err != nil {
|
||||
t.Fatalf("update expiry zero failed: %v", err)
|
||||
}
|
||||
if updated.ExpiresAt != nil {
|
||||
t.Errorf("expected nil expiresAt for 0, got %v", updated.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantUpdate_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
updated, err := repo.Update("nonexistent", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if updated != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantDelete(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
rec, _, _ := repo.Create("source", "src1", "mihomo", nil)
|
||||
|
||||
err := repo.Delete(rec.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get(rec.ID)
|
||||
if got != nil {
|
||||
t.Error("expected nil after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantRestoreFromSnapshot(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
|
||||
snapshot := map[string]any{
|
||||
"id": "restored-grant",
|
||||
"tokenHash": "abc123",
|
||||
"resourceType": "collection",
|
||||
"resourceId": "col1",
|
||||
"target": "surge",
|
||||
"enabled": true,
|
||||
"createdAt": float64(1000),
|
||||
}
|
||||
err := repo.RestoreFromSnapshot(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("restore failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get("restored-grant")
|
||||
if got == nil {
|
||||
t.Fatal("expected restored grant")
|
||||
}
|
||||
if got.ResourceType != "collection" {
|
||||
t.Errorf("expected resourceType collection, got %s", got.ResourceType)
|
||||
}
|
||||
if got.Target != "surge" {
|
||||
t.Errorf("expected target surge, got %s", got.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantRestoreFromSnapshot_DefaultsAndExpiry(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
|
||||
// resourceType not "collection" -> defaults to "source"; enabled false; expiresAt positive
|
||||
snapshot := map[string]any{
|
||||
"id": "grant-defaults",
|
||||
"tokenHash": "hash2",
|
||||
"resourceType": "source",
|
||||
"resourceId": "src1",
|
||||
"target": "",
|
||||
"enabled": false,
|
||||
"expiresAt": float64(5000),
|
||||
}
|
||||
err := repo.RestoreFromSnapshot(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("restore failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get("grant-defaults")
|
||||
if got == nil {
|
||||
t.Fatal("expected restored grant")
|
||||
}
|
||||
if got.ResourceType != "source" {
|
||||
t.Errorf("expected source, got %s", got.ResourceType)
|
||||
}
|
||||
if got.Enabled {
|
||||
t.Error("expected enabled false")
|
||||
}
|
||||
if got.ExpiresAt == nil || *got.ExpiresAt != 5000 {
|
||||
t.Errorf("expected expiresAt 5000, got %v", got.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantRestoreFromSnapshot_NoCreatedAt(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
|
||||
snapshot := map[string]any{
|
||||
"id": "grant-no-created",
|
||||
"tokenHash": "hash3",
|
||||
"resourceType": "source",
|
||||
"resourceId": "src1",
|
||||
}
|
||||
err := repo.RestoreFromSnapshot(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("restore failed: %v", err)
|
||||
}
|
||||
got, _ := repo.Get("grant-no-created")
|
||||
if got == nil {
|
||||
t.Fatal("expected restored grant")
|
||||
}
|
||||
if got.CreatedAt == 0 {
|
||||
t.Error("expected non-zero createdAt (defaulted to now)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantAuthorizeScoped_EmptyToken(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
if repo.AuthorizeScoped("", "source", "src1", "mihomo") {
|
||||
t.Error("expected false for empty token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantAuthorizeScoped_ExpiredGrant(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
exp := time.Now().UnixMilli() - 1000
|
||||
rec, token, _ := repo.Create("source", "src1", "mihomo", &exp)
|
||||
_ = rec
|
||||
if repo.AuthorizeScoped(token, "source", "src1", "mihomo") {
|
||||
t.Error("expected false for expired grant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantAuthorizeScoped_UnrestrictedTarget(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewGrantRepo(db)
|
||||
// target = "" means unrestricted
|
||||
_, token, _ := repo.Create("source", "src1", "", nil)
|
||||
if !repo.AuthorizeScoped(token, "source", "src1", "surge") {
|
||||
t.Error("expected true for unrestricted target")
|
||||
}
|
||||
if !repo.AuthorizeScoped(token, "source", "src1", "mihomo") {
|
||||
t.Error("expected true for unrestricted target (mihomo)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- RecycleRepo.parseSnapshot edge cases (via List/Get) ---
|
||||
|
||||
func TestParseSnapshot_Empty(t *testing.T) {
|
||||
result := parseSnapshot("")
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty map, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapshot_InvalidJSON(t *testing.T) {
|
||||
result := parseSnapshot("not json")
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty map for invalid json, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapshot_Valid(t *testing.T) {
|
||||
result := parseSnapshot(`{"key":"value"}`)
|
||||
if result["key"] != "value" {
|
||||
t.Errorf("expected key=value, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecycleList_Empty(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewRecycleRepo(db)
|
||||
list, err := repo.List()
|
||||
if err != nil {
|
||||
t.Fatalf("list failed: %v", err)
|
||||
}
|
||||
if len(list) != 0 {
|
||||
t.Errorf("expected empty list, got %d", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecycleGet_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewRecycleRepo(db)
|
||||
got, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecycleArchiveAndDelete_TrimExcess(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewRecycleRepo(db)
|
||||
// Insert more than MaxRecycleEntries to test trimming
|
||||
for i := 0; i < 55; i++ {
|
||||
_, err := repo.ArchiveAndDelete("source", "nonexistent", map[string]any{"i": i}, "SELECT 1", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("archive %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
list, _ := repo.List()
|
||||
if len(list) > 50 {
|
||||
t.Errorf("expected at most 50 entries after trim, got %d", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
if s := marshalJSON(map[string]any{"a": 1}); s != `{"a":1}` {
|
||||
t.Errorf("expected {\"a\":1}, got %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolToInt(t *testing.T) {
|
||||
if boolToInt(true) != 1 {
|
||||
t.Error("expected 1 for true")
|
||||
}
|
||||
if boolToInt(false) != 0 {
|
||||
t.Error("expected 0 for false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonUnmarshal(t *testing.T) {
|
||||
var m map[string]any
|
||||
jsonUnmarshal(`{"a":1}`, &m)
|
||||
if m["a"] != float64(1) {
|
||||
t.Errorf("expected a=1, got %v", m["a"])
|
||||
}
|
||||
// Empty string should be a no-op
|
||||
var m2 map[string]any
|
||||
jsonUnmarshal("", &m2)
|
||||
if m2 != nil {
|
||||
t.Error("expected nil for empty string")
|
||||
}
|
||||
}
|
||||
|
||||
// --- CacheRepo.Get returns nil on error ---
|
||||
|
||||
func TestCacheGet_Miss(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCacheRepo(db)
|
||||
entry, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error for miss, got %v", err)
|
||||
}
|
||||
if entry != nil {
|
||||
t.Error("expected nil entry for miss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachePut_PanicRecovery(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCacheRepo(db)
|
||||
// SafePut should not panic even with nil metadata (normal case)
|
||||
repo.SafePut("key", "content", nil, 300)
|
||||
entry, ok := repo.SafeGet("key")
|
||||
if !ok {
|
||||
t.Fatal("expected cache hit")
|
||||
}
|
||||
if entry.Content != "content" {
|
||||
t.Errorf("expected content, got %s", entry.Content)
|
||||
}
|
||||
if entry.Metadata == nil {
|
||||
t.Error("expected non-nil metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheGet_Expired(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCacheRepo(db)
|
||||
repo.Put("expired", "old", map[string]any{}, 1)
|
||||
time.Sleep(2 * time.Second)
|
||||
entry, err := repo.Get("expired")
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error, got %v", err)
|
||||
}
|
||||
if entry != nil {
|
||||
t.Error("expected nil for expired")
|
||||
}
|
||||
}
|
||||
|
||||
// --- SourceRepo additional coverage ---
|
||||
|
||||
func TestSourceUpsert_AutoId(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewSourceRepo(db)
|
||||
rec, err := repo.Upsert(model.SourceRecord{
|
||||
Name: "My Source", Type: "local", Content: "ss://pass@host:port#name",
|
||||
Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("upsert failed: %v", err)
|
||||
}
|
||||
if rec.ID != "my-source" {
|
||||
t.Errorf("expected id my-source, got %s", rec.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceUpsert_DefaultTypeRemote(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewSourceRepo(db)
|
||||
repo.Upsert(model.SourceRecord{
|
||||
ID: "test", Name: "Test", Type: "", URL: "https://example.com/sub",
|
||||
Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{},
|
||||
})
|
||||
// Read back — sourceFromRow normalizes empty type to "remote"
|
||||
got, _ := repo.Get("test")
|
||||
if got.Type != "remote" {
|
||||
t.Errorf("expected type remote, got %s", got.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceGet_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewSourceRepo(db)
|
||||
got, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
// --- TemplateRepo additional coverage ---
|
||||
|
||||
func TestTemplateUpsert_DefaultTargetMihomo(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewTemplateRepo(db)
|
||||
rec, _ := repo.Upsert(model.TemplateRecord{
|
||||
ID: "test", Name: "Test", Target: "", Config: nil,
|
||||
})
|
||||
if rec.Target != "mihomo" {
|
||||
t.Errorf("expected target mihomo, got %s", rec.Target)
|
||||
}
|
||||
if rec.Config == nil {
|
||||
t.Error("expected non-nil config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateGet_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewTemplateRepo(db)
|
||||
got, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateFromRow_EmptyTarget(t *testing.T) {
|
||||
rec := templateFromRow(templateRow{ID: "x", Target: "", ConfigJSON: "{}"})
|
||||
if rec.Target != "mihomo" {
|
||||
t.Errorf("expected mihomo, got %s", rec.Target)
|
||||
}
|
||||
}
|
||||
|
||||
// --- CollectionRepo additional coverage ---
|
||||
|
||||
func TestCollectionUpsert_Defaults(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCollectionRepo(db)
|
||||
rec, _ := repo.Upsert(model.CollectionRecord{
|
||||
ID: "test", Name: "Test",
|
||||
})
|
||||
if rec.TemplateId != "acl4ssr-mihomo" {
|
||||
t.Errorf("expected default template, got %s", rec.TemplateId)
|
||||
}
|
||||
if rec.SourceIds == nil {
|
||||
t.Error("expected non-nil SourceIds")
|
||||
}
|
||||
if rec.Filters == nil {
|
||||
t.Error("expected non-nil Filters")
|
||||
}
|
||||
if rec.Meta == nil {
|
||||
t.Error("expected non-nil Meta")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionGet_NotFound(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer db.Close()
|
||||
repo := NewCollectionRepo(db)
|
||||
got, err := repo.Get("nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("expected nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
// --- RunMigrations is already tested via testDB; test error case ---
|
||||
|
||||
func TestRunMigrations_OnFreshDB(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "fresh.db")
|
||||
db, err := sqlx.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err := RunMigrations(db); err != nil {
|
||||
t.Fatalf("migrations failed: %v", err)
|
||||
}
|
||||
// Tables should exist
|
||||
var count int
|
||||
db.Get(&count, "SELECT COUNT(*) FROM sources")
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 sources, got %d", count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user