Varun Grover

217 papers B 1C 15Journal 190Unranked 11
YearRankTypeTitle / Venue / Authors
2025 J jnl
MIS Q. Executive
Iris A. Junglas, Noel Carroll, Varun Grover, Blake Ives, Bill Kettinger, Hope Koch, Martin Mocker, Fred Niederman, Gabriele Piccoli, Federico Pigni, Jeffrey Proudfoot, Richard T. Watson
2025 J jnl
Inf. Syst. Res.
Varun Grover, Xinhui Zhan, Heshan Sun, Dan Claire Jiang
2025 J jnl
Int. J. Inf. Manag.
Arpan Kumar Kar, Wu He, Fay Cobb Payton, Varun Grover, Adil S. Al-Busaidi, Yogesh K. Dwivedi
2025 J jnl
J. Assoc. Inf. Syst.
Franck Soh, Varun Grover
2025 J jnl
Inf. Syst. Res.
Franck Soh, Pankaj Setia, Varun Grover
2025 J jnl
Inf. Manag.
Sachithra Lokuge, Darshana Sedera, Varun Grover, Suprateek Sarker
2024 J jnl
Scalable Comput. Pract. Exp.
Varun Grover, Jagadeeshwaran J, Nithish K, Ashick Sanjey D. S, Venkatesh V, Ashokkumar P
2024 J jnl
Internet Res.
Mohammad AlMarzouq, Varun Grover, Jason Thatcher, Richard Klein
2024 J jnl
J. Strateg. Inf. Syst.
Gabriele Piccoli, Varun Grover, Joaquin Rodriguez
2024 C conf
ICIS
Elizabeth J. Davidson, Robert M. Davison, Varun Grover, Dirk Hovorka, Helmut Krcmar, Fred Niederman
2024 J jnl
J. Assoc. Inf. Syst.
Nikhil Bandodkar, Varun Grover, Renu Singh
2024 J jnl
Inf. Comput. Secur.
Hamid Reza Nikkhah, Varun Grover, Rajiv Sabherwal
2024 J jnl
J. Manag. Inf. Syst.
Hamid Reza Nikkhah, Varun Grover
2024 J jnl
J. Assoc. Inf. Syst.
Rajiv Sabherwal, Varun Grover
2023 J jnl
Int. J. Inf. Manag.
Christian Maier, Jason Bennett Thatcher, Varun Grover, Yogesh K. Dwivedi
2023 J jnl
Inf. Syst. Res.
Abayomi Baiyere, Varun Grover, Kalle J. Lyytinen, Stephanie L. Woerner, Alok Gupta
2023 J jnl
J. Manag. Inf. Syst.
Franck Soh, Varun Grover
2023 J jnl
Comput. Hum. Behav.
Pamela G. Schlosser, Tingting (Rachel) Chung, Varun Grover
2023 J jnl
Decis. Support Syst.
Shih-Lun Tseng, Shuya Lu, Danny Weathers, Varun Grover
2023 J jnl
J. Inf. Technol.
Varun Grover, Kalle Lyytinen
2023 J jnl
J. Inf. Technol.
Varun Grover, Kalle Lyytinen
2023 J jnl
Int. J. Inf. Manag.
Yogesh K. Dwivedi, Nir Kshetri, Laurie Hughes, Emma L. Slade, Anand Jeyaraj, Arpan Kumar Kar, Abdullah M. Baabdullah, Alex Koohang, Vishnupriya Raghavan, Manju Ahuja, Hanaa Albanna, Mousa Ahmad Albashrawi, Adil S. Al-Busaidi, Janarthanan Balakrishnan, Yves Barlette, Sriparna Basu, Indranil Bose, Laurence D. Brooks, Dimitrios Buhalis, Lemuria D. Carter, Soumyadeb Chowdhury, Tom Crick, Scott W. Cunningham, Gareth H. Davies, Robert M. Davison, Rahul De', Denis Dennehy, Yanqing Duan, Rameshwar Dubey, Rohita Dwivedi, John S. Edwards, Carlos Flavián, Robin Gauld, Varun Grover, Mei-Chih Hu, Marijn Janssen, Paul Jones, Iris A. Junglas, Sangeeta Khorana, Sascha Kraus, Kai R. Larsen, Paul Latreille, Sven Laumer, F. Tegwen Malik, Abbas Mardani, Marcello Mariani, Sunil Mithas, Emmanuel Mogaji, Jeretta Horn Nord, Siobhán O'Connor, Fevzi Okumus, Margherita Pagani, Neeraj Pandey, Savvas Papagiannidis, Ilias O. Pappas, Nishith Pathak, Jan Pries-Heje, Ramakrishnan Raman, Nripendra P. Rana, Sven-Volker Rehm, Samuel Ribeiro-Navarrete, Alexander Richter, Frantz Rowe, Suprateek Sarker, Bernd Carsten Stahl, Manoj Kumar Tiwari, Wil M. P. van der Aalst, Viswanath Venkatesh, Giampaolo Viglia, Michael R. Wade, Paul Walton, Jochen Wirtz, Ryan T. Wright
2023 conf
HICSS
Carolina Reis, Varun Grover
2023 J jnl
J. Inf. Technol.
Varun Grover, Kalle Lyytinen
2023 J jnl
J. Syst. Inf. Technol.
Ali Balapour, Rajiv Sabherwal, Varun Grover
2022 J jnl
J. Assoc. Inf. Syst.
Pratyush Nidhi Sharma, Sherae L. Daniel, Tingting (Rachel) Chung, Varun Grover
2022 J jnl
Inf. Manag.
Varun Grover, Shih-Lun Tseng, Wenxi Pu
2022 J jnl
Eur. J. Inf. Syst.
Varun Grover
2022 J jnl
J. Assoc. Inf. Syst.
Nikhil Bandodkar, Varun Grover
2022 J jnl
Inf. Syst. E Bus. Manag.
Stefan Tams, Varun Grover, Jason Thatcher, Manju Ahuja
2022 J jnl
Found. Trends Inf. Syst.
Michelle Carter, Victoria Reibenspiess, Yafang Li, Varun Grover
2022 J jnl
ACM SIGMIS Database
Neil Chueh-An Lee, Eric T. G. Wang, Varun Grover
2022 J jnl
J. Manag. Inf. Syst.
Franck Soh, Varun Grover
2022 J jnl
MIS Q.
Jingmei Zhou, Yulin Fang, Varun Grover
2022 J jnl
Inf. Manag.
Darshana Sedera, Sachithra Lokuge, Varun Grover
2022 J jnl
Int. J. Inf. Manag.
Yaping Zhu, Varun Grover
2022 J jnl
Commun. Assoc. Inf. Syst.
Mekhala Egodawele, Darshana Sedera, Varun Grover, Dilanjan Seneviratne, Banura Sooriyapperuma
2022 J jnl
MIS Q. Executive
Varun Grover, Kalle Lyytinen
2022 J jnl
Commun. Assoc. Inf. Syst.
Aron Lindberg, Kalle Lyytinen, Varun Grover
2022 J jnl
J. Assoc. Inf. Syst.
Michael Dinger, Jason Bennett Thatcher, Varun Grover, John F. Tripp
2021 J jnl
J. Assoc. Inf. Syst.
James Burleson, Varun Grover, Jason Bennett Thatcher, Heshan Sun
2021 conf
AMCIS
Jackie London, Varun Grover
2021 J jnl
J. Assoc. Inf. Syst.
Varun Grover, Fred Niederman
2021 J jnl
MIS Q. Executive
Varun Grover, Kalle Lyytinen
2020 J jnl
J. Manag. Inf. Syst.
Franck Soh, Varun Grover
2020 C conf
ICIS
Joey F. George, Brian Fitzgerald, Souren Paul, Narayana Murthy, Cynthia Mathis Beath, Varun Grover, Elena Karahanna, Jae Kyu Lee
2020 J jnl
J. Strateg. Inf. Syst.
Neil Chueh-An Lee, Eric T. G. Wang, Varun Grover
2020 J jnl
J. Assoc. Inf. Syst.
Michelle Carter, Stacie Petter, Varun Grover, Jason Bennett Thatcher
2020 J jnl
MIS Q.
Michelle Carter, Stacie Petter, Varun Grover, Jason Bennett Thatcher
2020 J jnl
Int. J. Inf. Manag.
Varun Grover, Rajiv Sabherwal
2020 C conf
ICIS
Gabriele Piccoli, Joaquin Rodriguez, Varun Grover
2020 J jnl
Inf. Syst. J.
Roopa Raman, Varun Grover
2020 J jnl
J. Assoc. Inf. Syst.
Varun Grover, Aron Lindberg, Izak Benbasat, Kalle Lyytinen
2020 J jnl
Inf. Manag.
Hengqi Tian, Varun Grover, Jing Zhao, Yi Jiang
2020 J jnl
J. Strateg. Inf. Syst.
Stefan Tams, Manju Ahuja, Jason Thatcher, Varun Grover
2019 J jnl
Commun. Assoc. Inf. Syst.
Varun Grover
2019 J jnl
J. Inf. Technol.
Varun Grover, Michelle Carter, Dan Jiang
2019 J jnl
Inf. Manag.
Sachithra Lokuge, Darshana Sedera, Varun Grover, Dongming Xu
2019 J jnl
Data Base
Varun Grover
2019 J jnl
J. Manag. Inf. Syst.
Kevin Craig, Jason Bennett Thatcher, Varun Grover
2019 J jnl
J. Inf. Technol.
Varun Grover, Michelle Carter, Dan Jiang
2018 J jnl
MIS Q. Executive
Varun Grover, Rajiv Kohli, Pradipkumar Ramanlal
2018 J jnl
J. Assoc. Inf. Syst.
Stefan Tams, Jason Bennett Thatcher, Varun Grover
2018 J jnl
J. Manag. Inf. Syst.
Varun Grover, Roger H. L. Chiang, Ting-Peng Liang, Dongsong Zhang
2018 J jnl
J. Manag. Inf. Syst.
Roger H. L. Chiang, Varun Grover, Ting-Peng Liang, Dongsong Zhang
2018 C conf
ICIS
Monica Chiarini Tremblay, Rajiv Kohli, Nicole Forsgren, Varun Grover, Ahmed Abassi, Alan R. Hevner, Roy Rapaport
2018 J jnl
Commun. Assoc. Inf. Syst.
Iaroslava Dutchak, Shih-Lun Tseng, Varun Grover
2017 J jnl
Inf. Syst. Manag.
Jennifer E. Gerow, Varun Grover, Jason Bennett Thatcher
2017 C conf
ICIS
Abayomi Baiyere, Varun Grover, Alok Gupta, Stephanie L. Woerner, Kalle J. Lyytinen
2017 J jnl
J. Assoc. Inf. Syst.
Kalle Lyytinen, Varun Grover
2017 conf
AMCIS
Jackie London, Kevin D. Matthews, Varun Grover
2017 conf
AMCIS
Shih-Lun Tseng, Shuya Lu, Varun Grover, Danny Weathers
2017 J jnl
Commun. Assoc. Inf. Syst.
Lianlian Jiang, Dan Jiang, Varun Grover
2017 conf
HICSS
Stefan Tams, Varun Grover, Jason Thatcher, Manju Ahuja
2017 J jnl
Inf. Manag.
Michelle Carter, Stefan Tams, Varun Grover
2016 J jnl
Commun. Assoc. Inf. Syst.
Varun Grover, Jackie London, Kevin Craig
2016 J jnl
Inf. Manag.
Jennifer E. Gerow, Varun Grover, Jason Bennett Thatcher
2016 J jnl
Commun. Assoc. Inf. Syst.
Nikhil Ramkrishna Bandodkar, Varun Grover
2016 J jnl
Inf. Manag.
Darshana Sedera, Sachithra Lokuge, Varun Grover, Suprateek Sarker, Saonee Sarker
2016 conf
AMCIS
Jennifer E. Gerow, Varun Grover, Jason Bennett Thatcher
2016 C conf
ICIS
Roopa Raman, Varun Grover
2016 C conf
ICIS
Dan Jiang, Lianlian Jiang, Jake London, Varun Grover, Heshan Sun
2016 C conf
ICIS
Kevin Craig, Varun Grover
2016 conf
PACIS
Sachithra Lokuge, Darshana Sedera, Varun Grover
2015 J jnl
Decis. Support Syst.
Danny Weathers, Scott D. Swain, Varun Grover
2015 conf
AMCIS
Varun Grover, Dan Jiang, Heshan Sun
2015 J jnl
J. Assoc. Inf. Syst.
Pamela S. Galluch, Varun Grover, Jason Bennett Thatcher
2015 J jnl
MIS Q.
Michelle Carter, Varun Grover
2015 J jnl
MIS Q.
Varun Grover, Kalle Lyytinen
2015 J jnl
Eur. J. Inf. Syst.
Jennifer E. Gerow, Jason Bennett Thatcher, Varun Grover
2015 J jnl
Decis. Support Syst.
Mohammad AlMarzouq, Varun Grover, Jason Bennett Thatcher
2014 J jnl
MIS Q.
Jennifer E. Gerow, Varun Grover, Jason Bennett Thatcher, Philip L. Roth
2014 J jnl
J. Strateg. Inf. Syst.
Stefan Tams, Varun Grover, Jason Bennett Thatcher
2014 J jnl
J. Assoc. Inf. Syst.
Stefan Tams, Kevin Hill, Ana Ortiz de Guinea, Jason Bennett Thatcher, Varun Grover
2014 C conf
ICIS
James Burleson, Varun Grover, Jason Bennett Thatcher, Heshan Sun
2014 J jnl
Commun. Assoc. Inf. Syst.
Varun Grover
2014 J jnl
Commun. Assoc. Inf. Syst.
Varun Grover, Roopa Raman, Adam Stubblefield
2013 J jnl
Inf. Manag.
Kathy S. Schwaig, Albert H. Segars, Varun Grover, Kirk D. Fiedler
2013 C conf
ICIS
Amol Kharabe, Kalle Lyytinen, Varun Grover
2013 J jnl
MIS Q.
Eric T. G. Wang, Jeffrey C. F. Tai, Varun Grover
2013 J jnl
J. Assoc. Inf. Syst.
Varun Grover
2013 J jnl
Inf. Syst. Res.
Kun Shin Im, Varun Grover, James T. C. Teng
2013 J jnl
J. Manag. Inf. Syst.
Ravi Narayanaswamy, Varun Grover, Raymond M. Henry
2012 J jnl
MIS Q.
Nicholas H. Roberts, Pamela S. Galluch, Michael Dinger, Varun Grover
2012 J jnl
MIS Q.
Varun Grover, Rajiv Kohli
2012 J jnl
J. Manag. Inf. Syst.
Nicholas H. Roberts, Varun Grover
2012 conf
AMCIS
Jennifer E. Gerow, Varun Grover, Jason Bennett Thatcher
2012 J jnl
Inf. Syst. J.
Samuel Otim, Varun Grover
2012 J jnl
IEEE Trans. Engineering Management
Jaejoo Lim, Varun Grover, Russell L. Purvis
2012 J jnl
J. Manag. Inf. Syst.
Samuel Otim, Kevin E. Dow, Varun Grover, Jeffrey A. Wong
2012 J jnl
J. Assoc. Inf. Syst.
Varun Grover
2011 J jnl
Decis. Sci.
Achita (Mi) Muthitcharoen, Prashant C. Palvia, Varun Grover
2011 J jnl
MIS Q. Executive
Varun Grover, Elena Karahanna, Omar El Sawy, Carol V. Brown
2011 J jnl
Decis. Sci.
Khawaja A. Saeed, Manoj K. Malhotra, Varun Grover
2011 C conf
ICIS
Michel Avital, Natalia Levina, Ritu Agarwal, Niels Bjørn-Andersen, Varun Grover, Eldon Y. Li, Dov Te'eni
2011 J jnl
MIS Q.
Ramakrishna Ayyagari, Varun Grover, Russell L. Purvis
2011 J jnl
MIS Q. Executive
Michelle Carter, Varun Grover, Jason Bennett Thatcher
2011 J jnl
Data Base
Khawaja Asjad Saeed, Varun Grover, William J. Kettinger, Subo Guha
2010 J jnl
Inf. Syst. J.
Rajiv Sabherwal, Varun Grover
2010 J jnl
Theor. Comput. Sci.
András Antos, Varun Grover, Csaba Szepesvári
2010 J jnl
Inf. Manag.
Ganesh D. Bhatt, Ali F. Emdad, Nicholas H. Roberts, Varun Grover
2010 C conf
ICIS
Alina M. Chircu, Varun Grover, Ann Majchrzak, Michael Rosemann
2010 J jnl
Electron. Mark.
Samuel Otim, Varun Grover
2010 J jnl
MIS Q.
Gimun Kim, Bongsik Shin, Varun Grover
2010 J jnl
Inf. Manag.
J. Christopher Zimmer, Riza Ergun Arsal, Mohammad AlMarzouq, Varun Grover
2010 J jnl
Decis. Support Syst.
J. Christopher Zimmer, Riza Ergun Arsal, Mohammad AlMarzouq, DeWayne Moore, Varun Grover
2010 J jnl
Commun. Assoc. Inf. Syst.
Stefan Tams, Varun Grover
2009 J jnl
Eur. J. Inf. Syst.
Chaojie Duan, Varun Grover, Nagraj (Raju) Balakrishnan
2009 J jnl
Commun. Assoc. Inf. Syst.
Fred Niederman, Shirley Gregor, Varun Grover, Kalle Lyytinen, Carol Saunders
2009 J jnl
Inf. Manag.
Guang Rong, Varun Grover
2009 J jnl
J. Assoc. Inf. Syst.
Varun Grover, Rahul Gokhale, Ravi S. Narayanswamy
2008 B conf
ALT
András Antos, Varun Grover, Csaba Szepesvári
2008 J jnl
J. Assoc. Inf. Syst.
Rajiv Kohli, Varun Grover
2008 J jnl
J. Assoc. Inf. Syst.
Bernard C. Y. Tan, Ananth Srinivasan, Kalle Lyytinen, Varun Grover
2008 C conf
ICIS
Shirley Gregor, Varun Grover, Kalle Lyytinen, Carol S. Saunders, Fred Niederman
2008 J jnl
Commun. Assoc. Inf. Syst.
Henry C. Lucas Jr., Varun Grover
2007 conf
AMCIS
Riza Ergun Arsal, Chris Zimmer, Mohammad AlMarzouq, Varun Grover
2007 J jnl
Commun. Assoc. Inf. Syst.
Jaejoo Lim, Guang Rong, Varun Grover
2007 J jnl
IEEE Trans. Engineering Management
Varun Grover, Russell L. Purvis, Albert H. Segars
2007 J jnl
Commun. ACM
Varun Grover, Raymond M. Henry, Jason Bennett Thatcher
2007 C conf
ICIS
Varun Grover, Henry C. Lucas, Frank Batavick
2007 J jnl
J. Manag. Inf. Syst.
Varun Grover, Khawaja Asjad Saeed
2006 J jnl
J. Assoc. Inf. Syst.
Varun Grover, Rahul Gokhale, Jaejoo Lim, John Coffey, Ramakrishna Ayyagari
2006 J jnl
J. Assoc. Inf. Syst.
Varun Grover, Rahul Gokhale, Jaejoo Lim, Ramakrishna Ayyagari
2006 J jnl
Eur. J. Inf. Syst.
Samuel Otim, Varun Grover
2006 J jnl
Data Base
Bonnie Glassberg, Varun Grover, James T. C. Teng
2006 J jnl
Decis. Sci.
Varun Grover, Jaejoo Lim, Ramakrishna Ayyagari
2005 J jnl
Inf. Manag.
Varun Grover, Albert H. Segars
2005 J jnl
Int. J. Bus. Inf. Syst.
Roopa Raman, Varun Grover
2005 J jnl
Decis. Sci.
Khawaja A. Saeed, Manoj K. Malhotra, Varun Grover
2005 J jnl
Commun. Assoc. Inf. Syst.
Mohammad AlMarzouq, Li Zheng, Guang Rong, Varun Grover
2005 J jnl
J. Manag. Inf. Syst.
Khawaja Asjad Saeed, Varun Grover, Yujong Hwang
2005 J jnl
J. Manag. Inf. Syst.
Ganesh D. Bhatt, Varun Grover
2004 J jnl
Inf. Syst. J.
Varun Grover, Khawaja Asjad Saeed
2004 conf
AMCIS
Bonnie Glassberg, Varun Grover, James T. C. Teng
2004 J jnl
J. Manag. Inf. Syst.
Robin R. Pennington, H. Dixon Wilcox, Varun Grover
2003 J jnl
Inf. Manag.
Gary Hackbarth, Varun Grover, Mun Yong Yi
2003 J jnl
Commun. ACM
Khawaja Asjad Saeed, Varun Grover, Yujong Hwang
2003 J jnl
Inf. Manag.
Kuo-Chung Chang, Joyce Jackson, Varun Grover
2003 J jnl
MIS Q.
V. Sambamurthy, Anandhi S. Bharadwaj, Varun Grover
2003 J jnl
Commun. ACM
Varun Grover, Khawaja Asjad Saeed
2002 J jnl
IEEE Trans. Engineering Management
James T. C. Teng, Varun Grover, Wolfgang Güttler
2002 J jnl
Int. J. Electron. Commer.
Khawaja A. Saeed, Yujong Hwang, Varun Grover
2002 J jnl
J. Assoc. Inf. Syst.
Varun Grover, James T. C. Teng, Kirk D. Fiedler
2002 J jnl
J. Manag. Inf. Syst.
Mark E. McMurtrey, Varun Grover, James T. C. Teng, Nancy J. Lightner
2002 J jnl
Decis. Sci.
Karen E. Papke-Shields, Manoj K. Malhotra, Varun Grover
2001 J jnl
Commun. ACM
Varun Grover, James T. C. Teng
2001 J jnl
J. Manag. Inf. Syst.
Thomas H. Davenport, Varun Grover
2001 J jnl
Inf. Syst. Res.
Kun Shin Im, Kevin E. Dow, Varun Grover
2001 J jnl
J. Manag. Inf. Syst.
Thomas H. Davenport, Varun Grover
2001 J jnl
Inf. Manag.
H. Dixon Wilcox, Kuo-Chung Chang, Varun Grover
2000 J jnl
Ind. Manag. Data Syst.
Mark E. McMurtrey, James T. C. Teng, Varun Grover, Hemant V. Kher
2000 J jnl
J. Manag. Inf. Syst.
Choong C. Lee, Varun Grover
2000 J jnl
Commun. ACM
Varun Grover, Pradeep Vaswani
1999 J jnl
Int. J. Electron. Commer.
Varun Grover, Pradipkumar Ramanlal, Albert H. Segars
1999 J jnl
Int. J. Electron. Commer.
Varun Grover, Albert H. Segars
1999 C conf
ICIS
Kalle Lyytinen, Varun Grover, Jane Linder, Haim Mendelson, James A. Senn, John J. Sviokla
1999 J jnl
Inf. Syst. Res.
Albert H. Segars, Varun Grover
1999 J jnl
MIS Q.
Varun Grover, Pradipkumar Ramanlal
1999 J jnl
Inf. Syst. Manag.
Gary Hackbarth, Varun Grover
1998 J jnl
Commun. ACM
Varun Grover, James T. C. Teng, Kirk D. Fiedler
1998 J jnl
Commun. ACM
James T. C. Teng, Seung Ryul Jeong, Varun Grover
1998 J jnl
MIS Q.
Albert H. Segars, Varun Grover
1998 J jnl
Inf. Syst. Manag.
Varun Grover, Seung Ryul Jeong, James T. C. Teng
1998 J jnl
Inf. Manag.
Varun Grover, James T. C. Teng, Albert H. Segars, Kirk D. Fiedler
1997 J jnl
J. Manag. Inf. Syst.
Subo Guha, Varun Grover, William J. Kettinger, James T. C. Teng
1997 J jnl
Inf. Syst. Res.
Varun Grover, Kirk D. Fiedler, James T. C. Teng
1997 J jnl
J. Manag. Inf. Syst.
Varun Grover, William J. Kettinger
1996 J jnl
J. Manag. Inf. Syst.
Kirk D. Fiedler, Varun Grover, James T. C. Teng
1996 J jnl
Data Base
Varun Grover
1996 J jnl
Data Base
Varun Grover, Albert H. Segars
1996 J jnl
Inf. Manag.
Varun Grover, Seung Ryul Jeong, Albert H. Segars
1996 J jnl
J. Manag. Inf. Syst.
Varun Grover, Myun Joong Cheon, James T. C. Teng
1996 J jnl
Inf. Syst. Res.
Steven J. Simon, Varun Grover, James T. C. Teng, Kathleen Whitcomb
1995 J jnl
J. Manag. Inf. Syst.
William J. Kettinger, Varun Grover
1995 J jnl
J. Manag. Inf. Syst.
Varun Grover, Seung Ryul Jeong, William J. Kettinger, Shouhong Wang
1995 J jnl
J. Inf. Technol.
Myun Joong Cheon, Varun Grover, James T. C. Teng
1994 J jnl
Inf. Manag.
Varun Grover, Myun Joong Cheon, James T. C. Teng
1994 J jnl
Inf. Syst. J.
Varun Grover, James T. C. Teng
1994 J jnl
J. Inf. Technol.
Kirk D. Fiedler, Varun Grover, James T. C. Teng
1994 J jnl
J. Strateg. Inf. Syst.
Varun Grover, Albert H. Segars, Douglas E. Durand
1994 J jnl
Data Base
Paul E. Cule, Varun Grover
1994 J jnl
MIS Q.
William J. Kettinger, Varun Grover, Subashish Guha, Albert H. Segars
1994 J jnl
J. Strateg. Inf. Syst.
Albert H. Segars, Varun Grover, William J. Kettinger
1993 J jnl
J. Manag. Inf. Syst.
F. Greg Burton, Yi-Ning Chen, Varun Grover, Kathy A. Stewart
1993 J jnl
Inf. Manag.
Varun Grover, Choong C. Lee, Douglas E. Durand
1993 J jnl
Data Base
Varun Grover, Martin D. Goslar
1993 J jnl
J. Manag. Inf. Syst.
Varun Grover, Seung Ryul Jeong, William J. Kettinger, Choong C. Lee
1993 J jnl
J. Manag. Inf. Syst.
Varun Grover, Martin D. Goslar
1993 J jnl
Inf. Manag.
Myun Joong Cheon, Varun Grover, Rajiv Sabherwal
1993 J jnl
J. Inf. Technol.
Varun Grover, Martin D. Goslar
1992 J jnl
Data Base
Varun Grover, Albert H. Segars, Steven J. Simon
1992 J jnl
Inf. Manag.
Varun Grover, James T. C. Teng
1992 J jnl
Data Base
Myun Joong Cheon, Choong C. Lee, Varun Grover
1989 J jnl
Inf. Manag.
Varun Grover, Rajiv Sabherwal
1989 J jnl
Inf. Manag.
William R. King, Varun Grover, Ellen H. Hufnagel
1988 J jnl
Inf. Manag.
Varun Grover, Albert L. Lederer, Rajiv Sabherwal
tests/unit/test_apk_extractors.py
← Index tests/unit/test_apk_extractors.py python
"""
Unit tests for APK extractors.

Tests tag(), get_clickhouse_table(), extract(), and prepare_export_data()
for all 8 APK extractors. Uses mocked androguard APK objects.
"""
import pytest
import os
import hashlib
import tempfile
import zipfile
from dataclasses import asdict
from unittest.mock import MagicMock, patch, PropertyMock

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


# ============================================================================
# Helpers
# ============================================================================

def _create_fake_apk(tmp_path, files=None):
    """Create a minimal valid ZIP file that pretends to be an APK.

    Args:
        tmp_path: Directory to write the fake APK into.
        files: Dict of {filename: content_bytes} to include in the ZIP.

    Returns:
        Path to the fake APK file.
    """
    apk_path = os.path.join(tmp_path, "test.apk")
    default_files = {
        "AndroidManifest.xml": b"<manifest/>",
        "classes.dex": b"dex\n035\x00" + b"\x00" * 100,
        "res/drawable/icon.png": b"\x89PNG" + b"\x00" * 50,
    }
    if files:
        default_files.update(files)

    with zipfile.ZipFile(apk_path, "w") as zf:
        for name, data in default_files.items():
            zf.writestr(name, data)
    return apk_path


def _make_mock_apk():
    """Create a mock androguard APK object with common return values."""
    mock_apk = MagicMock()
    mock_apk.get_package.return_value = "com.example.test"
    mock_apk.get_app_name.return_value = "Test App"
    mock_apk.get_androidversion_code.return_value = "10"
    mock_apk.get_androidversion_name.return_value = "1.2.3"
    mock_apk.get_min_sdk_version.return_value = "21"
    mock_apk.get_target_sdk_version.return_value = "33"
    mock_apk.get_effective_target_sdk_version.return_value = "33"
    mock_apk.get_main_activity.return_value = "com.example.test.MainActivity"
    mock_apk.get_attribute_value.return_value = None
    mock_apk.get_libraries.return_value = []
    mock_apk.get_files.return_value = [
        "AndroidManifest.xml", "classes.dex",
        "res/drawable/icon.png", "lib/armeabi-v7a/libnative.so",
        "assets/data.bin",
    ]
    mock_apk.get_permissions.return_value = [
        "android.permission.INTERNET",
        "android.permission.CAMERA",
    ]
    mock_apk.get_declared_permissions.return_value = {}
    mock_apk.get_activities.return_value = ["com.example.test.MainActivity"]
    mock_apk.get_services.return_value = ["com.example.test.MyService"]
    mock_apk.get_receivers.return_value = ["com.example.test.BootReceiver"]
    mock_apk.get_providers.return_value = []
    mock_apk.get_uses_implied_permission_list.return_value = []
    mock_apk.is_valid_APK.return_value = True
    mock_apk.get_certificates.return_value = []
    mock_apk.get_certificates_v1.return_value = []
    mock_apk.get_certificates_v2.return_value = []
    mock_apk.get_certificates_v3.return_value = []
    mock_apk.get_signature_names.return_value = []
    mock_apk.is_signed_v1.return_value = True
    mock_apk.is_signed_v2.return_value = True
    mock_apk.is_signed_v3.return_value = False
    return mock_apk


@pytest.fixture
def fake_apk_env(tmp_path):
    """Create a fake APK file and mock APK object."""
    apk_path = _create_fake_apk(str(tmp_path))
    mock_apk = _make_mock_apk()
    mock_logger = MagicMock()
    mock_exporters = [MagicMock()]
    return apk_path, mock_apk, mock_logger, mock_exporters


# ============================================================================
# APKFeaturesExtractor
# ============================================================================

class TestAPKFeaturesExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_features"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_features"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor
        from redb.models.dataclasses import APKFeatures

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKFeatures)
        assert result.package_name == "com.example.test"
        assert result.app_name == "Test App"
        assert result.version_code == 10
        assert result.version_name == "1.2.3"
        assert result.min_sdk_version == 21
        assert result.target_sdk_version == 33
        assert result.main_activity == "com.example.test.MainActivity"

    def test_extract_detects_native_code(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.has_native_code is True
        assert result.has_assets is True

    def test_extract_invalid_apk_returns_none(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_valid_APK.return_value = False
        # APKExtractor sets self.apk to the mock_apk directly; _is_valid_apk checks self.apk is not None
        # For invalid APK, set apk to None
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=None)
        # Override _is_valid_apk to simulate parsing failure
        ext.apk = None
        result = ext.extract()
        assert result is None

    def test_extract_handles_missing_version_code(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_androidversion_code.return_value = None
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.version_code == 0

    def test_prepare_export_data_clickhouse(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        ext.extract()
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        data, column_names, column_type_names = result
        assert "sha256" in column_names
        assert "package_name" in column_names
        assert "analysis_date" in column_names
        assert len(column_names) == len(column_type_names)
        assert len(data) == 1  # One row
        assert len(data[0]) == len(column_names)

    def test_prepare_export_data_no_extract(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        # Don't call extract() first
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is None


# ============================================================================
# APKManifestExtractor
# ============================================================================

class TestAPKManifestExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_manifest"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_manifest"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor
        from redb.models.dataclasses import APKManifest

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Setup intent filter mocking
        mock_apk.get_intent_filters.return_value = {}
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKManifest)
        assert result.activity_count == 1
        assert result.service_count == 1
        assert result.receiver_count == 1
        assert result.provider_count == 0

    def test_extract_counts_components(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_activities.return_value = ["A1", "A2", "A3"]
        mock_apk.get_services.return_value = ["S1", "S2"]
        mock_apk.get_intent_filters.return_value = {}
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.activity_count == 3
        assert result.service_count == 2


# ============================================================================
# APKPermissionsExtractor
# ============================================================================

class TestAPKPermissionsExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_permissions"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_permissions"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor
        from redb.models.dataclasses import APKPermissions

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKPermissions)
        assert result.total_permission_count == 2
        assert "android.permission.INTERNET" in result.permissions
        assert "android.permission.CAMERA" in result.permissions

    def test_extract_classifies_dangerous(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # CAMERA is dangerous, INTERNET is normal
        assert result.dangerous_permission_count >= 1
        assert "android.permission.CAMERA" in result.dangerous_permissions

    def test_extract_no_permissions(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_permissions.return_value = []
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.total_permission_count == 0
        assert result.dangerous_permission_count == 0


# ============================================================================
# APKSignatureExtractor
# ============================================================================

class TestAPKSignatureExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_signature"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_signature"

    def test_extract_unsigned_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor
        from redb.models.dataclasses import APKCodeSigningInfo

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_signed_v1.return_value = False
        mock_apk.is_signed_v2.return_value = False
        mock_apk.is_signed_v3.return_value = False
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKCodeSigningInfo)

    def test_extract_detects_signature_schemes(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_signed_v1.return_value = True
        mock_apk.is_signed_v2.return_value = True
        mock_apk.is_signed_v3.return_value = True
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert 1 in result.signature_scheme_versions
        assert 2 in result.signature_scheme_versions
        assert 3 in result.signature_scheme_versions

    def test_certificate_subject_uses_short_names(self, fake_apk_env):
        """Ensure subject/issuer use RFC 4514 short names (CN=, O=) not long names."""
        from unittest.mock import MagicMock
        from asn1crypto import x509
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env

        # Build a real asn1crypto Name so the conversion is exercised
        subject_name = x509.Name.build({
            'country_name': 'US',
            'state_or_province_name': 'California',
            'organization_name': 'Android',
            'common_name': 'Android Debug',
        })

        mock_cert = MagicMock()
        mock_cert.subject = subject_name
        mock_cert.issuer = subject_name
        mock_cert.serial_number = 12345
        mock_cert.not_valid_before = '2020-01-01 00:00:00'
        mock_cert.not_valid_after = '2030-01-01 00:00:00'
        mock_cert.hash_algo = 'sha256'
        mock_cert.public_key.bit_size = 2048
        mock_cert.dump.return_value = b'fake_cert_der_data'

        mock_apk.is_signed_v1.return_value = False
        mock_apk.is_signed_v2.return_value = True
        mock_apk.is_signed_v3.return_value = False
        mock_apk.get_certificates_v2.return_value = [mock_cert]

        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert result.apk_signer_subject is not None
        assert 'CN=' in result.apk_signer_subject
        assert 'O=' in result.apk_signer_subject
        assert 'Common Name' not in result.apk_signer_subject
        assert 'Organization' not in result.apk_signer_subject

        # Also check the certificate list
        assert len(result.x509_certificates) == 1
        cert = result.x509_certificates[0]
        assert 'CN=' in cert.subject
        assert 'Common Name' not in cert.subject


# ============================================================================
# APKDexExtractor
# ============================================================================

class TestAPKDexExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_dex"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_dex"

    def test_extract_returns_list(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Mock DEX data access
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [b"dex\n035\x00" + b"\x00" * 100]

        # Mock the androguard DEX parser
        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0].filename == "classes.dex"

    def test_extract_computes_tlsh(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Need enough data for TLSH (>50 bytes of varied content)
        import os
        dex_data = os.urandom(256)
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [dex_data]

        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_instance.get_encoded_methods.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        assert result[0].tlsh is not None
        assert result[0].tlsh.startswith("T1")

    def test_extract_tlsh_none_for_small_dex(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Too small / uniform for TLSH
        dex_data = b"\x00" * 10
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [dex_data]

        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_instance.get_encoded_methods.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        # TLSH returns empty string for insufficient data, which we convert to None
        assert result[0].tlsh is None

    def test_prepare_export_clickhouse_includes_tlsh(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor
        from redb.models.dataclasses import APKDexFile

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # Manually set dex_files to test export
        ext.dex_files = [APKDexFile(
            filename="classes.dex",
            sha256="aa" * 32,
            class_count=10,
            method_count=50,
            string_count=100,
            tlsh="T1" + "B" * 68,
        )]

        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        dex_export = result['dex']
        assert 'dex_tlsh' in dex_export['column_names']
        tlsh_idx = dex_export['column_names'].index('dex_tlsh')
        assert dex_export['data'][0][tlsh_idx] == "T1" + "B" * 68
        assert len(dex_export['column_names']) == len(dex_export['column_type_names'])

    def test_extract_no_dex_returns_none(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_dex_names.return_value = []
        mock_apk.get_all_dex.return_value = []
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is None


# ============================================================================
# APKResourceExtractor
# ============================================================================

class TestAPKResourceExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_resources"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_resources"

    def test_extract_returns_dict(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # Returns dict with 'resources' key containing list of APKResource
        assert result is None or isinstance(result, dict)
        if result:
            assert "resources" in result
            assert "total_resource_count" in result
            assert isinstance(result["resources"], list)


# ============================================================================
# APKResourceExtractor — _identify_android_format
# ============================================================================

class TestIdentifyAndroidFormat:
    """Unit tests for Android-specific binary format magic detection."""

    def _make_extractor(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor
        apk_path, mock_apk, logger, exporters = fake_apk_env
        return APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

    def test_axml_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x03\x00\x08\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_binary_xml"

    def test_arsc_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x02\x00\x0c\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_resource_table"

    def test_art_prof_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'pro\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_art_profile"

    def test_art_profm_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'prm\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_art_profile_metadata"

    def test_unknown_magic_returns_none(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\xff\xff\xff\xff' + b'\x00' * 12
        assert ext._identify_android_format(header) is None

    def test_short_header_returns_none(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._identify_android_format(b'\x03\x00') is None
        assert ext._identify_android_format(b'') is None

    def test_png_magic_not_matched(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x89PNG\r\n\x1a\n' + b'\x00' * 8
        assert ext._identify_android_format(header) is None


# ============================================================================
# APKResourceExtractor — _is_suspicious_resource
# ============================================================================

class TestIsSuspiciousResource:
    """Unit tests for the layered suspicious resource classification."""

    def _make_extractor(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor
        apk_path, mock_apk, logger, exporters = fake_apk_env
        return APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

    # ── Layer 1: Allowlisted paths ──────────────────────────────────────

    def test_allowlisted_baseline_prof_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/dexopt/baseline.prof",
            magika_type="unknown", entropy=7.9,
            android_type=None,
        ) is False

    def test_allowlisted_baseline_profm_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/dexopt/baseline.profm",
            magika_type="unknown", entropy=7.9,
            android_type=None,
        ) is False

    # ── Layer 2: Android-specific formats → always benign ───────────────

    def test_axml_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/layout/activity_main.xml",
            magika_type="gzip", entropy=6.5,
            android_type="android_binary_xml",
        ) is False

    def test_arsc_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/values/resources.arsc",
            magika_type="unknown", entropy=7.2,
            android_type="android_resource_table",
        ) is False

    def test_art_profile_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/custom/baseline.prof",
            magika_type="unknown", entropy=7.8,
            android_type="android_art_profile",
        ) is False

    def test_art_profile_metadata_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/other/baseline.profm",
            magika_type="unknown", entropy=7.5,
            android_type="android_art_profile_metadata",
        ) is False

    # ── Layer 3: Image extension / Magika type mismatch ─────────────────

    def test_fake_png_extension_with_non_image_content_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/payload.png",
            magika_type="zip", entropy=7.2,
            android_type=None,
        ) is True

    def test_fake_jpg_extension_with_unknown_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.jpg",
            magika_type="unknown", entropy=6.5,
            android_type=None,
        ) is True

    def test_image_extension_mismatch_low_entropy_not_suspicious(self, fake_apk_env):
        """Tiny/low-entropy files with image extensions shouldn't be flagged."""
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/tiny.png",
            magika_type="unknown", entropy=3.0,
            android_type=None,
        ) is False

    # ── Layer 4: Magika-confirmed images → benign ───────────────────────

    def test_confirmed_png_high_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/photo.png",
            magika_type="png", entropy=7.95,
            android_type=None,
        ) is False

    def test_confirmed_webp_high_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/image.webp",
            magika_type="webp", entropy=7.99,
            android_type=None,
        ) is False

    def test_confirmed_jpeg_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/photos/bg.jpg",
            magika_type="jpeg", entropy=7.9,
            android_type=None,
        ) is False

    # ── Layer 5: Known suspicious file types ────────────────────────────

    def test_elf_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/payload",
            magika_type="elf", entropy=5.0,
            android_type=None,
        ) is True

    def test_dex_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/classes.dex",
            magika_type="dex", entropy=5.5,
            android_type=None,
        ) is True

    def test_shell_script_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/run.sh",
            magika_type="shell", entropy=4.0,
            android_type=None,
        ) is True

    def test_zip_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.zip",
            magika_type="zip", entropy=7.5,
            android_type=None,
        ) is True

    def test_jar_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/plugin.jar",
            magika_type="jar", entropy=6.0,
            android_type=None,
        ) is True

    def test_lzma_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/blob.lzma",
            magika_type="lzma", entropy=7.9,
            android_type=None,
        ) is True

    # ── Layer 6: High-entropy unrecognized blobs ────────────────────────

    def test_unknown_type_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/xyzdata",
            magika_type="unknown", entropy=7.5,
            android_type=None,
        ) is True

    def test_empty_type_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/blob.dat",
            magika_type="empty", entropy=7.2,
            android_type=None,
        ) is True

    def test_unknown_type_low_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/config.dat",
            magika_type="unknown", entropy=4.5,
            android_type=None,
        ) is False

    # ── Layer 7: Extreme entropy on non-image recognized types ──────────

    def test_xml_extreme_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/config.xml",
            magika_type="xml", entropy=7.9,
            android_type=None,
        ) is True

    def test_txt_extreme_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/readme.txt",
            magika_type="txt", entropy=7.86,
            android_type=None,
        ) is True

    def test_xml_normal_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/config.xml",
            magika_type="xml", entropy=4.0,
            android_type=None,
        ) is False

    # ── Benign types that should NOT be flagged ─────────────────────────

    def test_html_not_in_suspicious_types(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/index.html",
            magika_type="html", entropy=5.0,
            android_type=None,
        ) is False

    def test_javascript_not_in_suspicious_types(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/app.js",
            magika_type="javascript", entropy=5.0,
            android_type=None,
        ) is False

    def test_sqlite_not_in_suspicious_types(self, fake_apk_env):
        """sqlite was intentionally excluded — too many legitimate uses."""
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.db",
            magika_type="sqlite", entropy=5.0,
            android_type=None,
        ) is False


# ============================================================================
# APKResourceExtractor — integration with extract()
# ============================================================================

class TestAPKResourceExtractorIntegration:
    """Integration tests: craft ZIP contents and verify end-to-end classification."""

    def test_extract_flags_elf_in_assets(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "assets/payload.bin": b"\x7fELF" + b"\x00" * 100,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        # Mock Magika to return "elf" for the payload — real Magika needs
        # a valid ELF structure, but we control classification in unit tests.
        mock_magika = MagicMock()
        mock_magika.identify_bytes.return_value = MagicMock(
            output=MagicMock(label="elf")
        )

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        with patch("magika.Magika", return_value=mock_magika):
            result = ext.extract()
        assert result is not None
        assert result["suspicious_file_count"] >= 1
        sus_paths = [s.path for s in result["suspicious_files"]]
        assert "assets/payload.bin" in sus_paths

    def test_extract_does_not_flag_normal_png(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "res/drawable/icon.png": b"\x89PNG\r\n\x1a\n" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        assert result is not None
        sus_paths = [s.path for s in result["suspicious_files"]]
        assert "res/drawable/icon.png" not in sus_paths

    def test_extract_stores_android_type_override(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        # AXML magic — Magika would likely misclassify, but stored_type
        # should be "android_binary_xml"
        axml_data = b'\x03\x00\x08\x00' + b'\x00' * 50
        apk_path = _create_fake_apk(str(tmp_path), {
            "res/layout/main.xml": axml_data,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        assert result is not None
        axml_resources = [r for r in result["resources"]
                          if r.path == "res/layout/main.xml"]
        assert len(axml_resources) == 1
        assert axml_resources[0].filetype_magika == "android_binary_xml"
        assert axml_resources[0].is_suspicious is False

    def test_extract_skips_non_res_non_assets(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libnative.so": b"\x7fELF" + b"\x00" * 50,
            "META-INF/MANIFEST.MF": b"Manifest-Version: 1.0\n",
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        if result:
            res_paths = [r.path for r in result["resources"]]
            assert "lib/armeabi-v7a/libnative.so" not in res_paths
            assert "META-INF/MANIFEST.MF" not in res_paths

    def test_prepare_export_includes_suspicious_flag(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "assets/evil.bin": b"\x7fELF" + b"\x00" * 100,
            "res/drawable/ok.png": b"\x89PNG\r\n\x1a\n" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        # Map filenames to Magika labels so the ELF payload gets flagged
        def _magika_side_effect(data):
            if data[:4] == b"\x7fELF":
                return MagicMock(output=MagicMock(label="elf"))
            return MagicMock(output=MagicMock(label="png"))

        mock_magika = MagicMock()
        mock_magika.identify_bytes.side_effect = _magika_side_effect

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        with patch("magika.Magika", return_value=mock_magika):
            ext.extract()
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        data, col_names, col_types = result
        assert "is_suspicious" in col_names
        sus_idx = col_names.index("is_suspicious")
        # At least one row should have is_suspicious=1
        sus_values = [row[sus_idx] for row in data]
        assert 1 in sus_values


# ============================================================================
# APKNativeLibExtractor
# ============================================================================

class TestAPKNativeLibExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_native_libs"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_native_libs"

    def test_extract_finds_native_libs(self, tmp_path):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor
        from redb.models.dataclasses import APKNativeLib

        # Create APK with a native lib inside
        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libnative.so": b"\x7fELF" + b"\x00" * 100,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()
        exporters = [MagicMock()]

        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is not None
        assert isinstance(result, dict)
        assert "native_libs" in result
        assert len(result["native_libs"]) >= 1
        assert isinstance(result["native_libs"][0], APKNativeLib)
        assert result["native_libs"][0].abi == "armeabi-v7a"

    def test_extract_detects_packer_libs(self, tmp_path):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        # Create APK with a known packer lib
        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libjiagu.so": b"\x7fELF" + b"\x00" * 50,
            "lib/armeabi-v7a/libnormal.so": b"\x7fELF" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()
        exporters = [MagicMock()]

        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is not None
        packer_libs = [lib for lib in result["native_libs"] if lib.is_known_packer]
        assert len(packer_libs) >= 1  # libjiagu.so is a known packer

    def test_extract_no_native_libs(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # The fake APK has no lib/*.so files
        mock_apk.get_files.return_value = ["classes.dex", "AndroidManifest.xml"]
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is None


# ============================================================================
# APKInconsistencyTestsExtractor
# ============================================================================

class TestAPKInconsistencyTestsExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_inconsistency_tests"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_inconsistency_tests"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor
        from redb.models.dataclasses import APKInconsistencyTests

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKInconsistencyTests)

    def test_extract_clean_apk_no_anomalies(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # A clean APK should have mostly False values
        assert result.test_zip_bomb is not None  # Should be set (True or False)


# ============================================================================
# APK workers.py integration — module registry
# ============================================================================

class TestAPKModuleRegistry:

    def test_all_apk_modules_in_registry(self):
        from redb.workers import get_module_by_name

        apk_modules = [
            "APKFeaturesExtractor",
            "APKManifestExtractor",
            "APKPermissionsExtractor",
            "APKSignatureExtractor",
            "APKDexExtractor",
            "APKResourceExtractor",
            "APKNativeLibExtractor",
            "APKInconsistencyTestsExtractor",
        ]
        for name in apk_modules:
            module_class = get_module_by_name(name)
            assert module_class is not None, f"{name} not found in module registry"

    def test_apk_module_classes_correct(self):
        from redb.workers import get_module_by_name
        from redb.extractors.apk_extractors import (
            APKFeaturesExtractor,
            APKManifestExtractor,
            APKPermissionsExtractor,
            APKSignatureExtractor,
            APKDexExtractor,
            APKResourceExtractor,
            APKNativeLibExtractor,
            APKInconsistencyTestsExtractor,
        )

        assert get_module_by_name("APKFeaturesExtractor") is APKFeaturesExtractor
        assert get_module_by_name("APKManifestExtractor") is APKManifestExtractor
        assert get_module_by_name("APKPermissionsExtractor") is APKPermissionsExtractor
        assert get_module_by_name("APKSignatureExtractor") is APKSignatureExtractor
        assert get_module_by_name("APKDexExtractor") is APKDexExtractor
        assert get_module_by_name("APKResourceExtractor") is APKResourceExtractor
        assert get_module_by_name("APKNativeLibExtractor") is APKNativeLibExtractor
        assert get_module_by_name("APKInconsistencyTestsExtractor") is APKInconsistencyTestsExtractor


# ============================================================================
# APK filetype guard — workers.py dispatch
# ============================================================================

class TestAPKWorkerDispatch:

    def test_apk_excluded_from_die_guard(self):
        """APK filetype should be in the exclusion set alongside macho for DIE/BasicProperties/Hash."""
        import redb.workers as workers_mod
        import inspect

        source = inspect.getsource(workers_mod.process_binary_file)
        # The guards should exclude "apk" from DIE, BasicProperties, HashExtractor.
        # The exact tuple may grow as new text-based formats are added (e.g.
        # "javascript"), so we check that "apk" is present in a filetype
        # exclusion guard rather than matching the full tuple literally.
        assert 'filetype not in ("macho", "apk"' in source

    def test_apk_dispatch_branch_exists(self):
        """The APK dispatch branch should exist and registry should provide all APK extractors."""
        import inspect
        import redb.workers as workers_mod
        from redb.extractor_registry import get_filetype_modules

        # Verify the APK branch exists in process_binary_file
        source = inspect.getsource(workers_mod.process_binary_file)
        assert 'elif filetype == "apk"' in source

        # Verify all APK extractors are registered and loadable
        apk_modules = get_filetype_modules("apk")
        apk_names = {m.__name__ for m in apk_modules}
        for expected in [
            "APKFeaturesExtractor", "APKManifestExtractor",
            "APKPermissionsExtractor", "APKSignatureExtractor",
            "APKDexExtractor", "APKResourceExtractor",
            "APKNativeLibExtractor", "APKInconsistencyTestsExtractor",
        ]:
            assert expected in apk_names, f"{expected} not in APK registry"

    def test_apk_dispatch_runs_basic_and_hash(self):
        """The APK dispatch branch should run BasicPropertiesExtractor and HashExtractor."""
        import redb.workers as workers_mod
        import inspect

        source = inspect.getsource(workers_mod.process_binary_file)
        # Find the apk block
        apk_idx = source.index('elif filetype == "apk"')
        apk_block = source[apk_idx:apk_idx + 4000]
        assert "BasicPropertiesExtractor" in apk_block
        assert "HashExtractor" in apk_block


# ============================================================================
# APKExtractor base class
# ============================================================================

class TestAPKExtractorBase:

    def test_binary_property(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # binary property should return the file content
        with open(apk_path, "rb") as f:
            expected = f.read()
        assert ext.binary == expected

    def test_is_valid_apk_with_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext._is_valid_apk() is True

    def test_is_valid_apk_without_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, _, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=None)
        ext.apk = None
        assert ext._is_valid_apk() is False

    def test_list_files(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        files = ext._list_files()
        assert len(files) == 5
        assert "classes.dex" in files

    def test_get_zip_file(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        zf = ext._get_zip_file()
        assert zf is not None
        zf.close()

    def test_sha256_computed_correctly(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # Verify sha256 matches actual file content
        with open(apk_path, "rb") as f:
            expected_sha256 = hashlib.sha256(f.read()).hexdigest()
        assert ext.sha256 == expected_sha256