From 9a5980e76bf6ac7fb01911fde5a34c7399daf64a Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Sun, 2 Aug 2026 18:39:42 -0400 Subject: [PATCH] fix user cache misses for mixed-case username and email lookups getUserByUsername/getUserByEmail normalize to lowercase before the cache lookup, but the cache comparators matched case-sensitively, so a mixed-case stored username never cache-hit and every login ran an unindexed LOWER() scan. Compare case-insensitively inside the cache. --- server/models/User.js | 8 ++++++-- test/server/models/User.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/server/models/User.js b/server/models/User.js index f71f80f3b..d93ebc02e 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -18,12 +18,16 @@ class UserCache { } getByEmail(email) { - const user = this.cache.find((u) => u.email === email) + if (!email) return null + const normalizedEmail = email.toLowerCase() + const user = this.cache.find((u) => u.email && u.email.toLowerCase() === normalizedEmail) return user } getByUsername(username) { - const user = this.cache.find((u) => u.username === username) + if (!username) return null + const normalizedUsername = username.toLowerCase() + const user = this.cache.find((u) => u.username && u.username.toLowerCase() === normalizedUsername) return user } diff --git a/test/server/models/User.test.js b/test/server/models/User.test.js index 734c31609..be317eb44 100644 --- a/test/server/models/User.test.js +++ b/test/server/models/User.test.js @@ -43,6 +43,38 @@ describe('User model', () => { expect(options.where.attribute.args[0].col).to.equal('email') expect(options.where.logic).to.equal('example.user@example.com') }) + + it('should hit the user cache for mixed-case username lookups', async () => { + User.sequelize = { + models: { + mediaProgress: {} + } + } + const cachedUser = { id: 'cache-test-user-1', username: 'CacheTestUser', email: 'cachetest@example.com', extraData: {} } + const findOneStub = sinon.stub(User, 'findOne').resolves(cachedUser) + + await User.getUserByUsername('CacheTestUser') + await User.getUserByUsername('cachetestuser') + await User.getUserByUsername('CACHETESTUSER') + + expect(findOneStub.callCount).to.equal(1) + }) + + it('should hit the user cache for mixed-case email lookups', async () => { + User.sequelize = { + models: { + mediaProgress: {} + } + } + const cachedUser = { id: 'cache-test-user-2', username: 'CacheEmailUser', email: 'CacheMail@Example.com', extraData: {} } + const findOneStub = sinon.stub(User, 'findOne').resolves(cachedUser) + + await User.getUserByEmail('CacheMail@Example.com') + await User.getUserByEmail('cachemail@example.com') + await User.getUserByEmail('CACHEMAIL@EXAMPLE.COM') + + expect(findOneStub.callCount).to.equal(1) + }) }) describe('getUserByIdOrOldId', () => {