mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-11 15:53:00 +03:00
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
190 lines
4.9 KiB
Go
190 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
|
)
|
|
|
|
func newTestAppLockService(t *testing.T, db *gorm.DB) *AppLockService {
|
|
t.Helper()
|
|
|
|
return &AppLockService{
|
|
db: db,
|
|
processID: 1,
|
|
hostID: "test-host",
|
|
lockID: "a13c7673-c7ae-49f1-9112-2cd2d0d4b0c1",
|
|
}
|
|
}
|
|
|
|
func insertLock(t *testing.T, db *gorm.DB, value lockValue) {
|
|
t.Helper()
|
|
|
|
raw, err := value.Marshal()
|
|
require.NoError(t, err)
|
|
|
|
err = db.Create(&model.KV{Key: lockKey, Value: &raw}).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func readLockValue(t *testing.T, db *gorm.DB) lockValue {
|
|
t.Helper()
|
|
|
|
var row model.KV
|
|
err := db.Take(&row, "key = ?", lockKey).Error
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, row.Value)
|
|
|
|
var value lockValue
|
|
err = value.Unmarshal(*row.Value)
|
|
require.NoError(t, err)
|
|
|
|
return value
|
|
}
|
|
|
|
func TestAppLockServiceAcquire(t *testing.T) {
|
|
t.Run("creates new lock when none exists", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
_, err := service.Acquire(context.Background(), false)
|
|
require.NoError(t, err)
|
|
|
|
stored := readLockValue(t, db)
|
|
require.Equal(t, service.processID, stored.ProcessID)
|
|
require.Equal(t, service.hostID, stored.HostID)
|
|
require.Greater(t, stored.ExpiresAt, time.Now().Unix())
|
|
})
|
|
|
|
t.Run("returns ErrLockUnavailable when lock held by another process", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
existing := lockValue{
|
|
ProcessID: 99,
|
|
HostID: "other-host",
|
|
ExpiresAt: time.Now().Add(ttl).Unix(),
|
|
}
|
|
insertLock(t, db, existing)
|
|
|
|
_, err := service.Acquire(context.Background(), false)
|
|
require.ErrorIs(t, err, ErrLockUnavailable)
|
|
|
|
current := readLockValue(t, db)
|
|
require.Equal(t, existing, current)
|
|
})
|
|
|
|
t.Run("force acquisition steals lock", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
insertLock(t, db, lockValue{
|
|
ProcessID: 99,
|
|
HostID: "other-host",
|
|
ExpiresAt: time.Now().Unix(),
|
|
})
|
|
|
|
_, err := service.Acquire(context.Background(), true)
|
|
require.NoError(t, err)
|
|
|
|
stored := readLockValue(t, db)
|
|
require.Equal(t, service.processID, stored.ProcessID)
|
|
require.Equal(t, service.hostID, stored.HostID)
|
|
require.Greater(t, stored.ExpiresAt, time.Now().Unix())
|
|
})
|
|
}
|
|
|
|
func TestAppLockServiceRelease(t *testing.T) {
|
|
t.Run("removes owned lock", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
_, err := service.Acquire(context.Background(), false)
|
|
require.NoError(t, err)
|
|
|
|
err = service.Release(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
var row model.KV
|
|
err = db.Take(&row, "key = ?", lockKey).Error
|
|
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
|
|
})
|
|
|
|
t.Run("ignores lock held by another owner", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
existing := lockValue{
|
|
ProcessID: 2,
|
|
HostID: "other-host",
|
|
ExpiresAt: time.Now().Add(ttl).Unix(),
|
|
}
|
|
insertLock(t, db, existing)
|
|
|
|
err := service.Release(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
stored := readLockValue(t, db)
|
|
require.Equal(t, existing, stored)
|
|
})
|
|
}
|
|
|
|
func TestAppLockServiceRenew(t *testing.T) {
|
|
t.Run("extends expiration when lock is still owned", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
_, err := service.Acquire(context.Background(), false)
|
|
require.NoError(t, err)
|
|
|
|
before := readLockValue(t, db)
|
|
|
|
err = service.renew(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
after := readLockValue(t, db)
|
|
require.Equal(t, service.processID, after.ProcessID)
|
|
require.Equal(t, service.hostID, after.HostID)
|
|
require.GreaterOrEqual(t, after.ExpiresAt, before.ExpiresAt)
|
|
})
|
|
|
|
t.Run("returns ErrLockLost when lock is missing", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
err := service.renew(context.Background())
|
|
require.ErrorIs(t, err, ErrLockLost)
|
|
})
|
|
|
|
t.Run("returns ErrLockLost when ownership changed", func(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newTestAppLockService(t, db)
|
|
|
|
_, err := service.Acquire(context.Background(), false)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate a different process taking the lock.
|
|
newOwner := lockValue{
|
|
ProcessID: 9,
|
|
HostID: "stolen-host",
|
|
ExpiresAt: time.Now().Add(ttl).Unix(),
|
|
}
|
|
raw, marshalErr := newOwner.Marshal()
|
|
require.NoError(t, marshalErr)
|
|
updateErr := db.Model(&model.KV{}).
|
|
Where("key = ?", lockKey).
|
|
Update("value", raw).Error
|
|
require.NoError(t, updateErr)
|
|
|
|
err = service.renew(context.Background())
|
|
require.ErrorIs(t, err, ErrLockLost)
|
|
})
|
|
}
|