Initial commit: OCA Server Auth packages (29 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit 3ed80311c4
1325 changed files with 127292 additions and 0 deletions

View file

@ -0,0 +1,7 @@
from . import test_change_password
from . import test_res_users
from . import test_login
from . import test_password_history
from . import test_reset_password
from . import test_signup
from . import test_migration

View file

@ -0,0 +1,147 @@
# Copyright 2023 Onestein (<https://www.onestein.eu>)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import re
from unittest import mock
from odoo import http
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, tagged
@tagged("-at_install", "post_install")
class TestPasswordSecurityChange(HttpCase):
def login(self, username, password):
"""Log in with provided credentials."""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
res_post = self.url_open(
"/web/login",
data={
"login": username,
"password": password,
"csrf_token": http.Request.csrf_token(self),
},
)
res_post.raise_for_status()
return res_post
def test_01_empty_password_fail(self):
"""It should fail when changing password to empty"""
# Log in: ensure we end up on the right page
res_login = self.login("admin", "admin")
self.assertEqual(res_login.request.path_url, "/web")
self.assertEqual(res_login.status_code, 200)
# Change password
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
self.assertTrue(user)
with self.assertRaises(UserError):
# UserError: Setting empty passwords is not allowed for security reasons
user._change_password("")
def test_02_change_password_fail(self):
"""It should fail when changing password to weak"""
# Log in: ensure we end up on the right page
res_login = self.login("admin", "admin")
self.assertEqual(res_login.request.path_url, "/web")
self.assertEqual(res_login.status_code, 200)
# Change password: error is raised because it's too short
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
self.assertTrue(user)
with self.assertRaises(UserError):
user._change_password("admin")
# Change password: error is raised by check password rules
with self.assertRaises(ValidationError):
user._change_password("adminadmin")
def test_03_change_password_session_expired(self):
"""Session expires when password is changed"""
# Log in: ensure we end up on the right page
res_login = self.login("admin", "admin")
self.assertEqual(res_login.request.path_url, "/web")
self.assertEqual(res_login.status_code, 200)
# Reload page: ensure we stay on the same page
res_page1 = self.url_open("/web")
res_page1.raise_for_status()
self.assertEqual(res_page1.request.path_url, "/web")
self.assertEqual(res_page1.status_code, 200)
# Change password: no error raised
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
self.assertTrue(user)
user._change_password("!asdQWE12345_3")
# Try to reload page: user kicked out
res_page2 = self.url_open("/web")
res_page2.raise_for_status()
self.assertEqual(res_page2.request.path_url, "/web/login")
self.assertEqual(res_page2.status_code, 200)
def test_04_change_password_check_password_history(self):
"""It should fail when chosen password was previously used"""
# Set password history limit
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
user.company_id.password_history = 3
self.assertEqual(len(user.password_history_ids), 0)
# Change password: password history records created
user._change_password("!asdQWE12345_4")
self.assertEqual(len(user.password_history_ids), 1)
user._change_password("!asdQWE12345_5")
self.assertEqual(len(user.password_history_ids), 2)
user._change_password("!asdQWE12345_6")
self.assertEqual(len(user.password_history_ids), 3)
user._change_password("!asdQWE12345_7")
self.assertEqual(len(user.password_history_ids), 4)
# Log in: ensure we end up on the right page
res_login = self.login("admin", "!asdQWE12345_7")
self.assertEqual(res_login.request.path_url, "/web")
# Change password: reuse password in history
with self.assertRaises(UserError):
user._change_password("!asdQWE12345_7")
self.assertEqual(len(user.password_history_ids), 4)
# Change password: reuse password in history
with self.assertRaises(UserError):
user._change_password("!asdQWE12345_6")
self.assertEqual(len(user.password_history_ids), 4)
# Change password: reuse password in history
with self.assertRaises(UserError):
user._change_password("!asdQWE12345_5")
self.assertEqual(len(user.password_history_ids), 4)
# Change password: reuse password in history but below limit
user._change_password("!asdQWE12345_4")
self.assertEqual(len(user.password_history_ids), 5)
# Try to log in with old password: it fails
res_login1 = self.login("admin", "!asdQWE12345_7")
self.assertEqual(res_login1.request.path_url, "/web/login")
# Log in with new password: ensure we end up on the right page
res_login2 = self.login("admin", "!asdQWE12345_4")
self.assertEqual(res_login2.request.path_url, "/web")
def test_20_write_password(self):
"""Detects expected singleton errors writing passwords for more than one user"""
users = self.env["res.users"].search([], limit=2)
self.assertEqual(len(users), 2)
res = users.write({"password": "!asdQWE12345"})
self.assertTrue(res)
msg = re.escape(users[0].password_match_message())
with self.assertRaisesRegex(ValidationError, msg):
users.write({"password": "12345678"})

View file

@ -0,0 +1,149 @@
# Copyright 2023 Onestein (<https://www.onestein.eu>)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from datetime import datetime, timedelta
from unittest import mock
from odoo import http, registry
from odoo.exceptions import UserError, ValidationError
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, new_test_user, tagged
@tagged("-at_install", "post_install")
class TestPasswordSecurityLogin(HttpCase):
def setUp(self):
super().setUp()
self.username = "jackoneill"
self.passwd = "!asdQWE12345_3"
# Create user with strong password: no error raised
new_test_user(self.env, self.username, password=self.passwd)
def login(self, username, password):
"""Log in with provided credentials."""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
res_post = self.url_open(
"/web/login",
data={
"login": username,
"password": password,
"csrf_token": http.Request.csrf_token(self),
},
)
res_post.raise_for_status()
return res_post
def test_01_create_user_fail(self):
"""It should fail when creating user with short password"""
# Short password: UserError is raised
with self.assertRaises(UserError):
new_test_user(self.env, "new_user", password="abc")
def test_02_create_user_fail(self):
"""It should fail when creating user with weak password"""
# Weak password: ValidationError is raised
with self.assertRaises(ValidationError):
new_test_user(self.env, "new_user", password="abcdefgh")
def test_03_web_login_success(self):
"""Allow authenticating by login"""
# Log in
response = self.login(self.username, self.passwd)
# Ensure we end up on the right page
self.assertEqual(response.request.path_url, "/web")
self.assertEqual(response.status_code, 200)
def test_04_web_login_fail(self):
"""Fail authenticating with wrong password"""
# Try to log in
response = self.login(self.username, "wrong")
# Ensure we stay on the login page
self.assertEqual(response.request.path_url, "/web/login")
self.assertEqual(response.status_code, 200)
self.assertIn(
"Wrong login/password",
response.text,
)
def test_05_web_login_expire_pass(self):
"""It should expire password if necessary"""
# Make password expired
three_days_ago = datetime.now() - timedelta(days=3)
with registry(get_db_name()).cursor() as cr:
env = self.env(cr)
user = env["res.users"].search([("login", "=", self.username)])
user.password_write_date = three_days_ago
user.company_id.password_expiration = 1
# Try to log in
response = self.login(self.username, self.passwd)
# Ensure we end up on the password reset page
self.assertIn("/web/reset_password", response.request.path_url)
def test_06_web_login_log_out_if_expired(self):
"""It should log out user if password expired"""
# Log in
response = self.login(self.username, self.passwd)
# Ensure we end up on the right page
self.assertEqual(response.request.path_url, "/web")
self.assertEqual(response.status_code, 200)
# Make password expired while still logged in
three_days_ago = datetime.now() - timedelta(days=3)
with registry(get_db_name()).cursor() as cr:
env = self.env(cr)
user = env["res.users"].search([("login", "=", self.username)])
user.password_write_date = three_days_ago
user.company_id.password_expiration = 1
# Try to access just a page
req_page1 = self.url_open("/web")
self.assertEqual(req_page1.request.path_url, "/web")
self.assertEqual(req_page1.status_code, 200)
# Try to log in again
response = self.login(self.username, self.passwd)
# Ensure we end up on the password reset page
self.assertIn("/web/reset_password", response.request.path_url)
# Try to access just a page: user kicked out
req_page2 = self.url_open("/web")
self.assertEqual("/web/login", req_page2.request.path_url)
self.assertEqual(req_page2.status_code, 200)
def test_07_web_login_redirect(self):
"""It should redirect w/ hash to reset after expiration"""
# Emulate password expired
with mock.patch(
"odoo.addons.password_security.models.res_users.ResUsers._password_has_expired"
) as func_password_has_expired:
func_password_has_expired.return_value = True
# Try to log in
response = self.login(self.username, self.passwd)
# Ensure we end up on the password reset page
self.assertIn("/web/reset_password", response.request.path_url)
# Try to access just a page: user kicked out
req_page = self.url_open("/web")
self.assertEqual("/web/login", req_page.request.path_url)
self.assertEqual(req_page.status_code, 200)

View file

@ -0,0 +1,33 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.modules.migration import load_script
from odoo.tests.common import TransactionCase
class PasswordSecurityMigration(TransactionCase):
def test_01_migration(self):
"""Test the migration of the password_length value into minlength"""
# minlength has default value
ICP = self.env["ir.config_parameter"]
old_value = ICP.get_param("auth_password_policy.minlength")
if self.env["res.company"]._fields.get("password_length"):
# set different password_length for multiple companies
company1 = self.env["res.company"].create({"name": "company1"})
company2 = self.env["res.company"].create({"name": "company2"})
company3 = self.env["res.company"].create({"name": "company3"})
company1.password_length = 8
company2.password_length = 15
company3.password_length = 11
# run migration script
mod = load_script(
"password_security/migrations/16.0.1.0.0/pre-migration.py",
"pre-migration",
)
mod.migrate(self.env.cr, "16.0.1.0.0")
# minlength updated to maximum value
new_value = ICP.get_param("auth_password_policy.minlength")
self.assertNotEqual(int(old_value), 15)
self.assertEqual(int(new_value), 15)

View file

@ -0,0 +1,47 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
class TestPasswordHistory(TransactionCase):
def test_check_password_history(self):
# Disable all password checks except for history
set_param = self.env["ir.config_parameter"].sudo().set_param
set_param("auth_password_policy.minlength", 0)
user = self.env.ref("base.user_admin")
user.company_id.update(
{
"password_lower": 0,
"password_history": 1,
"password_numeric": 0,
"password_special": 0,
"password_upper": 0,
}
)
self.assertEqual(len(user.password_history_ids), 0)
user.write({"password": "admin"})
self.assertEqual(len(user.password_history_ids), 1)
with self.assertRaises(UserError):
user.write({"password": "admin"})
user.write({"password": "admit"})
self.assertEqual(len(user.password_history_ids), 2)
user.company_id.password_history = 2
with self.assertRaises(UserError):
user.write({"password": "admin"})
with self.assertRaises(UserError):
user.write({"password": "admit"})
user.write({"password": "badminton"})
self.assertEqual(len(user.password_history_ids), 3)
user.company_id.password_history = 0
user.write({"password": "badminton"})
self.assertEqual(len(user.password_history_ids), 4)
user.company_id.password_history = -1
with self.assertRaises(UserError):
user.write({"password": "admin"})

View file

@ -0,0 +1,166 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import time
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
class TestResUsers(TransactionCase):
def setUp(self):
super(TestResUsers, self).setUp()
self.login = "foslabs@example.com"
self.partner_vals = {
"name": "Partner",
"is_company": False,
"email": self.login,
}
self.password = "asdQWE123$%^"
self.main_comp = self.env.ref("base.main_company")
self.vals = {
"name": "User",
"login": self.login,
"password": self.password,
"company_id": self.main_comp.id,
}
self.model_obj = self.env["res.users"]
def _new_record(self):
partner_id = self.env["res.partner"].create(self.partner_vals)
self.vals["partner_id"] = partner_id.id
return self.model_obj.create(self.vals)
def test_password_write_date_is_saved_on_create(self):
rec_id = self._new_record()
self.assertTrue(
rec_id.password_write_date,
"Password write date was not saved to db.",
)
def test_password_write_date_is_updated_on_write(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({"password": "asdQWE123$%^2"})
rec_id.invalidate_recordset()
new_write_date = rec_id.password_write_date
self.assertNotEqual(
old_write_date,
new_write_date,
"Password write date was not updated on write.",
)
def test_does_not_update_write_date_if_password_unchanged(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({"name": "Luser"})
rec_id.invalidate_recordset()
new_write_date = rec_id.password_write_date
self.assertEqual(
old_write_date,
new_write_date,
"Password not changed but write date updated anyway.",
)
def test_check_password_returns_true_for_valid_password(self):
rec_id = self._new_record()
self.assertTrue(
rec_id._check_password("asdQWE123$%^3"),
"Password is valid but check failed.",
)
def test_check_password_raises_error_for_invalid_password(self):
rec_id = self._new_record()
with self.assertRaises(UserError):
rec_id._check_password("password")
def test_save_password_crypt(self):
rec_id = self._new_record()
self.assertEqual(
1,
len(rec_id.password_history_ids),
)
def test_check_password_crypt(self):
"""It should raise UserError if previously used"""
rec_id = self._new_record()
with self.assertRaises(UserError):
rec_id.write({"password": self.password})
def test_password_is_expired_if_record_has_no_write_date(self):
rec_id = self._new_record()
rec_id.write({"password_write_date": None})
rec_id.invalidate_recordset()
self.assertTrue(
rec_id._password_has_expired(),
"Record has no password write date but check failed.",
)
def test_an_old_password_is_expired(self):
rec_id = self._new_record()
old_write_date = "1970-01-01 00:00:00"
rec_id.write({"password_write_date": old_write_date})
rec_id.invalidate_recordset()
self.assertTrue(
rec_id._password_has_expired(),
"Password is out of date but check failed.",
)
def test_a_new_password_is_not_expired(self):
rec_id = self._new_record()
self.assertFalse(
rec_id._password_has_expired(),
"Password was just created but has already expired.",
)
def test_expire_password_generates_token(self):
rec_id = self._new_record()
rec_id.sudo().action_expire_password()
rec_id.invalidate_recordset()
token = rec_id.partner_id.signup_token
self.assertTrue(
token,
"A token was not generated.",
)
def test_validate_pass_reset_error(self):
"""It should throw UserError on reset inside min threshold"""
rec_id = self._new_record()
with self.assertRaises(UserError):
rec_id._validate_pass_reset()
def test_validate_pass_reset_allow(self):
"""It should allow reset pass when outside threshold"""
rec_id = self._new_record()
rec_id.password_write_date = "2016-01-01"
self.assertEqual(
True,
rec_id._validate_pass_reset(),
)
def test_validate_pass_reset_zero(self):
"""It should allow reset pass when <= 0"""
rec_id = self._new_record()
rec_id.company_id.password_minimum = 0
self.assertEqual(
True,
rec_id._validate_pass_reset(),
)
def test_underscore_is_special_character(self):
self.assertTrue(self.main_comp.password_special)
rec_id = self._new_record()
rec_id._check_password("asdQWE12345_3")
def test_user_with_admin_rights_can_create_users(self):
demo = self.env.ref("base.user_demo")
demo.groups_id |= self.env.ref("base.group_erp_manager")
test1 = self.model_obj.with_user(demo).create(
{
"login": "test1",
"name": "test1",
}
)
test1.unlink()

View file

@ -0,0 +1,87 @@
# Copyright 2023 Onestein (<https://www.onestein.eu>)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from unittest import mock
from odoo import http
from odoo.exceptions import UserError
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, new_test_user, tagged
@tagged("-at_install", "post_install")
class TestPasswordSecurityReset(HttpCase):
def setUp(self):
super().setUp()
# Create user with strong password: no error raised
new_test_user(self.env, "jackoneill", password="!asdQWE12345_3")
def reset_password(self, username):
"""Reset user password"""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
res_post = self.url_open(
"/web/reset_password",
data={
"login": username,
"name": username,
"csrf_token": http.Request.csrf_token(self),
},
)
res_post.raise_for_status()
return res_post
def test_01_reset_password_fail(self):
"""It should fail when reset password below Minimum Hours"""
# Enable check on Minimum Hours
self.env.company.password_minimum = 24
# Reset password
response = self.reset_password("jackoneill")
# Ensure we stay in the reset password page
self.assertEqual(response.request.path_url, "/web/reset_password")
self.assertEqual(response.status_code, 200)
self.assertIn(
"Passwords can only be reset every %s hour(s). "
"Please contact an administrator for assistance."
% self.env.company.password_minimum,
response.text,
)
def test_02_reset_password_success(self):
"""It should succeed when check on Minimum Hours is disabled"""
# Disable check on Minimum Hours
self.env.company.password_minimum = 0
# Reset password
response = self.reset_password("jackoneill")
# Password reset instructions sent to user's email
self.assertEqual(response.request.path_url, "/web/reset_password")
self.assertEqual(response.status_code, 200)
self.assertIn(
"Password reset instructions sent to your email",
response.text,
)
def test_03_reset_password_admin(self):
"""It should succeed when reset password is executed by Admin"""
# Enable check on Minimum Hours
self.env.company.password_minimum = 24
# Executed by Admin: no error is raised
self.assertTrue(self.env.user._is_admin())
self.env["res.users"].reset_password("demo")
# Executed by non-admin user: error is raised
self.env = self.env(user=self.env.ref("base.user_demo"))
self.assertFalse(self.env.user._is_admin())
with self.assertRaises(UserError):
self.env["res.users"].reset_password("demo")

View file

@ -0,0 +1,184 @@
# Copyright 2016 LasLabs Inc.
# Copyright 2023 Onestein (<https://www.onestein.eu>)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from unittest import mock
from freezegun import freeze_time
from requests.exceptions import HTTPError
from odoo import http
from odoo.exceptions import ValidationError
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, tagged
from odoo.addons.auth_signup.models.res_users import SignupError
class EndTestException(Exception):
"""It allows for isolation of resources by raise"""
@tagged("-at_install", "post_install")
class TestPasswordSecuritySignup(HttpCase):
def signup(self, username, password):
"""Signup user"""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
res_post = self.url_open(
"/web/signup",
data={
"login": username,
"name": username,
"password": password,
"confirm_password": password,
"csrf_token": http.Request.csrf_token(self),
},
)
res_post.raise_for_status()
return res_post
def test_01_signup_user_fail(self):
"""It should fail when signup user with weak password"""
# Weak password: signup failed
response = self.signup("jackoneill", "jackoneill")
# Ensure we stay in the signup page
self.assertEqual(response.request.path_url, "/web/signup")
self.assertEqual(response.status_code, 200)
self.assertIn(
"Must contain the following:",
response.text,
)
def test_02_signup_user_success(self):
"""It should succeed when signup user with strong password"""
# Weak password: signup failed
response = self.signup("jackoneill", "!asdQWE12345_3")
# Ensure we were logged in
self.assertEqual(
response.request.path_url, "/web/login_successful?account_created=True"
)
self.assertEqual(response.status_code, 200)
def test_03_create_user_signup(self):
"""Password is checked when signup to create a new user"""
partner = self.env["res.partner"].create({"name": "test partner"})
vals = {
"name": "Test User",
"login": "test_user",
"email": "test_user@odoo.com",
"password": "test_user_password",
"partner_id": partner.id,
}
# Weak password: SignupError is raised
with self.assertRaises(SignupError):
self.env["res.users"].signup(vals)
# Stronger password: no error raised
vals["password"] = "asdQWE12345_3"
with freeze_time("2020-01-01"):
login, pwd = self.env["res.users"].signup(vals)
# check created user
created_user = self.env["res.users"].search([("login", "=", "test_user")])
self.assertEqual(login, "test_user")
password_write_date = created_user.password_write_date
self.assertTrue(password_write_date)
# Weak password: ValidationError is raised
with self.assertRaises(ValidationError):
created_user.password = "test_user_password"
self.assertEqual(password_write_date, created_user.password_write_date)
# Stronger password: no error raised
created_user.password = "!asdQWE12345_3"
self.assertNotEqual(password_write_date, created_user.password_write_date)
def test_04_web_auth_signup_invalid_qcontext(self):
"""It should catch AttributeError"""
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
with self.assertRaises(AttributeError):
# 'TestPasswordSecuritySignup' object has no attribute 'session'
self.url_open(
"/web/signup",
data={
"csrf_token": http.Request.csrf_token(self),
},
)
def test_05_web_auth_signup_invalid_qcontext(self):
"""It should catch EndTestException on signup qcontext"""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch(
"odoo.addons.auth_signup.controllers.main.AuthSignupHome.get_auth_signup_qcontext"
) as qcontext:
qcontext.side_effect = EndTestException
with self.assertRaises(HTTPError):
# Catch HTTPError: 400 Client Error: BAD REQUEST
self.signup("jackoneill", "!asdQWE12345_3")
def test_06_web_auth_signup_invalid_render(self):
"""It should render & return signup form on invalid"""
self.session = http.root.session_store.new()
self.opener = Opener(self.env.cr)
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
with mock.patch("odoo.http.db_filter") as db_filter:
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
# Signup: no name or partner given for new user
response = self.url_open(
"/web/signup",
data={
"login": "test@test.com",
"password": "!asdQWE12345_7",
"confirm_password": "!asdQWE12345_7",
"csrf_token": http.Request.csrf_token(self),
},
)
# Ensure we stay in the signup page
self.assertEqual(response.request.path_url, "/web/signup")
self.assertEqual(response.status_code, 200)
self.assertIn(
"Signup: no name or partner given for new user",
response.text,
)
self.assertIn("X-Frame-Options", response.headers)
self.assertEqual(response.headers["X-Frame-Options"], "SAMEORIGIN")
self.assertIn("Content-Security-Policy", response.headers)
self.assertEqual(
response.headers["Content-Security-Policy"], "frame-ancestors 'self'"
)
def test_07_cloned_user_password_write_date(self):
"""Users that are cloned should have their password_write_date updated"""
partner = self.env["res.partner"].create({"name": "test partner"})
vals = {
"name": "Test User",
"login": "test_user",
"email": "test_user@odoo.com",
"password": "Test_user_password123$",
"partner_id": partner.id,
}
with freeze_time("2020-01-01"):
self.env["res.users"].signup(vals)
original_user = self.env["res.users"].search([("login", "=", "test_user")])
copied_user = original_user.copy()
self.assertTrue(
copied_user.password_write_date > original_user.password_write_date
)