package postgres import ( "context" "database/sql" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) // TestGetUserByPATHashWithMissingData tests the fix for #5611 and #5612. // Verifies that GetUserByPATHash handles missing/malformed data gracefully // instead of throwing PostgreSQL JSONB errors. func TestGetUserByPATHashWithMissingData(t *testing.T) { if testing.Short() { t.Skip("Skipping PostgreSQL integration test in short mode") } // This test requires a real PostgreSQL connection // If DSN is not provided, skip the test dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() // Create test database ctx := context.Background() driver := &DB{db: db} // Setup: Create user_setting table if needed _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) // Cleanup defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id IN (1001, 1002, 1003)") }() t.Run("NoTokensKeyAtAll", func(t *testing.T) { // Test case: User has no PERSONAL_ACCESS_TOKENS key // This simulates fresh users or users upgraded from v0.25.3 result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("EmptyTokensArray", func(t *testing.T) { // Insert user with empty tokens array _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", `{"tokens":[]}`) require.NoError(t, err) result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("MalformedJSON", func(t *testing.T) { // Insert user with malformed JSON _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1002, "PERSONAL_ACCESS_TOKENS", `{invalid json}`) require.NoError(t, err) // Should handle gracefully without crashing result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "PAT not found") }) t.Run("MissingTokensField", func(t *testing.T) { // Insert user with valid JSON but missing 'tokens' field _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1003, "PERSONAL_ACCESS_TOKENS", `{"someOtherField":"value"}`) require.NoError(t, err) // Should handle gracefully result, err := driver.GetUserByPATHash(ctx, "any-hash") assert.Error(t, err) assert.Nil(t, result) }) t.Run("ValidTokenFound", func(t *testing.T) { // Insert user with valid PAT validJSON := `{ "tokens": [ { "tokenId": "pat-test", "tokenHash": "hash-test-123", "description": "Test PAT" } ] }` _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", validJSON) require.NoError(t, err) // Should find the token result, err := driver.GetUserByPATHash(ctx, "hash-test-123") assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(1001), result.UserID) assert.Equal(t, "pat-test", result.PAT.TokenId) assert.Equal(t, "hash-test-123", result.PAT.TokenHash) }) t.Run("MultipleUsersWithMixedData", func(t *testing.T) { // User 1001: Valid PAT validJSON := `{ "tokens": [ { "tokenId": "pat-user1", "tokenHash": "hash-user1", "description": "User 1 PAT" } ] }` _, err := db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1001, "PERSONAL_ACCESS_TOKENS", validJSON) require.NoError(t, err) // User 1002: Malformed JSON (should be skipped) _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1002, "PERSONAL_ACCESS_TOKENS", `{invalid}`) require.NoError(t, err) // User 1003: Empty array (should be skipped) _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, 1003, "PERSONAL_ACCESS_TOKENS", `{"tokens":[]}`) require.NoError(t, err) // Should still find user 1001's token despite other users having bad data result, err := driver.GetUserByPATHash(ctx, "hash-user1") assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(1001), result.UserID) }) } // TestGetUserByPATHashPerformance ensures the simplified query doesn't cause performance issues. func TestGetUserByPATHashPerformance(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() ctx := context.Background() driver := &DB{db: db} // Setup table _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) // Cleanup defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id >= 2000 AND user_id < 2100") }() // Insert 100 users with PATs for i := 2000; i < 2100; i++ { json := `{ "tokens": [ { "tokenId": "pat-` + string(rune(i)) + `", "tokenHash": "hash-` + string(rune(i)) + `", "description": "Test PAT" } ] }` _, err = db.ExecContext(ctx, ` INSERT INTO user_setting (user_id, key, value) VALUES ($1, $2, $3) ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value `, i, "PERSONAL_ACCESS_TOKENS", json) require.NoError(t, err) } // Query should complete quickly even with 100 users result, err := driver.GetUserByPATHash(ctx, "hash-"+string(rune(2050))) assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(2050), result.UserID) } // getTestDSN returns PostgreSQL DSN from environment or returns empty string. func getTestDSN() string { // For unit tests, we expect TEST_POSTGRES_DSN to be set. // Example: TEST_POSTGRES_DSN="postgresql://user:pass@localhost:5432/memos_test?sslmode=disable". return "" } // TestUpsertUserSetting tests basic upsert functionality. func TestUpsertUserSetting(t *testing.T) { dsn := getTestDSN() if dsn == "" { t.Skip("PostgreSQL DSN not provided, skipping test") } db, err := sql.Open("postgres", dsn) require.NoError(t, err) defer db.Close() ctx := context.Background() driver := &DB{db: db} // Setup _, err = db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS user_setting ( user_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE(user_id, key) ) `) require.NoError(t, err) defer func() { db.ExecContext(ctx, "DELETE FROM user_setting WHERE user_id = 9999") }() // Test insert setting := &store.UserSetting{ UserID: 9999, Key: storepb.UserSetting_GENERAL, Value: `{"locale":"en"}`, } result, err := driver.UpsertUserSetting(ctx, setting) assert.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int32(9999), result.UserID) // Test update (upsert on conflict) setting.Value = `{"locale":"zh"}` result, err = driver.UpsertUserSetting(ctx, setting) assert.NoError(t, err) assert.Equal(t, `{"locale":"zh"}`, result.Value) }