storage: test postgres via docker

This commit is contained in:
nkanaev
2026-06-23 13:44:10 +01:00
parent 14b06dcbaf
commit 21c7f9a4a4
3 changed files with 87 additions and 60 deletions

4
go.mod
View File

@@ -1,8 +1,6 @@
module github.com/nkanaev/yarr
go 1.23.0
toolchain go1.23.5
go 1.25.0
require (
fyne.io/systray v1.12.0

View File

@@ -291,14 +291,12 @@ func TestListItemsPaginated(t *testing.T) {
})
}
func TestMarkItemsRead(t *testing.T) {
// NOTE: starred items must not be marked as read
func TestMarkAllItemsRead(t *testing.T) {
var read model.ItemStatus = model.READ
dbtest(t, func(t *testing.T, db1 storage.Storage) {
testItemsSetup(db1)
db1.MarkItemsRead(model.MarkFilter{})
have := getItemGuids(db1.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
dbtest(t, func(t *testing.T, db storage.Storage) {
testItemsSetup(db)
db.MarkItemsRead(model.MarkFilter{})
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want := []string{
"item111", "item112", "item121", "item122",
"item211", "item011", "item012",
@@ -309,11 +307,14 @@ func TestMarkItemsRead(t *testing.T) {
t.Fail()
}
})
}
dbtest(t, func(t *testing.T, db2 storage.Storage) {
scope2 := testItemsSetup(db2)
db2.MarkItemsRead(model.MarkFilter{FolderID: &scope2.folder1.Id})
have := getItemGuids(db2.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
func TestMarkItemsReadByFolder(t *testing.T) {
var read model.ItemStatus = model.READ
dbtest(t, func(t *testing.T, db storage.Storage) {
scope := testItemsSetup(db)
db.MarkItemsRead(model.MarkFilter{FolderID: &scope.folder1.Id})
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want := []string{
"item111", "item112", "item121", "item122",
"item211", "item012",
@@ -324,11 +325,14 @@ func TestMarkItemsRead(t *testing.T) {
t.Fail()
}
})
}
dbtest(t, func(t *testing.T, db3 storage.Storage) {
scope3 := testItemsSetup(db3)
db3.MarkItemsRead(model.MarkFilter{FeedID: &scope3.feed11.Id})
have := getItemGuids(db3.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
func TestMarkItemsReadByFeed(t *testing.T) {
var read model.ItemStatus = model.READ
dbtest(t, func(t *testing.T, db storage.Storage) {
scope := testItemsSetup(db)
db.MarkItemsRead(model.MarkFilter{FeedID: &scope.feed11.Id})
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
want := []string{
"item111", "item112", "item122",
"item211", "item012",

View File

@@ -1,30 +1,32 @@
package tests
import (
"crypto/rand"
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"testing"
"time"
_ "github.com/lib/pq"
"github.com/nkanaev/yarr/src/storage"
)
func dbtest(t *testing.T, testcase func(t *testing.T, db storage.Storage)) {
t.Parallel()
testurls := map[string]string{
"sqlite": ":memory:",
}
if pgUrl := os.Getenv("YARR_POSTGRES_TEST_URL"); pgUrl != "" {
dburl, cleanup, err := createPostgresDB(pgUrl)
if err != nil {
t.Fatalf("failed to create postgres test database: %v", err)
}
if pgImage := os.Getenv("YARR_POSTGRES_TEST_IMAGE"); pgImage != "" {
dburl, cleanup := startPostgresContainer(t, pgImage)
t.Cleanup(cleanup)
testurls["postgres"] = dburl
} else if !testing.Short() {
t.Fatalf("YARR_POSTGRES_TEST_IMAGE not set; use -short to skip docker tests")
}
for testname, url := range testurls {
@@ -38,49 +40,72 @@ func dbtest(t *testing.T, testcase func(t *testing.T, db storage.Storage)) {
}
}
func createPostgresDB(pgUrl string) (string, func(), error) {
u, err := url.Parse(pgUrl)
func startPostgresContainer(t *testing.T, image string) (string, func()) {
// database credentials
dbUser := "testuser"
dbPass := "password"
dbName := "yarrtest"
// generate unique container name
testHash := sha256.Sum256([]byte(t.Name()))
containerName := fmt.Sprintf("yarr-test-pg-%x-%d", testHash[:8], time.Now().UnixNano())
cmd := exec.Command(
"docker", "run", "-d", "--rm",
"--name", containerName,
"-p", "0:5432",
"-e", "POSTGRES_USER="+dbUser,
"-e", "POSTGRES_PASSWORD="+dbPass,
"-e", "POSTGRES_DB="+dbName,
image,
)
out, err := cmd.CombinedOutput()
if err != nil {
return "", nil, err
t.Fatalf("failed to start postgres container: %v\n%s", err, string(out))
}
u.Path = "/postgres"
adminConnStr := u.String()
adminDB, err := sql.Open("postgres", adminConnStr)
// retrieve the host port assigned by docker
portCmd := exec.Command("docker", "port", containerName, "5432/tcp")
portOut, err := portCmd.Output()
if err != nil {
return "", nil, fmt.Errorf("admin connect: %w", err)
t.Fatalf("failed to get container port: %v", err)
}
parts := strings.Split(strings.TrimSpace(string(portOut)), ":")
dbPort := parts[len(parts)-1]
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
adminDB.Close()
return "", nil, fmt.Errorf("generate suffix: %w", err)
}
// build connection string
pgUrl := fmt.Sprintf(
"postgres://%s:%s@localhost:%s/%s?sslmode=disable",
dbUser,
dbPass,
dbPort,
dbName,
)
testDBName := "yarr_test_" + hex.EncodeToString(b)
if _, err := adminDB.Exec(fmt.Sprintf(`CREATE DATABASE "%s"`, testDBName)); err != nil {
adminDB.Close()
return "", nil, fmt.Errorf("create database: %w", err)
}
adminDB.Close()
u.Path = "/" + testDBName
testURL := u.String()
cleanup := func() {
dropDB, err := sql.Open("postgres", adminConnStr)
// wait up to 15 seconds for the container to accept connections
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
db, err := sql.Open("postgres", pgUrl)
if err != nil {
return
continue
}
defer dropDB.Close()
dropDB.Exec(fmt.Sprintf(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()`,
testDBName,
))
dropDB.Exec(fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, testDBName))
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
err = db.PingContext(ctx)
cancel()
db.Close()
if err == nil {
goto ready
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("timed out waiting for postgres container to be ready")
return testURL, cleanup, nil
ready:
// return connection url and a cleanup function that stops the container
return pgUrl, func() {
stop := exec.Command("docker", "stop", containerName)
if err := stop.Run(); err != nil {
t.Logf("failed to stop container %s: %v", containerName, err)
}
}
}