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
224 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|