Initial commit: OCA Ai packages (4 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 0adb4b78b1
170 changed files with 12385 additions and 0 deletions

View file

@ -0,0 +1,4 @@
from . import test_bridge
from . import test_frontend
from . import test_connection
from . import test_mixin

View file

@ -0,0 +1,9 @@
from odoo import fields, models
class BridgeTest(models.Model):
_name = "bridge.test"
_inherit = "ai.bridge.thread"
_description = "Test Model for AI Bridge"
name = fields.Char()

View file

@ -0,0 +1,367 @@
# Copyright 2025 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json
from unittest import mock
from odoo.tests.common import TransactionCase
class TestBridge(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.bridge = cls.env["ai.bridge"].create(
{
"name": "Test Bridge",
"model_id": cls.env.ref("base.model_res_partner").id,
"url": "https://example.com/api",
"auth_type": "none",
"usage": "thread",
}
)
# We add this in order to simplify tests, as jsons will be filled.
cls.bridge_extra = cls.env["ai.bridge"].create(
{
"name": "Test Bridge Extra",
"model_id": cls.env.ref("base.model_res_partner").id,
"url": "https://example.com/api",
"auth_type": "none",
"usage": "thread",
}
)
cls.partner = cls.env["res.partner"].create(
{
"name": "Test Partner",
"email": "test@example.com",
}
)
cls.group = cls.env["res.groups"].create(
{
"name": "Test Group",
}
)
def test_bridge_none_auth(self):
self.assertEqual(self.bridge.auth_type, "none")
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertTrue(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertEqual(execution.res_id, self.partner.id)
self.assertNotIn("name", execution.payload)
def test_bridge_none_auth_fields_record_v0(self):
self.bridge.write(
{
"payload_type": "record_v0",
"auth_type": "none",
"field_ids": [
(4, self.env.ref("base.field_res_partner__name").id),
(4, self.env.ref("base.field_res_partner__create_date").id),
(4, self.env.ref("base.field_res_partner__image_1920").id),
],
}
)
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertTrue(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertEqual(execution.res_id, self.partner.id)
self.assertIn("name", execution.payload)
self.assertEqual(execution.payload["name"], self.partner.name)
self.assertEqual(1, self.bridge.execution_count)
def test_bridge_none_auth_fields_record(self):
self.bridge.write(
{
"payload_type": "record",
"auth_type": "none",
"field_ids": [
(4, self.env.ref("base.field_res_partner__name").id),
(4, self.env.ref("base.field_res_partner__create_date").id),
(4, self.env.ref("base.field_res_partner__image_1920").id),
],
}
)
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertTrue(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertEqual(execution.res_id, self.partner.id)
self.assertIn("name", execution.payload["record"])
self.assertEqual(execution.payload["record"]["name"], self.partner.name)
self.assertEqual(1, self.bridge.execution_count)
def test_bridge_basic_auth(self):
self.bridge.write(
{
"auth_type": "basic",
"auth_username": "test_user",
"auth_password": "test_pass",
}
)
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertTrue(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
def test_bridge_token_auth(self):
self.bridge.write(
{
"auth_type": "token",
"auth_token": "test_token",
}
)
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertTrue(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
def test_bridge_error(self):
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertTrue(execution)
self.assertTrue(execution.error)
def test_bridge_unactive(self):
self.bridge.toggle_active()
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertFalse(execution)
def test_bridge_check_group(self):
self.bridge.group_ids = [(4, self.group.id)]
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertFalse(execution)
def test_bridge_domain_filtering(self):
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.bridge.write({"domain": f"[('id', '!=', {self.partner.id})]"})
self.partner.invalidate_recordset()
self.assertNotIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
def test_bridge_group_filtering(self):
self.assertTrue(self.partner.ai_bridge_info)
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.bridge.write({"group_ids": [(4, self.group.id)]})
self.partner.invalidate_recordset()
self.assertNotIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
self.env.user.groups_id |= self.group
self.partner.invalidate_recordset()
self.assertIn(
self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info]
)
def test_view_fields(self):
view = self.partner.get_view(view_type="form")
self.assertIn("ai_bridge_info", view["models"][self.partner._name])
self.assertIn(b'name="ai_bridge_info"', view["arch"])
def test_sample(self):
self.assertTrue(self.bridge.sample_payload)
self.assertIn("_id", self.bridge.sample_payload)
def test_bridge_result_message(self):
self.bridge.write({"result_type": "message"})
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
message_count = self.env["mail.message"].search_count(
[("model", "=", self.partner._name), ("res_id", "=", self.partner.id)]
)
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200, json=lambda: {"body": "My message"}
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertEqual(
self.env["mail.message"].search_count(
[("model", "=", self.partner._name), ("res_id", "=", self.partner.id)]
),
message_count + 1,
)
def test_bridge_result_message_async(self):
self.bridge.write({"result_type": "message", "result_kind": "async"})
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
message_count = self.env["mail.message"].search_count(
[("model", "=", self.partner._name), ("res_id", "=", self.partner.id)]
)
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200, json=lambda: {"body": "My message"}
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertEqual(
self.env["mail.message"].search_count(
[("model", "=", self.partner._name), ("res_id", "=", self.partner.id)]
),
message_count,
)
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertTrue(execution.expiration_date)
execution._process_response({"body": "My message"})
self.assertEqual(
self.env["mail.message"].search_count(
[("model", "=", self.partner._name), ("res_id", "=", self.partner.id)]
),
message_count + 1,
)
self.assertFalse(execution.expiration_date)
def test_bridge_result_action_immediate(self):
self.bridge.write({"result_type": "action", "result_kind": "immediate"})
self.assertFalse(
self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
)
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200,
json=lambda: {
"action": "ai_oca_bridge.ai_bridge_act_window",
"context": {"key": "value"},
},
)
result = self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
self.assertIn("action", result)
self.assertEqual(
result["action"]["id"],
self.env.ref("ai_oca_bridge.ai_bridge_act_window").id,
)
def test_bridge_execute_computed_fields(self):
with mock.patch("requests.post") as mock_post:
mock_post.return_value = mock.Mock(
status_code=200, json=lambda: {"body": "My message"}
)
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
mock_post.assert_called_once()
execution = self.env["ai.bridge.execution"].search(
[("ai_bridge_id", "=", self.bridge.id)]
)
self.assertEqual(
execution.payload["_id"], json.loads(execution.payload_txt)["_id"]
)

View file

@ -0,0 +1,116 @@
# Copyright 2025 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from unittest import mock
from werkzeug import urls
from odoo.tests.common import HttpCase, tagged
@tagged("post_install", "-at_install")
class TestAsyncConnection(HttpCase):
def setUp(self):
super().setUp()
self.bridge = self.env["ai.bridge"].create(
{
"name": "Test Bridge",
"model_id": self.env.ref("base.model_res_partner").id,
"url": "https://example.com/api",
"auth_type": "none",
"result_type": "message",
"result_kind": "async",
"usage": "thread",
}
)
self.partner = self.env["res.partner"].create(
{
"name": "Test Partner",
"email": "test@example.com",
}
)
with mock.patch("requests.post") as mock_post:
self.bridge.execute_ai_bridge(self.partner._name, self.partner.id)
self.url = mock_post.call_args[1]["json"]["_response_url"]
self.message_count = self.env["mail.message"].search_count(
[
("model", "=", self.partner._name),
("res_id", "=", self.partner.id),
]
)
def test_wrong_key(self):
result = self.opener.post(f"{self.url}1234", json={"body": "Test response"})
self.assertEqual(
result.status_code, 404, "Should return 404 for wrong key in URL."
)
def test_wrong_id(self):
result = self.opener.post(
f"{self.base_url()}/ai/response/-1/TOKEN", json={"body": "Test response"}
)
self.assertEqual(
result.status_code, 404, "Should return 404 for wrong key in URL."
)
def test_connection(self):
self.assertTrue(
self.env["ai.bridge.execution"].search(
[
("ai_bridge_id", "=", self.bridge.id),
("expiration_date", "!=", False),
]
)
)
self.opener.post(self.url, json={"body": "Test response"})
self.assertEqual(
self.env["mail.message"].search_count(
[
("model", "=", self.partner._name),
("res_id", "=", self.partner.id),
]
),
self.message_count + 1,
"A new message should be created in the thread.",
)
self.assertFalse(
self.env["ai.bridge.execution"].search(
[
("ai_bridge_id", "=", self.bridge.id),
("expiration_date", "!=", False),
]
)
)
# Key is wrong, so no message should be created
result = self.opener.post(self.url, json={"body": "Test response"})
self.assertEqual(
result.status_code, 404, "Should return 404 for wrong key in URL."
)
def test_connection_expired(self):
self.assertTrue(
self.env["ai.bridge.execution"].search(
[
("ai_bridge_id", "=", self.bridge.id),
("expiration_date", "!=", False),
]
)
)
execution = self.env["ai.bridge.execution"].search(
[
("ai_bridge_id", "=", self.bridge.id),
("expiration_date", "!=", False),
]
)
execution.expiration_date = "2020-01-01 00:00:00"
token = execution._generate_token()
result = self.opener.post(
urls.url_join(
execution.get_base_url(), f"/ai/response/{execution.id}/{token}"
),
json={"body": "Test response"},
)
self.assertEqual(
result.status_code, 404, "Should return 404 for expired execution."
)

View file

@ -0,0 +1,12 @@
# Copyright 2025 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import common
@common.tagged("post_install", "-at_install")
class TestFrontend(common.HttpCase):
def test_javascript(self):
self.browser_js(
"/web/tests?module=ai_oca_bridge", "", login="admin", timeout=1800
)

View file

@ -0,0 +1,174 @@
# Copyright 2025 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from unittest import mock
from odoo_test_helper import FakeModelLoader
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, TransactionCase
class TestBridge(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Load fake models ->/
cls.loader = FakeModelLoader(cls.env, cls.__module__)
cls.loader.backup_registry()
from .fake_models import BridgeTest
cls.loader.update_registry((BridgeTest,))
cls.bridge = cls.env["ai.bridge"].create(
{
"name": "Test Bridge",
"model_id": cls.env["ir.model"]._get_id("bridge.test"),
"url": "https://example.com/api",
"auth_type": "none",
"usage": "none",
}
)
@classmethod
def tearDownClass(cls):
cls.loader.restore_registry()
super().tearDownClass()
def test_bridge_thread_creation(self):
self.bridge.write({"usage": "ai_thread_create"})
with mock.patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"result": "success"}
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
# Create a test record
record = self.env["bridge.test"].create({"name": "Test Record"})
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
mock_post.assert_called_once()
record.write({"name": "Updated Record"})
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
record.unlink()
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
mock_post.assert_called_once()
def test_bridge_thread_write(self):
self.bridge.write({"usage": "ai_thread_write"})
with mock.patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"result": "success"}
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
# Create a test record
record = self.env["bridge.test"].create({"name": "Test Record"})
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
record.write({"name": "Updated Record"})
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
record.unlink()
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
mock_post.assert_called_once()
def test_bridge_thread_unlink(self):
self.assertNotEqual(self.bridge.payload_type, "none")
with Form(self.bridge) as bridge_form:
bridge_form.usage = "ai_thread_unlink"
self.assertEqual(self.bridge.payload_type, "none")
with mock.patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"result": "success"}
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
# Create a test record
record = self.env["bridge.test"].create({"name": "Test Record"})
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
record.write({"name": "Updated Record"})
self.assertEqual(
0,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
record.unlink()
self.assertEqual(
1,
self.env["ai.bridge.execution"].search_count(
[("ai_bridge_id", "=", self.bridge.id)]
),
)
mock_post.assert_called_once()
def test_bridge_thread_unlink_constrains(self):
self.assertNotEqual(self.bridge.payload_type, "none")
with Form(self.bridge) as bridge_form:
bridge_form.usage = "ai_thread_unlink"
self.assertEqual(self.bridge.payload_type, "none")
with self.assertRaises(ValidationError):
self.bridge.payload_type = "record"
def test_bridge_model_search(self):
models = self.env["ir.model"].search([("ai_usage", "=", "thread")])
model = self.env["ir.model"]._get_id("bridge.test")
self.assertTrue(models)
self.assertIn(self.env.ref("base.model_res_partner"), models)
self.assertNotIn(model, models.ids)
models = self.env["ir.model"].search([("ai_usage", "=", "ai_thread_create")])
self.assertTrue(models)
self.assertNotIn(self.env.ref("base.model_res_partner"), models)
self.assertIn(model, models.ids)
models = self.env["ir.model"].search([("ai_usage", "=", "none")])
self.assertTrue(models)
self.assertIn(self.env.ref("base.model_res_partner"), models)
self.assertIn(model, models.ids)
def test_bridge_model_required(self):
self.assertFalse(self.bridge.model_required)
self.bridge.usage = "ai_thread_create"
self.assertTrue(self.bridge.model_required)
self.bridge.usage = "thread"
self.assertTrue(self.bridge.model_required)