runbot/runbot_test/tests/test_spec.py
William Braeckman b51909fab6 [IMP] runbot: implement public api mixin
Until now api routes on runbot have been created through custom website
pages in production.
We want to unify the API by making a 'public' api, inspired by the way
`web_search_read` works.

This commit adds:
 - A route to list all publicly available models
 - A route to do a read on a public model
 - A route to fetch the publicly available specification for a model
 - A public model mixin that provides all the tools required to support
   the above mentionned routes.

The mixin adds the ability to add the `public` attribute on fields.
Any field marked as public can then be publicly queried through the
controller.
Relational fields work in a nested manner (`fields` key in the field's
sub-specification) (up to a depth of 10).
The public api does not allow going through a relationship back and
front (parent->child->parent is NOT allowed).

Because we are based on `web_search_read`, we heavily focus on
validating the specification, for security reasons, and offset the load
of reading to the `web_read` function (we currently don't provide limit
metadata).
2025-03-17 13:44:35 +01:00

296 lines
10 KiB
Python

from odoo import fields
from odoo.tests.common import TransactionCase, tagged, new_test_user
@tagged('-at_install', 'post_install')
class TestSpec(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.Parent = cls.env['runbot.test.model.parent']
cls.Child = cls.env['runbot.test.model.child']
cls.user_basic = new_test_user(
cls.env, 'basic',
)
def test_parent_spec(self):
spec = self.Parent._api_public_specification()
self.assertNotIn('private_field', spec)
self.assertEqual(
spec['field_bool'],
{
'__type': 'boolean',
}
)
self.assertIn('field_many2one', spec)
sub_spec = spec['field_many2one']['fields']
self.assertIn('field_many2many', spec)
self.assertIn('field_many2many', sub_spec)
self.assertTrue(self.Parent._api_verify_specification(spec))
def test_child_spec(self):
spec = self.Child._api_public_specification()
self.assertIn('parent_id', spec)
self.assertIn('data', spec)
sub_spec = spec['parent_id']
# The reverse relation should not be part of the sub spec, as we already
# traversed that relation
self.assertNotIn('field_one2many', sub_spec)
self.assertTrue(self.Child._api_verify_specification(spec))
def test_parent_read_basic(self):
today = fields.Date.today()
now = fields.Datetime.now()
parent = self.Parent.create({
'field_bool': True,
'field_date': today,
'field_datetime': now,
'field_json': {'test': 1}
})
self.assertEqual(
parent._api_read({'field_bool': {}, 'field_date': {}, 'field_datetime': {}, 'field_json': {}}),
[{
'id': parent.id,
'field_bool': True,
'field_date': today,
'field_datetime': now,
'field_json': {'test': 1}
}]
)
def test_parent_read_m2o(self):
parent_1 = self.Parent.create({'field_integer': 1})
parent_2 = self.Parent.create({'field_integer': 2, 'field_many2one': parent_1.id})
# Read without sub spec
self.assertEqual(
parent_1._api_read({'field_many2one': {}}),
[{'id': parent_1.id, 'field_many2one': False}]
)
self.assertEqual(
parent_2._api_read({'field_many2one': {}}),
[{'id': parent_2.id, 'field_many2one': parent_1.id}]
)
# Read with sub spec
self.assertEqual(
parent_1._api_read({'field_many2one': {'fields': {'field_integer': {}}}}),
[{'id': parent_1.id, 'field_many2one': False}]
)
self.assertEqual(
parent_2._api_read({'field_many2one': {'fields': {'field_integer': {}}}}),
[{'id': parent_2.id, 'field_many2one': {'id': parent_1.id, 'field_integer': parent_1.field_integer}}]
)
both = parent_1 | parent_2
# Read both with sub spec
self.assertEqual(
both._api_read({'field_integer': {}, 'field_many2one': {'fields': {'field_integer': {}}}}),
[
{'id': parent_1.id, 'field_integer': parent_1.field_integer, 'field_many2one': False},
{'id': parent_2.id, 'field_integer': parent_2.field_integer, 'field_many2one': {
'id': parent_1.id, 'field_integer': parent_1.field_integer,
}},
]
)
def test_parent_read_one2many(self):
parent = self.Parent.create({
'field_float': 13.37,
'field_one2many': [
(0, 0, {'data': 1}),
(0, 0, {'data': 2}),
(0, 0, {'data': 3}),
]
})
parent_no_o2m = self.Parent.create({
'field_float': 13.37,
})
# Basic read
self.assertEqual(
parent._api_read({
'field_float': {},
'field_one2many': {
'fields': {
'data': {}
},
}
}),
[{
'id': parent.id,
'field_float': 13.37,
'field_one2many': [
{'id': parent.field_one2many[0].id, 'data': 1},
{'id': parent.field_one2many[1].id, 'data': 2},
{'id': parent.field_one2many[2].id, 'data': 3},
]
}]
)
self.assertEqual(
parent_no_o2m._api_read({
'field_float': {},
'field_one2many': {
'fields': {
'data': {},
}
},
}),
[{
'id': parent_no_o2m.id,
'field_float': 13.37,
'field_one2many': [],
}]
)
# Reading parent_id through field_one2many_computed is allowed since relationship is not known
self.env.invalidate_all()
spec = {
'field_float': {},
'field_one2many': {
'fields': {
'data': {},
'parent_id': {
'fields': {
'field_float': {},
}
},
}
},
}
self.assertEqual(
parent._api_read(spec),
[{
'id': parent.id,
'field_float': 13.37,
'field_one2many': [
{'id': parent.field_one2many[0].id, 'data': 1, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
{'id': parent.field_one2many[1].id, 'data': 2, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
{'id': parent.field_one2many[2].id, 'data': 3, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
]
}]
)
# It should not work with basic user
self.assertFalse(parent.with_user(self.user_basic)._api_verify_specification(spec))
# It should however work with the computed version
spec['field_one2many_computed'] = spec['field_one2many']
spec.pop('field_one2many')
self.assertEqual(
parent.with_user(self.user_basic)._api_read(spec),
[{
'id': parent.id,
'field_float': 13.37,
'field_one2many_computed': [
{'id': parent.field_one2many[0].id, 'data': 1, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
{'id': parent.field_one2many[1].id, 'data': 2, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
{'id': parent.field_one2many[2].id, 'data': 3, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
]
}]
)
def test_parent_read_one2many_private(self):
# Check the private one2many (field is public, model is not), we can read id but nothing else
parent = self.Parent.create({
'field_float': 13.37,
'field_one2many_private': [
(0, 0, {'data': 1}),
]
}).with_user(self.user_basic)
spec = parent._api_public_specification()
self.assertNotIn('fields', spec['field_one2many_private'])
result = parent._api_read(spec)
self.assertEqual(
result[0]['field_one2many_private'],
[parent.field_one2many_private.id],
)
with self.assertRaises(ValueError):
parent._api_verify_specification({
'field_one2many_private': {
'fields': {
'data': {}
}
}
})
def test_parent_read_m2m(self):
parent = self.Parent.create({
'field_integer': 1,
'field_many2many': [
(0, 0, {'field_integer': 2}),
],
})
other_parent = parent.field_many2many
self.assertEqual(
parent._api_read({'field_integer': {}, 'field_many2many': {}}),
[
{'id': parent.id, 'field_many2many': list(parent.field_many2many.ids), 'field_integer': 1},
],
)
self.env.invalidate_all()
spec = {
'field_many2many_computed': {
'fields': {
'field_many2many_computed': {
'fields': {
'field_integer': {}
}
}
}
}
}
self.assertEqual(
parent._api_read(spec),
[
{'id': parent.id, 'field_many2many_computed': [
{'id': other_parent.id, 'field_many2many_computed': [
{'id': parent.id, 'field_integer': 1}
]}
]}
]
)
self.assertFalse(parent.with_user(self.user_basic)._api_verify_specification(spec))
def test_parent_read_cyclic_parent(self):
parent_1 = self.Parent.create({
'field_integer': 1,
})
parent_2 = self.Parent.create({
'field_integer': 2,
'field_many2one': parent_1.id,
})
parent_1.field_many2one = parent_2
self.env.invalidate_all()
both = (parent_1 | parent_2)
self.assertEqual(
both._api_read({'field_many2one': {}}),
[
{'id': parent_1.id, 'field_many2one': parent_2.id},
{'id': parent_2.id, 'field_many2one': parent_1.id},
]
)
self.assertEqual(
both.with_user(self.user_basic)._api_read({'field_many2one': {}}),
[
{'id': parent_1.id, 'field_many2one': parent_2.id},
{'id': parent_2.id, 'field_many2one': parent_1.id},
]
)
spec = {'field_many2one': {'fields': {'field_many2one': {}}}}
self.assertEqual(
both._api_read(spec),
[
{'id': parent_1.id, 'field_many2one': {'id': parent_2.id, 'field_many2one': parent_1.id}},
{'id': parent_2.id, 'field_many2one': {'id': parent_1.id, 'field_many2one': parent_2.id}},
]
)
self.assertFalse(both.with_user(self.user_basic)._api_verify_specification(spec))