This commit is contained in:
2026-04-16 18:58:18 +02:00
commit fa1d01527d
124 changed files with 13989 additions and 0 deletions

0
tests/e2e/__init__.py Normal file
View File

126
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,126 @@
import sys
import os
import pytest
import time
from unittest.mock import MagicMock
from GramAddict.core import utils
# Force Qdrant mocking globally across ALL E2E tests so we never
# block on connection refused trying to hit localhost:6344
mock_qdrant = MagicMock()
# Setup correct return types for dimension check warnings in qdrant_memory
mock_collection = MagicMock()
mock_collection.config.params.vectors.size = 768
mock_qdrant.get_collection.return_value = mock_collection
sys.modules["qdrant_client"].QdrantClient = MagicMock(return_value=mock_qdrant)
@pytest.fixture
def e2e_device_dump_injector():
"""
Provides a factory to mock device.deviceV2.dump_hierarchy using real XML files.
Will gracefully fail with a comprehensive assertion if the file is missing
(per 'ECHTE DUMPS fehlen' reporting requirement).
"""
def _inject_dump(device_mock, xml_filename):
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
xml_path = os.path.join(fix_dir, xml_filename)
if not os.path.exists(xml_path):
pytest.fail(f"MISSING REAL DUMP: required XML fixture '{xml_filename}' for full E2E workflow testing could not be found at {xml_path}. FAKE_NOTHING policy implies dropping this test execution until it is captured.", pytrace=False)
with open(xml_path, "r") as f:
real_xml = f.read()
device_mock.deviceV2.dump_hierarchy.return_value = real_xml
return real_xml
return _inject_dump
@pytest.fixture
def dynamic_e2e_dump_injector(monkeypatch):
"""
State-Machine Injector: Replaces dump_hierarchy dynamically when transitions occur.
Validates that the Telepathic Engine's pathfinding truly worked.
"""
def _inject(device_mock, state_map, initial_xml):
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def load_xml(filename):
path = os.path.join(fix_dir, filename)
if not os.path.exists(path):
pytest.fail(f"MISSING REAL DUMP: {filename} not found.")
with open(path, "r") as f:
return f.read()
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(initial_xml)
from GramAddict.core.q_nav_graph import QNavGraph
original_execute = QNavGraph._execute_transition
def _mock_execute_transition(nav_self, action, zero_engine):
if action == 'tap_post_username':
return True
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
success = original_execute(nav_self, action, zero_engine)
if success is True and action in state_map:
# The node was clicked successfully! Swap the XML to the target state.
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(state_map[action])
return success
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
return _inject
@pytest.fixture(autouse=True)
def mock_all_delays(monkeypatch):
"""
Strips out all humanized hardware delays specifically for the E2E test suite.
Ensures loops evaluate instantly using the injected dumps.
"""
monkeypatch.setattr(time, "sleep", lambda x: None)
monkeypatch.setattr(utils, "random_sleep", lambda *args, **kwargs: None)
monkeypatch.setattr(utils, "sleep", lambda x: None)
# Standardize DarwinEngine across tests to prevent mockup math errors on session end
try:
from GramAddict.core.darwin_engine import DarwinEngine
monkeypatch.setattr(DarwinEngine, "evaluate_session_end", lambda *args, **kwargs: None)
except ImportError:
pass
@pytest.fixture
def e2e_configs():
import argparse
configs = MagicMock()
configs.args = argparse.Namespace(
username="testuser",
device="emulator-5554",
app_id="com.instagram.android",
debug=True,
feed=None,
carousel_percentage=0,
carousel_count="1",
explore=None,
reels=None,
stories=None,
interact_percentage=0,
likes_percentage=0,
follow_percentage=0,
comment_percentage=0,
working_hours=[0.0, 24.0],
time_delta_session=0,
speed_multiplier=1.0,
disable_filters=False,
interaction_users_amount="1",
scrape_profiles=False,
disable_ai_messaging=True,
total_unfollows_limit=0,
ai_telepathic_url="http://localhost",
ai_telepathic_model="llama3",
ai_condenser_url="http://localhost",
ai_condenser_model="llama3"
)
return configs

View File

@@ -0,0 +1,50 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow._humanized_horizontal_swipe")
def test_full_e2e_carousel_handling(
mock_swipe, mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector, e2e_configs
):
"""
Tests that the core feed loop successfully identifies native Carousel identifiers
in the XML and initiates organic swiping inputs.
"""
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, False, True]
mock_d_inst.wants_to_change_feed.return_value = False
mock_d_inst.wants_to_doomscroll.return_value = False
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Carousel")]
e2e_configs.args.feed = "1-2"
e2e_configs.args.carousel_percentage = 100
e2e_configs.args.carousel_count = "3-3"
# Load the captured UI dump containing native carousel_page_indicator
dynamic_e2e_dump_injector(device, {}, "carousel_post_dump.xml")
try:
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
with patch("secrets.choice", return_value="HomeFeed"):
with patch("random.random", return_value=0.0):
start_bot()
except Exception as e:
assert str(e) == "Clean Exit for Carousel"
# Verify that the bot accurately parsed the JSON/XML, detected the Carousel,
# and initiated exactly 3 horizontal right-to-left swipes as requested by args.
assert mock_swipe.call_count == 3

View File

@@ -0,0 +1,46 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_dm_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for DM")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
disable_ai_messaging = False
feed = None
reels = None
explore = None
stories = None
total_unfollows_limit = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_message_icon': 'dm_inbox_dump.xml'}, "home_feed_with_ad.xml")
# Let the core system hit its real execution loop with actual XMLs instead of circumventing it
try:
with patch("secrets.choice", return_value="MessageInbox"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for DM"
mock_open.assert_called()

View File

@@ -0,0 +1,44 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DojoEngine")
def test_dojo_lifecycle_integration(
mock_dojo, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_dojo_inst = mock_dojo.get_instance.return_value
mock_dojo_inst.is_running = True
mock_sess.inside_working_hours.side_effect = [Exception("Lifecycle Exit")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "1"
working_hours = "00:00-23:59"
time_delta_session = "0"
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml'}, "home_feed_with_ad.xml")
try:
start_bot(configs=configs)
except Exception as e:
assert "Lifecycle Exit" in str(e)
mock_dojo.get_instance.assert_called()
mock_dojo_inst.start.assert_called()
mock_dojo_inst.stop.assert_called()

View File

@@ -0,0 +1,49 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_explore_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Explore")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
explore = "5-8"
feed = None
reels = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
# The actual dump we need for this workflow (available in fixtures/fixtures)
# The fixture will automatically hit pytest.fail if the dump vanishes.
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="ExploreFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Explore"
mock_open.assert_called()

View File

@@ -0,0 +1,54 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_home_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
"""
Test a full E2E sequence for Home Feed using actual real XML dumps.
"""
device = MagicMock()
mock_create_device.return_value = device
# Setup mock dopamine & session
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Home")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "5-8"
explore = None
reels = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {}, "home_feed_with_ad.xml")
try:
# We must also mock secrets.choice to ensure HomeFeed is picked
with patch("secrets.choice", return_value="HomeFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Home"
mock_open.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_reels_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Reels")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
reels = "10"
feed = None
explore = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_reels_tab': 'reels_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="ReelsFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Reels"
mock_open.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow.ResonanceEngine")
@patch("GramAddict.core.bot_flow._interact_with_profile")
def test_full_e2e_scraping_sequence(
mock_interact, mock_resonance, mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector, e2e_configs
):
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.wants_to_change_feed.return_value = False
mock_d_inst.wants_to_doomscroll.return_value = False
type(mock_d_inst).boredom = PropertyMock(return_value=0.0)
mock_d_inst.is_app_session_over.side_effect = [False, False, False, False, False, True]
mock_res_inst = mock_resonance.return_value
mock_res_inst.calculate_resonance.return_value = 100.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit Scrape")]
e2e_configs.args.scrape_profiles = True
e2e_configs.args.interact_percentage = 100
e2e_configs.args.feed = "1"
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml'}, "carousel_post_dump.xml")
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node", return_value={"bounds": "[0,0][100,100]"}):
with patch("secrets.choice", return_value="HomeFeed"):
try:
start_bot()
except Exception as e:
if "Clean Exit Scrape" not in str(e):
raise e
mock_interact.assert_called()

View File

@@ -0,0 +1,52 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_search_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Search")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
search = "coding"
feed = None
reels = None
explore = None
stories = None
working_hours = "00:00-23:59"
time_delta_session = "0"
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="SearchFeed"):
start_bot(configs=configs)
except Exception as e:
assert "Clean Exit" in str(e)
mock_open.assert_called()

View File

@@ -0,0 +1,63 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow.GrowthBrain")
def test_full_start_bot_e2e_working_hours_limits(
mock_brain, mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
"""
Test start_bot full loop with working hours limits.
Verifies that the bot correctly sleeps when outside working hours
and exits the loop when session limits are reached.
"""
device = MagicMock()
mock_create_device.return_value = device
# Setup mock dopamine
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "5-8"
explore = None
reels = None
stories = None
total_unfollows_limit = 0
working_hours = ["10.00-11.00", "15.00-16.00"]
time_delta_session = 10
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
# On iteration 1: valid working hours
# On iteration 2: Exception to jump out of loop
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit limits test")]
dynamic_e2e_dump_injector(device, {}, "home_feed_with_ad.xml")
try:
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit limits test"
# Verify key interactions
mock_sess.inside_working_hours.assert_called()
mock_open.assert_called()
mock_sleep.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_stories_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Stories")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
stories = "5-8"
feed = None
reels = None
explore = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_home_tab': 'stories_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="StoriesFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Stories"
mock_open.assert_called()

View File

@@ -0,0 +1,48 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_unfollow_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Unfollow")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
total_unfollows_limit = 10
feed = None
reels = None
explore = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml', 'tap_following_list': 'unfollow_list_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="FollowingList"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Unfollow"
mock_open.assert_called()