feat(fusion_plating_shopfloor): /fp/tablet/unlock with per-user lockout (P6.1.3)
Verifies PIN, resets failure counter on success, increments + locks out on 5 consecutive failures (configurable via ir.config_parameter fp.shopfloor.tablet_pin_fail_threshold + tablet_pin_fail_lockout_minutes, both defaulting to 5). Returns informative payloads: ok=true current_tech_id, current_tech_name needs_setup=true user has no PIN yet locked_until lockout in effect (rejects even correct PIN) attempts_remaining failed but not yet locked Logs INFO on success, WARNING on failure (with running counter + locked flag). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,3 +124,60 @@ class TestTabletResetPinFor(HttpCase):
|
||||
self.assertTrue(res['ok'])
|
||||
self.target.invalidate_recordset(['x_fc_tablet_pin_hash'])
|
||||
self.assertFalse(self.target.sudo().x_fc_tablet_pin_hash)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
|
||||
class TestTabletUnlock(HttpCase):
|
||||
"""P6.1.3 — /fp/tablet/unlock endpoint + lockout."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.authenticate("admin", "admin")
|
||||
self.target = self.env['res.users'].create({
|
||||
'name': 'Unlock Target', 'login': 'unlock@example.com',
|
||||
})
|
||||
self.target.sudo().set_tablet_pin('1234')
|
||||
|
||||
def _unlock(self, pin):
|
||||
return _rpc(self, '/fp/tablet/unlock',
|
||||
user_id=self.target.id, pin=pin)
|
||||
|
||||
def test_unlock_correct_pin(self):
|
||||
res = self._unlock('1234')
|
||||
self.assertTrue(res['ok'])
|
||||
self.assertEqual(res['current_tech_id'], self.target.id)
|
||||
self.assertEqual(res['current_tech_name'], 'Unlock Target')
|
||||
|
||||
def test_unlock_correct_pin_resets_fail_counter(self):
|
||||
self._unlock('0000') # fail once
|
||||
self._unlock('1234') # succeed
|
||||
self.target.invalidate_recordset(['x_fc_tablet_pin_failed_count'])
|
||||
self.assertEqual(self.target.sudo().x_fc_tablet_pin_failed_count, 0)
|
||||
|
||||
def test_unlock_wrong_pin_increments_counter(self):
|
||||
self._unlock('0000')
|
||||
self.target.invalidate_recordset(['x_fc_tablet_pin_failed_count'])
|
||||
self.assertEqual(self.target.sudo().x_fc_tablet_pin_failed_count, 1)
|
||||
|
||||
def test_lockout_after_5_fails(self):
|
||||
for _ in range(5):
|
||||
self._unlock('0000')
|
||||
res = self._unlock('0000') # 6th
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertIn('locked', res['error'].lower())
|
||||
self.target.invalidate_recordset(['x_fc_tablet_locked_until'])
|
||||
self.assertTrue(self.target.sudo().x_fc_tablet_locked_until)
|
||||
|
||||
def test_lockout_blocks_even_correct_pin(self):
|
||||
for _ in range(5):
|
||||
self._unlock('0000')
|
||||
# Even the correct PIN now rejected
|
||||
res = self._unlock('1234')
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertIn('locked', res['error'].lower())
|
||||
|
||||
def test_unlock_no_pin_set(self):
|
||||
self.target.clear_tablet_pin()
|
||||
res = self._unlock('1234')
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertTrue(res.get('needs_setup'))
|
||||
|
||||
Reference in New Issue
Block a user