package store import ( "context" "errors" "path/filepath" "testing" "time" "glchat/internal/database" ) // newLinkPreviewTestStore открывает временную БД со всеми миграциями. // В пакете store это первый тест, поэтому помощник живёт здесь. func newLinkPreviewTestStore(t *testing.T) *Store { t.Helper() db, err := database.Open(context.Background(), database.Options{ Path: filepath.Join(t.TempDir(), "glchat.db"), ReadPool: 2, Migrate: true, }) if err != nil { t.Fatalf("open test database: %v", err) } t.Cleanup(func() { if err := db.Close(); err != nil { t.Errorf("close test database: %v", err) } }) return New(db) } func TestLinkPreviewSaveAndGet(t *testing.T) { ctx := context.Background() st := newLinkPreviewTestStore(t) fetchedAt := time.Now().UTC() params := SaveLinkPreviewParams{ URLHash: "hash-1", URL: "https://example.com/page", Title: "Заголовок", Description: "Описание", SiteName: "Пример", ImageURL: "https://example.com/pic.png", Status: LinkPreviewStatusOK, FetchedAt: fetchedAt, ExpiresAt: fetchedAt.Add(time.Hour), } if err := st.SaveLinkPreview(ctx, params); err != nil { t.Fatalf("SaveLinkPreview() вернул ошибку: %v", err) } preview, err := st.GetLinkPreview(ctx, params.URLHash) if err != nil { t.Fatalf("GetLinkPreview() вернул ошибку: %v", err) } if preview.URLHash != params.URLHash || preview.URL != params.URL { t.Errorf("URLHash/URL = %q/%q, want %q/%q", preview.URLHash, preview.URL, params.URLHash, params.URL) } if preview.Title != params.Title || preview.Description != params.Description { t.Errorf("Title/Description = %q/%q", preview.Title, preview.Description) } if preview.SiteName != params.SiteName || preview.ImageURL != params.ImageURL { t.Errorf("SiteName/ImageURL = %q/%q", preview.SiteName, preview.ImageURL) } if preview.Status != LinkPreviewStatusOK { t.Errorf("Status = %q, want %q", preview.Status, LinkPreviewStatusOK) } if want := st.Timestamp(params.FetchedAt); preview.FetchedAt != want { t.Errorf("FetchedAt = %q, want %q", preview.FetchedAt, want) } if want := st.Timestamp(params.ExpiresAt); preview.ExpiresAt != want { t.Errorf("ExpiresAt = %q, want %q", preview.ExpiresAt, want) } } func TestLinkPreviewUpsert(t *testing.T) { ctx := context.Background() st := newLinkPreviewTestStore(t) now := time.Now().UTC() first := SaveLinkPreviewParams{ URLHash: "hash-upsert", URL: "https://example.com/page", Title: "Старый заголовок", Status: LinkPreviewStatusEmpty, FetchedAt: now, ExpiresAt: now.Add(time.Hour), } if err := st.SaveLinkPreview(ctx, first); err != nil { t.Fatalf("SaveLinkPreview() вернул ошибку: %v", err) } second := SaveLinkPreviewParams{ URLHash: first.URLHash, URL: first.URL, Title: "Новый заголовок", Description: "Новое описание", ImageURL: "https://example.com/new.png", Status: LinkPreviewStatusOK, FetchedAt: now.Add(time.Minute), ExpiresAt: now.Add(2 * time.Hour), } if err := st.SaveLinkPreview(ctx, second); err != nil { t.Fatalf("SaveLinkPreview() при перезаписи вернул ошибку: %v", err) } preview, err := st.GetLinkPreview(ctx, first.URLHash) if err != nil { t.Fatalf("GetLinkPreview() вернул ошибку: %v", err) } if preview.Title != second.Title || preview.Description != second.Description { t.Errorf("после upsert Title/Description = %q/%q, want %q/%q", preview.Title, preview.Description, second.Title, second.Description) } if preview.Status != LinkPreviewStatusOK { t.Errorf("после upsert Status = %q, want %q", preview.Status, LinkPreviewStatusOK) } if want := st.Timestamp(second.ExpiresAt); preview.ExpiresAt != want { t.Errorf("после upsert ExpiresAt = %q, want %q", preview.ExpiresAt, want) } var count int if err := st.Reader().QueryRowContext(ctx, `SELECT COUNT(*) FROM link_previews WHERE url_hash = ?`, first.URLHash).Scan(&count); err != nil { t.Fatalf("подсчёт строк: %v", err) } if count != 1 { t.Errorf("строк с тем же url_hash = %d, want 1", count) } } func TestLinkPreviewExpiredNotFound(t *testing.T) { ctx := context.Background() st := newLinkPreviewTestStore(t) now := time.Now().UTC() params := SaveLinkPreviewParams{ URLHash: "hash-expired", URL: "https://example.com/old", Title: "Просроченное", Status: LinkPreviewStatusOK, FetchedAt: now.Add(-2 * time.Hour), ExpiresAt: now.Add(-time.Hour), } if err := st.SaveLinkPreview(ctx, params); err != nil { t.Fatalf("SaveLinkPreview() вернул ошибку: %v", err) } if _, err := st.GetLinkPreview(ctx, params.URLHash); !errors.Is(err, ErrNotFound) { t.Fatalf("GetLinkPreview() err = %v, want ErrNotFound", err) } } func TestDeleteExpiredLinkPreviews(t *testing.T) { ctx := context.Background() st := newLinkPreviewTestStore(t) now := time.Now().UTC() fresh := SaveLinkPreviewParams{ URLHash: "hash-fresh", URL: "https://example.com/fresh", Title: "Свежее", Status: LinkPreviewStatusOK, FetchedAt: now, ExpiresAt: now.Add(time.Hour), } expired := SaveLinkPreviewParams{ URLHash: "hash-old", URL: "https://example.com/old", Title: "Просроченное", Status: LinkPreviewStatusError, FetchedAt: now.Add(-2 * time.Hour), ExpiresAt: now.Add(-time.Hour), } for _, params := range []SaveLinkPreviewParams{fresh, expired} { if err := st.SaveLinkPreview(ctx, params); err != nil { t.Fatalf("SaveLinkPreview(%q) вернул ошибку: %v", params.URLHash, err) } } deleted, err := st.DeleteExpiredLinkPreviews(ctx, now) if err != nil { t.Fatalf("DeleteExpiredLinkPreviews() вернул ошибку: %v", err) } if deleted != 1 { t.Errorf("удалено записей = %d, want 1", deleted) } var left int if err := st.Reader().QueryRowContext(ctx, `SELECT COUNT(*) FROM link_previews`).Scan(&left); err != nil { t.Fatalf("подсчёт строк: %v", err) } if left != 1 { t.Errorf("в таблице осталось %d записей, want 1", left) } if _, err := st.GetLinkPreview(ctx, fresh.URLHash); err != nil { t.Errorf("свежая запись недоступна: %v", err) } }