indexer/store/store_test.go
Ricardo (XenGi) Band 63c6f4b761
Some checks failed
CI pipeline for ffd-indexer / checks (go test + golangci) (push) Failing after 26s
CI pipeline for ffd-indexer / build binaries + containers (push) Failing after 3s
CI pipeline for ffd-indexer / format (gofmt + nixfmt) (push) Failing after 3s
CI pipeline for ffd-indexer / integration (live scrape + Typesense) (push) Failing after 20s
🔥
2026-10-01 00:37:44 +02:00

224 lines
6.3 KiB
Go

package store
import (
"database/sql"
"path/filepath"
"testing"
"time"
_ "modernc.org/sqlite"
"git.xengi.de/ffd/indexer/scraper"
)
func newTestStore(t *testing.T) *Store {
t.Helper()
path := filepath.Join(t.TempDir(), "test.db")
db, err := Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
return db
}
func TestRecordAndReadForgeStatus(t *testing.T) {
db := newTestStore(t)
now := time.Now().UTC()
if err := db.RecordForgeStatus(ForgeStatus{
URL: "https://example.test",
ForgeType: "forgejo",
Version: "16.0.5",
Online: true,
LastScrapedAt: now,
}); err != nil {
t.Fatalf("RecordForgeStatus: %v", err)
}
// Offline forge also gets a row so metrics always see it.
if err := db.RecordForgeStatus(ForgeStatus{
URL: "https://offline.test",
ForgeType: "forgejo",
Online: false,
LastScrapedAt: now,
LastError: "timeout",
}); err != nil {
t.Fatalf("RecordForgeStatus: %v", err)
}
sts, err := db.ForgeStatuses()
if err != nil {
t.Fatalf("ForgeStatuses: %v", err)
}
if len(sts) != 2 {
t.Fatalf("want 2 statuses, got %d", len(sts))
}
byURL := map[string]ForgeStatus{}
for _, s := range sts {
byURL[s.URL] = s
}
if s, ok := byURL["https://example.test"]; !ok || !s.Online || s.ForgeType != "forgejo" || s.Version != "16.0.5" || s.LastError != "" {
t.Errorf("unexpected online status: %+v", s)
}
if s, ok := byURL["https://offline.test"]; !ok || s.Online || s.LastError != "timeout" || s.Version != "" {
t.Errorf("unexpected offline status: %+v", s)
}
// Single-forge lookup returns the row, and the version comes back.
single, err := db.ForgeStatus("https://example.test")
if err != nil {
t.Fatalf("ForgeStatus: %v", err)
}
if single.Version != "16.0.5" || single.ForgeType != "forgejo" {
t.Errorf("single lookup: %+v", single)
}
// A never-seen URL yields the zero value, not an error.
missing, err := db.ForgeStatus("https://nope.test")
if err != nil {
t.Fatalf("ForgeStatus(missing): %v", err)
}
if missing.URL != "" || missing.Online {
t.Errorf("miss zero-value status: %+v", missing)
}
}
func TestUpsertRecordsForgeIdentity(t *testing.T) {
db := newTestStore(t)
p := scraper.Project{
ForgeIdentity: scraper.ForgeIdentity{
ForgeURL: "https://example.test",
ForgeType: "forgejo",
},
Name: "owner/repo",
Url: "https://example.test/owner/repo",
Languages: []string{"Go", "Rust"},
}
if err := db.Upsert(p); err != nil {
t.Fatalf("Upsert: %v", err)
}
repos, err := db.RepoIdentities()
if err != nil {
t.Fatalf("RepoIdentities: %v", err)
}
if len(repos) != 1 {
t.Fatalf("want 1 repo, got %d", len(repos))
}
r := repos[0]
if r.URL != p.Url || r.ForgeURL != "https://example.test" || r.ForgeType != "forgejo" {
t.Errorf("unexpected repo identity: %+v", r)
}
if len(r.Languages) != 2 || r.Languages[0] != "Go" || r.Languages[1] != "Rust" {
t.Errorf("unexpected languages: %v", r.Languages)
}
}
// TestOldSchemaUpgrades checks that a database created with the pre-forge
// schema (with a forge_name column and no forge_url/forge_type) is migrated in
// place: forge_url/forge_type are added and forge_name is dropped.
func TestOldSchemaUpgrades(t *testing.T) {
path := filepath.Join(t.TempDir(), "old.db")
// Build an old-schema database by hand (as if it predated the metrics
// feature), insert a row, close it, and reopen via the normal path.
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatalf("raw open: %v", err)
}
_, err = db.Exec(`CREATE TABLE projects (
url TEXT PRIMARY KEY, name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
topics TEXT NOT NULL DEFAULT '[]',
languages TEXT NOT NULL DEFAULT '[]',
licenses TEXT NOT NULL DEFAULT '[]',
open_issues INTEGER NOT NULL DEFAULT 0,
open_prs INTEGER NOT NULL DEFAULT 0,
latest_commit INTEGER NOT NULL DEFAULT 0,
latest_release_name TEXT NOT NULL DEFAULT '',
latest_release_date INTEGER NOT NULL DEFAULT 0,
readme TEXT NOT NULL DEFAULT '',
uses_ai INTEGER NOT NULL DEFAULT 0,
scraped_at INTEGER NOT NULL DEFAULT 0,
forge_name TEXT NOT NULL DEFAULT ''
)`)
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`CREATE TABLE forge_status (
url TEXT PRIMARY KEY, name TEXT NOT NULL,
forge_type TEXT NOT NULL DEFAULT '',
online INTEGER NOT NULL DEFAULT 0,
last_scraped_at INTEGER NOT NULL DEFAULT 0,
last_error TEXT NOT NULL DEFAULT ''
)`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO projects (url, name, languages) VALUES ('https://example.test/r', 'r', '["Go"]')`); err != nil {
t.Fatal(err)
}
_ = db.Close()
// Reopen via the normal path: the migrations must add forge_url/
// forge_type, drop forge_name from both tables, and leave existing rows
// readable.
up, err := Open(path)
if err != nil {
t.Fatalf("Open after migration: %v", err)
}
defer func() { _ = up.Close() }()
repos, err := up.RepoIdentities()
if err != nil {
t.Fatalf("RepoIdentities after migration: %v", err)
}
if len(repos) != 1 {
t.Fatalf("want 1 repo after migration, got %d", len(repos))
}
if repos[0].URL != "https://example.test/r" || repos[0].ForgeURL != "" {
t.Errorf("unexpected repo after migration: %+v", repos[0])
}
// The up-to-date schema must not retain the dropped columns.
for _, c := range []string{"forge_name"} {
has, err := hasColumn(up.db, "projects", c)
if err != nil {
t.Fatal(err)
}
if has {
t.Errorf("column %q should have been dropped from projects", c)
}
}
for _, c := range []string{"name"} {
has, err := hasColumn(up.db, "forge_status", c)
if err != nil {
t.Fatal(err)
}
if has {
t.Errorf("column %q should have been dropped from forge_status", c)
}
}
// The version column must have been added to the migrated forge_status
// table, so a status recorded after upgrade persists its version.
if err := up.RecordForgeStatus(ForgeStatus{
URL: "https://example.test",
ForgeType: "forgejo",
Version: "16.0.5",
Online: true,
LastScrapedAt: time.Now().UTC(),
}); err != nil {
t.Fatalf("RecordForgeStatus after migration: %v", err)
}
got, err := up.ForgeStatus("https://example.test")
if err != nil {
t.Fatalf("ForgeStatus after migration: %v", err)
}
if !got.Online || got.Version != "16.0.5" {
t.Errorf("status after migration lost version: %+v", got)
}
}