teableio/teable

View on GitHub
apps/nestjs-backend/src/features/auth/session/session-store.service.spec.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/naming-convention */
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { mockDeep, mockReset } from 'vitest-mock-extended';
import { CacheService } from '../../../cache/cache.service';
import type { IAuthConfig } from '../../../configs/auth.config';
import { AuthConfig } from '../../../configs/auth.config';
import { GlobalModule } from '../../../global/global.module';
import type { ISessionData } from '../../../types/session';
import { SessionStoreService } from './session-store.service';
import { SessionModule } from './session.module';

describe('SessionStoreService', () => {
  let sessionStoreService: SessionStoreService;
  const cacheService = mockDeep<CacheService>();
  const authConfig = mockDeep<IAuthConfig>({
    session: { expiresIn: '1d' },
  });
  const sid = 'session-id';
  const sessionData = { passport: { user: { id: 'user-id' } } } as ISessionData;
  const callbackMock = vitest.fn();

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [GlobalModule, SessionModule],
    })
      .overrideProvider(SessionStoreService)
      .useValue(sessionStoreService)
      .overrideProvider(CacheService)
      .useValue(cacheService)
      .overrideProvider(AuthConfig)
      .useValue(authConfig)
      .compile();

    sessionStoreService = module.get<SessionStoreService>(SessionStoreService);
  });

  afterEach(() => {
    vitest.resetAllMocks();
    mockReset(cacheService);
    mockReset(authConfig);
    callbackMock.mockReset();
  });

  it('should be defined', () => {
    expect(sessionStoreService).toBeDefined();
  });

  describe('setCache', () => {
    it('should set cache correctly', async () => {
      cacheService.get.mockResolvedValue({ 'session-id': 1234567890 });

      await sessionStoreService['setCache'](sid, sessionData);

      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-user:user-id`,
        {
          'session-id': Math.floor(Date.now() / 1000) + sessionStoreService['userSessionExpire'],
        },
        expect.any(Number)
      );
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-store:${sid}`,
        sessionData,
        expect.any(Number)
      );
    });

    it('should set cache correctly when userSessions is undefined', async () => {
      cacheService.get.mockResolvedValue(undefined);

      await sessionStoreService['setCache'](sid, sessionData);

      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-user:user-id`,
        {
          'session-id': Math.floor(Date.now() / 1000) + sessionStoreService['userSessionExpire'],
        },
        expect.any(Number)
      );
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-store:${sid}`,
        sessionData,
        expect.any(Number)
      );
    });

    it('should delete user session correctly when session is expired', async () => {
      const userSessions = {
        'session-id': 1234567890,
        'session-id-2': 1,
        'session-id-3':
          Math.floor(Date.now() / 1000) + sessionStoreService['userSessionExpire'] + 1000,
      };
      cacheService.get.mockResolvedValue(userSessions);

      await sessionStoreService['setCache'](sid, sessionData);

      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-user:user-id`,
        {
          'session-id': Math.floor(Date.now() / 1000) + sessionStoreService['userSessionExpire'],
          'session-id-3': userSessions['session-id-3'],
        },
        expect.any(Number)
      );
      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-store:${sid}`,
        sessionData,
        expect.any(Number)
      );
    });
  });

  describe('getCache', () => {
    it('should return null if expire flag is set', async () => {
      // Mock the necessary cacheService methods
      cacheService.get.mockResolvedValueOnce(true);

      const result = await sessionStoreService['getCache'](sid);

      // Verify that cacheService.get was called with the expected parameter
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-expire:${sid}`);
      // Verify that the result is null when the expire flag is set
      expect(result).toBeNull();
    });

    it('should return null if session is not found', async () => {
      // Mock the necessary cacheService methods
      cacheService.get.mockResolvedValueOnce(undefined);
      cacheService.get.mockResolvedValueOnce(undefined);

      const result = await sessionStoreService['getCache'](sid);

      // Verify that cacheService.get was called with the expected parameters
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-expire:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      // Verify that the result is null when session is not found
      expect(result).toBeNull();
    });

    it('should return undefined and delete session if user session is not found', async () => {
      // Mock the necessary cacheService methods
      cacheService.get.mockResolvedValueOnce(undefined);
      cacheService.get.mockResolvedValueOnce(sessionData);
      cacheService.get.mockResolvedValueOnce(undefined);
      cacheService.del.mockResolvedValueOnce();

      const result = await sessionStoreService['getCache'](sid);

      // Verify that cacheService.get and cacheService.del were called with the expected parameters
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-expire:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      // Verify that the result is null and session is deleted when user session is not found
      expect(result).toBeNull();
    });

    it('should return undefined and delete session if user session is expired', async () => {
      // Mock the necessary cacheService methods
      const nowSec = Math.floor(Date.now() / 1000);
      cacheService.get.mockResolvedValueOnce(false);
      cacheService.get.mockResolvedValueOnce(sessionData);
      cacheService.get.mockResolvedValueOnce({ [sid]: nowSec - 1, 'session-id-x': nowSec + 22 }); // Expired user session
      cacheService.del.mockResolvedValueOnce();
      cacheService.del.mockResolvedValueOnce();
      cacheService.set.mockResolvedValueOnce();

      const result = await sessionStoreService['getCache'](sid);

      // Verify that cacheService.get, cacheService.del, and cacheService.set were called with the expected parameters
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-expire:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      cacheService.del.mockResolvedValueOnce();

      expect(cacheService.set).toHaveBeenCalledWith(
        `auth:session-user:user-id`,
        { 'session-id-x': nowSec + 22 },
        expect.any(Number)
      );
      // Verify that the result is null and session is deleted when user session is expired
      expect(result).toBeNull();
    });

    it('should return session if user session is valid', async () => {
      // Mock the necessary cacheService methods
      const nowSec = Math.floor(Date.now() / 1000);
      cacheService.get.mockResolvedValueOnce(undefined);
      cacheService.get.mockResolvedValueOnce(sessionData);
      cacheService.get.mockResolvedValueOnce({ [sid]: nowSec + 1 }); // Valid user session

      const result = await sessionStoreService['getCache'](sid);

      // Verify that cacheService.get was called with the expected parameters
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-expire:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:user-id`);
      // Verify that the result is the expected session when user session is valid
      expect(result).toEqual(sessionData);
    });
  });

  describe('get', () => {
    it('should get session and invoke callback with null error and session', async () => {
      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'getCache').mockResolvedValueOnce(sessionData);

      await sessionStoreService.get(sid, callbackMock);

      // Verify that getCache method was called with the expected parameter
      expect(sessionStoreService['getCache']).toHaveBeenCalledWith(sid);
      // Verify that the callback was invoked with null error and the expected session
      expect(callbackMock).toHaveBeenCalledWith(null, sessionData);
    });

    it('should handle getCache error and invoke callback with error', async () => {
      const error = new Error('Get cache error');

      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'getCache').mockRejectedValueOnce(error);

      await sessionStoreService.get(sid, callbackMock);

      // Verify that getCache method was called with the expected parameter
      expect(sessionStoreService['getCache']).toHaveBeenCalledWith(sid);
      // Verify that the callback was invoked with the expected error
      expect(callbackMock).toHaveBeenCalledWith(error);
    });
  });

  describe('set', () => {
    const callbackMock = vitest.fn();

    afterEach(() => {
      callbackMock.mockReset();
    });

    it('should set cache and call callback', async () => {
      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'setCache').mockResolvedValueOnce(true);

      await sessionStoreService.set(sid, sessionData, callbackMock);

      // Verify that setCache method was called with the expected parameters
      expect(sessionStoreService['setCache']).toHaveBeenCalledWith(sid, sessionData);
      // Verify that the callback was called
      expect(callbackMock).toHaveBeenCalled();
    });

    it('should handle setCache error and call callback with error', async () => {
      const error = new Error('Set cache error');

      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'setCache').mockRejectedValueOnce(error);

      await sessionStoreService.set(sid, sessionData, callbackMock);

      // Verify that setCache method was called with the expected parameters
      expect(sessionStoreService['setCache']).toHaveBeenCalledWith(sid, sessionData);
      // Verify that the callback was called with the error
      expect(callbackMock).toHaveBeenCalledWith(error);
    });
  });

  describe('destroy', () => {
    it('should delete session from cache and call callback', async () => {
      // Mock the necessary methods
      cacheService.del.mockResolvedValueOnce();

      await sessionStoreService.destroy(sid, callbackMock);

      // Verify that cacheService.del method was called with the expected parameter
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      // Verify that the callback was called
      expect(callbackMock).toHaveBeenCalled();
    });

    it('should handle cacheService.del error and call callback with error', async () => {
      const error = new Error('Cache service del error');

      // Mock the necessary methods
      cacheService.del.mockRejectedValueOnce(error);

      await sessionStoreService.destroy(sid, callbackMock);

      // Verify that cacheService.del method was called with the expected parameter
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:${sid}`);
      // Verify that the callback was called with the error
      expect(callbackMock).toHaveBeenCalledWith(error);
    });
  });
  describe('touch', () => {
    it('should touch session, set it, and call callback', async () => {
      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'getCache').mockResolvedValueOnce(sessionData);
      vitest.spyOn(sessionStoreService as any, 'setCache').mockResolvedValueOnce(null);

      await sessionStoreService.touch(sid, sessionData, callbackMock);

      // Verify that getCache and set methods were called with the expected parameters
      expect(sessionStoreService['getCache']).toHaveBeenCalledWith(sid);
      expect(sessionStoreService['setCache']).toHaveBeenCalledWith(sid, sessionData);
      // Verify that the callback was called
      expect(callbackMock).toHaveBeenCalled();
    });

    it('should handle getCache undefined and call callback with error', async () => {
      const error = new Error('Session not found');

      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'getCache').mockResolvedValueOnce(undefined);

      await sessionStoreService.touch(sid, sessionData, callbackMock);

      // Verify that getCache method was called with the expected parameter
      expect(sessionStoreService['getCache']).toHaveBeenCalledWith(sid);
      // Verify that the callback was called with the error
      expect(callbackMock).toHaveBeenCalledWith(error);
    });

    it('should handle getCache error and call callback with error', async () => {
      const error = new Error('Get cache error');

      // Mock the necessary methods
      vitest.spyOn(sessionStoreService as any, 'getCache').mockRejectedValueOnce(error);

      await sessionStoreService.touch(sid, sessionData, callbackMock);

      // Verify that getCache method was called with the expected parameter
      expect(sessionStoreService['getCache']).toHaveBeenCalledWith(sid);
      // Verify that the callback was called with the error
      expect(callbackMock).toHaveBeenCalledWith(error);
    });
  });

  describe('clearByUserId', () => {
    const userId = 'user-id';

    it('should clear user sessions and set expire flag', async () => {
      // Mock the necessary methods
      cacheService.get.mockResolvedValueOnce({ 'session-id': 123 });
      cacheService.set.mockResolvedValueOnce();
      cacheService.del.mockResolvedValueOnce();

      await sessionStoreService.clearByUserId(userId);

      // Verify that cacheService.get, set, and del methods were called with the expected parameters
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:${userId}`);
      expect(cacheService.set).toHaveBeenCalledWith(`auth:session-expire:session-id`, true, 60);
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-store:session-id`);
      expect(cacheService.del).toHaveBeenCalledWith(`auth:session-user:${userId}`);
    });

    it('should handle empty user sessions in clearByUserId method', async () => {
      // Mock the necessary methods
      cacheService.get.mockResolvedValueOnce(undefined);

      await sessionStoreService.clearByUserId(userId);

      // Verify that cacheService.get was called with the expected parameter
      expect(cacheService.get).toHaveBeenCalledWith(`auth:session-user:${userId}`);
    });
  });
});