Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mail_environment/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from .hooks import uninstall_hook
1 change: 1 addition & 0 deletions mail_environment/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
"license": "AGPL-3",
"website": "https://github.com/OCA/server-env",
"depends": ["mail", "server_environment"],
"uninstall_hook": "uninstall_hook",
}
41 changes: 41 additions & 0 deletions mail_environment/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
def uninstall_hook(env):
"""Restore database columns that server.env.mixin dropped for mail models.

When mail_environment is uninstalled, ``ir.mail_server`` and
``fetchmail.server`` would be left without the columns that the ORM
dropped when this addon was first installed. This hook recreates those
columns and repopulates them with the current effective values so the
database remains usable after removal.
"""
mixin = env["server.env.mixin"]
mixin.restore_env_managed_columns(
"ir.mail_server",
[
"smtp_host",
"smtp_port",
"smtp_user",
"smtp_pass",
"smtp_encryption",
"smtp_authentication",
],
field_defaults={
"smtp_authentication": "login", # Fallback for required field
},
)
mixin.restore_env_managed_columns(
"fetchmail.server",
[
"server",
"port",
"server_type",
"user",
"password",
"is_ssl",
"attach",
"original",
],
field_defaults={
"server": "localhost", # Fallback for required field
"server_type": "imap", # Fallback for required field
},
)
205 changes: 205 additions & 0 deletions mail_environment/tests/test_mail_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)


from odoo.tools import sql

from odoo.addons.server_environment.tests.common import ServerEnvironmentCase

fetchmail_config = """
Expand Down Expand Up @@ -75,3 +77,206 @@ def test_fetchmail_search_is_ssl(self):
fetchmail2,
self.env["fetchmail.server"].search([("is_ssl", "!=", True)]),
)


_outgoing_config = """
[outgoing_mail.test_outgoing]
smtp_host = smtp.example.com
smtp_port = 587
smtp_encryption = starttls
smtp_authentication = login
smtp_user = testuser
smtp_pass = testpass
"""

_incoming_config = """
[incoming_mail.test_incoming]
server = imap.example.com
port = 993
server_type = imap
is_ssl = 1
user = imap_user
password = imap_pass
attach = 1
original = 0
"""


class TestRestoreEnvManagedColumns(ServerEnvironmentCase):
"""Test column restoration performed by the uninstall hook."""

def _drop_columns(self, model_name, field_names):
"""Drop columns created by restore_env_managed_columns (test cleanup)."""
model = self.env[model_name]
cr = self.env.cr
for field_name in field_names:
if sql.column_exists(cr, model._table, field_name):
cr.execute( # noqa: S608
f'ALTER TABLE {model._table} DROP COLUMN "{field_name}"'
)

def test_restore_ir_mail_server_columns(self):
"""Outgoing mail columns are recreated and populated with config values."""
field_names = [
"smtp_host",
"smtp_port",
"smtp_encryption",
"smtp_authentication",
"smtp_user",
"smtp_pass",
]
server = self.env["ir.mail_server"].create({"name": "test_outgoing"})
try:
with self.load_config(public=_outgoing_config):
self.env["server.env.mixin"].restore_env_managed_columns(
"ir.mail_server", field_names
)
table = self.env["ir.mail_server"]._table
for field_name in field_names:
self.assertTrue(
sql.column_exists(self.env.cr, table, field_name),
f"Column {field_name} was not created",
)
self.env.cr.execute(
"SELECT smtp_host, smtp_port, smtp_encryption,"
" smtp_authentication, smtp_user, smtp_pass"
" FROM ir_mail_server WHERE id = %s",
[server.id],
)
row = self.env.cr.dictfetchone()
self.assertEqual(row["smtp_host"], "smtp.example.com")
self.assertEqual(row["smtp_port"], 587)
self.assertEqual(row["smtp_encryption"], "starttls")
self.assertEqual(row["smtp_authentication"], "login")
self.assertEqual(row["smtp_user"], "testuser")
self.assertEqual(row["smtp_pass"], "testpass")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_ir_mail_server_columns_with_default(self):
"""Columns are populated with default values when no config is loaded."""
field_names = ["smtp_host", "smtp_port"]
# Write via the inverse to set the x_smtp_host_env_default sparse field.
server = self.env["ir.mail_server"].create(
{"name": "test_default_outgoing", "smtp_host": "default.example.com"}
)
try:
# No config loaded — values come from x_smtp_host_env_default.
self.env["server.env.mixin"].restore_env_managed_columns(
"ir.mail_server", field_names
)
table = self.env["ir.mail_server"]._table
self.assertTrue(sql.column_exists(self.env.cr, table, "smtp_host"))
self.env.cr.execute(
"SELECT smtp_host FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertEqual(self.env.cr.fetchone()[0], "default.example.com")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_fetchmail_server_columns(self):
"""Incoming mail columns are recreated and populated with config values."""
field_names = [
"server",
"port",
"server_type",
"is_ssl",
"user",
"password",
"attach",
"original",
]
fetchmail = self.env["fetchmail.server"].create({"name": "test_incoming"})
try:
with self.load_config(public=_incoming_config):
self.env["server.env.mixin"].restore_env_managed_columns(
"fetchmail.server", field_names
)
table = self.env["fetchmail.server"]._table
for field_name in field_names:
self.assertTrue(
sql.column_exists(self.env.cr, table, field_name),
f"Column {field_name} was not created",
)
self.env.cr.execute(
'SELECT server, port, server_type, is_ssl, "user",'
" password, attach, original"
" FROM fetchmail_server WHERE id = %s",
[fetchmail.id],
)
row = self.env.cr.dictfetchone()
self.assertEqual(row["server"], "imap.example.com")
self.assertEqual(row["port"], 993)
self.assertEqual(row["server_type"], "imap")
self.assertTrue(row["is_ssl"])
self.assertEqual(row["user"], "imap_user")
self.assertEqual(row["password"], "imap_pass")
self.assertTrue(row["attach"])
self.assertFalse(row["original"])
finally:
self._drop_columns("fetchmail.server", field_names)

def test_restore_env_managed_columns_idempotent(self):
"""Calling restore_env_managed_columns twice is safe and idempotent."""
field_names = ["smtp_host", "smtp_port"]
self.env["ir.mail_server"].create({"name": "test_idempotent"})
try:
with self.load_config(public=_outgoing_config):
mixin = self.env["server.env.mixin"]
mixin.restore_env_managed_columns("ir.mail_server", field_names)
# Second call must not raise or corrupt data.
mixin.restore_env_managed_columns("ir.mail_server", field_names)
table = self.env["ir.mail_server"]._table
for field_name in field_names:
self.assertTrue(sql.column_exists(self.env.cr, table, field_name))
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_with_fallback_defaults(self):
"""Required fields can be restored with fallback values via field_defaults."""

field_names = ["smtp_authentication"]
server = self.env["ir.mail_server"].create({"name": "test_fallback"})
try:
# smtp_authentication is required but we provide no config or default.
# Without field_defaults, this would raise UserError.
# With field_defaults, it should succeed.
self.env["server.env.mixin"].restore_env_managed_columns(
"ir.mail_server",
field_names,
field_defaults={"smtp_authentication": "login"},
)
table = self.env["ir.mail_server"]._table
column_exists = sql.column_exists(self.env.cr, table, "smtp_authentication")
self.assertTrue(column_exists)
self.env.cr.execute(
"SELECT smtp_authentication FROM ir_mail_server WHERE id = %s",
[server.id],
)
self.assertEqual(self.env.cr.fetchone()[0], "login")
finally:
self._drop_columns("ir.mail_server", field_names)

def test_restore_env_managed_columns_required_field_no_fallback(self):
"""Restoring a required field without value and no fallback raises UserError."""
from odoo.exceptions import UserError

field_names = ["smtp_authentication"]
self.env["ir.mail_server"].create({"name": "test_no_fallback"})
try:
# smtp_authentication is required but we have no config, no default,
# and no field_defaults provided. This should raise UserError.
with self.assertRaises(UserError) as cm:
self.env["server.env.mixin"].restore_env_managed_columns(
"ir.mail_server", field_names
)
self.assertIn("smtp_authentication", str(cm.exception))
self.assertIn("field_defaults", str(cm.exception))
finally:
# Manually drop in case the column was partially created.
model = self.env["ir.mail_server"]
if sql.column_exists(self.env.cr, model._table, "smtp_authentication"):
self.env.cr.execute( # noqa: S608
f'ALTER TABLE {model._table} DROP COLUMN "smtp_authentication"'
)
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
odoo-test-helper
odoo-addon-server_environment @ git+https://github.com/OCA/server-env@refs/pull/261/head#subdirectory=server_environment
Loading