feat(fusion_plating_shopfloor): res.users tablet PIN fields + hash helpers (P6.1.1)
PBKDF2-SHA256 + 16-byte salt + 200k iterations on res.users. Format of the stored hash string is <salt_hex>$<digest_hex>. Field is manager-readable only (groups=group_fusion_plating_manager); helpers that need to read or write it use .sudo() internally so operator-level callers can still set/verify their own PIN. Adds set_tablet_pin / verify_tablet_pin / clear_tablet_pin model methods + action_open_tablet_pin_setup that triggers the OWL setup modal (Phase 6.2). Tests cover hash uniqueness, verify, clear with chatter post, and the 4-digit format guard. Tests verified on entech: -u fusion_plating_shopfloor --test-tags fp_tablet_pin Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_workspace_controller
|
||||
from . import test_landing_kanban
|
||||
from . import test_tablet_pin
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
"""Phase 6 — Tablet PIN gate tests."""
|
||||
import json
|
||||
|
||||
from odoo.tests.common import HttpCase, TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
|
||||
class TestTabletPinHash(TransactionCase):
|
||||
"""P6.1.1 — model fields + hash helpers + set/verify/clear methods."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = self.env['res.users'].create({
|
||||
'name': 'Pin Test',
|
||||
'login': 'pintest@example.com',
|
||||
})
|
||||
|
||||
def test_set_pin_stores_salted_hash(self):
|
||||
self.user.sudo().set_tablet_pin('1234')
|
||||
stored = self.user.sudo().x_fc_tablet_pin_hash
|
||||
self.assertTrue(stored)
|
||||
self.assertIn('$', stored, 'hash must include salt separator')
|
||||
# Hash is non-deterministic — setting same PIN twice gives different stored values
|
||||
self.user.sudo().set_tablet_pin('1234')
|
||||
self.assertNotEqual(stored, self.user.sudo().x_fc_tablet_pin_hash)
|
||||
|
||||
def test_verify_correct_pin(self):
|
||||
self.user.sudo().set_tablet_pin('1234')
|
||||
self.assertTrue(self.user.sudo().verify_tablet_pin('1234'))
|
||||
|
||||
def test_verify_wrong_pin(self):
|
||||
self.user.sudo().set_tablet_pin('1234')
|
||||
self.assertFalse(self.user.sudo().verify_tablet_pin('0000'))
|
||||
|
||||
def test_verify_pin_no_hash_set(self):
|
||||
self.assertFalse(self.user.sudo().verify_tablet_pin('1234'))
|
||||
|
||||
def test_set_pin_rejects_invalid_format(self):
|
||||
from odoo.exceptions import UserError
|
||||
for bad in ('123', '12345', 'abcd', '', None):
|
||||
with self.assertRaises(UserError):
|
||||
self.user.sudo().set_tablet_pin(bad)
|
||||
|
||||
def test_clear_pin_wipes_hash_and_posts_chatter(self):
|
||||
self.user.sudo().set_tablet_pin('1234')
|
||||
self.user.clear_tablet_pin()
|
||||
self.user.invalidate_recordset(['x_fc_tablet_pin_hash'])
|
||||
self.assertFalse(self.user.sudo().x_fc_tablet_pin_hash)
|
||||
self.assertFalse(self.user.sudo().x_fc_tablet_pin_set_date)
|
||||
# Check chatter
|
||||
last_msg = self.user.message_ids[:1]
|
||||
self.assertIn('reset by', (last_msg.body or '').lower())
|
||||
Reference in New Issue
Block a user