memos/store/test/user_setting_test.go

658 lines
19 KiB
Go

package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func TestUserSettingStore(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, 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)
list, err := ts.ListUserSettings(ctx, &store.FindUserSetting{})
require.NoError(t, err)
require.Equal(t, 1, len(list))
ts.Close()
}
func TestUserSettingGetByUserID(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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()
}
func TestUserSettingGetUserByPATHash(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create a PAT with a known hash
patHash := "test-pat-hash-12345"
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-test-1",
TokenHash: patHash,
Description: "Test PAT for lookup",
}
err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Lookup user by PAT hash
result, err := ts.GetUserByPATHash(ctx, patHash)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, user.ID, result.UserID)
require.NotNil(t, result.User)
require.Equal(t, user.Username, result.User.Username)
require.NotNil(t, result.PAT)
require.Equal(t, "pat-test-1", result.PAT.TokenId)
require.Equal(t, "Test PAT for lookup", result.PAT.Description)
ts.Close()
}
func TestUserSettingGetUserByPATHashNotFound(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
_, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Lookup non-existent PAT hash
result, err := ts.GetUserByPATHash(ctx, "non-existent-hash")
require.Error(t, err)
require.Nil(t, result)
ts.Close()
}
func TestUserSettingGetUserByPATHashMultipleUsers(t *testing.T) {
t.Parallel()
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)
// Create PATs for both users
pat1Hash := "user1-pat-hash"
err = ts.AddUserPersonalAccessToken(ctx, user1.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-user1",
TokenHash: pat1Hash,
Description: "User 1 PAT",
})
require.NoError(t, err)
pat2Hash := "user2-pat-hash"
err = ts.AddUserPersonalAccessToken(ctx, user2.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-user2",
TokenHash: pat2Hash,
Description: "User 2 PAT",
})
require.NoError(t, err)
// Lookup user1's PAT
result1, err := ts.GetUserByPATHash(ctx, pat1Hash)
require.NoError(t, err)
require.Equal(t, user1.ID, result1.UserID)
require.Equal(t, user1.Username, result1.User.Username)
// Lookup user2's PAT
result2, err := ts.GetUserByPATHash(ctx, pat2Hash)
require.NoError(t, err)
require.Equal(t, user2.ID, result2.UserID)
require.Equal(t, user2.Username, result2.User.Username)
ts.Close()
}
func TestUserSettingGetUserByPATHashMultiplePATsSameUser(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create multiple PATs for the same user
pat1Hash := "first-pat-hash"
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-1",
TokenHash: pat1Hash,
Description: "First PAT",
})
require.NoError(t, err)
pat2Hash := "second-pat-hash"
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-2",
TokenHash: pat2Hash,
Description: "Second PAT",
})
require.NoError(t, err)
// Both PATs should resolve to the same user
result1, err := ts.GetUserByPATHash(ctx, pat1Hash)
require.NoError(t, err)
require.Equal(t, user.ID, result1.UserID)
require.Equal(t, "pat-1", result1.PAT.TokenId)
result2, err := ts.GetUserByPATHash(ctx, pat2Hash)
require.NoError(t, err)
require.Equal(t, user.ID, result2.UserID)
require.Equal(t, "pat-2", result2.PAT.TokenId)
ts.Close()
}
func TestUserSettingUpdatePATLastUsed(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create a PAT
patHash := "pat-hash-for-update"
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-update-test",
TokenHash: patHash,
Description: "PAT for update test",
})
require.NoError(t, err)
// Update last used timestamp
now := timestamppb.Now()
err = ts.UpdatePATLastUsed(ctx, user.ID, "pat-update-test", now)
require.NoError(t, err)
// Verify the update
pats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
require.Len(t, pats, 1)
require.NotNil(t, pats[0].LastUsedAt)
ts.Close()
}
func TestUserSettingGetUserByPATHashWithExpiredToken(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create a PAT with expiration info
patHash := "pat-hash-with-expiry"
expiresAt := timestamppb.Now()
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-expiry-test",
TokenHash: patHash,
Description: "PAT with expiry",
ExpiresAt: expiresAt,
}
err = ts.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Should still be able to look up by hash (expiry check is done at auth level)
result, err := ts.GetUserByPATHash(ctx, patHash)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, user.ID, result.UserID)
require.NotNil(t, result.PAT.ExpiresAt)
ts.Close()
}
func TestUserSettingGetUserByPATHashAfterRemoval(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create a PAT
patHash := "pat-hash-to-remove"
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-remove-test",
TokenHash: patHash,
Description: "PAT to be removed",
})
require.NoError(t, err)
// Verify it exists
result, err := ts.GetUserByPATHash(ctx, patHash)
require.NoError(t, err)
require.NotNil(t, result)
// Remove the PAT
err = ts.RemoveUserPersonalAccessToken(ctx, user.ID, "pat-remove-test")
require.NoError(t, err)
// Should no longer be found
result, err = ts.GetUserByPATHash(ctx, patHash)
require.Error(t, err)
require.Nil(t, result)
ts.Close()
}
func TestUserSettingGetUserByPATHashSpecialCharacters(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create PATs with special characters in hash (simulating real hash values)
testCases := []struct {
tokenID string
tokenHash string
}{
{"pat-special-1", "abc123+/=XYZ"},
{"pat-special-2", "sha256:abcdef1234567890"},
{"pat-special-3", "$2a$10$N9qo8uLOickgx2ZMRZoMy"},
}
for _, tc := range testCases {
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tc.tokenID,
TokenHash: tc.tokenHash,
Description: "PAT with special chars",
})
require.NoError(t, err)
// Verify lookup works with special characters
result, err := ts.GetUserByPATHash(ctx, tc.tokenHash)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, tc.tokenID, result.PAT.TokenId)
}
ts.Close()
}
func TestUserSettingGetUserByPATHashLargeTokenCount(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create many PATs for the same user
tokenCount := 10
hashes := make([]string, tokenCount)
for i := 0; i < tokenCount; i++ {
hashes[i] = "pat-hash-" + string(rune('A'+i)) + "-large-test"
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-large-" + string(rune('A'+i)),
TokenHash: hashes[i],
Description: "PAT for large count test",
})
require.NoError(t, err)
}
// Verify each hash can be looked up
for i, hash := range hashes {
result, err := ts.GetUserByPATHash(ctx, hash)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, user.ID, result.UserID)
require.Equal(t, "pat-large-"+string(rune('A'+i)), result.PAT.TokenId)
}
ts.Close()
}
func TestUserSettingMultipleSettingTypes(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
// Create GENERAL setting
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_GENERAL,
Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "ja"}},
})
require.NoError(t, err)
// Create SHORTCUTS setting
_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_SHORTCUTS,
Value: &storepb.UserSetting_Shortcuts{Shortcuts: &storepb.ShortcutsUserSetting{
Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{
{Id: "s1", Title: "Shortcut 1"},
},
}},
})
require.NoError(t, err)
// Add a PAT
err = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: "pat-multi",
TokenHash: "hash-multi",
})
require.NoError(t, err)
// List all settings for user
settings, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID})
require.NoError(t, err)
require.Len(t, settings, 3)
// Verify each setting type
generalSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_GENERAL})
require.NoError(t, err)
require.Equal(t, "ja", generalSetting.GetGeneral().Locale)
shortcutsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS})
require.NoError(t, err)
require.Len(t, shortcutsSetting.GetShortcuts().Shortcuts, 1)
patsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS})
require.NoError(t, err)
require.Len(t, patsSetting.GetPersonalAccessTokens().Tokens, 1)
ts.Close()
}