Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
55231820
Unverified
Commit
55231820
authored
Jan 20, 2026
by
Johnny
Committed by
GitHub
Jan 20, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: resolve flaky migration tests and add stable upgrade test (#5514)
parent
00f21b86
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
140 additions
and
375 deletions
+140
-375
backend-tests.yml
.github/workflows/backend-tests.yml
+5
-2
Dockerfile
store/test/Dockerfile
+0
-13
containers.go
store/test/containers.go
+17
-150
main_test.go
store/test/main_test.go
+1
-16
migrator_test.go
store/test/migrator_test.go
+117
-194
No files found.
.github/workflows/backend-tests.yml
View file @
55231820
...
@@ -61,7 +61,10 @@ jobs:
...
@@ -61,7 +61,10 @@ jobs:
run
:
|
run
:
|
case "${{ matrix.test-group }}" in
case "${{ matrix.test-group }}" in
store)
store)
go test -v -race -coverprofile=coverage.out -covermode=atomic ./store/...
# Run store tests for all drivers (sqlite, mysql, postgres)
# The TestMain in store/test runs all drivers when DRIVER is not set
# Note: We run without -race for container tests due to testcontainers race issues
go test -v -coverprofile=coverage.out -covermode=atomic ./store/...
;;
;;
server)
server)
go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/...
go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/...
...
@@ -75,7 +78,7 @@ jobs:
...
@@ -75,7 +78,7 @@ jobs:
;;
;;
esac
esac
env
:
env
:
DRIVER
:
sqlite
# Use SQLite for fastest test execution
DRIVER
:
${{ matrix.test-group == 'store' && '' || 'sqlite' }}
-
name
:
Upload coverage
-
name
:
Upload coverage
if
:
github.event_name == 'push' && github.ref == 'refs/heads/main'
if
:
github.event_name == 'push' && github.ref == 'refs/heads/main'
...
...
store/test/Dockerfile
deleted
100644 → 0
View file @
00f21b86
FROM
golang:1.25-alpine AS backend
WORKDIR
/backend-build
COPY
. .
RUN
go build
-o
memos ./cmd/memos
FROM
alpine:latest
WORKDIR
/usr/local/memos
COPY
--from=backend /backend-build/memos /usr/local/memos/
EXPOSE
5230
RUN
mkdir
-p
/var/opt/memos
ENV
MEMOS_MODE="prod"
ENV
MEMOS_PORT="5230"
ENTRYPOINT
["./memos"]
store/test/containers.go
View file @
55231820
...
@@ -30,18 +30,10 @@ const (
...
@@ -30,18 +30,10 @@ const (
// Memos container settings for migration testing.
// Memos container settings for migration testing.
MemosDockerImage
=
"neosmemo/memos"
MemosDockerImage
=
"neosmemo/memos"
StableMemosVersion
=
"stable"
StableMemosVersion
=
"stable"
// Always points to the latest stable release
)
)
var
(
var
(
// MemosStartupWaitStrategy defines the wait strategy for Memos container startup.
// It waits for the "started" log message (compatible with both old and new versions)
// and checks if port 5230 is listening.
MemosStartupWaitStrategy
=
wait
.
ForAll
(
wait
.
ForLog
(
"started"
),
wait
.
ForListeningPort
(
"5230/tcp"
),
)
.
WithDeadline
(
180
*
time
.
Second
)
mysqlContainer
atomic
.
Pointer
[
mysql
.
MySQLContainer
]
mysqlContainer
atomic
.
Pointer
[
mysql
.
MySQLContainer
]
postgresContainer
atomic
.
Pointer
[
postgres
.
PostgresContainer
]
postgresContainer
atomic
.
Pointer
[
postgres
.
PostgresContainer
]
mysqlOnce
sync
.
Once
mysqlOnce
sync
.
Once
...
@@ -235,105 +227,6 @@ func GetPostgresDSN(t *testing.T) string {
...
@@ -235,105 +227,6 @@ func GetPostgresDSN(t *testing.T) string {
return
strings
.
Replace
(
dsn
,
"/init_db?"
,
"/"
+
dbName
+
"?"
,
1
)
return
strings
.
Replace
(
dsn
,
"/init_db?"
,
"/"
+
dbName
+
"?"
,
1
)
}
}
// GetDedicatedMySQLDSN starts a dedicated MySQL container for migration testing.
// This is needed because older Memos versions have bugs when connecting to a MySQL
// server that has other initialized databases (they incorrectly query migration_history
// on a fresh database without checking if the DB is initialized).
// Returns: DSN for host access, container hostname for internal network access, cleanup function.
func
GetDedicatedMySQLDSN
(
t
*
testing
.
T
)
(
dsn
string
,
containerHost
string
,
cleanup
func
())
{
ctx
:=
context
.
Background
()
nw
,
err
:=
getTestNetwork
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to create test network: %v"
,
err
)
}
container
,
err
:=
mysql
.
Run
(
ctx
,
"mysql:8"
,
mysql
.
WithDatabase
(
"memos"
),
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
),
),
network
.
WithNetwork
(
nil
,
nw
),
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to start dedicated MySQL container: %v"
,
err
)
}
hostDSN
,
err
:=
container
.
ConnectionString
(
ctx
,
"multiStatements=true"
)
if
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"failed to get MySQL connection string: %v"
,
err
)
}
if
err
:=
waitForDB
(
"mysql"
,
hostDSN
,
30
*
time
.
Second
);
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"MySQL not ready for connections: %v"
,
err
)
}
name
,
_
:=
container
.
Name
(
ctx
)
host
:=
strings
.
TrimPrefix
(
name
,
"/"
)
return
hostDSN
,
host
,
func
()
{
container
.
Terminate
(
ctx
)
}
}
// GetDedicatedPostgresDSN starts a dedicated PostgreSQL container for migration testing.
// This is needed for isolation when testing migrations with older Memos versions.
// Returns: DSN for host access, container hostname for internal network access, cleanup function.
func
GetDedicatedPostgresDSN
(
t
*
testing
.
T
)
(
dsn
string
,
containerHost
string
,
cleanup
func
())
{
ctx
:=
context
.
Background
()
nw
,
err
:=
getTestNetwork
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to create test network: %v"
,
err
)
}
container
,
err
:=
postgres
.
Run
(
ctx
,
"postgres:18"
,
postgres
.
WithDatabase
(
"memos"
),
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
),
),
network
.
WithNetwork
(
nil
,
nw
),
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to start dedicated PostgreSQL container: %v"
,
err
)
}
hostDSN
,
err
:=
container
.
ConnectionString
(
ctx
,
"sslmode=disable"
)
if
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"failed to get PostgreSQL connection string: %v"
,
err
)
}
if
err
:=
waitForDB
(
"postgres"
,
hostDSN
,
30
*
time
.
Second
);
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"PostgreSQL not ready for connections: %v"
,
err
)
}
name
,
_
:=
container
.
Name
(
ctx
)
host
:=
strings
.
TrimPrefix
(
name
,
"/"
)
return
hostDSN
,
host
,
func
()
{
container
.
Terminate
(
ctx
)
}
}
// TerminateContainers cleans up all running containers and network.
// TerminateContainers cleans up all running containers and network.
// This is typically called from TestMain.
// This is typically called from TestMain.
func
TerminateContainers
()
{
func
TerminateContainers
()
{
...
@@ -349,45 +242,28 @@ func TerminateContainers() {
...
@@ -349,45 +242,28 @@ func TerminateContainers() {
}
}
}
}
// GetMySQLContainerHost returns the MySQL container hostname for use within the Docker network.
func
GetMySQLContainerHost
()
string
{
container
:=
mysqlContainer
.
Load
()
if
container
==
nil
{
return
""
}
name
,
_
:=
container
.
Name
(
context
.
Background
())
// Remove leading slash from container name
return
strings
.
TrimPrefix
(
name
,
"/"
)
}
// GetPostgresContainerHost returns the PostgreSQL container hostname for use within the Docker network.
func
GetPostgresContainerHost
()
string
{
container
:=
postgresContainer
.
Load
()
if
container
==
nil
{
return
""
}
name
,
_
:=
container
.
Name
(
context
.
Background
())
return
strings
.
TrimPrefix
(
name
,
"/"
)
}
// MemosContainerConfig holds configuration for starting a Memos container.
// MemosContainerConfig holds configuration for starting a Memos container.
type
MemosContainerConfig
struct
{
type
MemosContainerConfig
struct
{
Version
string
// Memos version tag (e.g., "0.2
5
")
Version
string
// Memos version tag (e.g., "0.2
4.0
")
Driver
string
// Database driver: sqlite, mysql, postgres
Driver
string
// Database driver: sqlite, mysql, postgres
DSN
string
// Database DSN (for mysql/postgres)
DSN
string
// Database DSN (for mysql/postgres)
DataDir
string
// Host directory to mount for SQLite data
DataDir
string
// Host directory to mount for SQLite data
}
}
// MemosStartupWaitStrategy defines the wait strategy for Memos container startup.
// Uses regex to match various log message formats across versions.
var
MemosStartupWaitStrategy
=
wait
.
ForAll
(
wait
.
ForLog
(
"(started successfully|has been started on port)"
)
.
AsRegexp
(),
wait
.
ForListeningPort
(
"5230/tcp"
),
)
.
WithDeadline
(
180
*
time
.
Second
)
// StartMemosContainer starts a Memos container for migration testing.
// StartMemosContainer starts a Memos container for migration testing.
// For SQLite, it mounts the dataDir to /var/opt/memos.
// For SQLite, it mounts the dataDir to /var/opt/memos.
// For MySQL/PostgreSQL, it connects to the provided DSN via the test network.
// If Version is "local", builds the image from the local Dockerfile.
func
StartMemosContainer
(
ctx
context
.
Context
,
cfg
MemosContainerConfig
)
(
testcontainers
.
Container
,
error
)
{
func
StartMemosContainer
(
ctx
context
.
Context
,
cfg
MemosContainerConfig
)
(
testcontainers
.
Container
,
error
)
{
env
:=
map
[
string
]
string
{
env
:=
map
[
string
]
string
{
"MEMOS_MODE"
:
"prod"
,
"MEMOS_MODE"
:
"prod"
,
}
}
var
mounts
[]
testcontainers
.
ContainerMount
var
opts
[]
testcontainers
.
ContainerCustomizer
var
opts
[]
testcontainers
.
ContainerCustomizer
switch
cfg
.
Driver
{
switch
cfg
.
Driver
{
...
@@ -396,37 +272,28 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
...
@@ -396,37 +272,28 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
opts
=
append
(
opts
,
testcontainers
.
WithHostConfigModifier
(
func
(
hc
*
container
.
HostConfig
)
{
opts
=
append
(
opts
,
testcontainers
.
WithHostConfigModifier
(
func
(
hc
*
container
.
HostConfig
)
{
hc
.
Binds
=
append
(
hc
.
Binds
,
fmt
.
Sprintf
(
"%s:%s"
,
cfg
.
DataDir
,
"/var/opt/memos"
))
hc
.
Binds
=
append
(
hc
.
Binds
,
fmt
.
Sprintf
(
"%s:%s"
,
cfg
.
DataDir
,
"/var/opt/memos"
))
}))
}))
case
"mysql"
:
env
[
"MEMOS_DRIVER"
]
=
"mysql"
env
[
"MEMOS_DSN"
]
=
cfg
.
DSN
opts
=
append
(
opts
,
network
.
WithNetwork
(
nil
,
testDockerNetwork
.
Load
()))
case
"postgres"
:
env
[
"MEMOS_DRIVER"
]
=
"postgres"
env
[
"MEMOS_DSN"
]
=
cfg
.
DSN
opts
=
append
(
opts
,
network
.
WithNetwork
(
nil
,
testDockerNetwork
.
Load
()))
default
:
default
:
return
nil
,
errors
.
Errorf
(
"unsupported driver: %s"
,
cfg
.
Driver
)
return
nil
,
errors
.
Errorf
(
"unsupported driver
for migration testing
: %s"
,
cfg
.
Driver
)
}
}
req
:=
testcontainers
.
ContainerRequest
{
req
:=
testcontainers
.
ContainerRequest
{
Image
:
fmt
.
Sprintf
(
"%s:%s"
,
MemosDockerImage
,
cfg
.
Version
),
Env
:
env
,
Env
:
env
,
Mounts
:
testcontainers
.
Mounts
(
mounts
...
),
ExposedPorts
:
[]
string
{
"5230/tcp"
},
ExposedPorts
:
[]
string
{
"5230/tcp"
},
WaitingFor
:
MemosStartupWaitStrategy
,
WaitingFor
:
MemosStartupWaitStrategy
,
User
:
fmt
.
Sprintf
(
"%d:%d"
,
os
.
Getuid
(),
os
.
Getgid
()),
}
}
// Use local
Dockerfile build or remote image
// Use local
image if specified
if
cfg
.
Version
==
"local"
{
if
cfg
.
Version
==
"local"
{
if
os
.
Getenv
(
"MEMOS_TEST_IMAGE_BUILT"
)
==
"1"
{
if
os
.
Getenv
(
"MEMOS_TEST_IMAGE_BUILT"
)
==
"1"
{
req
.
Image
=
"memos-test:local"
req
.
Image
=
"memos-test:local"
}
else
{
}
else
{
req
.
FromDockerfile
=
testcontainers
.
FromDockerfile
{
req
.
FromDockerfile
=
testcontainers
.
FromDockerfile
{
Context
:
"../../"
,
Context
:
"../../"
,
Dockerfile
:
"
store/test/Dockerfile"
,
// Simple Dockerfile without BuildKit requirements
Dockerfile
:
"
Dockerfile"
,
}
}
}
}
}
else
{
req
.
Image
=
fmt
.
Sprintf
(
"%s:%s"
,
MemosDockerImage
,
cfg
.
Version
)
}
}
genericReq
:=
testcontainers
.
GenericContainerRequest
{
genericReq
:=
testcontainers
.
GenericContainerRequest
{
...
@@ -434,17 +301,17 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
...
@@ -434,17 +301,17 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
Started
:
true
,
Started
:
true
,
}
}
// Apply
network
options
// Apply options
for
_
,
opt
:=
range
opts
{
for
_
,
opt
:=
range
opts
{
if
err
:=
opt
.
Customize
(
&
genericReq
);
err
!=
nil
{
if
err
:=
opt
.
Customize
(
&
genericReq
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to apply container option"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to apply container option"
)
}
}
}
}
c
ontaine
r
,
err
:=
testcontainers
.
GenericContainer
(
ctx
,
genericReq
)
c
t
r
,
err
:=
testcontainers
.
GenericContainer
(
ctx
,
genericReq
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to start memos container"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to start memos container"
)
}
}
return
c
ontaine
r
,
nil
return
c
t
r
,
nil
}
}
store/test/main_test.go
View file @
55231820
...
@@ -26,28 +26,13 @@ func runAllDrivers() {
...
@@ -26,28 +26,13 @@ func runAllDrivers() {
_
,
currentFile
,
_
,
_
:=
runtime
.
Caller
(
0
)
_
,
currentFile
,
_
,
_
:=
runtime
.
Caller
(
0
)
projectRoot
:=
filepath
.
Dir
(
filepath
.
Dir
(
filepath
.
Dir
(
currentFile
)))
projectRoot
:=
filepath
.
Dir
(
filepath
.
Dir
(
filepath
.
Dir
(
currentFile
)))
// Build the docker image once for all tests to use
fmt
.
Println
(
"Building memos docker image for tests (memos-test:local)..."
)
buildCmd
:=
exec
.
Command
(
"docker"
,
"build"
,
"-f"
,
"store/test/Dockerfile"
,
"-t"
,
"memos-test:local"
,
"."
)
buildCmd
.
Dir
=
projectRoot
buildCmd
.
Stdout
=
os
.
Stdout
buildCmd
.
Stderr
=
os
.
Stderr
if
err
:=
buildCmd
.
Run
();
err
!=
nil
{
fmt
.
Printf
(
"Failed to build docker image: %v
\n
"
,
err
)
// We don't exit here, we let the tests try to run (and maybe fail or rebuild)
// strictly speaking we should probably fail, but let's be robust.
// Actually, if build fails, tests relying on it will fail or try to rebuild.
// Let's exit to be clear.
panic
(
fmt
.
Sprintf
(
"failed to build docker image: %v"
,
err
))
}
var
failed
[]
string
var
failed
[]
string
for
_
,
driver
:=
range
drivers
{
for
_
,
driver
:=
range
drivers
{
fmt
.
Printf
(
"
\n
==================== %s ====================
\n\n
"
,
driver
)
fmt
.
Printf
(
"
\n
==================== %s ====================
\n\n
"
,
driver
)
cmd
:=
exec
.
Command
(
"go"
,
"test"
,
"-v"
,
"-count=1"
,
"./store/test/..."
)
cmd
:=
exec
.
Command
(
"go"
,
"test"
,
"-v"
,
"-count=1"
,
"./store/test/..."
)
cmd
.
Dir
=
projectRoot
cmd
.
Dir
=
projectRoot
cmd
.
Env
=
append
(
os
.
Environ
(),
"DRIVER="
+
driver
,
"MEMOS_TEST_IMAGE_BUILT=1"
)
cmd
.
Env
=
append
(
os
.
Environ
(),
"DRIVER="
+
driver
)
cmd
.
Stdout
=
os
.
Stdout
cmd
.
Stdout
=
os
.
Stdout
cmd
.
Stderr
=
os
.
Stderr
cmd
.
Stderr
=
os
.
Stderr
...
...
store/test/migrator_test.go
View file @
55231820
...
@@ -3,7 +3,7 @@ package test
...
@@ -3,7 +3,7 @@ package test
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"
string
s"
"
o
s"
"testing"
"testing"
"time"
"time"
...
@@ -33,245 +33,168 @@ func TestFreshInstall(t *testing.T) {
...
@@ -33,245 +33,168 @@ func TestFreshInstall(t *testing.T) {
require
.
Equal
(
t
,
currentSchemaVersion
,
instanceSetting
.
SchemaVersion
)
require
.
Equal
(
t
,
currentSchemaVersion
,
instanceSetting
.
SchemaVersion
)
}
}
// TestMigrationDataPersistence verifies that data created in the old version
// TestMigrationReRun verifies that re-running the migration on an already
// is preserved and accessible after migration to the new version.
// migrated database does not fail or cause issues. This simulates a
func
TestMigrationDataPersistence
(
t
*
testing
.
T
)
{
// scenario where the server is restarted.
func
TestMigrationReRun
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
// Only run for SQLite for simplicity and speed in this edge case test,
// but the logic applies to all drivers.
if
getDriverFromEnv
()
!=
"sqlite"
{
t
.
Skip
(
"skipping data persistence test for non-sqlite driver"
)
}
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
dataDir
:=
t
.
TempDir
()
// Use the shared testing store which already runs migrations on init
ts
:=
NewTestingStore
(
ctx
,
t
)
// 1. Start Old Memos container (Stable)
oldCfg
:=
MemosContainerConfig
{
Driver
:
"sqlite"
,
DataDir
:
dataDir
,
Version
:
StableMemosVersion
,
}
t
.
Logf
(
"Starting Memos %s container..."
,
oldCfg
.
Version
)
oldContainer
,
err
:=
StartMemosContainer
(
ctx
,
oldCfg
)
require
.
NoError
(
t
,
err
,
"failed to start old memos container"
)
// Wait for startup
time
.
Sleep
(
5
*
time
.
Second
)
err
=
oldContainer
.
Terminate
(
ctx
)
require
.
NoError
(
t
,
err
,
"failed to stop old memos container"
)
// 2. Start New Memos container (Local) - this triggers migration
newCfg
:=
MemosContainerConfig
{
Driver
:
"sqlite"
,
DataDir
:
dataDir
,
Version
:
"local"
,
}
t
.
Log
(
"Starting new Memos container to trigger migration..."
)
newContainer
,
err
:=
StartMemosContainer
(
ctx
,
newCfg
)
require
.
NoError
(
t
,
err
,
"failed to start new memos container"
)
defer
newContainer
.
Terminate
(
ctx
)
// Wait for migration to complete
time
.
Sleep
(
5
*
time
.
Second
)
// 3. Verify Data Access using Store
dsn
:=
fmt
.
Sprintf
(
"%s/memos_prod.db"
,
dataDir
)
// Create a store instance connected to the migrated DB
ts
:=
createTestingStoreWithDSN
(
t
,
"sqlite"
,
dsn
)
//
Check schema
version
//
Get current
version
current
Version
,
err
:=
ts
.
GetCurrentSchemaVersion
()
initial
Version
,
err
:=
ts
.
GetCurrentSchemaVersion
()
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotEmpty
(
t
,
currentVersion
,
"schema version should be present"
)
t
.
Logf
(
"Migrated schema version: %s"
,
currentVersion
)
//
Check if we can write new data
//
Manually trigger migration again
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
err
=
ts
.
Migrate
(
ctx
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
,
"re-running migration should not fail"
)
memo
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
// Verify version hasn't changed (or at least is valid)
UID
:
"migrated-test-memo"
,
finalVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
CreatorID
:
user
.
ID
,
Content
:
"Post-migration content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"Post-migration content"
,
memo
.
Content
)
require
.
Equal
(
t
,
initialVersion
,
finalVersion
,
"version should match after re-run"
)
}
}
// TestMigration
Idempotency verifies that running the migration multiple times
// TestMigration
WithData verifies that migration preserves data integrity.
//
(e.g. container restart) is safe and doesn't corrupt data
.
//
Creates data, then re-runs migration and verifies data is still accessible
.
func
TestMigration
Idempotency
(
t
*
testing
.
T
)
{
func
TestMigration
WithData
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
if
getDriverFromEnv
()
!=
"sqlite"
{
t
.
Skip
(
"skipping idempotency test for non-sqlite driver"
)
}
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
dataDir
:=
t
.
TempDir
()
ts
:=
NewTestingStore
(
ctx
,
t
)
// 1. Initial Migration (Local version)
cfg
:=
MemosContainerConfig
{
Driver
:
"sqlite"
,
DataDir
:
dataDir
,
Version
:
"local"
,
}
t
.
Log
(
"Run 1: Initial migration..."
)
// Create a user and memo before re-running migration
container1
,
err
:=
StartMemosContainer
(
ctx
,
cfg
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
,
"should create user"
)
time
.
Sleep
(
5
*
time
.
Second
)
container1
.
Terminate
(
ctx
)
// 2. Second Run (Restart)
originalMemo
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
t
.
Log
(
"Run 2: Restart (should be idempotent)..."
)
UID
:
"migration-data-test"
,
container2
,
err
:=
StartMemosContainer
(
ctx
,
cfg
)
CreatorID
:
user
.
ID
,
require
.
NoError
(
t
,
err
)
Content
:
"Data before migration re-run"
,
defer
container2
.
Terminate
(
ctx
)
Visibility
:
store
.
Public
,
time
.
Sleep
(
5
*
time
.
Second
)
})
require
.
NoError
(
t
,
err
,
"should create memo"
)
//
3. Verify Store Integrity
//
Re-run migration
dsn
:=
fmt
.
Sprintf
(
"%s/memos_prod.db"
,
dataDir
)
err
=
ts
.
Migrate
(
ctx
)
ts
:=
createTestingStoreWithDSN
(
t
,
"sqlite"
,
dsn
)
require
.
NoError
(
t
,
err
,
"re-running migration should not fail"
)
// Ensure we can still use the DB
// Verify data is still accessible
_
,
err
=
ts
.
GetCurrentSchemaVersion
()
memo
,
err
:=
ts
.
GetMemo
(
ctx
,
&
store
.
FindMemo
{
UID
:
&
originalMemo
.
UID
})
require
.
NoError
(
t
,
err
,
"database should be healthy after restart"
)
require
.
NoError
(
t
,
err
,
"should retrieve memo after migration"
)
require
.
Equal
(
t
,
"Data before migration re-run"
,
memo
.
Content
,
"memo content should be preserved"
)
}
}
// TestMigrationReRun verifies that re-running the migration on an already
// TestMigrationMultipleReRuns verifies that migration is idempotent
// migrated database does not fail or cause issues. This simulates a
// even when run multiple times in succession.
// scenario where the server is restarted.
func
TestMigrationMultipleReRuns
(
t
*
testing
.
T
)
{
func
TestMigrationReRun
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
// Use the shared testing store which already runs migrations on init
ts
:=
NewTestingStore
(
ctx
,
t
)
ts
:=
NewTestingStore
(
ctx
,
t
)
// Get
current
version
// Get
initial
version
initialVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
initialVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
// Manually trigger migration again
// Run migration multiple times
for
i
:=
0
;
i
<
3
;
i
++
{
err
=
ts
.
Migrate
(
ctx
)
err
=
ts
.
Migrate
(
ctx
)
require
.
NoError
(
t
,
err
,
"re-running migration should not fail"
)
require
.
NoError
(
t
,
err
,
"migration run %d should not fail"
,
i
+
1
)
}
// Verify version
hasn't changed (or at least is valid)
// Verify version
is still correct
finalVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
finalVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
initialVersion
,
finalVersion
,
"version should
match after re-run
"
)
require
.
Equal
(
t
,
initialVersion
,
finalVersion
,
"version should
remain unchanged after multiple re-runs
"
)
}
}
// createTestingStoreWithDSN helper to connect to an existing DB file.
// TestMigrationFromStableVersion verifies that upgrading from a stable Memos version
func
createTestingStoreWithDSN
(
t
*
testing
.
T
,
driver
,
dsn
string
)
*
store
.
Store
{
// to the current version works correctly. This is the critical upgrade path test.
ctx
:=
context
.
Background
()
//
return
NewTestingStoreWithDSN
(
ctx
,
t
,
driver
,
dsn
)
// Test flow:
}
// 1. Start a stable Memos container to create a database with the old schema
// 2. Stop the container and wait for cleanup
// 3. Use the store directly to run migration with current code
// 4. Verify the migration succeeded and data can be written
//
// Note: This test is skipped when running with -race flag because testcontainers
// has known race conditions in its reaper code that are outside our control.
func
TestMigrationFromStableVersion
(
t
*
testing
.
T
)
{
// Skip for non-SQLite drivers (simplifies the test)
if
getDriverFromEnv
()
!=
"sqlite"
{
t
.
Skip
(
"skipping upgrade test for non-sqlite driver"
)
}
// testMigration is a helper function that orchestrates the migration test flow.
// Skip if explicitly disabled (e.g., in environments without Docker)
// It starts the stable version, waits for initialization, and then starts the local version.
if
os
.
Getenv
(
"SKIP_CONTAINER_TESTS"
)
==
"1"
{
func
testMigration
(
t
*
testing
.
T
,
driver
string
,
prepareFunc
func
()
(
MemosContainerConfig
,
func
()))
{
t
.
Skip
(
"skipping container-based test (SKIP_CONTAINER_TESTS=1)"
)
if
getDriverFromEnv
()
!=
driver
{
t
.
Skipf
(
"skipping %s migration test for non-%s driver"
,
driver
,
driver
)
}
}
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
dataDir
:=
t
.
TempDir
()
// Prepare resources (temp dir or dedicated container)
// 1. Start stable Memos container to create database with old schema
cfg
,
cleanup
:=
prepareFunc
()
cfg
:=
MemosContainerConfig
{
if
cleanup
!=
nil
{
Driver
:
"sqlite"
,
defer
cleanup
()
DataDir
:
dataDir
,
Version
:
StableMemosVersion
,
}
}
// 1. Start Old Memos container (Stable)
t
.
Logf
(
"Starting Memos %s container to create old-schema database..."
,
cfg
.
Version
)
cfg
.
Version
=
StableMemosVersion
container
,
err
:=
StartMemosContainer
(
ctx
,
cfg
)
t
.
Logf
(
"Starting Memos %s container to initialize %s database..."
,
cfg
.
Version
,
driver
)
require
.
NoError
(
t
,
err
,
"failed to start stable memos container"
)
oldContainer
,
err
:=
StartMemosContainer
(
ctx
,
cfg
)
require
.
NoError
(
t
,
err
,
"failed to start old memos container"
)
// Wait for
database to be fully initialized
// Wait for
the container to fully initialize the database
time
.
Sleep
(
5
*
time
.
Second
)
time
.
Sleep
(
10
*
time
.
Second
)
// Stop the old container
// Stop the container gracefully
err
=
oldContainer
.
Terminate
(
ctx
)
t
.
Log
(
"Stopping stable Memos container..."
)
require
.
NoError
(
t
,
err
,
"failed to stop old memos container"
)
err
=
container
.
Terminate
(
ctx
)
require
.
NoError
(
t
,
err
,
"failed to stop memos container"
)
t
.
Log
(
"Old Memos container stopped, starting new container with local build..."
)
// Wait for file handles to be released
time
.
Sleep
(
2
*
time
.
Second
)
// 2. Start New Memos container (Local)
// 2. Connect to the database directly and run migration with current code
cfg
.
Version
=
"local"
// Triggers local build in StartMemosContainer
dsn
:=
fmt
.
Sprintf
(
"%s/memos_prod.db"
,
dataDir
)
newContainer
,
err
:=
StartMemosContainer
(
ctx
,
cfg
)
t
.
Logf
(
"Connecting to database at %s..."
,
dsn
)
require
.
NoError
(
t
,
err
,
"failed to start new memos container - migration may have failed"
)
defer
newContainer
.
Terminate
(
ctx
)
t
.
Logf
(
"Migration successful: %s -> local build"
,
StableMemosVersion
)
ts
:=
NewTestingStoreWithDSN
(
ctx
,
t
,
"sqlite"
,
dsn
)
}
// TestMigrationFromPreviousVersion_SQLite verifies that migrating from the previous
// Get the schema version before migration
// Memos version to the current version works correctly for SQLite.
oldSetting
,
err
:=
ts
.
GetInstanceBasicSetting
(
ctx
)
func
TestMigrationFromPreviousVersion_SQLite
(
t
*
testing
.
T
)
{
require
.
NoError
(
t
,
err
)
t
.
Parallel
()
t
.
Logf
(
"Old schema version: %s"
,
oldSetting
.
SchemaVersion
)
testMigration
(
t
,
"sqlite"
,
func
()
(
MemosContainerConfig
,
func
())
{
// Create a temp directory for SQLite data that persists across container restarts
dataDir
:=
t
.
TempDir
()
return
MemosContainerConfig
{
Driver
:
"sqlite"
,
DataDir
:
dataDir
,
},
nil
})
}
// TestMigrationFromPreviousVersion_MySQL verifies that migrating from the previous
// 3. Run migration with current code
// Memos version to the current version works correctly for MySQL.
t
.
Log
(
"Running migration with current code..."
)
func
TestMigrationFromPreviousVersion_MySQL
(
t
*
testing
.
T
)
{
err
=
ts
.
Migrate
(
ctx
)
t
.
Parallel
()
require
.
NoError
(
t
,
err
,
"migration from stable version should succeed"
)
testMigration
(
t
,
"mysql"
,
func
()
(
MemosContainerConfig
,
func
())
{
// For migration testing, we need a dedicated MySQL container
dsn
,
containerHost
,
cleanup
:=
GetDedicatedMySQLDSN
(
t
)
// Extract database name from DSN
parts
:=
strings
.
Split
(
dsn
,
"/"
)
dbNameWithParams
:=
parts
[
len
(
parts
)
-
1
]
dbName
:=
strings
.
Split
(
dbNameWithParams
,
"?"
)[
0
]
// Container DSN uses internal network hostname
containerDSN
:=
fmt
.
Sprintf
(
"%s:%s@tcp(%s:3306)/%s"
,
testUser
,
testPassword
,
containerHost
,
dbName
)
return
MemosContainerConfig
{
Driver
:
"mysql"
,
DSN
:
containerDSN
,
},
cleanup
})
}
// TestMigrationFromPreviousVersion_Postgres verifies that migrating from the previous
// 4. Verify migration succeeded
// Memos version to the current version works correctly for PostgreSQL.
newVersion
,
err
:=
ts
.
GetCurrentSchemaVersion
()
func
TestMigrationFromPreviousVersion_Postgres
(
t
*
testing
.
T
)
{
require
.
NoError
(
t
,
err
)
t
.
Parallel
()
t
.
Logf
(
"New schema version: %s"
,
newVersion
)
testMigration
(
t
,
"postgres"
,
func
()
(
MemosContainerConfig
,
func
())
{
// For migration testing, we need a dedicated PostgreSQL container
newSetting
,
err
:=
ts
.
GetInstanceBasicSetting
(
ctx
)
dsn
,
containerHost
,
cleanup
:=
GetDedicatedPostgresDSN
(
t
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
newVersion
,
newSetting
.
SchemaVersion
,
"schema version should be updated"
)
// Extract database name from DSN
parts
:=
strings
.
Split
(
dsn
,
"/"
)
// Verify we can write data to the migrated database
dbNameWithParams
:=
parts
[
len
(
parts
)
-
1
]
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
dbName
:=
strings
.
Split
(
dbNameWithParams
,
"?"
)[
0
]
require
.
NoError
(
t
,
err
,
"should create user after migration"
)
// Container DSN uses internal network hostname
memo
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
containerDSN
:=
fmt
.
Sprintf
(
"postgres://%s:%s@%s:5432/%s?sslmode=disable"
,
UID
:
"post-upgrade-memo"
,
testUser
,
testPassword
,
containerHost
,
dbName
)
CreatorID
:
user
.
ID
,
Content
:
"Content after upgrade from stable"
,
return
MemosContainerConfig
{
Visibility
:
store
.
Public
,
Driver
:
"postgres"
,
DSN
:
containerDSN
,
},
cleanup
})
})
require
.
NoError
(
t
,
err
,
"should create memo after migration"
)
require
.
Equal
(
t
,
"Content after upgrade from stable"
,
memo
.
Content
)
t
.
Logf
(
"Migration successful: %s -> %s"
,
oldSetting
.
SchemaVersion
,
newVersion
)
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment