diff --git a/go.mod b/go.mod index ba2a7bc..5832d84 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/src/storage/tests/item_test.go b/src/storage/tests/item_test.go index 09a0d9b..0ccd79f 100644 --- a/src/storage/tests/item_test.go +++ b/src/storage/tests/item_test.go @@ -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", diff --git a/src/storage/tests/storage_test.go b/src/storage/tests/storage_test.go index f892831..8981381 100644 --- a/src/storage/tests/storage_test.go +++ b/src/storage/tests/storage_test.go @@ -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) + } + } }