test(bt): tablet PIN self-service entech smoke (Task 7)
10-step smoke via odoo-shell: 1. Pick real no-PIN shop user 2. _generate_for_user -> assert 4-digit code + active row 3. Wrong code -> assert rejected + attempt_count incremented 4. Correct code -> assert ok + used_at set 5. _sign_reset_token + _verify_reset_token roundtrip 6. set_tablet_pin (mirrors set_pin endpoint reset_token branch) 7. verify_tablet_pin -> assert new PIN works 8. mail.template ref resolves 9. fp.notification.template ref resolves 10. Cleanup cron ref resolves Cleans up: reverts PIN + deletes reset rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
129
fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py
Normal file
129
fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tablet PIN self-service — entech smoke.
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-25-tablet-pin-self-service-design.md
|
||||
Plan: docs/superpowers/plans/2026-05-25-tablet-pin-self-service-plan.md
|
||||
|
||||
Run on entech via odoo-shell. Picks a real shop-floor user with no
|
||||
current PIN, runs the full create flow end-to-end via the model
|
||||
helpers (controller endpoints exercise the same paths under the
|
||||
hood; this script lets us verify pre-HTTP).
|
||||
|
||||
Cleans up at the end: reverts PIN + deletes reset rows for the user.
|
||||
"""
|
||||
|
||||
|
||||
def _ok(cond, label):
|
||||
if cond:
|
||||
print('OK -', label)
|
||||
else:
|
||||
print('FAIL -', label)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
# Pick a real user — first active shop-branch user with no PIN.
|
||||
gids = []
|
||||
for xmlid in (
|
||||
'fusion_plating.group_fp_technician',
|
||||
'fusion_plating.group_fp_manager',
|
||||
'fusion_plating.group_fp_quality_manager',
|
||||
'fusion_plating.group_fp_owner',
|
||||
):
|
||||
g = env.ref(xmlid, raise_if_not_found=False)
|
||||
if g:
|
||||
gids.append(g.id)
|
||||
user = env['res.users'].sudo().search([
|
||||
('all_group_ids', 'in', gids),
|
||||
('share', '=', False),
|
||||
('active', '=', True),
|
||||
('x_fc_tablet_pin_hash', '=', False),
|
||||
], limit=1)
|
||||
_ok(bool(user), f'found a no-PIN shop user: {user.name if user else None}')
|
||||
|
||||
original_hash = user.sudo().x_fc_tablet_pin_hash # for cleanup
|
||||
|
||||
Reset = env['fp.tablet.pin.reset']
|
||||
|
||||
# 1. Generate a code
|
||||
rec, code = Reset._generate_for_user(user)
|
||||
_ok(rec.exists(), 'reset row created')
|
||||
_ok(len(code) == 4 and code.isdigit(), f'code is 4 digits: {code}')
|
||||
_ok(not rec.used_at, 'row is active (used_at is null)')
|
||||
|
||||
# 2. Verify wrong code
|
||||
wrong = '9999' if code != '9999' else '0000'
|
||||
ok, err = rec._verify_and_consume(wrong)
|
||||
_ok(not ok and err == 'wrong_code', f'wrong code rejected: err={err}')
|
||||
|
||||
rec.invalidate_recordset(['attempt_count'])
|
||||
_ok(rec.attempt_count == 1, f'attempt_count is 1: {rec.attempt_count}')
|
||||
|
||||
# 3. Verify correct code
|
||||
ok, err = rec._verify_and_consume(code)
|
||||
_ok(ok and err is None, f'correct code accepted: err={err}')
|
||||
|
||||
rec.invalidate_recordset(['used_at'])
|
||||
_ok(bool(rec.used_at), 'row marked used after success')
|
||||
|
||||
# 4. Sign reset token + verify
|
||||
token = Reset._sign_reset_token(user.id)
|
||||
_ok('.' in token, 'token has body.sig shape')
|
||||
|
||||
uid = Reset._verify_reset_token(token)
|
||||
_ok(uid == user.id, f'token verifies to correct uid: {uid}')
|
||||
|
||||
# 5. Set PIN (simulates the set_pin endpoint reset_token branch)
|
||||
user.set_tablet_pin('4321')
|
||||
user.invalidate_recordset(['x_fc_tablet_pin_hash'])
|
||||
_ok(bool(user.sudo().x_fc_tablet_pin_hash), 'PIN hash set on user')
|
||||
|
||||
# 6. Verify the new PIN works
|
||||
_ok(user.verify_tablet_pin('4321'), 'new PIN verifies')
|
||||
|
||||
# 7. Audit events written
|
||||
audit_count = env['fp.tablet.session.event'].sudo().search_count([
|
||||
('attempted_user_id', '=', user.id),
|
||||
('event_type', 'in', (
|
||||
'pin_reset_requested',
|
||||
'pin_reset_code_verified',
|
||||
'failed_unlock',
|
||||
)),
|
||||
])
|
||||
# Direct model calls don't write audit (controller does); so we don't
|
||||
# assert > 0 here. Just print the count for visibility.
|
||||
print(f' audit events for user (informational): {audit_count}')
|
||||
|
||||
# 8. Mail template exists
|
||||
tpl = env.ref(
|
||||
'fusion_plating_shopfloor.fp_mail_template_tablet_pin_reset',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
_ok(bool(tpl), 'mail.template fp_mail_template_tablet_pin_reset exists')
|
||||
|
||||
# 9. Notification template wrapper exists
|
||||
notif = env.ref(
|
||||
'fusion_plating_shopfloor.fp_notif_tablet_pin_reset',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
_ok(bool(notif), 'fp.notification.template fp_notif_tablet_pin_reset exists')
|
||||
|
||||
# 10. Cleanup cron exists
|
||||
cron = env.ref(
|
||||
'fusion_plating_shopfloor.cron_purge_expired_pin_resets',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
_ok(bool(cron), 'cleanup cron cron_purge_expired_pin_resets exists')
|
||||
|
||||
# Cleanup
|
||||
user.sudo().write({
|
||||
'x_fc_tablet_pin_hash': original_hash or False,
|
||||
})
|
||||
env['fp.tablet.pin.reset'].sudo().search([
|
||||
('user_id', '=', user.id),
|
||||
]).unlink()
|
||||
env.cr.commit()
|
||||
print('cleanup: PIN reverted, reset rows deleted')
|
||||
|
||||
print()
|
||||
print('--- bt_pin_reset: ALL PASS ---')
|
||||
print(f' Tested user: {user.name} (uid={user.id})')
|
||||
Reference in New Issue
Block a user