Unverified Commit bd02de98 authored by Johnny's avatar Johnny Committed by GitHub

chore: add store tests (#5397)

parent 12f32acd
......@@ -21,11 +21,14 @@ require (
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.40.0
github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
github.com/yuin/goldmark v1.7.13
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.43.0
golang.org/x/mod v0.28.0
golang.org/x/net v0.43.0
golang.org/x/net v0.45.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.17.0
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1
......@@ -35,23 +38,64 @@ require (
require (
cel.dev/expr v0.24.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
......@@ -86,8 +130,8 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v3 v3.0.1 // indirect
......
This diff is collapsed.
......@@ -125,8 +125,11 @@ func NewSchema() Schema {
Type: FieldTypeTimestamp,
Column: Column{Table: "memo", Name: "created_ts"},
Expressions: map[DialectName]string{
DialectMySQL: "UNIX_TIMESTAMP(%s)",
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch
DialectMySQL: "UNIX_TIMESTAMP(%s)",
// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed
DialectPostgres: "%s",
DialectSQLite: "%s",
},
},
"updated_ts": {
......@@ -135,8 +138,11 @@ func NewSchema() Schema {
Type: FieldTypeTimestamp,
Column: Column{Table: "memo", Name: "updated_ts"},
Expressions: map[DialectName]string{
DialectMySQL: "UNIX_TIMESTAMP(%s)",
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
// MySQL stores updated_ts as TIMESTAMP, needs conversion to epoch
DialectMySQL: "UNIX_TIMESTAMP(%s)",
// PostgreSQL and SQLite store updated_ts as BIGINT (epoch), no conversion needed
DialectPostgres: "%s",
DialectSQLite: "%s",
},
},
"pinned": {
......@@ -267,15 +273,18 @@ func NewAttachmentSchema() Schema {
Type: FieldTypeTimestamp,
Column: Column{Table: "resource", Name: "created_ts"},
Expressions: map[DialectName]string{
DialectMySQL: "UNIX_TIMESTAMP(%s)",
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch
DialectMySQL: "UNIX_TIMESTAMP(%s)",
// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed
DialectPostgres: "%s",
DialectSQLite: "%s",
},
},
"memo": {
Name: "memo",
"memo_id": {
Name: "memo_id",
Kind: FieldKindScalar,
Type: FieldTypeString,
Column: Column{Table: "resource", Name: "memo_uid"},
Type: FieldTypeInt,
Column: Column{Table: "resource", Name: "memo_id"},
Expressions: map[DialectName]string{},
AllowedComparisonOps: map[ComparisonOperator]bool{
CompareEq: true,
......@@ -288,7 +297,7 @@ func NewAttachmentSchema() Schema {
cel.Variable("filename", cel.StringType),
cel.Variable("mime_type", cel.StringType),
cel.Variable("create_time", cel.IntType),
cel.Variable("memo", cel.StringType),
cel.Variable("memo_id", cel.IntType),
nowFunction,
}
......
......@@ -50,7 +50,7 @@ func (d *DB) Close() error {
func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
var exists bool
err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE TABLE_NAME = 'memo' AND TABLE_TYPE = 'BASE TABLE')").Scan(&exists)
err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'memo' AND TABLE_TYPE = 'BASE TABLE')").Scan(&exists)
if err != nil {
return false, errors.Wrap(err, "failed to check if database is initialized")
}
......
......@@ -93,7 +93,7 @@ func TestConvertExprToSQL(t *testing.T) {
},
{
filter: `created_ts > now() - 60 * 60 * 24`,
want: "EXTRACT(EPOCH FROM TO_TIMESTAMP(memo.created_ts)) > $1",
want: "memo.created_ts > $1",
args: []any{time.Now().Unix() - 60*60*24},
},
{
......
......@@ -49,7 +49,7 @@ func (d *DB) Close() error {
func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
var exists bool
err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'memo' AND table_type = 'BASE TABLE')").Scan(&exists)
err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_catalog = current_database() AND table_name = 'memo' AND table_type = 'BASE TABLE')").Scan(&exists)
if err != nil {
return false, errors.Wrap(err, "failed to check if database is initialized")
}
......
......@@ -32,3 +32,70 @@ func TestActivityStore(t *testing.T) {
require.Equal(t, activity, activities[0])
ts.Close()
}
func TestActivityGetByID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
activity, err := ts.CreateActivity(ctx, &store.Activity{
CreatorID: user.ID,
Type: store.ActivityTypeMemoComment,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{},
})
require.NoError(t, err)
// Get activity by ID
found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, activity.ID, found.ID)
// Get non-existent activity
nonExistentID := int32(99999)
notFound, err := ts.GetActivity(ctx, &store.FindActivity{ID: &nonExistentID})
require.NoError(t, err)
require.Nil(t, notFound)
ts.Close()
}
func TestActivityListMultiple(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create multiple activities
_, err = ts.CreateActivity(ctx, &store.Activity{
CreatorID: user.ID,
Type: store.ActivityTypeMemoComment,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{},
})
require.NoError(t, err)
_, err = ts.CreateActivity(ctx, &store.Activity{
CreatorID: user.ID,
Type: store.ActivityTypeMemoComment,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{},
})
require.NoError(t, err)
// List all activities
allActivities, err := ts.ListActivities(ctx, &store.FindActivity{})
require.NoError(t, err)
require.Equal(t, 2, len(allActivities))
// List by type
commentType := store.ActivityTypeMemoComment
commentActivities, err := ts.ListActivities(ctx, &store.FindActivity{Type: &commentType})
require.NoError(t, err)
require.Equal(t, 2, len(commentActivities))
require.Equal(t, store.ActivityTypeMemoComment, commentActivities[0].Type)
ts.Close()
}
This diff is collapsed.
......@@ -2,6 +2,7 @@ package test
import (
"context"
"fmt"
"testing"
"github.com/lithammer/shortuuid/v4"
......@@ -120,3 +121,121 @@ func TestAttachmentStoreWithFilter(t *testing.T) {
ts.Close()
}
func TestAttachmentUpdate(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
attachment, err := ts.CreateAttachment(ctx, &store.Attachment{
UID: shortuuid.New(),
CreatorID: 101,
Filename: "original.png",
Blob: []byte("test"),
Type: "image/png",
Size: 1000,
})
require.NoError(t, err)
// Update filename
newFilename := "updated.png"
err = ts.UpdateAttachment(ctx, &store.UpdateAttachment{
ID: attachment.ID,
Filename: &newFilename,
})
require.NoError(t, err)
// Verify update
found, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})
require.NoError(t, err)
require.Equal(t, newFilename, found.Filename)
ts.Close()
}
func TestAttachmentGetByUID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
uid := shortuuid.New()
_, err := ts.CreateAttachment(ctx, &store.Attachment{
UID: uid,
CreatorID: 101,
Filename: "test.png",
Blob: []byte("test"),
Type: "image/png",
Size: 1000,
})
require.NoError(t, err)
// Get by UID
found, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &uid})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, uid, found.UID)
// Get non-existent UID
nonExistentUID := "non-existent-uid"
notFound, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &nonExistentUID})
require.NoError(t, err)
require.Nil(t, notFound)
ts.Close()
}
func TestAttachmentListWithPagination(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Create 5 attachments
for i := 0; i < 5; i++ {
_, err := ts.CreateAttachment(ctx, &store.Attachment{
UID: shortuuid.New(),
CreatorID: 101,
Filename: fmt.Sprintf("test%d.png", i),
Blob: []byte("test"),
Type: "image/png",
Size: int64(1000 + i),
})
require.NoError(t, err)
}
// Test limit
limit := 3
attachments, err := ts.ListAttachments(ctx, &store.FindAttachment{
CreatorID: &[]int32{101}[0],
Limit: &limit,
})
require.NoError(t, err)
require.Equal(t, 3, len(attachments))
// Test offset
offset := 2
offsetAttachments, err := ts.ListAttachments(ctx, &store.FindAttachment{
CreatorID: &[]int32{101}[0],
Limit: &limit,
Offset: &offset,
})
require.NoError(t, err)
require.Equal(t, 3, len(offsetAttachments))
ts.Close()
}
func TestAttachmentInvalidUID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Create with invalid UID (contains spaces)
_, err := ts.CreateAttachment(ctx, &store.Attachment{
UID: "invalid uid with spaces",
CreatorID: 101,
Filename: "test.png",
Blob: []byte("test"),
Type: "image/png",
Size: 1000,
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid uid")
ts.Close()
}
package test
import (
"context"
"database/sql"
"fmt"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pkg/errors"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mysql"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
// Database drivers for connection verification.
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
)
const (
testUser = "root"
testPassword = "test"
)
var (
mysqlContainer *mysql.MySQLContainer
postgresContainer *postgres.PostgresContainer
mysqlOnce sync.Once
postgresOnce sync.Once
mysqlBaseDSN string
postgresBaseDSN string
dbCounter atomic.Int64
)
// GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test.
func GetMySQLDSN(t *testing.T) string {
ctx := context.Background()
mysqlOnce.Do(func() {
container, err := mysql.Run(ctx,
"mysql:8",
mysql.WithDatabase("init_db"),
mysql.WithUsername("root"),
mysql.WithPassword(testPassword),
testcontainers.WithEnv(map[string]string{
"MYSQL_ROOT_PASSWORD": testPassword,
}),
testcontainers.WithWaitStrategy(
wait.ForAll(
wait.ForLog("ready for connections").WithOccurrence(2),
wait.ForListeningPort("3306/tcp"),
).WithDeadline(120*time.Second),
),
)
if err != nil {
t.Fatalf("failed to start MySQL container: %v", err)
}
mysqlContainer = container
dsn, err := container.ConnectionString(ctx, "multiStatements=true")
if err != nil {
t.Fatalf("failed to get MySQL connection string: %v", err)
}
if err := waitForDB("mysql", dsn, 30*time.Second); err != nil {
t.Fatalf("MySQL not ready for connections: %v", err)
}
mysqlBaseDSN = dsn
})
if mysqlBaseDSN == "" {
t.Fatal("MySQL container failed to start in a previous test")
}
// Create a fresh database for this test
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
db, err := sql.Open("mysql", mysqlBaseDSN)
if err != nil {
t.Fatalf("failed to connect to MySQL: %v", err)
}
defer db.Close()
if _, err := db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE `%s`", dbName)); err != nil {
t.Fatalf("failed to create database %s: %v", dbName, err)
}
// Return DSN pointing to the new database
return strings.Replace(mysqlBaseDSN, "/init_db?", "/"+dbName+"?", 1)
}
// waitForDB polls the database until it's ready or timeout is reached.
func waitForDB(driver, dsn string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
var lastErr error
for {
select {
case <-ctx.Done():
if lastErr != nil {
return errors.Errorf("timeout waiting for %s database: %v", driver, lastErr)
}
return errors.Errorf("timeout waiting for %s database to be ready", driver)
case <-ticker.C:
db, err := sql.Open(driver, dsn)
if err != nil {
lastErr = err
continue
}
err = db.PingContext(ctx)
db.Close()
if err == nil {
return nil
}
lastErr = err
}
}
}
// GetPostgresDSN starts a PostgreSQL container (if not already running) and creates a fresh database for this test.
func GetPostgresDSN(t *testing.T) string {
ctx := context.Background()
postgresOnce.Do(func() {
container, err := postgres.Run(ctx,
"postgres:18",
postgres.WithDatabase("init_db"),
postgres.WithUsername(testUser),
postgres.WithPassword(testPassword),
testcontainers.WithWaitStrategy(
wait.ForAll(
wait.ForLog("database system is ready to accept connections").WithOccurrence(2),
wait.ForListeningPort("5432/tcp"),
).WithDeadline(120*time.Second),
),
)
if err != nil {
t.Fatalf("failed to start PostgreSQL container: %v", err)
}
postgresContainer = container
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
if err != nil {
t.Fatalf("failed to get PostgreSQL connection string: %v", err)
}
if err := waitForDB("postgres", dsn, 30*time.Second); err != nil {
t.Fatalf("PostgreSQL not ready for connections: %v", err)
}
postgresBaseDSN = dsn
})
if postgresBaseDSN == "" {
t.Fatal("PostgreSQL container failed to start in a previous test")
}
// Create a fresh database for this test
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
db, err := sql.Open("postgres", postgresBaseDSN)
if err != nil {
t.Fatalf("failed to connect to PostgreSQL: %v", err)
}
defer db.Close()
if _, err := db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE %s", dbName)); err != nil {
t.Fatalf("failed to create database %s: %v", dbName, err)
}
// Return DSN pointing to the new database
return strings.Replace(postgresBaseDSN, "/init_db?", "/"+dbName+"?", 1)
}
// TerminateContainers cleans up all running containers.
// This is typically called from TestMain.
func TerminateContainers() {
ctx := context.Background()
if mysqlContainer != nil {
_ = mysqlContainer.Terminate(ctx)
}
if postgresContainer != nil {
_ = postgresContainer.Terminate(ctx)
}
}
package test
import (
"context"
"strconv"
"testing"
"github.com/lithammer/shortuuid/v4"
"github.com/stretchr/testify/require"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
// =============================================================================
// Formatting Helpers
// =============================================================================
func formatInt64(n int64) string {
return strconv.FormatInt(n, 10)
}
func formatInt32(n int32) string {
return strconv.FormatInt(int64(n), 10)
}
func formatInt(n int) string {
return strconv.Itoa(n)
}
// =============================================================================
// Pointer Helpers
// =============================================================================
func boolPtr(b bool) *bool {
return &b
}
// =============================================================================
// Test Fixture Builders
// =============================================================================
// MemoBuilder provides a fluent API for creating test memos.
type MemoBuilder struct {
memo *store.Memo
}
// NewMemoBuilder creates a new memo builder with required fields.
func NewMemoBuilder(uid string, creatorID int32) *MemoBuilder {
return &MemoBuilder{
memo: &store.Memo{
UID: uid,
CreatorID: creatorID,
Visibility: store.Public,
},
}
}
func (b *MemoBuilder) Content(content string) *MemoBuilder {
b.memo.Content = content
return b
}
func (b *MemoBuilder) Visibility(v store.Visibility) *MemoBuilder {
b.memo.Visibility = v
return b
}
func (b *MemoBuilder) Tags(tags ...string) *MemoBuilder {
if b.memo.Payload == nil {
b.memo.Payload = &storepb.MemoPayload{}
}
b.memo.Payload.Tags = tags
return b
}
func (b *MemoBuilder) Property(fn func(*storepb.MemoPayload_Property)) *MemoBuilder {
if b.memo.Payload == nil {
b.memo.Payload = &storepb.MemoPayload{}
}
if b.memo.Payload.Property == nil {
b.memo.Payload.Property = &storepb.MemoPayload_Property{}
}
fn(b.memo.Payload.Property)
return b
}
func (b *MemoBuilder) Build() *store.Memo {
return b.memo
}
// AttachmentBuilder provides a fluent API for creating test attachments.
type AttachmentBuilder struct {
attachment *store.Attachment
}
// NewAttachmentBuilder creates a new attachment builder with required fields.
func NewAttachmentBuilder(creatorID int32) *AttachmentBuilder {
return &AttachmentBuilder{
attachment: &store.Attachment{
UID: shortuuid.New(),
CreatorID: creatorID,
Blob: []byte("test"),
Size: 1000,
},
}
}
func (b *AttachmentBuilder) Filename(filename string) *AttachmentBuilder {
b.attachment.Filename = filename
return b
}
func (b *AttachmentBuilder) MimeType(mimeType string) *AttachmentBuilder {
b.attachment.Type = mimeType
return b
}
func (b *AttachmentBuilder) MemoID(memoID *int32) *AttachmentBuilder {
b.attachment.MemoID = memoID
return b
}
func (b *AttachmentBuilder) Size(size int64) *AttachmentBuilder {
b.attachment.Size = size
return b
}
func (b *AttachmentBuilder) Build() *store.Attachment {
return b.attachment
}
// =============================================================================
// Test Context Helpers
// =============================================================================
// MemoFilterTestContext holds common test dependencies for memo filter tests.
type MemoFilterTestContext struct {
Ctx context.Context
T *testing.T
Store *store.Store
User *store.User
}
// NewMemoFilterTestContext creates a new test context with store and user.
func NewMemoFilterTestContext(t *testing.T) *MemoFilterTestContext {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
return &MemoFilterTestContext{
Ctx: ctx,
T: t,
Store: ts,
User: user,
}
}
// CreateMemo creates a memo using the builder pattern.
func (tc *MemoFilterTestContext) CreateMemo(b *MemoBuilder) *store.Memo {
memo, err := tc.Store.CreateMemo(tc.Ctx, b.Build())
require.NoError(tc.T, err)
return memo
}
// PinMemo pins a memo by ID.
func (tc *MemoFilterTestContext) PinMemo(memoID int32) {
err := tc.Store.UpdateMemo(tc.Ctx, &store.UpdateMemo{
ID: memoID,
Pinned: boolPtr(true),
})
require.NoError(tc.T, err)
}
// ListWithFilter lists memos with the given filter and returns the count.
func (tc *MemoFilterTestContext) ListWithFilter(filter string) []*store.Memo {
memos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{
Filters: []string{filter},
})
require.NoError(tc.T, err)
return memos
}
// ListWithFilters lists memos with multiple filters and returns the count.
func (tc *MemoFilterTestContext) ListWithFilters(filters ...string) []*store.Memo {
memos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{
Filters: filters,
})
require.NoError(tc.T, err)
return memos
}
// Close closes the test store.
func (tc *MemoFilterTestContext) Close() {
tc.Store.Close()
}
// AttachmentFilterTestContext holds common test dependencies for attachment filter tests.
type AttachmentFilterTestContext struct {
Ctx context.Context
T *testing.T
Store *store.Store
User *store.User
CreatorID int32
}
// NewAttachmentFilterTestContext creates a new test context for attachments.
func NewAttachmentFilterTestContext(t *testing.T) *AttachmentFilterTestContext {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
return &AttachmentFilterTestContext{
Ctx: ctx,
T: t,
Store: ts,
CreatorID: 101,
}
}
// NewAttachmentFilterTestContextWithUser creates a new test context with a user.
func NewAttachmentFilterTestContextWithUser(t *testing.T) *AttachmentFilterTestContext {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
return &AttachmentFilterTestContext{
Ctx: ctx,
T: t,
Store: ts,
User: user,
CreatorID: user.ID,
}
}
// CreateAttachment creates an attachment using the builder pattern.
func (tc *AttachmentFilterTestContext) CreateAttachment(b *AttachmentBuilder) *store.Attachment {
attachment, err := tc.Store.CreateAttachment(tc.Ctx, b.Build())
require.NoError(tc.T, err)
return attachment
}
// CreateMemo creates a memo (for attachment tests that need memos).
func (tc *AttachmentFilterTestContext) CreateMemo(uid, content string) *store.Memo {
memo, err := tc.Store.CreateMemo(tc.Ctx, &store.Memo{
UID: uid,
CreatorID: tc.CreatorID,
Content: content,
Visibility: store.Public,
})
require.NoError(tc.T, err)
return memo
}
// ListWithFilter lists attachments with the given filter.
func (tc *AttachmentFilterTestContext) ListWithFilter(filter string) []*store.Attachment {
attachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{
CreatorID: &tc.CreatorID,
Filters: []string{filter},
})
require.NoError(tc.T, err)
return attachments
}
// ListWithFilters lists attachments with multiple filters.
func (tc *AttachmentFilterTestContext) ListWithFilters(filters ...string) []*store.Attachment {
attachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{
CreatorID: &tc.CreatorID,
Filters: filters,
})
require.NoError(tc.T, err)
return attachments
}
// Close closes the test store.
func (tc *AttachmentFilterTestContext) Close() {
tc.Store.Close()
}
// =============================================================================
// Filter Test Case Definition
// =============================================================================
// FilterTestCase defines a single filter test case for table-driven tests.
type FilterTestCase struct {
Name string
Filter string
ExpectedCount int
}
......@@ -29,3 +29,220 @@ func TestInstanceSettingV1Store(t *testing.T) {
require.Equal(t, instanceSetting, setting)
ts.Close()
}
func TestInstanceSettingGetNonExistent(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Get non-existent setting
setting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{
Name: storepb.InstanceSettingKey_STORAGE.String(),
})
require.NoError(t, err)
require.Nil(t, setting)
ts.Close()
}
func TestInstanceSettingUpsertUpdate(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Create setting
_, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_GENERAL,
Value: &storepb.InstanceSetting_GeneralSetting{
GeneralSetting: &storepb.InstanceGeneralSetting{
AdditionalScript: "console.log('v1')",
},
},
})
require.NoError(t, err)
// Update setting
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_GENERAL,
Value: &storepb.InstanceSetting_GeneralSetting{
GeneralSetting: &storepb.InstanceGeneralSetting{
AdditionalScript: "console.log('v2')",
},
},
})
require.NoError(t, err)
// Verify update
setting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{
Name: storepb.InstanceSettingKey_GENERAL.String(),
})
require.NoError(t, err)
require.Equal(t, "console.log('v2')", setting.GetGeneralSetting().AdditionalScript)
// Verify only one setting exists
list, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{
Name: storepb.InstanceSettingKey_GENERAL.String(),
})
require.NoError(t, err)
require.Equal(t, 1, len(list))
ts.Close()
}
func TestInstanceSettingBasicSetting(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Get default basic setting (should return empty defaults)
basicSetting, err := ts.GetInstanceBasicSetting(ctx)
require.NoError(t, err)
require.NotNil(t, basicSetting)
// Set basic setting
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_BASIC,
Value: &storepb.InstanceSetting_BasicSetting{
BasicSetting: &storepb.InstanceBasicSetting{
SecretKey: "my-secret-key",
},
},
})
require.NoError(t, err)
// Verify
basicSetting, err = ts.GetInstanceBasicSetting(ctx)
require.NoError(t, err)
require.Equal(t, "my-secret-key", basicSetting.SecretKey)
ts.Close()
}
func TestInstanceSettingGeneralSetting(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Get default general setting
generalSetting, err := ts.GetInstanceGeneralSetting(ctx)
require.NoError(t, err)
require.NotNil(t, generalSetting)
// Set general setting
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_GENERAL,
Value: &storepb.InstanceSetting_GeneralSetting{
GeneralSetting: &storepb.InstanceGeneralSetting{
AdditionalScript: "console.log('test')",
AdditionalStyle: "body { color: red; }",
},
},
})
require.NoError(t, err)
// Verify
generalSetting, err = ts.GetInstanceGeneralSetting(ctx)
require.NoError(t, err)
require.Equal(t, "console.log('test')", generalSetting.AdditionalScript)
require.Equal(t, "body { color: red; }", generalSetting.AdditionalStyle)
ts.Close()
}
func TestInstanceSettingMemoRelatedSetting(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Get default memo related setting (should have defaults)
memoSetting, err := ts.GetInstanceMemoRelatedSetting(ctx)
require.NoError(t, err)
require.NotNil(t, memoSetting)
require.GreaterOrEqual(t, memoSetting.ContentLengthLimit, int32(store.DefaultContentLengthLimit))
require.NotEmpty(t, memoSetting.Reactions)
// Set custom memo related setting
customReactions := []string{"👍", "👎", "🚀"}
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_MEMO_RELATED,
Value: &storepb.InstanceSetting_MemoRelatedSetting{
MemoRelatedSetting: &storepb.InstanceMemoRelatedSetting{
ContentLengthLimit: 16384,
Reactions: customReactions,
},
},
})
require.NoError(t, err)
// Verify
memoSetting, err = ts.GetInstanceMemoRelatedSetting(ctx)
require.NoError(t, err)
require.Equal(t, int32(16384), memoSetting.ContentLengthLimit)
require.Equal(t, customReactions, memoSetting.Reactions)
ts.Close()
}
func TestInstanceSettingStorageSetting(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Get default storage setting (should have defaults)
storageSetting, err := ts.GetInstanceStorageSetting(ctx)
require.NoError(t, err)
require.NotNil(t, storageSetting)
require.Equal(t, storepb.InstanceStorageSetting_DATABASE, storageSetting.StorageType)
require.Equal(t, int64(30), storageSetting.UploadSizeLimitMb)
require.Equal(t, "assets/{timestamp}_{filename}", storageSetting.FilepathTemplate)
// Set custom storage setting
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_STORAGE,
Value: &storepb.InstanceSetting_StorageSetting{
StorageSetting: &storepb.InstanceStorageSetting{
StorageType: storepb.InstanceStorageSetting_LOCAL,
UploadSizeLimitMb: 100,
FilepathTemplate: "uploads/{date}/{filename}",
},
},
})
require.NoError(t, err)
// Verify
storageSetting, err = ts.GetInstanceStorageSetting(ctx)
require.NoError(t, err)
require.Equal(t, storepb.InstanceStorageSetting_LOCAL, storageSetting.StorageType)
require.Equal(t, int64(100), storageSetting.UploadSizeLimitMb)
require.Equal(t, "uploads/{date}/{filename}", storageSetting.FilepathTemplate)
ts.Close()
}
func TestInstanceSettingListAll(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Count initial settings
initialList, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{})
require.NoError(t, err)
initialCount := len(initialList)
// Create multiple settings
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_GENERAL,
Value: &storepb.InstanceSetting_GeneralSetting{
GeneralSetting: &storepb.InstanceGeneralSetting{},
},
})
require.NoError(t, err)
_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
Key: storepb.InstanceSettingKey_STORAGE,
Value: &storepb.InstanceSetting_StorageSetting{
StorageSetting: &storepb.InstanceStorageSetting{},
},
})
require.NoError(t, err)
// List all - should have 2 more than initial
list, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{})
require.NoError(t, err)
require.Equal(t, initialCount+2, len(list))
ts.Close()
}
package test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestMain(m *testing.M) {
// If DRIVER is set, run tests for that driver only
if os.Getenv("DRIVER") != "" {
defer TerminateContainers()
m.Run()
return
}
// No DRIVER set - run tests for all drivers sequentially
runAllDrivers()
}
func runAllDrivers() {
drivers := []string{"sqlite", "mysql", "postgres"}
_, currentFile, _, _ := runtime.Caller(0)
projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(currentFile)))
var failed []string
for _, driver := range drivers {
fmt.Printf("\n==================== %s ====================\n\n", driver)
cmd := exec.Command("go", "test", "-v", "-count=1", "./store/test/...")
cmd.Dir = projectRoot
cmd.Env = append(os.Environ(), "DRIVER="+driver)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
failed = append(failed, driver)
}
}
fmt.Println()
if len(failed) > 0 {
fmt.Printf("FAIL: %v\n", failed)
panic("some drivers failed")
}
fmt.Println("PASS: all drivers")
}
This diff is collapsed.
......@@ -60,3 +60,182 @@ func TestMemoRelationStore(t *testing.T) {
require.NoError(t, err)
ts.Close()
}
func TestMemoRelationListByMemoID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create main memo
mainMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "main-memo",
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
})
require.NoError(t, err)
// Create related memos
relatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{
UID: "related-memo-1",
CreatorID: user.ID,
Content: "related memo 1 content",
Visibility: store.Public,
})
require.NoError(t, err)
relatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{
UID: "related-memo-2",
CreatorID: user.ID,
Content: "related memo 2 content",
Visibility: store.Public,
})
require.NoError(t, err)
// Create relations
_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: mainMemo.ID,
RelatedMemoID: relatedMemo1.ID,
Type: store.MemoRelationReference,
})
require.NoError(t, err)
_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: mainMemo.ID,
RelatedMemoID: relatedMemo2.ID,
Type: store.MemoRelationComment,
})
require.NoError(t, err)
// List by memo ID
relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &mainMemo.ID,
})
require.NoError(t, err)
require.Equal(t, 2, len(relations))
// List by type
refType := store.MemoRelationReference
refRelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &mainMemo.ID,
Type: &refType,
})
require.NoError(t, err)
require.Equal(t, 1, len(refRelations))
require.Equal(t, store.MemoRelationReference, refRelations[0].Type)
// List by related memo ID
relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{
RelatedMemoID: &relatedMemo1.ID,
})
require.NoError(t, err)
require.Equal(t, 1, len(relations))
ts.Close()
}
func TestMemoRelationDelete(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create memos
mainMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "main-memo",
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
})
require.NoError(t, err)
relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "related-memo",
CreatorID: user.ID,
Content: "related memo content",
Visibility: store.Public,
})
require.NoError(t, err)
// Create relation
_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: mainMemo.ID,
RelatedMemoID: relatedMemo.ID,
Type: store.MemoRelationReference,
})
require.NoError(t, err)
// Verify relation exists
relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &mainMemo.ID,
})
require.NoError(t, err)
require.Equal(t, 1, len(relations))
// Delete relation by memo ID
relType := store.MemoRelationReference
err = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
MemoID: &mainMemo.ID,
RelatedMemoID: &relatedMemo.ID,
Type: &relType,
})
require.NoError(t, err)
// Verify relation is deleted
relations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &mainMemo.ID,
})
require.NoError(t, err)
require.Equal(t, 0, len(relations))
ts.Close()
}
func TestMemoRelationDifferentTypes(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
mainMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "main-memo",
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
})
require.NoError(t, err)
relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "related-memo",
CreatorID: user.ID,
Content: "related memo content",
Visibility: store.Public,
})
require.NoError(t, err)
// Create reference relation
_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: mainMemo.ID,
RelatedMemoID: relatedMemo.ID,
Type: store.MemoRelationReference,
})
require.NoError(t, err)
// Create comment relation (same memos, different type - should be allowed)
_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: mainMemo.ID,
RelatedMemoID: relatedMemo.ID,
Type: store.MemoRelationComment,
})
require.NoError(t, err)
// Verify both relations exist
relations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &mainMemo.ID,
})
require.NoError(t, err)
require.Equal(t, 2, len(relations))
ts.Close()
}
......@@ -2,6 +2,7 @@ package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
......@@ -114,3 +115,294 @@ func TestDeleteMemoStore(t *testing.T) {
require.NoError(t, err)
ts.Close()
}
func TestMemoGetByID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "test-memo-1",
CreatorID: user.ID,
Content: "test content",
Visibility: store.Public,
})
require.NoError(t, err)
// Get by ID
found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, memo.ID, found.ID)
require.Equal(t, memo.Content, found.Content)
// Get non-existent
nonExistentID := int32(99999)
notFound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &nonExistentID})
require.NoError(t, err)
require.Nil(t, notFound)
ts.Close()
}
func TestMemoGetByUID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
uid := "unique-memo-uid"
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: uid,
CreatorID: user.ID,
Content: "test content",
Visibility: store.Public,
})
require.NoError(t, err)
// Get by UID
found, err := ts.GetMemo(ctx, &store.FindMemo{UID: &uid})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, memo.UID, found.UID)
// Get non-existent UID
nonExistentUID := "non-existent-uid"
notFound, err := ts.GetMemo(ctx, &store.FindMemo{UID: &nonExistentUID})
require.NoError(t, err)
require.Nil(t, notFound)
ts.Close()
}
func TestMemoListByVisibility(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create memos with different visibilities
_, err = ts.CreateMemo(ctx, &store.Memo{
UID: "public-memo",
CreatorID: user.ID,
Content: "public content",
Visibility: store.Public,
})
require.NoError(t, err)
_, err = ts.CreateMemo(ctx, &store.Memo{
UID: "protected-memo",
CreatorID: user.ID,
Content: "protected content",
Visibility: store.Protected,
})
require.NoError(t, err)
_, err = ts.CreateMemo(ctx, &store.Memo{
UID: "private-memo",
CreatorID: user.ID,
Content: "private content",
Visibility: store.Private,
})
require.NoError(t, err)
// List public memos only
publicMemos, err := ts.ListMemos(ctx, &store.FindMemo{
VisibilityList: []store.Visibility{store.Public},
})
require.NoError(t, err)
require.Equal(t, 1, len(publicMemos))
require.Equal(t, store.Public, publicMemos[0].Visibility)
// List protected memos only
protectedMemos, err := ts.ListMemos(ctx, &store.FindMemo{
VisibilityList: []store.Visibility{store.Protected},
})
require.NoError(t, err)
require.Equal(t, 1, len(protectedMemos))
require.Equal(t, store.Protected, protectedMemos[0].Visibility)
// List public and protected (multiple visibility)
publicAndProtected, err := ts.ListMemos(ctx, &store.FindMemo{
VisibilityList: []store.Visibility{store.Public, store.Protected},
})
require.NoError(t, err)
require.Equal(t, 2, len(publicAndProtected))
// List all
allMemos, err := ts.ListMemos(ctx, &store.FindMemo{})
require.NoError(t, err)
require.Equal(t, 3, len(allMemos))
ts.Close()
}
func TestMemoListWithPagination(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create 10 memos
for i := 0; i < 10; i++ {
_, err := ts.CreateMemo(ctx, &store.Memo{
UID: fmt.Sprintf("memo-%d", i),
CreatorID: user.ID,
Content: fmt.Sprintf("content %d", i),
Visibility: store.Public,
})
require.NoError(t, err)
}
// Test limit
limit := 5
limitedMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit})
require.NoError(t, err)
require.Equal(t, 5, len(limitedMemos))
// Test offset
offset := 3
offsetMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit, Offset: &offset})
require.NoError(t, err)
require.Equal(t, 5, len(offsetMemos))
// Verify offset works correctly (different memos)
require.NotEqual(t, limitedMemos[0].ID, offsetMemos[0].ID)
ts.Close()
}
func TestMemoUpdatePinned(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "pinnable-memo",
CreatorID: user.ID,
Content: "content",
Visibility: store.Public,
})
require.NoError(t, err)
require.False(t, memo.Pinned)
// Pin the memo
pinned := true
err = ts.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Pinned: &pinned,
})
require.NoError(t, err)
// Verify pinned
found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.True(t, found.Pinned)
// Unpin
unpinned := false
err = ts.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Pinned: &unpinned,
})
require.NoError(t, err)
found, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.False(t, found.Pinned)
ts.Close()
}
func TestMemoUpdateVisibility(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "visibility-memo",
CreatorID: user.ID,
Content: "content",
Visibility: store.Public,
})
require.NoError(t, err)
require.Equal(t, store.Public, memo.Visibility)
// Change to private
privateVisibility := store.Private
err = ts.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Visibility: &privateVisibility,
})
require.NoError(t, err)
found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.Equal(t, store.Private, found.Visibility)
// Change to protected
protectedVisibility := store.Protected
err = ts.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Visibility: &protectedVisibility,
})
require.NoError(t, err)
found, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.Equal(t, store.Protected, found.Visibility)
ts.Close()
}
func TestMemoInvalidUID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create memo with invalid UID (contains special characters)
_, err = ts.CreateMemo(ctx, &store.Memo{
UID: "invalid uid with spaces",
CreatorID: user.ID,
Content: "content",
Visibility: store.Public,
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid uid")
ts.Close()
}
func TestMemoWithPayload(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create memo with tags in payload
tags := []string{"tag1", "tag2", "tag3"}
memo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "memo-with-payload",
CreatorID: user.ID,
Content: "content with tags",
Visibility: store.Public,
Payload: &storepb.MemoPayload{
Tags: tags,
},
})
require.NoError(t, err)
require.NotNil(t, memo.Payload)
require.Equal(t, tags, memo.Payload.Tags)
// Fetch and verify
found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.NotNil(t, found.Payload)
require.Equal(t, tags, found.Payload.Tags)
ts.Close()
}
......@@ -65,3 +65,121 @@ func TestReactionStore(t *testing.T) {
ts.Close()
}
func TestReactionListByCreatorID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user1, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser)
require.NoError(t, err)
contentID := "shared_content"
// User 1 creates reaction
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user1.ID,
ContentID: contentID,
ReactionType: "👍",
})
require.NoError(t, err)
// User 2 creates reaction
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user2.ID,
ContentID: contentID,
ReactionType: "❤️",
})
require.NoError(t, err)
// List all reactions for content
reactions, err := ts.ListReactions(ctx, &store.FindReaction{
ContentID: &contentID,
})
require.NoError(t, err)
require.Len(t, reactions, 2)
// List by creator ID
user1Reactions, err := ts.ListReactions(ctx, &store.FindReaction{
CreatorID: &user1.ID,
})
require.NoError(t, err)
require.Len(t, user1Reactions, 1)
require.Equal(t, "👍", user1Reactions[0].ReactionType)
ts.Close()
}
func TestReactionMultipleContentIDs(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
contentID1 := "content_1"
contentID2 := "content_2"
// Create reactions for different contents
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user.ID,
ContentID: contentID1,
ReactionType: "👍",
})
require.NoError(t, err)
_, err = ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user.ID,
ContentID: contentID2,
ReactionType: "❤️",
})
require.NoError(t, err)
// List by content ID list
reactions, err := ts.ListReactions(ctx, &store.FindReaction{
ContentIDList: []string{contentID1, contentID2},
})
require.NoError(t, err)
require.Len(t, reactions, 2)
ts.Close()
}
func TestReactionUpsertDifferentTypes(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
contentID := "test_content"
// Create first reaction
reaction1, err := ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user.ID,
ContentID: contentID,
ReactionType: "👍",
})
require.NoError(t, err)
// Create second reaction with different type (should create new, not update)
reaction2, err := ts.UpsertReaction(ctx, &store.Reaction{
CreatorID: user.ID,
ContentID: contentID,
ReactionType: "❤️",
})
require.NoError(t, err)
// Both reactions should exist
require.NotEqual(t, reaction1.ID, reaction2.ID)
reactions, err := ts.ListReactions(ctx, &store.FindReaction{
ContentID: &contentID,
})
require.NoError(t, err)
require.Len(t, reactions, 2)
ts.Close()
}
......@@ -3,7 +3,6 @@ package test
import (
"context"
"fmt"
"log/slog"
"net"
"os"
"testing"
......@@ -19,63 +18,25 @@ import (
"github.com/usememos/memos/store/db"
)
// NewTestingStore creates a new testing store with a fresh database.
// Each test gets its own isolated database:
// - SQLite: new temp file per test
// - MySQL/PostgreSQL: new database per test in shared container
func NewTestingStore(ctx context.Context, t *testing.T) *store.Store {
profile := getTestingProfile(t)
driver := getDriverFromEnv()
profile := getTestingProfileForDriver(t, driver)
dbDriver, err := db.NewDBDriver(profile)
if err != nil {
slog.Error("failed to create db driver", slog.String("error", err.Error()))
t.Fatalf("failed to create db driver: %v", err)
}
resetTestingDB(ctx, profile, dbDriver)
store := store.New(dbDriver, profile)
if err := store.Migrate(ctx); err != nil {
slog.Error("failed to migrate db", slog.String("error", err.Error()))
t.Fatalf("failed to migrate db: %v", err)
}
return store
}
func resetTestingDB(ctx context.Context, profile *profile.Profile, dbDriver store.Driver) {
if profile.Driver == "mysql" {
_, err := dbDriver.GetDB().ExecContext(ctx, `
DROP TABLE IF EXISTS system_setting;
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS user_setting;
DROP TABLE IF EXISTS memo;
DROP TABLE IF EXISTS memo_organizer;
DROP TABLE IF EXISTS memo_relation;
DROP TABLE IF EXISTS resource;
DROP TABLE IF EXISTS tag;
DROP TABLE IF EXISTS activity;
DROP TABLE IF EXISTS storage;
DROP TABLE IF EXISTS idp;
DROP TABLE IF EXISTS inbox;
DROP TABLE IF EXISTS reaction;`)
if err != nil {
slog.Error("failed to reset testing db", slog.String("error", err.Error()))
panic(err)
}
} else if profile.Driver == "postgres" {
_, err := dbDriver.GetDB().ExecContext(ctx, `
DROP TABLE IF EXISTS system_setting CASCADE;
DROP TABLE IF EXISTS "user" CASCADE;
DROP TABLE IF EXISTS user_setting CASCADE;
DROP TABLE IF EXISTS memo CASCADE;
DROP TABLE IF EXISTS memo_organizer CASCADE;
DROP TABLE IF EXISTS memo_relation CASCADE;
DROP TABLE IF EXISTS resource CASCADE;
DROP TABLE IF EXISTS tag CASCADE;
DROP TABLE IF EXISTS activity CASCADE;
DROP TABLE IF EXISTS storage CASCADE;
DROP TABLE IF EXISTS idp CASCADE;
DROP TABLE IF EXISTS inbox CASCADE;
DROP TABLE IF EXISTS reaction CASCADE;`)
if err != nil {
slog.Error("failed to reset testing db", slog.String("error", err.Error()))
panic(err)
}
}
}
func getUnusedPort() int {
// Get a random unused port
listener, err := net.Listen("tcp", "localhost:0")
......@@ -89,20 +50,28 @@ func getUnusedPort() int {
return port
}
func getTestingProfile(t *testing.T) *profile.Profile {
if err := godotenv.Load(".env"); err != nil {
t.Log("failed to load .env file, but it's ok")
}
// getTestingProfileForDriver creates a testing profile for a specific driver.
func getTestingProfileForDriver(t *testing.T, driver string) *profile.Profile {
// Attempt to load .env file if present (optional, for local development)
_ = godotenv.Load(".env")
// Get a temporary directory for the test data.
dir := t.TempDir()
mode := "prod"
port := getUnusedPort()
driver := getDriverFromEnv()
dsn := os.Getenv("DSN")
if driver == "sqlite" {
var dsn string
switch driver {
case "sqlite":
dsn = fmt.Sprintf("%s/memos_%s.db", dir, mode)
case "mysql":
dsn = GetMySQLDSN(t)
case "postgres":
dsn = GetPostgresDSN(t)
default:
t.Fatalf("unsupported driver: %s", driver)
}
return &profile.Profile{
Mode: mode,
Port: port,
......
......@@ -26,3 +26,285 @@ func TestUserSettingStore(t *testing.T) {
require.Equal(t, 1, len(list))
ts.Close()
}
func TestUserSettingGetByUserID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create setting
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_GENERAL,
Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "zh"}},
})
require.NoError(t, err)
// Get by user ID
setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_GENERAL,
})
require.NoError(t, err)
require.NotNil(t, setting)
require.Equal(t, "zh", setting.GetGeneral().Locale)
// Get non-existent key
nonExistentSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_SHORTCUTS,
})
require.NoError(t, err)
require.Nil(t, nonExistentSetting)
ts.Close()
}
func TestUserSettingUpsertUpdate(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create initial setting
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_GENERAL,
Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "en"}},
})
require.NoError(t, err)
// Update setting
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_GENERAL,
Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "fr"}},
})
require.NoError(t, err)
// Verify update
setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_GENERAL,
})
require.NoError(t, err)
require.Equal(t, "fr", setting.GetGeneral().Locale)
// Verify only one setting exists
list, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID})
require.NoError(t, err)
require.Equal(t, 1, len(list))
ts.Close()
}
func TestUserSettingRefreshTokens(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Initially no tokens
tokens, err := ts.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
require.Empty(t, tokens)
// Add a refresh token
token1 := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: "token-1",
Description: "Chrome browser session",
}
err = ts.AddUserRefreshToken(ctx, user.ID, token1)
require.NoError(t, err)
// Verify token was added
tokens, err = ts.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, tokens, 1)
require.Equal(t, "token-1", tokens[0].TokenId)
// Add another token
token2 := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: "token-2",
Description: "Firefox browser session",
}
err = ts.AddUserRefreshToken(ctx, user.ID, token2)
require.NoError(t, err)
tokens, err = ts.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, tokens, 2)
// Get specific token by ID
foundToken, err := ts.GetUserRefreshTokenByID(ctx, user.ID, "token-1")
require.NoError(t, err)
require.NotNil(t, foundToken)
require.Equal(t, "Chrome browser session", foundToken.Description)
// Get non-existent token
notFound, err := ts.GetUserRefreshTokenByID(ctx, user.ID, "non-existent")
require.NoError(t, err)
require.Nil(t, notFound)
// Remove token
err = ts.RemoveUserRefreshToken(ctx, user.ID, "token-1")
require.NoError(t, err)
tokens, err = ts.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, tokens, 1)
require.Equal(t, "token-2", tokens[0].TokenId)
ts.Close()
}
func TestUserSettingPersonalAccessTokens(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Initially no PATs
pats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
require.Empty(t, pats)
// Add a PAT
pat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-1",
TokenHash: "pat-hash-1",
Description: "API Token for external access",
}
err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat1)
require.NoError(t, err)
// Verify PAT was added
pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, pats, 1)
require.Equal(t, "API Token for external access", pats[0].Description)
// Add another PAT
pat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-2",
TokenHash: "pat-hash-2",
Description: "CI Token",
}
err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat2)
require.NoError(t, err)
pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, pats, 2)
// Remove PAT
err = ts.RemoveUserPersonalAccessToken(ctx, user.ID, "pat-1")
require.NoError(t, err)
pats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, pats, 1)
require.Equal(t, "pat-2", pats[0].TokenId)
ts.Close()
}
func TestUserSettingWebhooks(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Initially no webhooks
webhooks, err := ts.GetUserWebhooks(ctx, user.ID)
require.NoError(t, err)
require.Empty(t, webhooks)
// Add a webhook
webhook1 := &storepb.WebhooksUserSetting_Webhook{
Id: "webhook-1",
Title: "Deploy Hook",
Url: "https://example.com/webhook",
}
err = ts.AddUserWebhook(ctx, user.ID, webhook1)
require.NoError(t, err)
// Verify webhook was added
webhooks, err = ts.GetUserWebhooks(ctx, user.ID)
require.NoError(t, err)
require.Len(t, webhooks, 1)
require.Equal(t, "Deploy Hook", webhooks[0].Title)
// Update webhook
webhook1Updated := &storepb.WebhooksUserSetting_Webhook{
Id: "webhook-1",
Title: "Updated Deploy Hook",
Url: "https://example.com/webhook/v2",
}
err = ts.UpdateUserWebhook(ctx, user.ID, webhook1Updated)
require.NoError(t, err)
webhooks, err = ts.GetUserWebhooks(ctx, user.ID)
require.NoError(t, err)
require.Len(t, webhooks, 1)
require.Equal(t, "Updated Deploy Hook", webhooks[0].Title)
require.Equal(t, "https://example.com/webhook/v2", webhooks[0].Url)
// Add another webhook
webhook2 := &storepb.WebhooksUserSetting_Webhook{
Id: "webhook-2",
Title: "Notification Hook",
Url: "https://slack.example.com/webhook",
}
err = ts.AddUserWebhook(ctx, user.ID, webhook2)
require.NoError(t, err)
webhooks, err = ts.GetUserWebhooks(ctx, user.ID)
require.NoError(t, err)
require.Len(t, webhooks, 2)
// Remove webhook
err = ts.RemoveUserWebhook(ctx, user.ID, "webhook-1")
require.NoError(t, err)
webhooks, err = ts.GetUserWebhooks(ctx, user.ID)
require.NoError(t, err)
require.Len(t, webhooks, 1)
require.Equal(t, "webhook-2", webhooks[0].Id)
ts.Close()
}
func TestUserSettingShortcuts(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create shortcuts setting
shortcuts := &storepb.ShortcutsUserSetting{
Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{
{Id: "shortcut-1", Title: "Work Notes", Filter: "tag:work"},
{Id: "shortcut-2", Title: "Personal", Filter: "tag:personal"},
},
}
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_SHORTCUTS,
Value: &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},
})
require.NoError(t, err)
// Retrieve and verify
setting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_SHORTCUTS,
})
require.NoError(t, err)
require.NotNil(t, setting)
require.Len(t, setting.GetShortcuts().Shortcuts, 2)
require.Equal(t, "Work Notes", setting.GetShortcuts().Shortcuts[0].Title)
ts.Close()
}
......@@ -2,6 +2,7 @@ package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
......@@ -38,13 +39,213 @@ func TestUserStore(t *testing.T) {
ts.Close()
}
func TestUserGetByID(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Get user by ID
found, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, user.ID, found.ID)
require.Equal(t, user.Username, found.Username)
// Get non-existent user
nonExistentID := int32(99999)
notFound, err := ts.GetUser(ctx, &store.FindUser{ID: &nonExistentID})
require.NoError(t, err)
require.Nil(t, notFound)
// Get system bot
systemBotID := store.SystemBotID
systemBot, err := ts.GetUser(ctx, &store.FindUser{ID: &systemBotID})
require.NoError(t, err)
require.NotNil(t, systemBot)
require.Equal(t, store.SystemBotID, systemBot.ID)
require.Equal(t, "system_bot", systemBot.Username)
ts.Close()
}
func TestUserGetByUsername(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Get user by username
found, err := ts.GetUser(ctx, &store.FindUser{Username: &user.Username})
require.NoError(t, err)
require.NotNil(t, found)
require.Equal(t, user.Username, found.Username)
// Get non-existent username
nonExistent := "nonexistent"
notFound, err := ts.GetUser(ctx, &store.FindUser{Username: &nonExistent})
require.NoError(t, err)
require.Nil(t, notFound)
ts.Close()
}
func TestUserListByRole(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Create users with different roles
_, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
adminUser, err := createTestingUserWithRole(ctx, ts, "admin_user", store.RoleAdmin)
require.NoError(t, err)
regularUser, err := createTestingUserWithRole(ctx, ts, "regular_user", store.RoleUser)
require.NoError(t, err)
// List all users
allUsers, err := ts.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err)
require.Equal(t, 3, len(allUsers))
// List only HOST users
hostRole := store.RoleHost
hostUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &hostRole})
require.NoError(t, err)
require.Equal(t, 1, len(hostUsers))
require.Equal(t, store.RoleHost, hostUsers[0].Role)
// List only ADMIN users
adminRole := store.RoleAdmin
adminUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &adminRole})
require.NoError(t, err)
require.Equal(t, 1, len(adminUsers))
require.Equal(t, adminUser.ID, adminUsers[0].ID)
// List only USER role users
userRole := store.RoleUser
regularUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &userRole})
require.NoError(t, err)
require.Equal(t, 1, len(regularUsers))
require.Equal(t, regularUser.ID, regularUsers[0].ID)
ts.Close()
}
func TestUserUpdateRowStatus(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
require.Equal(t, store.Normal, user.RowStatus)
// Archive user
archivedStatus := store.Archived
updated, err := ts.UpdateUser(ctx, &store.UpdateUser{
ID: user.ID,
RowStatus: &archivedStatus,
})
require.NoError(t, err)
require.Equal(t, store.Archived, updated.RowStatus)
// Verify by fetching
fetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})
require.NoError(t, err)
require.Equal(t, store.Archived, fetched.RowStatus)
// Restore to normal
normalStatus := store.Normal
restored, err := ts.UpdateUser(ctx, &store.UpdateUser{
ID: user.ID,
RowStatus: &normalStatus,
})
require.NoError(t, err)
require.Equal(t, store.Normal, restored.RowStatus)
ts.Close()
}
func TestUserUpdateAllFields(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Update all fields
newUsername := "updated_username"
newEmail := "updated@test.com"
newNickname := "Updated Nickname"
newAvatarURL := "https://example.com/avatar.png"
newDescription := "Updated description"
newRole := store.RoleAdmin
newPasswordHash := "new_password_hash"
updated, err := ts.UpdateUser(ctx, &store.UpdateUser{
ID: user.ID,
Username: &newUsername,
Email: &newEmail,
Nickname: &newNickname,
AvatarURL: &newAvatarURL,
Description: &newDescription,
Role: &newRole,
PasswordHash: &newPasswordHash,
})
require.NoError(t, err)
require.Equal(t, newUsername, updated.Username)
require.Equal(t, newEmail, updated.Email)
require.Equal(t, newNickname, updated.Nickname)
require.Equal(t, newAvatarURL, updated.AvatarURL)
require.Equal(t, newDescription, updated.Description)
require.Equal(t, newRole, updated.Role)
require.Equal(t, newPasswordHash, updated.PasswordHash)
// Verify by fetching again
fetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})
require.NoError(t, err)
require.Equal(t, newUsername, fetched.Username)
ts.Close()
}
func TestUserListWithLimit(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
// Create 5 users
for i := 0; i < 5; i++ {
role := store.RoleUser
if i == 0 {
role = store.RoleHost
}
_, err := createTestingUserWithRole(ctx, ts, fmt.Sprintf("user%d", i), role)
require.NoError(t, err)
}
// List with limit
limit := 3
users, err := ts.ListUsers(ctx, &store.FindUser{Limit: &limit})
require.NoError(t, err)
require.Equal(t, 3, len(users))
ts.Close()
}
func createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) {
return createTestingUserWithRole(ctx, ts, "test", store.RoleHost)
}
func createTestingUserWithRole(ctx context.Context, ts *store.Store, username string, role store.Role) (*store.User, error) {
userCreate := &store.User{
Username: "test",
Role: store.RoleHost,
Email: "test@test.com",
Nickname: "test_nickname",
Description: "test_description",
Username: username,
Role: role,
Email: username + "@test.com",
Nickname: username + "_nickname",
Description: username + "_description",
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte("test_password"), bcrypt.DefaultCost)
if err != nil {
......
......@@ -34,7 +34,7 @@ const LocationMarker = (props: MarkerProps) => {
// Call the parent onChange function.
props.onChange(e.latlng);
},
locationfound() { },
locationfound() {},
});
useEffect(() => {
......@@ -246,7 +246,7 @@ const LeafletMap = (props: MapProps) => {
isDark ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" : "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
}
/>
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => { }} />
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => {}} />
<MapControls position={props.latlng} />
<MapCleanup />
</MapContainer>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment