Weihong Huang

68 papers A* 3B 2Misc 1Journal 34Unranked 27
YearRankTypeTitle / Venue / Authors
2026 A* conf
AAAI
Qiang Zhang, Feng Yang, Weihong Huang, Jing Feng, Juan Liu
2026 A* conf
AAAI
Weihong Huang, Feng Yang, Qiang Zhang, Juan Liu
2026 J jnl
Health Inf. Sci. Syst.
Zhongteng Zhang, Liu Zhang, Qing Peng, Zihao Zhang, Weihong Huang
2026 J jnl
CoRR
Jiaqi Lai, Hou Liang, Weihong Huang
2025 J jnl
IEEE Trans. Intell. Transp. Syst.
Chong Liu, Xu Han, Weihong Huang, Chen Long, Wang Wang, Zhen Dong, Bisheng Yang
2025 conf
ICIC (6)
Zhongteng Zhang, Qing Peng, Liu Zhang, Zihao Zhang, Jing Chong, Weihong Huang
2025 J jnl
IEEE Trans. Comput. Biol. Bioinform.
Qiang Zhang, Feng Yang, Weihong Huang, Jing Feng, Juan Liu
2024 J jnl
Briefings Bioinform.
Weihong Huang, Feng Yang, Qiang Zhang, Juan Liu
2024 J jnl
BMC Bioinform.
Zijian Feng, Weihong Huang, Haohao Li, Hancan Zhu, Yanlei Kang, Zhong Li
2024 conf
BCB
Zhongteng Zhang, Qing Peng, Liu Zhang, Zihao Zhang, Weihong Huang
2024 J jnl
Symmetry
Hongsheng Qiu, Kaikun Wu, Mo'men Ayasrah, Weihong Huang
2023 conf
CSCloud/EdgeCom
Honglong Chen, Mindong Liu, Jing Huang, Zhiling Zheng, Weihong Huang, Yufeng Xiao
2023 conf
CSCloud/EdgeCom
Jing Huang, Wenyu Wu, Weihong Huang, Yufeng Xiao, Li Lisi, Jinxi Sun
2023 J jnl
Int. J. Comput. Sci. Eng.
Jiaming Guo, Hong Wen, Weihong Huang, Ce Yang
2023 J jnl
Bioinform.
Yanlei Kang, Arne Elofsson, Yunliang Jiang, Weihong Huang, Minzhe Yu, Zhong Li
2023 J jnl
Int. J. Intell. Syst.
Yong Wang, Like Zhong, Weihong Huang, Xiaoyu He
2023 conf
CSCloud/EdgeCom
Huan Zhang, Rui Xie, Kuan-Ching Li, Weihong Huang, Chaoyi Yang, Jingnian Liu
2023 conf
CSCloud/EdgeCom
Weihong Huang, Kuan Jiang, Jing Huang, Li Lisi, Yufeng Xiao, Zihao Deng
2023 J jnl
Int. J. Commun. Syst.
Shaomiao Chen, Ce Yang, Weihong Huang, Wei Liang, Nai Ke, Alireza Souri, Kuan-Ching Li
2023 J jnl
Int. J. Inf. Technol. Syst. Approach
Yongfu Yu, Nannan Du, Zhongteng Zhang, Weihong Huang, Min Li
2023 conf
SmartCloud
Gaojing Yuan, Weihong Huang, Jiahong Cai
2022 conf
ACAI
Zhengheng Yan, Pei He, Weihong Huang, Suping Liu
2022 conf
SmartCom
Nengxiang Xu, Jiahong Cai, Yinyan Gong, Huan Zhang, Weihong Huang, Kuan-Ching Li
2022 conf
SmartCom
Ying Long, Yinyan Gong, Weihong Huang, Jiahong Cai, Nengxiang Xu, Kuan-Ching Li
2022 conf
SmartCom
Jingnian Liu, Weihong Huang, Lijun Xiao, Yingzi Huo, Huixuan Xiong, Xiong Li, Weidong Xiao
2022 conf
CISP-BMEI
Haoyu Wu, Shaohong Zhang, Han Ding, Weihong Huang
2021 J jnl
IEEE ACM Trans. Comput. Biol. Bioinform.
Lu Yan, Weihong Huang, Liming Wang, Song Feng, Yonghong Peng, Jie Peng
2021 conf
BigDataSecurity
Yue Wang, Xinlian Zhou, Shiwen Zhang, Weihong Huang
2021 B conf
ICIP
Xuhui Li, Yong Xu, Feng Xiang, Qing Liu, Weihong Huang, Bin Xie
2021 J jnl
Adv. Eng. Informatics
Qi Chen, Yuchun Huang, Hui Sun, Weihong Huang
2021 J jnl
Swarm Evol. Comput.
Xiaoyu He, Yong Wang, Xiaojing Wang, Weihong Huang, Shuang Zhao, Xiang Chen
2020 J jnl
IEEE Access
Lijun Xiao, Weihong Huang, Yong Xie, Weidong Xiao, Kuan-Ching Li
2020 J jnl
IEEE Access
Wang Shi Hui, Weihong Huang, Jianzhong Hu, Kun Tao, Yonghong Peng
2020 conf
SmartCloud
Xiaohe Wu, Jianbo Xu, Weihong Huang, Wei Jian
2020 conf
SmartCom
Han Deng, Chong Wang, Qiaohong Wu, Qin Nie, Weihong Huang, Shiwen Zhang
2020 J jnl
IEEE Internet Things J.
Wei Liang, Weihong Huang, Jing Long, Ke Zhang, Kuan-Ching Li, Dafang Zhang
2020 conf
SmartCom
Minfu Tan, Yujie Hong, Guiyan Quan, Yinhua Lin, Erhui Xi, Shiwen Zhang, Weihong Huang
2020 conf
SmartCom
Han Deng, Chong Wang, Shuang Xie, Aishan Mai, Weihong Huang, Shiwen Zhang
2020 conf
SmartCom
Minfu Tan, Jing Zheng, Jinshou Zhang, Yan Cheng, Weihong Huang
2020 conf
SmartCom
Chengjun Yang, Minfu Tan, Weihong Huang
2020 J jnl
Concurr. Comput. Pract. Exp.
Haibo Wu, Shiliang Shi, Yi Lu, Yong Liu, Weihong Huang
2019 J jnl
IEEE Access
Yisha Lan, Yimin Ding, Xin Luo, Yanzhao Zhang, Chenxi Huang, E. Y. K. Ng, Weihong Huang, Xuezhong Zhou, Jie Su, Yonghong Peng, Zhicheng Wang, Yongqiang Cheng, Wenliang Che
2019 conf
SmartCom
Weidong Xiao, Weihong Huang, Wei Liang, Xia Lei, Jiahong Cai, Yuanming Wang, Yanting Li
2019 J jnl
Int. J. Embed. Syst.
Weihong Huang, Renfa Li, Jianbo Xu, Yin Huang, Yong Sheng
2019 conf
SmartCloud
Lijun Xiao, Weihong Huang, Han Deng, Weidong Xiao
2019 J jnl
IEEE Access
Yanli Wang, Yuchun Huang, Weihong Huang
2019 J jnl
Sensors
Wei Liang, Weihong Huang, Wuhui Chen, Kuan-Ching Li, Keqin Li
2019 J jnl
IEEE Access
Zhe Wu, Shuang Zhao, Yonghong Peng, Xiaoyu He, Xinyu Zhao, Kai Huang, Xian Wu, Wei Fan, Fangfang Li, Mingliang Chen, Jie Li, Weihong Huang, Xiang Chen, Yi Li
2019 J jnl
J. Medical Syst.
Xinyu Zhao, Xian Wu, Fangfang Li, Yi Li, Weihong Huang, Kai Huang, Xiaoyu He, Wei Fan, Zhe Wu, Mingliang Chen, Jie Li, Zhong-ling Luo, Juan Su, Bin Xie, Shuang Zhao
2018 J jnl
IEEE Access
Shihui Wang, Qijian Zhang, Weihong Huang, Hanzhang Tian, Jianzhong Hu, Yongqiang Cheng, Yonghong Peng
2018 conf
CCHI
Shihui Wang, Weihong Huang, Xiumei Xie, Meifang Chen, Jianzhong Hu
2017 ch.
Handbook of Large-Scale Distributed Computing in Smart Healthcare
Jianzhong Hu, Weihong Huang, Yedong Huang
2016 J jnl
Comput. Sci. Inf. Syst.
Jing Long, Dafang Zhang, Chen Zuo, Jiajun Duan, Weihong Huang
2015 J jnl
Math. Comput. Simul.
Weihong Huang, Zhenxi Chen
2014 conf
CIFEr
Weihong Huang, Yu Zhang
2006 J jnl
Br. J. Educ. Technol.
Weihong Huang, David Webster, Dawn Wood, Tanko Ishaya
2006 J jnl
Multim. Tools Appl.
Weihong Huang, Alain Mille
2006 A* conf
WWW
David Webster, Weihong Huang, Darren Mundy, Paul Warren
2006 J jnl
Multim. Syst.
Weihong Huang, Emmanuel Eze, David Webster
2005 B conf
ICALT
Weihong Huang, Emmanuel Eze
2005 J jnl
J. Adv. Comput. Intell. Intell. Informatics
Weihong Huang
2004 conf
Web Intelligence
Weihong Huang, David Webster
2003 conf
ISMSE
Weihong Huang, M. O'Dea, Alain Mille
2003 Misc conf
IRI
Weihong Huang, Mohand-Said Hacid, Alain Mille
2003 conf
MMDB
Weihong Huang, Ting Tao, Mohand-Said Hacid, Alain Mille
2002 J jnl
Sci. China Ser. F Inf. Sci.
Weihong Huang, Fuyan Zhang, Zhengxing Sun
2002 conf
Multimedia Information Systems
Weihong Huang, Yannick Prié, Pierre-Antoine Champin, Alain Mille
2001 J jnl
Inf. Sci.
Andrew K. C. Wong, David K. Y. Chiu, Weihong Huang
tests/unit/test_apk_strings_ioc.py
← Index tests/unit/test_apk_strings_ioc.py python
"""Tests for APK string extraction and IOC extraction integration.

Covers:
- APKCodeAnalyzer._extract_strings() — string extraction from DEX objects
- APKCodeAnalyzer._string_entropy() — Shannon entropy computation
- DecompileAPK strings export table schema
- IOCExtractorFromResults with APK-format analysis_results
- Worker IOC wiring for APK
"""
import os
import tempfile
from dataclasses import asdict
from unittest.mock import MagicMock, patch

import pytest

from redb.extractors.ioc_extractor.ioc_extractor import IOCExtractorFromResults
from redb.extractors.ioc_extractor.standalone_ioc_extractor import (
    IOCType,
    SourceType,
)

pytestmark = [pytest.mark.unit, pytest.mark.apk]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_analyzer(min_instructions=1):
    """Create an APKCodeAnalyzer with mocked dependencies."""
    with patch(
        "redb.extractors.decompiler.apk.analyzer.JADXDecompiler"
    ), patch(
        "redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler"
    ):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        analyzer.min_instructions = min_instructions
        return analyzer


def _make_mock_dex(strings):
    """Create a mock DEX object that returns the given strings."""
    dex = MagicMock()
    dex.get_strings.return_value = strings
    return dex


def _create_fake_apk_file():
    """Create a temporary file to act as a fake APK."""
    fd, path = tempfile.mkstemp(suffix=".apk")
    os.write(fd, b"PK\x03\x04fake apk content for hashing")
    os.close(fd)
    return path


# ---------------------------------------------------------------------------
# Tests: _string_entropy
# ---------------------------------------------------------------------------

class TestStringEntropy:
    """Tests for Shannon entropy computation."""

    def test_empty_string(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("") == 0.0

    def test_single_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("a") == 0.0

    def test_repeated_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("aaaa") == 0.0

    def test_two_equal_chars(self):
        analyzer = _make_analyzer()
        # "ab" -> entropy = 1.0
        assert abs(analyzer._string_entropy("ab") - 1.0) < 0.01

    def test_higher_entropy(self):
        analyzer = _make_analyzer()
        # More variety = higher entropy
        low = analyzer._string_entropy("aabb")
        high = analyzer._string_entropy("abcd")
        assert high > low

    def test_url_string_entropy(self):
        analyzer = _make_analyzer()
        entropy = analyzer._string_entropy("https://evil.com/payload")
        assert entropy > 2.0  # URLs have moderate entropy


# ---------------------------------------------------------------------------
# Tests: _extract_strings
# ---------------------------------------------------------------------------

class TestExtractStrings:
    """Tests for DEX string extraction."""

    def test_basic_extraction(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["hello", "world"])
        result = analyzer._extract_strings([dex])

        assert len(result) == 2
        assert result[0]["string"] == "hello"
        assert result[1]["string"] == "world"

    def test_string_fields_present(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["test_string"])
        result = analyzer._extract_strings([dex])

        entry = result[0]
        assert "string" in entry
        assert "string_encoding" in entry
        assert "string_offset" in entry
        assert "string_length" in entry
        assert "string_entropy" in entry
        assert entry["string_encoding"] == "UTF8"
        assert entry["string_length"] == len("test_string")

    def test_deduplication_across_dex(self):
        """Same string in multiple DEX files is only extracted once."""
        analyzer = _make_analyzer()
        dex1 = _make_mock_dex(["shared", "unique1"])
        dex2 = _make_mock_dex(["shared", "unique2"])
        result = analyzer._extract_strings([dex1, dex2])

        string_values = [s["string"] for s in result]
        assert string_values.count("shared") == 1
        assert "unique1" in string_values
        assert "unique2" in string_values
        assert len(result) == 3

    def test_empty_strings_filtered(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["", "valid", None, "also_valid"])
        result = analyzer._extract_strings([dex])

        string_values = [s["string"] for s in result]
        assert "" not in string_values
        assert None not in string_values
        assert "valid" in string_values
        assert "also_valid" in string_values

    def test_no_dex_files(self):
        analyzer = _make_analyzer()
        result = analyzer._extract_strings([])
        assert result == []

    def test_dex_get_strings_returns_none(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.return_value = None
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_dex_get_strings_exception(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.side_effect = RuntimeError("corrupt DEX")
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_entropy_computed(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["abcdefgh"])
        result = analyzer._extract_strings([dex])
        assert result[0]["string_entropy"] > 0.0

    def test_incremental_offsets(self):
        """String offsets are incremented sequentially."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["a", "b", "c"])
        result = analyzer._extract_strings([dex])

        offsets = [s["string_offset"] for s in result]
        assert offsets == [0, 1, 2]

    def test_url_strings_preserved(self):
        """URL strings are extracted without modification."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex([
            "https://evil.com/payload",
            "http://c2.malware.org/gate",
        ])
        result = analyzer._extract_strings([dex])
        strings = [s["string"] for s in result]
        assert "https://evil.com/payload" in strings
        assert "http://c2.malware.org/gate" in strings


# ---------------------------------------------------------------------------
# Tests: strings in extract() integration
# ---------------------------------------------------------------------------

class TestExtractStringsIntegration:
    """Tests that strings are included in extract() results."""

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_includes_strings(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        mock_dex = _make_mock_dex(["hello", "world"])
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        mock_androguard.return_value = (MagicMock(), [mock_dex], mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        assert "strings" in results
        assert len(results["strings"]) == 2
        assert results["strings"][0]["string"] == "hello"
        analyzer.cleanup()

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_strings_error_is_non_fatal(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        # Return a bad dex list that will cause _extract_strings to fail
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        # dexs is not iterable — triggers the except block
        mock_androguard.return_value = (MagicMock(), 42, mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        # Should not crash, strings should be empty, error recorded
        assert results["strings"] == []
        assert any(
            e.get("error_location") == "strings"
            for e in results["analysis_errors"]
        )
        analyzer.cleanup()


# ---------------------------------------------------------------------------
# Tests: DecompileAPK strings export table
# ---------------------------------------------------------------------------

class TestDecompileAPKStringsExport:
    """Tests for strings table in DecompileAPK.prepare_export_data()."""

    def _make_extractor_with_strings(self, strings_data):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        ext = DecompileAPK(apk_path, MagicMock())
        ext.analysis_results = {
            "sha256": "a" * 64,
            "sha1": "b" * 40,
            "md5": "c" * 32,
            "decompiled_content": [],
            "decompiled_refs": [],
            "smali_content": [],
            "smali_refs": [],
            "similarity_metrics": [],
            "strings": strings_data,
            "analysis_errors": [],
        }
        return ext, apk_path

    def test_strings_table_present(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" in export
        finally:
            os.unlink(path)

    def test_strings_table_schema(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sr = export["strings_raw"]
            assert sr["table"] == "code_binja_strings_raw"
            assert len(sr["column_names"]) == 8
            assert len(sr["column_type_names"]) == 8
            assert "sha256" in sr["column_names"]
            assert "string" in sr["column_names"]
            assert "string_raw" in sr["column_names"]
            assert "string_encoding" in sr["column_names"]
            assert "string_offset" in sr["column_names"]
            assert "string_length" in sr["column_names"]
            assert "string_raw_length" in sr["column_names"]
            assert "string_entropy" in sr["column_names"]
            assert len(sr["data"]) == 1
            assert len(sr["data"][0]) == 8
        finally:
            os.unlink(path)

    def test_strings_sha256_propagated(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "test", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 4, "string_entropy": 2.0}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            row = export["strings_raw"]["data"][0]
            assert row[0] == "a" * 64  # sha256 is first column
        finally:
            os.unlink(path)

    def test_empty_strings_not_exported(self):
        ext, path = self._make_extractor_with_strings([])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" not in export
        finally:
            os.unlink(path)


# ---------------------------------------------------------------------------
# Tests: IOCExtractorFromResults with APK format
# ---------------------------------------------------------------------------

class TestIOCExtractorAPKFormat:
    """Tests that IOCExtractorFromResults handles APK analysis_results."""

    def setup_method(self):
        self.log = MagicMock()

    def test_extract_iocs_from_apk_strings(self):
        """IOCs extracted from APK strings (same format as Binja)."""
        results = {
            "strings": [
                {"string": "https://evil.com/payload", "string_offset": 0},
                {"string": "Contact [email protected]", "string_offset": 1},
            ],
            "decompiled": [],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1
        assert any("evil.com" in i.ioc_value for i in url_iocs)

    def test_extract_iocs_from_apk_decompiled_content(self):
        """IOCs extracted from APK's decompiled_content key."""
        results = {
            "strings": [],
            "decompiled": [],  # Binja format — empty
            "decompiled_content": [  # APK format
                {
                    "decompiled_method": "connect('https://c2.malware.org/gate');",
                    "decompiled_method_hash": "hash123",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_apk_library_methods_skipped(self):
        """LIBRARY methods in decompiled_content are skipped."""
        results = {
            "strings": [],
            "decompiled": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://should-skip.com",
                    "decompiled_method_hash": "lib_hash",
                    "method_type": "LIBRARY",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        assert len(iocs) == 0

    def test_combined_binja_and_apk_sources(self):
        """Both Binja and APK format decompiled entries are processed."""
        results = {
            "strings": [],
            "decompiled": [
                {
                    "decompiled_function": "call('https://binja.example.com');",
                    "decompiled_function_hash": "binja_hash",
                    "function_type": "USER",
                }
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "send('https://apk.example.com');",
                    "decompiled_method_hash": "apk_hash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        urls = [i.ioc_value for i in url_iocs]
        assert any("binja.example.com" in u for u in urls)
        assert any("apk.example.com" in u for u in urls)

    def test_apk_results_only(self):
        """Works when only APK keys are present (no 'decompiled' key)."""
        results = {
            "strings": [
                {"string": "https://evil.com", "string_offset": 0},
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "x = 1",
                    "decompiled_method_hash": "h",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        # Should not crash, and should find URL from strings
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_source_type_is_string_for_strings(self):
        results = {
            "strings": [
                {"string": "https://evil.com/test", "string_offset": 42},
            ],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.STRING
        assert url_iocs[0].source_identifier == "42"

    def test_source_type_is_decompiled_for_methods(self):
        results = {
            "strings": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://evil.com/method",
                    "decompiled_method_hash": "myhash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.DECOMPILED_FUNCTION
        assert url_iocs[0].source_identifier == "myhash"


# ---------------------------------------------------------------------------
# Tests: Worker IOC wiring for APK
# ---------------------------------------------------------------------------

class TestWorkerAPKIOCWiring:
    """Tests that workers.py wires IOC extraction for APK."""

    def test_worker_source_contains_ioc_for_apk(self):
        """Verify workers.py source has IOC extraction in APK branch."""
        import inspect
        from redb import workers
        source = inspect.getsource(workers)
        # APK branch should import IOCExtractorFromResults
        assert "IOCExtractorFromResults" in source
        # Should be in the APK section (near DecompileAPK)
        apk_section = source[source.index("DecompileAPK("):]
        ioc_pos = apk_section.find("IOCExtractorFromResults")
        assert ioc_pos != -1, "IOCExtractorFromResults not found after DecompileAPK"