mirror of
https://github.com/go-shiori/shiori.git
synced 2025-09-06 04:54:59 +08:00
fix: saving bookmarks inconsistencies (#500)
* chore: updated go-migrate dependencies * fix: specify if we're saving bookmarks expecting a creation up until now the SaveBookmarks method was doing some "magic" to do "upserts" on the databases, but consistency between engines was scarce and not knowing if we were expecting saving a new bookmark or updating an existing one was leading to errors and inconsistencies in logic all around the place. Now we need to specify a creation boolean when saving and a differnt query will be make (INSERT vs UPDATE). * fix(api): using incorrect bookmark for content downlaod * test(db): added test pipeline for databases Added functions that will share logic among the engines and will be called on fresh databases on each test run * dev: added basic docker-compose for development * chore: uncommented tests * ci(test): added mysql service * typo * test(mysql): select database after reset * fix(mysql): ignore empty row errors when parsing tags * fix(mysql): handle insert errors * chore: added mysql variables to compose * ci: explicit mysql service port exposed
This commit is contained in:
parent
040dc5c5d1
commit
05fee53bd0
19 changed files with 385 additions and 83 deletions
16
.github/workflows/_test.yml
vendored
16
.github/workflows/_test.yml
vendored
|
@ -21,6 +21,21 @@ jobs:
|
|||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mariadb:
|
||||
image: mariadb
|
||||
env:
|
||||
MYSQL_USER: shiori
|
||||
MYSQL_PASSWORD: shiori
|
||||
MYSQL_DATABASE: shiori
|
||||
MYSQL_ROOT_PASSWORD: shiori
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping"
|
||||
--health-interval=5s
|
||||
--health-timeout=2s
|
||||
--health-retries=3
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
name: Go ${{ matrix.go }} unit tests
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -40,4 +55,5 @@ jobs:
|
|||
- run: go test ./...
|
||||
env:
|
||||
SHIORI_TEST_PG_URL: "postgres://shiori:shiori@localhost:5432/shiori?sslmode=disable"
|
||||
SHIORI_TEST_MYSQL_URL: "shiori:shiori@(localhost:3306)/shiori"
|
||||
- run: CGO_ENABLED=0 go build -tags osusergo,netgo -ldflags="-s -w -X main.version=$(git describe --tags) -X main.date=$(date --iso-8601=seconds)"
|
||||
|
|
6
Dockerfile.compose
Normal file
6
Dockerfile.compose
Normal file
|
@ -0,0 +1,6 @@
|
|||
FROM docker.io/golang:1.19-alpine3.16
|
||||
|
||||
WORKDIR /src/shiori
|
||||
|
||||
ENTRYPOINT ["go", "run", "main.go"]
|
||||
CMD ["server"]
|
46
docker-compose.yaml
Normal file
46
docker-compose.yaml
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Docker compose for development purposes only
|
||||
version: "3"
|
||||
services:
|
||||
shiori:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.compose
|
||||
container_name: shiori
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "./dev-data:/srv/shiori"
|
||||
- ".:/src/shiori"
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "postgres"
|
||||
- "mariadb"
|
||||
environment:
|
||||
SHIORI_DBMS: mysql
|
||||
SHIORI_PG_USER: shiori
|
||||
SHIORI_PG_PASS: shiori
|
||||
SHIORI_PG_NAME: shiori
|
||||
SHIORI_PG_HOST: postgres
|
||||
SHIORI_PG_PORT: 5432
|
||||
SHIORI_MYSQL_USER: shiori
|
||||
SHIORI_MYSQL_PASS: shiori
|
||||
SHIORI_MYSQL_NAME: shiori
|
||||
SHIORI_MYSQL_ADDRESS: (mariadb)
|
||||
|
||||
postgres:
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: shiori
|
||||
POSTGRES_USER: shiori
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.9
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: toor
|
||||
MYSQL_DATABASE: shiori
|
||||
MYSQL_USER: shiori
|
||||
MYSQL_PASSWORD: shiori
|
||||
ports:
|
||||
- "3306:3306"
|
2
go.mod
2
go.mod
|
@ -47,7 +47,7 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1170,8 +1170,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
|||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
|
|
|
@ -111,7 +111,7 @@ func addHandler(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// Save bookmark to database
|
||||
_, err = db.SaveBookmarks(cmd.Context(), book)
|
||||
_, err = db.SaveBookmarks(cmd.Context(), true, book)
|
||||
if err != nil {
|
||||
cError.Printf("Failed to save bookmark: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -154,7 +154,7 @@ func importHandler(cmd *cobra.Command, args []string) {
|
|||
})
|
||||
|
||||
// Save bookmark to database
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), true, bookmarks...)
|
||||
if err != nil {
|
||||
cError.Printf("Failed to save bookmarks: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -112,7 +112,7 @@ func pocketHandler(cmd *cobra.Command, args []string) {
|
|||
})
|
||||
|
||||
// Save bookmark to database
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), true, bookmarks...)
|
||||
if err != nil {
|
||||
cError.Printf("Failed to save bookmarks: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -285,7 +285,7 @@ func updateHandler(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// Save bookmarks to database
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), bookmarks...)
|
||||
bookmarks, err = db.SaveBookmarks(cmd.Context(), false, bookmarks...)
|
||||
if err != nil {
|
||||
cError.Printf("Failed to save bookmark: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -49,7 +49,7 @@ type DB interface {
|
|||
Migrate() error
|
||||
|
||||
// SaveBookmarks saves bookmarks data to database.
|
||||
SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error)
|
||||
SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error)
|
||||
|
||||
// GetBookmarks fetch list of bookmarks based on submitted options.
|
||||
GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.Bookmark, error)
|
||||
|
|
103
internal/database/database_test.go
Normal file
103
internal/database/database_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-shiori/shiori/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type databaseTestCase func(t *testing.T, db DB)
|
||||
type testDatabaseFactory func(ctx context.Context) (DB, error)
|
||||
|
||||
func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
|
||||
tests := map[string]databaseTestCase{
|
||||
"testCreateBookmark": testCreateBookmark,
|
||||
"testCreateBookmarkTwice": testCreateBookmarkTwice,
|
||||
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
|
||||
"testUpdateBookmark": testUpdateBookmark,
|
||||
}
|
||||
|
||||
for testName, testCase := range tests {
|
||||
t.Run(testName, func(tInner *testing.T) {
|
||||
ctx := context.TODO()
|
||||
db, err := dbFactory(ctx)
|
||||
assert.NoError(tInner, err, "Error recreating database")
|
||||
testCase(tInner, db)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testCreateBookmark(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.Bookmark{
|
||||
URL: "https://github.com/go-shiori/obelisk",
|
||||
Title: "shiori",
|
||||
}
|
||||
|
||||
result, err := db.SaveBookmarks(ctx, true, book)
|
||||
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
assert.Equal(t, 1, result[0].ID, "Saved bookmark must have an ID set")
|
||||
}
|
||||
|
||||
func testCreateBookmarkWithTag(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.Bookmark{
|
||||
URL: "https://github.com/go-shiori/obelisk",
|
||||
Title: "shiori",
|
||||
Tags: []model.Tag{
|
||||
{
|
||||
Name: "test-tag",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := db.SaveBookmarks(ctx, true, book)
|
||||
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
assert.Equal(t, book.URL, result[0].URL)
|
||||
assert.Equal(t, book.Tags[0].Name, result[0].Tags[0].Name)
|
||||
}
|
||||
|
||||
func testCreateBookmarkTwice(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.Bookmark{
|
||||
URL: "https://github.com/go-shiori/shiori",
|
||||
Title: "shiori",
|
||||
}
|
||||
|
||||
result, err := db.SaveBookmarks(ctx, true, book)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
savedBookmark := result[0]
|
||||
savedBookmark.Title = "modified"
|
||||
|
||||
_, err = db.SaveBookmarks(ctx, true, savedBookmark)
|
||||
assert.Error(t, err, "Save bookmarks must fail")
|
||||
}
|
||||
|
||||
func testUpdateBookmark(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.Bookmark{
|
||||
URL: "https://github.com/go-shiori/shiori",
|
||||
Title: "shiori",
|
||||
}
|
||||
|
||||
result, err := db.SaveBookmarks(ctx, true, book)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
savedBookmark := result[0]
|
||||
savedBookmark.Title = "modified"
|
||||
|
||||
result, err = db.SaveBookmarks(ctx, false, savedBookmark)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
assert.Equal(t, "modified", result[0].Title)
|
||||
assert.Equal(t, savedBookmark.ID, result[0].ID)
|
||||
}
|
|
@ -64,23 +64,27 @@ func (db *MySQLDatabase) Migrate() error {
|
|||
|
||||
// SaveBookmarks saves new or updated bookmarks to database.
|
||||
// Returns the saved ID and error message if any happened.
|
||||
func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
|
||||
func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
|
||||
var result []model.Bookmark
|
||||
|
||||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(id, url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
url = VALUES(url),
|
||||
title = VALUES(title),
|
||||
excerpt = VALUES(excerpt),
|
||||
author = VALUES(author),
|
||||
public = VALUES(public),
|
||||
content = VALUES(content),
|
||||
html = VALUES(html),
|
||||
modified = VALUES(modified)`)
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
stmtUpdateBook, err := tx.Preparex(`UPDATE bookmark
|
||||
SET url = ?,
|
||||
title = ?,
|
||||
excerpt = ?,
|
||||
author = ?,
|
||||
public = ?,
|
||||
content = ?,
|
||||
html = ?,
|
||||
modified = ?`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -113,11 +117,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
|
|||
// Execute statements
|
||||
|
||||
for _, book := range bookmarks {
|
||||
// Check ID, URL and title
|
||||
if book.ID == 0 {
|
||||
return errors.New("ID must not be empty")
|
||||
}
|
||||
|
||||
// Check URL and title
|
||||
if book.URL == "" {
|
||||
return errors.New("URL must not be empty")
|
||||
}
|
||||
|
@ -132,9 +132,25 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
|
|||
}
|
||||
|
||||
// Save bookmark
|
||||
_, err := stmtInsertBook.ExecContext(ctx, book.ID,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
var err error
|
||||
if create {
|
||||
var res sql.Result
|
||||
res, err = stmtInsertBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
bookID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
book.ID = int(bookID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -158,8 +174,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.B
|
|||
|
||||
// If tag doesn't have any ID, fetch it from database
|
||||
if tag.ID == 0 {
|
||||
err = stmtGetTag.Get(&tag.ID, tagName)
|
||||
if err != nil {
|
||||
if err := stmtGetTag.GetContext(ctx, &tag.ID, tagName); err != nil && err != sql.ErrNoRows {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
|
60
internal/database/mysql_test.go
Normal file
60
internal/database/mysql_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
connString := os.Getenv("SHIORI_TEST_MYSQL_URL")
|
||||
if connString == "" {
|
||||
log.Fatal("mysql tests can't run without a MysQL database, set SHIORI_TEST_MYSQL_URL environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
func mysqlTestDatabaseFactory(ctx context.Context) (DB, error) {
|
||||
connString := os.Getenv("SHIORI_TEST_MYSQL_URL")
|
||||
db, err := OpenMySQLDatabase(ctx, connString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dbname string
|
||||
err = db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
err := tx.QueryRow("SELECT DATABASE()").Scan(&dbname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "DROP DATABASE IF EXISTS "+dbname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "CREATE DATABASE "+dbname)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := db.Exec("USE " + dbname); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, err
|
||||
}
|
||||
|
||||
func TestMysqlsDatabase(t *testing.T) {
|
||||
testDatabase(t, mysqlTestDatabaseFactory)
|
||||
}
|
|
@ -64,14 +64,19 @@ func (db *PGDatabase) Migrate() error {
|
|||
|
||||
// SaveBookmarks saves new or updated bookmarks to database.
|
||||
// Returns the saved ID and error message if any happened.
|
||||
func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {
|
||||
func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {
|
||||
result = []model.Bookmark{}
|
||||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT(url) DO UPDATE SET
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
stmtUpdateBook, err := tx.Preparex(`UPDATE bookmark SET
|
||||
url = $1,
|
||||
title = $2,
|
||||
excerpt = $3,
|
||||
|
@ -79,8 +84,7 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Book
|
|||
public = $5,
|
||||
content = $6,
|
||||
html = $7,
|
||||
modified = $8
|
||||
RETURNING id`)
|
||||
modified = $8`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -128,9 +132,16 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Book
|
|||
}
|
||||
|
||||
// Save bookmark
|
||||
err := stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
|
||||
var err error
|
||||
if create {
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
|
|
@ -7,42 +7,34 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-shiori/shiori/internal/model"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
testPsqlURL := os.Getenv("SHIORI_TEST_PG_URL")
|
||||
if testPsqlURL == "" {
|
||||
log.Fatal("psql tests can't run without a PSQL database")
|
||||
connString := os.Getenv("SHIORI_TEST_PG_URL")
|
||||
if connString == "" {
|
||||
log.Fatal("psql tests can't run without a PSQL database, set SHIORI_TEST_PG_URL environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPsqlSaveBookmarkWithTag(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
pgDB, err := OpenPGDatabase(ctx, os.Getenv("SHIORI_TEST_PG_URL"))
|
||||
func postgresqlTestDatabaseFactory(ctx context.Context) (DB, error) {
|
||||
db, err := OpenPGDatabase(ctx, os.Getenv("SHIORI_TEST_PG_URL"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pgDB.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
|
||||
t.Error(err)
|
||||
_, err = db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
book := model.Bookmark{
|
||||
URL: "https://github.com/go-shiori/obelisk",
|
||||
Title: "shiori",
|
||||
Tags: []model.Tag{
|
||||
{
|
||||
Name: "test-tag",
|
||||
},
|
||||
},
|
||||
if err := db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := pgDB.SaveBookmarks(ctx, book)
|
||||
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
assert.Equal(t, book.URL, result[0].URL)
|
||||
assert.Equal(t, book.Tags[0].Name, result[0].Tags[0].Name)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func TestPostgresDatabase(t *testing.T) {
|
||||
testDatabase(t, postgresqlTestDatabaseFactory)
|
||||
}
|
||||
|
|
|
@ -73,15 +73,20 @@ func (db *SQLiteDatabase) Migrate() error {
|
|||
|
||||
// SaveBookmarks saves new or updated bookmarks to database.
|
||||
// Returns the saved ID and error message if any happened.
|
||||
func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
|
||||
func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.Bookmark) ([]model.Bookmark, error) {
|
||||
var result []model.Bookmark
|
||||
|
||||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
|
||||
stmtInsertBook, err := tx.PreparexContext(ctx, `INSERT INTO bookmark
|
||||
(id, url, title, excerpt, author, public, modified, has_content)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
(url, title, excerpt, author, public, modified, has_content)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
stmtUpdateBook, err := tx.PreparexContext(ctx, `UPDATE bookmark SET
|
||||
url = ?, title = ?, excerpt = ?, author = ?,
|
||||
public = ?, modified = ?, has_content = ?`)
|
||||
if err != nil {
|
||||
|
@ -130,11 +135,7 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
|
|||
// Execute statements
|
||||
|
||||
for _, book := range bookmarks {
|
||||
// Check ID, URL and title
|
||||
if book.ID == 0 {
|
||||
return errors.New("ID must not be empty")
|
||||
}
|
||||
|
||||
// Check URL and title
|
||||
if book.URL == "" {
|
||||
return errors.New("URL must not be empty")
|
||||
}
|
||||
|
@ -148,11 +149,18 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
|
|||
book.Modified = modifiedTime
|
||||
}
|
||||
|
||||
// Save bookmark
|
||||
hasContent := book.Content != ""
|
||||
_, err = stmtInsertBook.ExecContext(ctx, book.ID,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent)
|
||||
|
||||
// Create or update bookmark
|
||||
var err error
|
||||
if create {
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -169,6 +177,13 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, bookmarks ...model.
|
|||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bookID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
book.ID = int(bookID)
|
||||
|
||||
if rows == 0 {
|
||||
_, err = stmtInsertBookContent.ExecContext(ctx, book.ID, book.Title, book.Content, book.HTML)
|
||||
if err != nil {
|
||||
|
|
36
internal/database/sqlite_test.go
Normal file
36
internal/database/sqlite_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var sqliteDatabaseTestPath string
|
||||
|
||||
func init() {
|
||||
sqliteDatabaseTestPath = filepath.Join(os.TempDir(), "shiori.db")
|
||||
}
|
||||
|
||||
func sqliteTestDatabaseFactory(ctx context.Context) (DB, error) {
|
||||
os.Remove(sqliteDatabaseTestPath)
|
||||
|
||||
db, err := OpenSQLiteDatabase(ctx, sqliteDatabaseTestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Migrate(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func TestSqliteDatabase(t *testing.T) {
|
||||
testDatabase(t, sqliteTestDatabaseFactory)
|
||||
}
|
|
@ -98,12 +98,12 @@ func (h *handler) apiInsertViaExtension(w http.ResponseWriter, r *http.Request,
|
|||
panic(fmt.Errorf("failed to process bookmark: %v", err))
|
||||
}
|
||||
}
|
||||
if _, err := h.DB.SaveBookmarks(ctx, book); err != nil {
|
||||
if _, err := h.DB.SaveBookmarks(ctx, true, book); err != nil {
|
||||
log.Printf("error saving bookmark after downloading content: %s", err)
|
||||
}
|
||||
|
||||
// Save bookmark to database
|
||||
results, err := h.DB.SaveBookmarks(ctx, book)
|
||||
results, err := h.DB.SaveBookmarks(ctx, false, book)
|
||||
if err != nil || len(results) == 0 {
|
||||
panic(fmt.Errorf("failed to save bookmark: %v", err))
|
||||
}
|
||||
|
|
|
@ -316,18 +316,20 @@ func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
}
|
||||
|
||||
// Save bookmark to database
|
||||
results, err := h.DB.SaveBookmarks(ctx, *book)
|
||||
results, err := h.DB.SaveBookmarks(ctx, true, *book)
|
||||
if err != nil || len(results) == 0 {
|
||||
panic(fmt.Errorf("failed to save bookmark: %v", err))
|
||||
}
|
||||
|
||||
book = &results[0]
|
||||
|
||||
if payload.Async {
|
||||
go func() {
|
||||
bookmark, err := downloadBookmarkContent(&results[0], h.DataDir, r)
|
||||
bookmark, err := downloadBookmarkContent(book, h.DataDir, r)
|
||||
if err != nil {
|
||||
log.Printf("error downloading boorkmark: %s", err)
|
||||
}
|
||||
if _, err := h.DB.SaveBookmarks(context.Background(), *bookmark); err != nil {
|
||||
if _, err := h.DB.SaveBookmarks(context.Background(), false, *bookmark); err != nil {
|
||||
log.Printf("failed to save bookmark: %s", err)
|
||||
}
|
||||
}()
|
||||
|
@ -338,7 +340,7 @@ func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
if err != nil {
|
||||
log.Printf("error downloading boorkmark: %s", err)
|
||||
}
|
||||
if _, err := h.DB.SaveBookmarks(ctx, *book); err != nil {
|
||||
if _, err := h.DB.SaveBookmarks(ctx, false, *book); err != nil {
|
||||
log.Printf("failed to save bookmark: %s", err)
|
||||
}
|
||||
}
|
||||
|
@ -442,7 +444,7 @@ func (h *handler) apiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
}
|
||||
|
||||
// Update database
|
||||
res, err := h.DB.SaveBookmarks(ctx, book)
|
||||
res, err := h.DB.SaveBookmarks(ctx, false, book)
|
||||
checkError(err)
|
||||
|
||||
// Add thumbnail image to the saved bookmarks again
|
||||
|
@ -566,7 +568,7 @@ func (h *handler) apiUpdateCache(w http.ResponseWriter, r *http.Request, ps http
|
|||
close(chDone)
|
||||
|
||||
// Update database
|
||||
_, err = h.DB.SaveBookmarks(ctx, bookmarks...)
|
||||
_, err = h.DB.SaveBookmarks(ctx, false, bookmarks...)
|
||||
checkError(err)
|
||||
|
||||
// Return new saved result
|
||||
|
@ -628,7 +630,7 @@ func (h *handler) apiUpdateBookmarkTags(w http.ResponseWriter, r *http.Request,
|
|||
}
|
||||
|
||||
// Update database
|
||||
bookmarks, err = h.DB.SaveBookmarks(ctx, bookmarks...)
|
||||
bookmarks, err = h.DB.SaveBookmarks(ctx, false, bookmarks...)
|
||||
checkError(err)
|
||||
|
||||
// Get image URL for each bookmark
|
||||
|
|
Loading…
Add table
Reference in a new issue