From 9223f8da7c9badcf6a2bdd01362b5b39fcd56ab2 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 25 May 2026 16:54:46 -0400 Subject: [PATCH] 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) --- .../scripts/bt_pin_reset.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py diff --git a/fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py b/fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py new file mode 100644 index 00000000..bfc68f4c --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/scripts/bt_pin_reset.py @@ -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})')