diff --git a/beam/tests/fixtures.py b/beam/tests/fixtures.py index 2a8f7a9..8bc59a8 100644 --- a/beam/tests/fixtures.py +++ b/beam/tests/fixtures.py @@ -728,7 +728,7 @@ }, "phone": "(704) 885-0542", "roles": ["Stock Manager", "Item Manager"], - # "department": "Operations", + "department": "Management - APC", "designation": "Bakery Manager", }, { @@ -745,7 +745,7 @@ "phone": "(658) 583-5499", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -762,7 +762,7 @@ "phone": "(962) 762-5895", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -779,7 +779,7 @@ "phone": "(366) 357-8223", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Bakery Manager", }, { @@ -796,7 +796,7 @@ "phone": "(930) 920-4520", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -813,7 +813,7 @@ "phone": "(054) 893-8970", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -830,7 +830,7 @@ "phone": "(814) 677-9322", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -847,7 +847,7 @@ "phone": "(133) 195-7828", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -864,7 +864,24 @@ "phone": "(041) 000-2569", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, + { + "name": "Marcus Reynolds", + "gender": "Male", + "date_of_birth": "1990-05-15", + "date_of_joining": "2023-03-01", + "address": { + "address_line1": "456 Testing Lane", + "city": "Boston", + "state": "MA", + "postal_code": "02101", + }, + "phone": "(555) 123-4567", + "roles": ["Stock User"], # NO BEAM Mobile User role - for restriction tests + "reports_to": "Tristan Hawkins", + "department": "Operations - APC", + "designation": "Warehouse Associate", + }, ] diff --git a/beam/tests/mobile/conftest.py b/beam/tests/mobile/conftest.py index 20ef800..367b98d 100644 --- a/beam/tests/mobile/conftest.py +++ b/beam/tests/mobile/conftest.py @@ -18,19 +18,24 @@ def browser_context_args(browser_context_args): @pytest.fixture(autouse=True) -def setup(page): +def setup(page, request): # delete all existing draft Purchase Receipts delete_draft_records(["Purchase Receipt", "Stock Entry"]) page.set_default_timeout(5000) base_url = frappe.utils.get_url() - page.goto(base_url) - # visiting the home page redirects to login page - page.get_by_role("textbox", name="Email").fill("support@agritheory.dev") - page.get_by_role("textbox", name="Password").fill("admin") - page.get_by_role("button", name="Login").click() # this will redirect to `/beam` + # Skip auto-login for scan-to-login tests (they need to start from login page) + is_login_test = "scan_to_login" in request.node.name + + if not is_login_test: + page.goto(base_url) + # visiting the home page redirects to login page + page.get_by_role("textbox", name="Email").fill("support@agritheory.dev") + page.get_by_role("textbox", name="Password").fill("admin") + page.get_by_role("button", name="Login").click() # this will redirect to `/beam` + yield # delete all Purchase Receipts created during the test diff --git a/beam/tests/mobile/test_manufacture.py b/beam/tests/mobile/test_manufacture.py index 12b1d17..76e8de8 100644 --- a/beam/tests/mobile/test_manufacture.py +++ b/beam/tests/mobile/test_manufacture.py @@ -50,12 +50,15 @@ def test_complete_partial_stock_entry(page): # navigate in the following order: Home -> Manufacture -> Work Order page.get_by_text("Manufacture").click() + + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() # get the selected Work Order order_id = page.url.split("/")[-1] assert order_id + expect(page.locator("css=.box .beam_list-item").first).to_be_visible() # ensure there are no existing Stock Entries against this Work Order entry = frappe.db.exists( "Stock Entry", diff --git a/beam/tests/mobile/test_mobile.py b/beam/tests/mobile/test_mobile.py index de57efa..e902d54 100644 --- a/beam/tests/mobile/test_mobile.py +++ b/beam/tests/mobile/test_mobile.py @@ -16,13 +16,17 @@ # `page.expect_navigation()` since the latter won't work with Beam's hash-based routes -@pytest.mark.order(6) +@pytest.mark.order(1) @pytest.mark.parametrize("route", ["Ship"]) def test_scan_item_barcode(page, route): # navigate in the following order: Home -> List -> Form page.get_by_text(route).click() + # wait for list to load + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() + # wait for items to load after navigation + expect(page.locator("css=.box .beam_list-item").first).to_be_visible() # find the first item in the list item = page.locator("css=.box .beam_list-item").first item_name, *others = item.inner_text().split("\n") diff --git a/beam/tests/mobile/test_receive.py b/beam/tests/mobile/test_receive.py index fac9273..ceb9f4e 100644 --- a/beam/tests/mobile/test_receive.py +++ b/beam/tests/mobile/test_receive.py @@ -19,9 +19,11 @@ # `page.expect_navigation()` won't work with Beam's hash-based routes -@pytest.mark.order(2) +@pytest.mark.order(1) def test_scan_invalid_barcode(page): page.get_by_text("Receive").click() + # wait for list to load + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() # get the selected Purchase Order @@ -30,6 +32,8 @@ def test_scan_invalid_barcode(page): order_id = path_parts[-1] if path_parts else None assert order_id + # wait for items to load after navigation + expect(page.locator("css=.box .beam_item-count").first).to_be_visible() # find all items in the list all_item_counts = page.locator("css=.box .beam_item-count") @@ -78,11 +82,13 @@ def test_scan_invalid_barcode(page): ), f"Invalid barcode scan should not create any Purchase Receipts, but found: {new_receipts}" -@pytest.mark.order(3) +@pytest.mark.order(2) def test_receive_without_scanning(page): """Test trying to receive without scanning any items""" # navigate to a Purchase Order page.get_by_text("Receive").click() + # wait for list to load + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() # get the selected Purchase Order @@ -91,6 +97,8 @@ def test_receive_without_scanning(page): order_id = path_parts[-1] if path_parts else None assert order_id + # wait for items to load after navigation + expect(page.locator("css=.box .beam_list-item").first).to_be_visible() item = page.locator("css=.box .beam_list-item").first item_code, *others = item.inner_text().split("\n") @@ -132,10 +140,12 @@ def test_receive_without_scanning(page): ), f"Expected no new receipts, but count changed from {initial_count} to {final_count}" -@pytest.mark.order(4) +@pytest.mark.order(3) def test_complete_partial_receipt(page): # navigate in the following order: Home -> Receive -> Purchase Order page.get_by_text("Receive").click() + + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() # get the selected Purchase Order @@ -148,6 +158,7 @@ def test_complete_partial_receipt(page): assert order_id + expect(page.locator("css=.box .beam_list-item").first).to_be_visible() # find the first item in the list item = page.locator("css=.box .beam_list-item").first item_code, *others = item.inner_text().split("\n") @@ -213,11 +224,13 @@ def test_complete_partial_receipt(page): assert receipts[0]["received_qty"] == 1 -@pytest.mark.order(5) +@pytest.mark.order(4) def test_rapid_barcode_scanning(page): """Test scanning multiple barcodes quickly""" # navigate to a Purchase Order page.get_by_text("Receive").click() + # wait for list to load + expect(page.locator("css=.beam_list-item").first).to_be_visible() page.locator("css=.beam_list-item").first.click() # get the selected Purchase Order @@ -226,6 +239,8 @@ def test_rapid_barcode_scanning(page): order_id = path_parts[-1] if path_parts else None assert order_id + # wait for items to load after navigation + expect(page.locator("css=.box .beam_list-item").first).to_be_visible() # find the first item in the list item = page.locator("css=.box .beam_list-item").first item_code, *others = item.inner_text().split("\n") diff --git a/beam/tests/mobile/test_repack.py b/beam/tests/mobile/test_repack.py index 25f6603..524bb74 100644 --- a/beam/tests/mobile/test_repack.py +++ b/beam/tests/mobile/test_repack.py @@ -34,7 +34,7 @@ def disable_handling_unit_for_tests(): frappe.db.commit() -@pytest.mark.order(8) +@pytest.mark.order(1) def test_repack_items_manually(page): page.get_by_text("Repack").click() expect(page).to_have_url(re.compile(r"#/repack"), timeout=15000) @@ -124,7 +124,7 @@ def test_repack_items_manually(page): assert submitted, f"Expected Stock Entry {stock_entry_name} to be submitted" -@pytest.mark.order(9) +@pytest.mark.order(2) def test_repack_using_bom(page): page.get_by_text("Repack").click() page.wait_for_load_state("networkidle") @@ -174,7 +174,7 @@ def test_repack_using_bom(page): assert entries, "Expected a draft Stock Entry to be created from BOM repack" -@pytest.mark.order(10) +@pytest.mark.order(3) def test_scan_item_for_repack(page): page.get_by_text("Repack").click() page.wait_for_load_state("networkidle") @@ -214,7 +214,7 @@ def test_scan_item_for_repack(page): expect(qty_input).to_have_value("2") -@pytest.mark.order(11) +@pytest.mark.order(4) def test_clear_repack_form(page): page.get_by_text("Repack").click() page.wait_for_load_state("networkidle") @@ -260,7 +260,7 @@ def test_clear_repack_form(page): expect(page.get_by_role("button", name="CLEAN", exact=True)).to_be_hidden() -@pytest.mark.order(12) +@pytest.mark.order(5) def test_repack_validation_single_warehouse_direction(page): page.get_by_text("Repack").click() page.wait_for_load_state("networkidle") diff --git a/beam/tests/mobile/test_scan_to_login.py b/beam/tests/mobile/test_scan_to_login.py new file mode 100644 index 0000000..2d30869 --- /dev/null +++ b/beam/tests/mobile/test_scan_to_login.py @@ -0,0 +1,263 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + +MOBILE_USER_EMAIL = "dsolomon@cfc.co" # Has "BEAM Mobile User" role +NON_MOBILE_USER_EMAIL = "mreynolds@cfc.co" # Does NOT have "BEAM Mobile User" role + + +def get_user_barcode(user_email): + """Get the barcode for a user from the database""" + barcode = frappe.db.get_value( + "Item Barcode", {"parent": user_email, "parenttype": "User"}, "barcode" + ) + return barcode + + +def set_beam_setting(field, value): + """Helper to update BEAM Settings for tests""" + company = frappe.defaults.get_defaults().get("company") + beam_settings = frappe.get_doc("BEAM Settings", {"company": company}) + beam_settings.set(field, value) + beam_settings.save() + frappe.db.commit() + + +def logout(page): + page.goto(f"{frappe.utils.get_url()}/api/method/logout") + page.wait_for_timeout(1000) + + +@pytest.mark.order(1) +def test_scan_to_login_success_all_users(page): + """Test successful login scanning - All Users mode""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "All Users") + mobile_user_barcode = get_user_barcode(MOBILE_USER_EMAIL) + assert mobile_user_barcode, f"Barcode not found for user {MOBILE_USER_EMAIL}" + + # Navigate to login page + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.user_login.scan_login" + ): + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", mobile_user_barcode + ) + + page.wait_for_timeout(2000) + expect(page).to_have_url(frappe.utils.get_url() + "/beam#/") + + # Verify user is authenticated by checking session in browser + logged_in_user = page.evaluate("() => frappe.session.user") + assert logged_in_user == MOBILE_USER_EMAIL, f"Expected {MOBILE_USER_EMAIL}, got {logged_in_user}" + + logout(page) + + +@pytest.mark.order(2) +def test_scan_to_login_invalid_barcode(page): + """Test rejection of non-user barcode""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "All Users") + item_barcode = frappe.get_value("Item Barcode", {"parent": "Butter"}, "barcode") + assert item_barcode, "Item barcode not found for test" + + # Navigate to login page + page.goto(f"{frappe.utils.get_url()}/login#login") + page.wait_for_load_state("networkidle") + + page.evaluate("barcode => window.scanHandler.scanner.simulate(document, barcode)", item_barcode) + page.wait_for_timeout(1000) + + # Verify error message appears + error_message = page.locator(".msgprint-dialog .modal-body") + expect(error_message).to_be_visible(timeout=5000) + expect(error_message).to_contain_text("Wrong barcode") + + # Verify we're still on login page + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + +@pytest.mark.order(3) +def test_scan_to_login_mobile_users_only_success(page): + """Test login with mobile user role restriction - success case""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "Mobile Users Only") + + mobile_user_barcode = get_user_barcode(MOBILE_USER_EMAIL) + assert mobile_user_barcode, f"Barcode not found for user {MOBILE_USER_EMAIL}" + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.user_login.scan_login" + ): + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", mobile_user_barcode + ) + + page.wait_for_timeout(2000) + expect(page).to_have_url(frappe.utils.get_url() + "/beam#/") + + logged_in_user = page.evaluate("() => frappe.session.user") + assert logged_in_user == MOBILE_USER_EMAIL + + logout(page) + + +@pytest.mark.order(4) +def test_scan_to_login_mobile_users_only_reject(page): + """Test rejection when user lacks BEAM Mobile User role""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "Mobile Users Only") + non_mobile_user_barcode = get_user_barcode(NON_MOBILE_USER_EMAIL) + assert non_mobile_user_barcode, f"Barcode not found for user {NON_MOBILE_USER_EMAIL}" + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", non_mobile_user_barcode + ) + page.wait_for_timeout(2000) + + error_message = page.locator(".msgprint-dialog .modal-body") + expect(error_message).to_be_visible(timeout=2000) + expect(error_message).to_contain_text("Not Beam mobile user") + + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + +@pytest.mark.order(5) +def test_scan_to_login_disabled(page): + """Test login when scanning is disabled""" + mobile_user_barcode = get_user_barcode(MOBILE_USER_EMAIL) + assert mobile_user_barcode, f"Barcode not found for user {MOBILE_USER_EMAIL}" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "Not Allowed") + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", mobile_user_barcode + ) + page.wait_for_timeout(1000) + + error_message = page.locator(".msgprint-dialog .modal-body") + expect(error_message).to_be_visible(timeout=2000) + expect(error_message).to_contain_text("Login scanning is not allowed") + + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + +@pytest.mark.order(6) +def test_scan_to_login_ip_restriction_allowed(page): + """Test IP restriction - allowed IP""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "All Users") + mobile_user_barcode = get_user_barcode(MOBILE_USER_EMAIL) + assert mobile_user_barcode, f"Barcode not found for user {MOBILE_USER_EMAIL}" + set_beam_setting("restrict_ip", "127.0.0.1") + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + # Scan user barcode (should work since we're on localhost) + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.user_login.scan_login" + ): + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", mobile_user_barcode + ) + + page.wait_for_timeout(2000) + expect(page).to_have_url(frappe.utils.get_url() + "/beam#/") + + logged_in_user = page.evaluate("() => frappe.session.user") + assert logged_in_user == MOBILE_USER_EMAIL + + logout(page) + with use_current_db_transaction(): + set_beam_setting("restrict_ip", "") # Clear IP restriction + + +@pytest.mark.order(7) +def test_scan_to_login_ip_restriction_blocked(page): + """Test IP restriction - blocked IP""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "All Users") + mobile_user_barcode = get_user_barcode(MOBILE_USER_EMAIL) + assert mobile_user_barcode, f"Barcode not found for user {MOBILE_USER_EMAIL}" + # Configure IP that won't match localhost (192.168.1.x subnet only) + set_beam_setting("restrict_ip", "192.168.1.") + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + # Scan user barcode - should be rejected due to IP restriction + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", mobile_user_barcode + ) + page.wait_for_timeout(1000) + + # Verify error message appears + error_message = page.locator(".msgprint-dialog .modal-body") + expect(error_message).to_be_visible(timeout=5000) + expect(error_message).to_contain_text("Network not available") + + # Verify we're still on login page + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + with use_current_db_transaction(): + set_beam_setting("restrict_ip", "") + + +@pytest.mark.order(8) +def test_scan_to_login_disabled_user(page): + """Test rejection when user account is disabled""" + with use_current_db_transaction(): + set_beam_setting("enable_scan_to_login", "All Users") + + user = frappe.get_doc("User", NON_MOBILE_USER_EMAIL) + original_enabled_status = user.enabled + + # Temporarily disable the user + user.enabled = 0 + user.save(ignore_permissions=True) + frappe.db.commit() + + disabled_user_barcode = get_user_barcode(NON_MOBILE_USER_EMAIL) + assert disabled_user_barcode, f"Barcode not found for user {NON_MOBILE_USER_EMAIL}" + + page.goto(f"{frappe.utils.get_url()}/login") + page.wait_for_load_state("networkidle") + + # Scan disabled user barcode + page.evaluate( + "barcode => window.scanHandler.scanner.simulate(document, barcode)", disabled_user_barcode + ) + page.wait_for_timeout(1000) + + error_message = page.locator(".msgprint-dialog .modal-body") + expect(error_message).to_be_visible(timeout=5000) + expect(error_message).to_contain_text("is disabled") + + expect(page).to_have_url(frappe.utils.get_url() + "/login#login") + + with use_current_db_transaction(): + user = frappe.get_doc("User", NON_MOBILE_USER_EMAIL) + user.enabled = original_enabled_status + user.save(ignore_permissions=True) + frappe.db.commit() diff --git a/beam/tests/setup.py b/beam/tests/setup.py index c17f836..df2f05f 100644 --- a/beam/tests/setup.py +++ b/beam/tests/setup.py @@ -201,6 +201,7 @@ def setup_beam_settings(settings): beams.enable_handling_units = True beams.receiving_workstation = "Receiving" beams.shipping_workstation = "Shipping" + beams.auto_barcode_doctypes = '["Item", "User", "Warehouse"]' beams.set("warehouse_types", [{"warehouse_type": "Quarantine"}]) beams.set( "routes", diff --git a/pyproject.toml b/pyproject.toml index 9b3c7c8..a63c5f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ frappe = ">=15.0.0,<16.0.0" erpnext = ">=15.0.0,<16.0.0" [tool.pytest.ini_options] -addopts = "--cov=beam --cov-report term-missing" +addopts = "--cov=beam --cov-report term-missing --order-scope=module" [tool.black] line-length = 99