Files
session_redis_public/session_redis/tests/test_session_store.py
T
Vincent Van Rossem aaabf06b55 [IMP] session_redis: add absolute expiration option
ODOO_SESSION_REDIS_EXPIRATION_ABSOLUTE=1 makes authenticated sessions expire
ODOO_SESSION_REDIS_EXPIRATION seconds after the login, whatever the activity.
2026-09-24 20:18:03 +02:00

142 lines
4.7 KiB
Python

# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from odoo import http
from odoo.tests import BaseCase
from ..session import RedisSessionStore
DAY = 60 * 60 * 24
HOUR = 60 * 60
class FakeRedis:
"""In-memory Redis with a manual clock, for the commands used by the store"""
def __init__(self):
self.now = 0
self.data = {}
self.expire_at = {}
def advance(self, seconds):
self.now += seconds
def _purge(self, key):
expire_at = self.expire_at.get(key)
if expire_at is not None and expire_at <= self.now:
self.data.pop(key, None)
self.expire_at.pop(key, None)
def set(self, key, value, ex=None, xx=False, keepttl=False):
self._purge(key)
if xx and key not in self.data:
return None
self.data[key] = value
if ex is not None:
self.expire_at[key] = self.now + int(ex)
elif not keepttl:
self.expire_at.pop(key, None)
return True
def get(self, key):
self._purge(key)
return self.data.get(key)
def delete(self, key):
self.expire_at.pop(key, None)
return int(self.data.pop(key, None) is not None)
def ttl(self, key):
self._purge(key)
if key not in self.data:
return -2
if key not in self.expire_at:
return -1
return self.expire_at[key] - self.now
class TestRedisSessionStore(BaseCase):
def _get_store(self, **kwargs):
self.redis = FakeRedis()
return RedisSessionStore(
redis=self.redis,
session_class=http.Session,
expiration=7 * DAY,
anon_expiration=3 * HOUR,
**kwargs,
)
def _login(self, store):
session = store.new()
session.update({"uid": 1, "login": "admin"})
store.save(session)
return session
def _ttl(self, store, session):
return self.redis.ttl(store.build_key(session.sid))
def _use(self, store, session):
"""Simulate a request of the user, which touches the session"""
session = store.get(session.sid)
session["context"] = {"lang": "en_US"}
store.save(session)
return session
def test_sliding_expiration(self):
"""By default, each save renews the expiration of the session"""
store = self._get_store()
session = self._login(store)
self.assertEqual(self._ttl(store, session), 7 * DAY)
for __ in range(10):
self.redis.advance(DAY)
session = self._use(store, session)
self.assertEqual(self._ttl(store, session), 7 * DAY)
# still logged in after 10 days of daily use
self.assertEqual(store.get(session.sid).uid, 1)
def test_absolute_expiration(self):
"""The session expires 7 days after the login, whatever the use"""
store = self._get_store(absolute_expiration=True)
session = self._login(store)
sid = session.sid
for day in range(1, 7):
self.redis.advance(DAY)
session = self._use(store, session)
self.assertEqual(session.uid, 1)
self.assertEqual(self._ttl(store, session), (7 - day) * DAY)
self.assertEqual(store.get(sid)["context"], {"lang": "en_US"})
self.redis.advance(DAY)
self.assertFalse(store.get(sid).uid)
def test_absolute_expiration_not_recreated(self):
"""A request ending after the expiration does not revive the session"""
store = self._get_store(absolute_expiration=True)
session = self._login(store)
session = store.get(session.sid)
self.redis.advance(7 * DAY)
session["context"] = {"lang": "en_US"}
store.save(session)
self.assertEqual(self._ttl(store, session), -2)
def test_absolute_expiration_rotation(self):
"""A rotated session (login, logout, MFA) gets a full expiration"""
store = self._get_store(absolute_expiration=True)
session = self._login(store)
old_sid = session.sid
self.redis.advance(3 * DAY)
store.rotate(session, None)
self.assertNotEqual(session.sid, old_sid)
self.assertIsNone(self.redis.get(store.build_key(old_sid)))
self.assertEqual(self._ttl(store, session), 7 * DAY)
def test_absolute_expiration_anonymous(self):
"""Anonymous sessions keep a sliding expiration"""
store = self._get_store(absolute_expiration=True)
session = store.new()
session["debug"] = ""
store.save(session)
self.assertEqual(self._ttl(store, session), 3 * HOUR)
self.redis.advance(2 * HOUR)
session = self._use(store, session)
self.assertEqual(self._ttl(store, session), 3 * HOUR)