feat(fusion_plating_shopfloor): /fp/tablet/set_pin + /fp/tablet/reset_pin_for endpoints (P6.1.2)

set_pin is self-service: requires old PIN if a hash exists, validates
4-digit format. reset_pin_for is manager-only (enforced server-side
via has_group); clears the hash + posts to chatter.

Both endpoints log INFO on success and WARNING on access-control denials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-23 00:14:18 -04:00
parent 395bd4949e
commit 58d02598da
3 changed files with 150 additions and 0 deletions

View File

@@ -6,6 +6,16 @@ import json
from odoo.tests.common import HttpCase, TransactionCase, tagged
def _rpc(case, url, **params):
"""Helper for HTTP/JSON-RPC tests — wraps Odoo's url_open."""
res = case.url_open(
url,
data=json.dumps({'jsonrpc': '2.0', 'params': params}),
headers={'Content-Type': 'application/json'},
)
return res.json()['result']
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
class TestTabletPinHash(TransactionCase):
"""P6.1.1 — model fields + hash helpers + set/verify/clear methods."""
@@ -52,3 +62,65 @@ class TestTabletPinHash(TransactionCase):
# Check chatter
last_msg = self.user.message_ids[:1]
self.assertIn('reset by', (last_msg.body or '').lower())
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
class TestTabletSetPin(HttpCase):
"""P6.1.2 — /fp/tablet/set_pin endpoint."""
def setUp(self):
super().setUp()
self.authenticate("admin", "admin")
def test_set_pin_first_time(self):
res = _rpc(self, '/fp/tablet/set_pin', new_pin='1234')
self.assertTrue(res['ok'])
def test_set_pin_change_requires_old(self):
admin = self.env.ref('base.user_admin')
admin.set_tablet_pin('1234')
# Change without old — rejected
res = _rpc(self, '/fp/tablet/set_pin', new_pin='5678')
self.assertFalse(res['ok'])
# Change with wrong old — rejected
res = _rpc(self, '/fp/tablet/set_pin', old_pin='9999', new_pin='5678')
self.assertFalse(res['ok'])
# Change with correct old — accepted
res = _rpc(self, '/fp/tablet/set_pin', old_pin='1234', new_pin='5678')
self.assertTrue(res['ok'])
def test_set_pin_rejects_non_4_digit(self):
for bad in ('123', '12345', 'abcd', ''):
res = _rpc(self, '/fp/tablet/set_pin', new_pin=bad)
self.assertFalse(res['ok'], f'PIN {bad!r} should be rejected')
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
class TestTabletResetPinFor(HttpCase):
"""P6.1.2 — /fp/tablet/reset_pin_for endpoint."""
def setUp(self):
super().setUp()
self.target = self.env['res.users'].create({
'name': 'Reset Target', 'login': 'reset@example.com',
})
self.target.sudo().set_tablet_pin('1234')
def test_reset_requires_manager_group(self):
# Plain operator can't reset
op_group = self.env.ref('fusion_plating.group_fusion_plating_operator')
self.env['res.users'].create({
'name': 'Op', 'login': 'op@example.com',
'password': 'op@example.com',
'group_ids': [(6, 0, [op_group.id])],
})
self.authenticate('op@example.com', 'op@example.com')
res = _rpc(self, '/fp/tablet/reset_pin_for', user_id=self.target.id)
self.assertFalse(res['ok'])
def test_reset_as_admin_clears_hash(self):
self.authenticate('admin', 'admin')
res = _rpc(self, '/fp/tablet/reset_pin_for', user_id=self.target.id)
self.assertTrue(res['ok'])
self.target.invalidate_recordset(['x_fc_tablet_pin_hash'])
self.assertFalse(self.target.sudo().x_fc_tablet_pin_hash)