diff --git a/test/server/auth/TokenManager.test.js b/test/server/auth/TokenManager.test.js index ba594be52..38c739d09 100644 --- a/test/server/auth/TokenManager.test.js +++ b/test/server/auth/TokenManager.test.js @@ -1,12 +1,10 @@ const { expect } = require('chai') const sinon = require('sinon') -const { Sequelize } = require('sequelize') const Database = require('../../../server/Database') const jwt = require('../../../server/libs/jsonwebtoken') -// TokenManager is first loaded via Database → Auth → TokenManager while Database is still -// initializing, so it can capture a stale Database reference. Reload after Database is ready. +// Database → Auth → TokenManager circular require can leave TokenManager with a partial Database reference; reload before each test function loadTokenManager() { delete require.cache[require.resolve('../../../server/auth/TokenManager')] return require('../../../server/auth/TokenManager') @@ -14,6 +12,7 @@ function loadTokenManager() { describe('TokenManager', () => { const secret = 'test-jwt-secret' + const userId = 'user-uuid-1' let TokenManager let tokenManager @@ -23,126 +22,47 @@ describe('TokenManager', () => { tokenManager = new TokenManager() }) - describe('isBearerAccessTokenPayload', () => { - const userId = 'user-uuid-1' - - it('rejects refresh tokens', () => { - expect(TokenManager.isBearerAccessTokenPayload({ userId, type: 'refresh' })).to.equal(false) - }) - - it('accepts access tokens', () => { - expect(TokenManager.isBearerAccessTokenPayload({ userId, type: 'access' })).to.equal(true) - }) - - it('accepts legacy tokens without type', () => { - expect(TokenManager.isBearerAccessTokenPayload({ userId })).to.equal(true) - }) - - it('rejects payloads without userId', () => { - expect(TokenManager.isBearerAccessTokenPayload({ type: 'access' })).to.equal(false) - }) + afterEach(() => { + sinon.restore() }) describe('validateAccessToken', () => { - const userId = 'user-uuid-1' - - it('returns null for a refresh token', () => { + it('rejects refresh tokens', () => { const refreshToken = jwt.sign({ userId, type: 'refresh' }, secret, { expiresIn: 3600 }) expect(TokenManager.validateAccessToken(refreshToken)).to.equal(null) }) - it('returns decoded payload for an access token', () => { + it('accepts access tokens', () => { const accessToken = jwt.sign({ userId, type: 'access' }, secret, { expiresIn: 3600 }) const decoded = TokenManager.validateAccessToken(accessToken) expect(decoded.userId).to.equal(userId) expect(decoded.type).to.equal('access') }) - - it('returns decoded payload for a legacy token without type', () => { - const legacyToken = jwt.sign({ userId, username: 'testuser' }, secret) - const decoded = TokenManager.validateAccessToken(legacyToken) - expect(decoded.userId).to.equal(userId) - expect(decoded.type).to.equal(undefined) - }) }) describe('jwtAuthCheck', () => { - let user + const user = { id: userId, username: 'testuser', isActive: true } - beforeEach(async () => { - Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) - Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') - await Database.buildModels() - - user = await Database.userModel.create({ - username: 'tokenuser', - pash: 'hash', - token: 'legacy-token', - type: 'user', - isActive: true, - permissions: Database.userModel.getDefaultPermissionsForUserType('user'), - bookmarks: [], - extraData: {} - }) - }) - - afterEach(async () => { - if (Database.sequelize) { - await Database.sequelize.sync({ force: true }) - Database.sequelize = null - } - sinon.restore() - }) - - it('denies refresh tokens on resource authentication', async () => { - const done = sinon.spy() + it('rejects refresh tokens for API auth', async () => { const refreshToken = tokenManager.generateRefreshToken(user) const decoded = jwt.verify(refreshToken, secret) + const done = sinon.spy() await tokenManager.jwtAuthCheck(decoded, done) - expect(done.calledOnce).to.equal(true) - expect(done.firstCall.args).to.deep.equal([null, null]) + expect(done.calledWith(null, null)).to.be.true }) it('allows access tokens for active users', async () => { - const done = sinon.spy() - const accessToken = tokenManager.generateTempAccessToken(user) - const decoded = jwt.verify(accessToken, secret) - - await tokenManager.jwtAuthCheck(decoded, done) - - expect(done.calledOnce).to.equal(true) - expect(done.firstCall.args[1].id).to.equal(user.id) - }) - - it('allows legacy tokens without type for active users', async () => { - const done = sinon.spy() - const legacyToken = TokenManager.generateAccessToken({ id: user.id, username: user.username }) - const decoded = jwt.verify(legacyToken, secret) - - await tokenManager.jwtAuthCheck(decoded, done) - - expect(done.calledOnce).to.equal(true) - expect(done.firstCall.args[1].id).to.equal(user.id) - expect(done.firstCall.args[1].isOldToken).to.equal(true) - }) - - it('authenticates API keys via the api branch', async () => { - const apiKey = await Database.apiKeyModel.create({ - name: 'test-key', - userId: user.id, - isActive: true, - expiresAt: null - }) - const apiToken = jwt.sign({ type: 'api', keyId: apiKey.id, exp: Math.floor(Date.now() / 1000) + 3600 }, secret) - const decoded = jwt.verify(apiToken, secret) + sinon.stub(Database, 'userModel').get(() => ({ + getUserByIdOrOldId: sinon.stub().resolves(user) + })) + const decoded = jwt.verify(tokenManager.generateTempAccessToken(user), secret) const done = sinon.spy() await tokenManager.jwtAuthCheck(decoded, done) - expect(done.calledOnce).to.equal(true) - expect(done.firstCall.args[1].id).to.equal(user.id) + expect(done.calledWith(null, user)).to.be.true }) }) })