Kaoru Sezaki

222 papers A* 6A 8B 27C 10Misc 13Journal 64Unranked 90
YearRankTypeTitle / Venue / Authors
2026 conf
CCNC
Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2026 J jnl
CoRR
Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2026 J jnl
CoRR
Helinyi Peng, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2026 J jnl
Proc. ACM Interact. Mob. Wearable Ubiquitous Technol.
Helinyi Peng, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2025 J jnl
Proc. ACM Interact. Mob. Wearable Ubiquitous Technol.
Yuuki Nishiyama, Subaru Atsumi, Kota Tsubouchi, Kaoru Sezaki
2025 conf
CHI Extended Abstracts
Helinyi Peng, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2025 J jnl
IEEE Trans. Signal Process.
Yutong Feng, Akihito Taya, Yuuki Nishiyama, Jun Liu, Kaoru Sezaki
2025 J jnl
IEEE Trans. Mob. Comput.
Zengyi Han, En Wang, Mohan Yu, Jie Wang, Yuuki Nishiyama, Kaoru Sezaki
2025 J jnl
IEICE Trans. Commun.
Eri Hosonuma, Taku Yamazaki, Takumi Miyoshi, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2025 conf
UbiComp Companion
Xuefu Dong, Liqiang Xu, Lixing He, Zengyi Han, Kenneth Christofferson, Yifei Chen, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2025 J jnl
CoRR
Xuefu Dong, Liqiang Xu, Lixing He, Zengyi Han, Kenneth Christofferson, Yifei Chen, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2025 conf
UbiComp Companion
Jia Tang, Xiuwen Gu, Akihito Taya, Kaoru Sezaki, Yuuki Nishiyama
2024 A conf
CIKM
Liqiang Xu, Yuuki Nishiyama, Kota Tsubouchi, Kaoru Sezaki
2024 conf
CCNC
Shota Ono, Taku Yamazaki, Takumi Miyoshi, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2024 conf
CCNC
Eri Hosonuma, Taku Yamazaki, Takumi Miyoshi, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2024 J jnl
CoRR
Eri Hosonuma, Taku Yamazaki, Takumi Miyoshi, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2024 conf
HuMob-Challenge@SIGSPATIAL
Peizhi Tang, Chuang Yang, Tong Xing, Xiaohang Xu, Renhe Jiang, Kaoru Sezaki
2024 J jnl
CoRR
Peizhi Tang, Chuang Yang, Tong Xing, Xiaohang Xu, Renhe Jiang, Kaoru Sezaki
2024 conf
UbiComp Companion
Manaka Ito, Kota Tsubouchi, Nobuhiko Nishio, Masamichi Shimosaka, Akihito Taya, Kaoru Sezaki, Yuuki Nishiyama
2024 J jnl
IEEE Pervasive Comput.
Ayaka Onodera, Riku Ishioka, Yuuki Nishiyama, Kaoru Sezaki
2024 A conf
MobiSys
Helinyi Peng, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2024 A conf
MobiSys
Subaru Atsumi, Riku Ishioka, Kota Tsubouchi, Yuuki Nishiyama, Kaoru Sezaki
2024 A* conf
CHI
Xuefu Dong, Yifei Chen, Yuuki Nishiyama, Kaoru Sezaki, Yuntao Wang, Kenneth Christofferson, Alex Mariakakis
2024 A conf
ICDCS
Zengyi Han, Xuefu Dong, Liqiang Xu, Zhen Zhu, En Wang, Yuuki Nishiyama, Kaoru Sezaki
2024 J jnl
CoRR
Chuang Yang, Renhe Jiang, Xiaohang Xu, Chuan Xiao, Kaoru Sezaki
2024 J jnl
Proc. VLDB Endow.
Chuang Yang, Renhe Jiang, Xiaohang Xu, Chuan Xiao, Kaoru Sezaki
2024 A* conf
NeurIPS
Xiaohang Xu, Renhe Jiang, Chuang Yang, Zipei Fan, Kaoru Sezaki
2024 J jnl
CoRR
Xiaohang Xu, Renhe Jiang, Chuang Yang, Zipei Fan, Kaoru Sezaki
2024 C conf
HealthCom
Xiuwen Gu, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2024 conf
UbiComp Companion
Zhenbo Wang, Akihito Taya, Takaaki Kato, Kaoru Sezaki, Yuuki Nishiyama
2023 J jnl
IEEE Access
Shota Ono, Taku Yamazaki, Takumi Miyoshi, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2023 conf
ICMI Companion
Ayaka Onodera, Riku Ishioka, Yuuki Nishiyama, Kaoru Sezaki
2023 J jnl
IEEE Signal Process. Lett.
Yutong Feng, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki, Jun Liu
2023 B conf
GLOBECOM
Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2023 J jnl
CoRR
Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki
2023 conf
CCNC
Shota Ono, Taku Yamazaki, Takumi Miyoshi, Yuuki Nishiyama, Kaoru Sezaki
2023 conf
HCI (27)
Haoyu Zhuang, Liqiang Xu, Yuuki Nishiyama, Kaoru Sezaki
2023 conf
CCNC
Eri Hosonuma, Yuuki Nishiyama, Kaoru Sezaki, Takumi Miyoshi, Taku Yamazaki
2023 A* conf
PERCOM
Zengyi Han, Liqiang Xu, Xuefu Dong, Yuuki Nishiyama, Kaoru Sezaki
2023 C conf
WoWMoM
Zengyi Han, Xuefu Dong, Yuuki Nishiyama, Kaoru Sezaki
2023 J jnl
CoRR
Takahiro Yabe, Kota Tsubouchi, Toru Shimizu, Yoshihide Sekimoto, Kaoru Sezaki, Esteban Moro, Alex Pentland
2023 conf
ICUFN
Eri Hosonuma, Nobuyuki Tanaka, Takuma Yamazaki, Akihito Taya, Yuuki Nishiyama, Kaoru Sezaki, Takumi Miyoshi, Taku Yamazaki
2023 conf
UbiComp/ISWC Adjunct
Yuuki Nishiyama, Kaoru Sezaki
2022 conf
UbiComp/ISWC Adjunct
Zengyi Han, Yuuki Nishiyama, Kaoru Sezaki
2022 conf
UbiComp/ISWC Adjunct
Kazuki Shimojo, Zengyi Han, Yuuki Nishiyama, Kaoru Sezaki
2022 conf
SIGSPATIAL/GIS
Suxing Lyu, Tianyang Han, Yuuki Nishiyama, Kaoru Sezaki, Takahiko Kusakabe
2022 Misc conf
SenSys
Liqiang Xu, Yuuki Nishiyama, Masamichi Shimosaka, Kota Tsubouchi, Kaoru Sezaki
2022 B conf
SMARTCOMP
Yuki Kasahara, Yuuki Nishiyama, Kaoru Sezaki
2022 C conf
HealthCom
Shota Ono, Yuuki Nishiyama, Kaoru Sezaki
2022 conf
PerCom Workshops
Xuefu Dong, Zengyi Han, Yuuki Nishiyama, Kaoru Sezaki
2022 B conf
SMC
Xuefu Dong, Zengyi Han, Yuuki Nishiyama, Kaoru Sezaki
2022 conf
HCI (25)
Liqiang Xu, Yuuki Nishiyama, Kaoru Sezaki
2022 A conf
MobiSys
Zengyi Han, Xuefu Dong, Yuuki Nishiyama, Kaoru Sezaki
2022 B conf
MobiHoc
Yuuki Nishiyama, Hiroaki Murakami, Ryoto Suzuki, Kazusato Oko, Issey Sukeda, Kaoru Sezaki, Yoshihiro Kawahara
2022 Misc conf
SenSys
Ryoto Suzuki, Yuuki Nishiyama, Hiroaki Murakami, Yoshihiro Kawahara, Kaoru Sezaki
2022 B conf
SMARTCOMP
Yuki Komatsu, Kazuki Shimojo, Yuuki Nishiyama, Kaoru Sezaki
2022 Misc conf
SenSys
Riku Ishioka, Kota Tsubouchi, Yuuki Nishiyama, Kaoru Sezaki
2021 J jnl
IEEE Wirel. Commun.
Di Zhang, Joel J. P. C. Rodrigues, Yunkai Zhai, Kaoru Sezaki
2021 conf
VTC Fall
Hong Duc Nguyen, Shunsuke Aoki, Yuuki Nishiyama, Kaoru Sezaki
2021 conf
ITSC
Hidenaga Ushijima, Helinyi Peng, Shunsuke Aoki, Yuuki Nishiyama, Kaoru Sezaki
2021 J jnl
IEEE Trans. Big Data
Shunsuke Aoki, Kaoru Sezaki, Nicholas Jing Yuan, Xing Xie
2021 conf
UbiComp/ISWC Adjunct
Xuefu Dong, Zengyi Han, Yuuki Nishiyama, Kaoru Sezaki
2021 conf
UbiComp/ISWC Adjunct
Yuuki Nishiyama, Kaoru Sezaki
2021 conf
UbiComp/ISWC Adjunct
Zengyi Han, Xuefu Dong, Yuuki Nishiyama, Kaoru Sezaki
2021 conf
ITSC
Zengyi Han, Hong Duc Nguyen, Shunsuke Aoki, Yuuki Nishiyama, Kaoru Sezaki
2021 B conf
SMARTCOMP
Soichiro Higuma, Kosuke Hatai, Yuuki Nishiyama, Kaoru Sezaki
2021 conf
HCI (21)
Hidenaga Ushijima, Shota Ono, Yuuki Nishiyama, Kaoru Sezaki
2020 conf
ICCE-TW
Shota Ono, Fuga Kikuchi, Taku Yamazaki, Takumi Miyoshi, Kaoru Sezaki
2020 J jnl
IEEE Access
Ruichao Zhang, Ryota Nakai, Kaoru Sezaki, Shinya Sugiura
2020 conf
HCI (23)
Yuuki Nishiyama, Denzil Ferreira, Yusaku Eigen, Wataru Sasaki, Tadashi Okoshi, Jin Nakazawa, Anind K. Dey, Kaoru Sezaki
2020 Misc conf
SenSys
Yuuki Nishiyama, Takuro Yonezawa, Kaoru Sezaki
2020 conf
UbiComp/ISWC Adjunct
Soichiro Higuma, Yuuki Nishiyama, Kaoru Sezaki
2020 conf
UbiComp/ISWC Adjunct
Yuuki Nishiyama, Denzil Ferreira, Wataru Sasaki, Tadashi Okoshi, Jin Nakazawa, Anind K. Dey, Kaoru Sezaki
2019 conf
HCI (22)
Chenwei Song, Masaki Ito, Kaoru Sezaki
2019 B conf
WCNC
Yao Sun, Masaki Ito, Kaoru Sezaki
2019 C conf
APNOMS
Shota Ono, Taku Yamazaki, Takumi Miyoshi, Kaoru Sezaki
2019 Misc conf
HotMobile
Yuichi Nakamura, Masaki Ito, Kaoru Sezaki
2019 conf
GeoHumanities@SIGSPATIAL
Mawo Kamakura, Hikaru Ikuta, Bo Zheng, Yoshihiro Sato, Masataka Kagesawa, Takeshi Oishi, Kaoru Sezaki, Takeshi Nakagawa, Katsushi Ikeuchi
2019 J jnl
ACM SIGSPATIAL Special
Satish V. Ukkusuri, Kaoru Sezaki, Takahiro Yabe, Kota Tsubouchi
2019 ed.
PredictGIS@SIGSPATIAL
Satish V. Ukkusuri, Kaoru Sezaki, Takahiro Yabe, Kota Tsubouchi
2019 conf
PredictGIS@SIGSPATIAL
Chenwei Song, Masaki Ito, Yuuki Nishiyama, Kaoru Sezaki
2019 J jnl
J. Ambient Intell. Humaniz. Comput.
Shin'ichi Konomi, Tomoyo Sasao, Simo Hosio, Kaoru Sezaki
2018 ch.
Multimedia Tools and Applications for Environmental & Biodiversity Informatics
Hill Hiroki Kobayashi, Hiromi Kudo, Hervé Glotin, Vincent Roger, Marion Poupard, Daisuké Shimotoku, Akio Fujiwara, Kazuhiko Nakamura, Kaoru Saito, Kaoru Sezaki
2018 conf
PerCom Workshops
Tomoya Kitazato, Masaki Ito, Kaoru Sezaki
2018 J jnl
Multimodal Technol. Interact.
Hill Hiroki Kobayashi, Keijiro Nakagawa, Ko Makiyama, Yuta Sasaki, Hiromi Kudo, Baburam Niraula, Kaoru Sezaki
2018 conf
HCI (21)
Keijiro Nakagawa, Atsuya Makita, Miho Nagasawa, Takefumi Kikusui, Kaoru Sezaki, Hill Hiroki Kobayashi
2018 J jnl
IEEE Access
Yu Nakayama, Kazuki Maruta, Takuya Tsutsumi, Kaoru Sezaki
2018 J jnl
Big Data Cogn. Comput.
Yu Nakayama, Kaoru Sezaki
2018 conf
HCI (21)
Yuichi Nakamura, Masaki Ito, Kaoru Sezaki
2018 conf
ICC Workshops
Yu Nakayama, Kazuki Maruta, Takuya Tsutsumi, Ryoma Yasunaga, Kazuaki Honda, Kaoru Sezaki
2018 J jnl
IEICE Trans. Commun.
Yu Nakayama, Kaoru Sezaki
2018 J jnl
IEEE Access
Yu Nakayama, Kazuki Maruta, Takuya Tsutsumi, Kaoru Sezaki
2017 A* conf
INFOCOM
Yu Nakayama, Takuya Tsutsumi, Kazuki Maruta, Kaoru Sezaki
2017 conf
SIGSPATIAL/GIS
Shunsuke Aoki, Kaoru Sezaki, Nicholas Jing Yuan, Xing Xie
2017 conf
NANOCOM
Yao Sun, Masaki Ito, Kaoru Sezaki
2017 J jnl
IEICE Trans. Commun.
Yu Nakayama, Kaoru Sezaki
2017 conf
IEEE BigData
Kyoichi Ito, Masaki Ito, Kosuke Miyazaki, Keishi Tanimoto, Kaoru Sezaki
2017 conf
AmI
Shin'ichi Konomi, Tomoyo Sasao, Simo Hosio, Kaoru Sezaki
2017 J jnl
Auton. Agents Multi Agent Syst.
Teemu Leppänen, Jose Alvarez Lacasia, Yoshito Tobe, Kaoru Sezaki, Jukka Riekki
2017 conf
HotWireless@MobiCom
Yu Nakayama, Kazuki Maruta, Takuya Tsutsumi, Kaoru Sezaki
2017 J jnl
CoRR
Hao Niu, Yao Sun, Kaoru Sezaki
2017 conf
HCI (21)
Tomoya Kitazato, Kyoichi Ito, Keisuke Umezawa, Masaki Ito, Kaoru Sezaki
2016 C conf
HealthCom
Yao Sun, Masaki Ito, Kaoru Sezaki
2016 conf
SIGSPATIAL/GIS
Satoshi Hyuga, Masaki Ito, Masayuki Iwai, Kaoru Sezaki
2016 conf
Urb-IoT
Dunstan Matekenya, Masaki Ito, Werner Creixell, Ryosuke Shibasaki, Kaoru Sezaki
2016 conf
PerCom Workshops
Hideki Mori, Masaki Ito, Kaoru Sezaki
2016 conf
ICC
Shunsuke Aoki, Kaoru Sezaki
2016 conf
UbiComp Adjunct
Dunstan Matekenya, Masaki Ito, Ryosuke Shibasaki, Kaoru Sezaki
2016 conf
PerCom Workshops
Yuya Matsuno, Masaki Ito, Kaoru Sezaki
2016 A conf
AAMAS
Teemu Leppänen, Jose Alvarez Lacasia, Yoshito Tobe, Kaoru Sezaki, Jukka Riekki
2016 conf
MobiGIS
Miku Hoshino, Masaki Ito, Kaoru Sezaki
2016 conf
MobiSys (Companion Volume)
Jiang Tiantian, Masaki Ito, Kaoru Sezaki
2016 conf
MobiSys (Companion Volume)
Takao Suzuki, Masaki Ito, Kaoru Sezaki
2016 conf
MobiSys (Companion Volume)
Yuya Kamma, Kaoru Sezaki, Hill Hiroki Kobayashi
2016 J jnl
Wirel. Networks
Hao Niu, Nanhao Zhu, Li Sun, Athanasios V. Vasilakos, Kaoru Sezaki
2016 conf
HCI (19)
Shin'ichi Konomi, Kazuki Wakasa, Masaki Ito, Kaoru Sezaki
2016 conf
ICC
Yu Nakayama, Kazuki Maruta, Takuya Tsutsumi, Kaoru Sezaki
2015 C conf
APCC
Yu Nakayama, Kaoru Sezaki
2015 J jnl
Interactions
Sang-won Leigh, Asta Roseway, Ann Paradiso, Hill Hiroki Kobayashi, Michitaka Hirose, Akio Fujiwara, Kazuhiko Nakamura, Kaoru Sezaki, Kaoru Saito, Conor Peterson, Bert Bongers, Cecilia Heffer
2015 conf
MELT@SIGSPATIAL
Satoshi Hyuga, Masaki Ito, Masayuki Iwai, Kaoru Sezaki
2015 conf
HCI (21)
Hill Hiroki Kobayashi, Hiromi Kudo, Kaoru Sezaki
2015 conf
IWGS
Dunstan Matekenya, Masaki Ito, Yoshito Tobe, Ryosuke Shibasaki, Kaoru Sezaki
2015 conf
HCI (2)
Ko Makiyama, Keijiro Nakagawa, Maki Katayama, Miho Nagasawa, Kaoru Sezaki, Hill Hiroki Kobayashi
2015 B conf
TEI
Hill Hiroki Kobayashi, Akio Fujiwara, Kazuhiko Nakamura, Kaoru Saito, Kaoru Sezaki
2014 conf
UbiComp Adjunct
Guangwen Liu, Khan Muhammad Asif Hossain, Masayuki Iwai, Masaki Ito, Yoshito Tobe, Kaoru Sezaki, Dunstan Matekenya
2014 conf
AH
Keijiro Nakagawa, Hill Hiroki Kobayashi, Kaoru Sezaki
2014 conf
ICMU
Congwei Dang, Kaoru Sezaki, Masayuki Iwai
2014 J jnl
IEEE Commun. Lett.
Hao Niu, Masayuki Iwai, Kaoru Sezaki, Li Sun, Qinghe Du
2014 J jnl
IEICE Trans. Commun.
Shunsuke Aoki, Kaoru Sezaki
2014 conf
ICC
Shunsuke Aoki, Kaoru Sezaki
2014 conf
GCCE
Hao Niu, Li Sun, Masaki Ito, Kaoru Sezaki
2014 B conf
PIMRC
Hao Niu, Li Sun, Masaki Ito, Kaoru Sezaki
2013 J jnl
Pervasive Mob. Comput.
Congwei Dang, Masayuki Iwai, Yoshito Tobe, Kazunori Umeda, Kaoru Sezaki
2013 J jnl
Inf. Media Technol.
Muhammad Asif Hossain Khan, Masayuki Iwai, Kaoru Sezaki
2013 J jnl
J. Inf. Process.
Muhammad Asif Hossain Khan, Masayuki Iwai, Kaoru Sezaki
2013 J jnl
Inf. Media Technol.
Guangwen Liu, Masayuki Iwai, Kaoru Sezaki
2013 B conf
MUM
Shunsuke Aoki, Hill Hiroki Kobayashi, Kaoru Sezaki
2013 Misc conf
SenSys
Teemu Leppänen, Jose Alvarez Lacasia, Archana Ramalingam, Meirong Liu, Erkki Harjula, Pauli Närhi, Jani Ylioja, Jukka Riekki, Kaoru Sezaki, Yoshito Tobe, Timo Ojala
2013 conf
SocialCom
Muhammad Asif Hossain Khan, Danushka Bollegala, Guangwen Liu, Kaoru Sezaki
2013 conf
COMPSAC Workshops
Shunsuke Aoki, Masayuki Iwai, Kaoru Sezaki
2013 B conf
WiMob
Guangwen Liu, Masayuki Iwai, Yoshito Tobe, Kaoru Sezaki
2013 conf
AINA Workshops
Kazuto Shimizu, Masayuki Iwai, Kaoru Sezaki
2013 A* conf
ACM Multimedia
Hill Hiroki Kobayashi, Michitaka Hirose, Akio Fujiwara, Kazuhiko Nakamura, Kaoru Sezaki, Kaoru Saito
2012 C conf
Healthcom
Muhammad Asif Hossain Khan, Masayuki Iwai, Kaoru Sezaki
2012 J jnl
CoRR
Yunlong Zhao, Zhao Dong, Masayuki Iwai, Kaoru Sezaki, Yoshito Tobe
2012 conf
ISGT
Sekyung Han, Soohee Han, Kaoru Sezaki
2012 conf
CloudNet
Shunsuke Aoki, Masayuki Iwai, Kaoru Sezaki
2012 A* conf
PerCom
Congwei Dang, Masayuki Iwai, Kazunori Umeda, Yoshito Tobe, Kaoru Sezaki
2012 conf
CSE
Yunlong Zhao, Shilong Kang, Masayuki Iwai, Kaoru Sezaki, Yoshito Tobe
2012 conf
ICITST
Shunsuke Aoki, Masayuki Iwai, Kaoru Sezaki
2012 conf
INSS
Muhammad Asif Hossain Khan, Masayuki Iwai, Kaoru Sezaki
2011 C conf
ICICS
Yunlong Zhao, Zhao Dong, Masayuki Iwai, Kaoru Sezaki, Yoshito Tobe
2011 conf
ICC
Hongyang Chen, Kaoru Sezaki
2011 J jnl
IEEE Trans. Smart Grid
Sekyung Han, Soohee Han, Kaoru Sezaki
2010 J jnl
KSII Trans. Internet Inf. Syst.
Sekyung Han, Yeonsuk Choi, Masayuki Iwai, Kaoru Sezaki
2010 J jnl
IEEE Trans. Smart Grid
Sekyung Han, Soohee Han, Kaoru Sezaki
2010 J jnl
IEICE Trans. Commun.
Tian Hao, Masayuki Iwai, Yoshito Tobe, Kaoru Sezaki
2010 ch.
Shared Encounters
Shin'ichi Konomi, Kaoru Sezaki, Masaru Kitsuregawa
2010 J jnl
EURASIP J. Wirel. Commun. Netw.
Bin Liu, Hongyang Chen, Xianfu Lei, Fengyuan Ren, Kaoru Sezaki
2010 B conf
WCNC
Beibei Kong, Hongyang Chen, Xiaohu Tang, Kaoru Sezaki
2010 B conf
GLOBECOM
Yunfei Guo, Anke Xue, Hongyang Chen, Huajie Chen, Kaoru Sezaki
2010 J jnl
IEEE Trans. Wirel. Commun.
Hongyang Chen, Qingjiang Shi, Rui Tan, H. Vincent Poor, Kaoru Sezaki
2010 J jnl
CoRR
Hongyang Chen, Yiu Tong Chan, H. Vincent Poor, Kaoru Sezaki
2010 conf
ICC
Hongyang Chen, Yiu Tong Chan, H. Vincent Poor, Kaoru Sezaki
2009 conf
Pervasive
Shin'ichi Konomi, Niwat Thepvilojanapong, Ryohei Suzuki, Susanna Pirttikangas, Kaoru Sezaki, Yoshito Tobe
2009 J jnl
IEICE Trans. Commun.
Kaoru Sezaki
2009 J jnl
CoRR
Hongyang Chen, Qingjiang Shi, Pei Huang, H. Vincent Poor, Kaoru Sezaki
2009 B conf
PIMRC
Hongyang Chen, Qingjiang Shi, Pei Huang, H. Vincent Poor, Kaoru Sezaki
2009 conf
ICUMT
Shuntaro Matsubara, Ryohei Suzuki, Masayuki Iwai, Kaoru Sezaki
2008 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Hongyang Chen, Kaoru Sezaki, Ping Deng, Hing-Cheung So
2008 Misc conf
SenSys
Ryohei Suzuki, Yoshito Tobe, Kaoru Sezaki
2008 conf
EUC (1)
Hongyang Chen, Marcelo H. T. Martins, Pei Huang, Hing-Cheung So, Kaoru Sezaki
2008 B conf
ICPADS
Hongyang Chen, Pei Huang, Hing-Cheung So, Kaoru Sezaki
2008 B conf
SECON
Kaoru Sezaki, Izumi Kamiya, Kohei Miyagawa, Shin'ichi Konomi
2008 conf
SAINT
Masatoshi Sekine, Kaoru Sezaki
2008 conf
WiNTECH
Mingmei Li, Shin'ichi Konomi, Kaoru Sezaki
2007 J jnl
IEICE Trans. Commun.
Ryohei Suzuki, Kaoru Sezaki, Yoshito Tobe
2007 B conf
PIMRC
Shinsuke Terada, Takumi Miyoshi, Hiroaki Morino, Masakatsu Ogawa, Kaoru Sezaki
2007 B conf
MDM
Oranat Sangratanachaikul, Leping Huang, Shin'ichi Konomi, Kaoru Sezaki
2007 J jnl
Scalable Comput. Pract. Exp.
Xinfa Wei, Kaoru Sezaki
2007 J jnl
IEICE Trans. Commun.
Xinfa Wei, Kaoru Sezaki
2007 ch.
Secure Localization and Time Synchronization for Wireless Sensor and Ad Hoc Networks
Leping Huang, Hiroshi Yamane, Kanta Matsuura, Kaoru Sezaki
2007 conf
UCS
Kaoru Sezaki, Niwat Thepvilojanapong, Yoshito Tobe
2007 J jnl
Int. J. Ad Hoc Ubiquitous Comput.
Werner Creixell, Kaoru Sezaki
2007 B conf
LCN
Ryohei Suzuki, Yoshito Tobe, Kaoru Sezaki
2007 J jnl
IEEE Distributed Syst. Online
Piotr D. Adamczyk, Kevin Hamilton, Alan Chamberlain, Steve Benford, Nick Tandavanitj, Amanda Oldroyd, Kate Hartman, Kati London, Sai Sriskandarajah, Eiman Kanjo, Peter Landshoff, Kaoru Sezaki, Shin'ichi Konomi, Muaz A. Niazi, Hafiz Farooq Ahmad, Fauzan Mirza, Arshad Ali, George Roussos, Dikaios Papadogkonas, Mark Levene
2006 B conf
WCNC
Shojiro Takeuchi, Kaoru Sezaki, Yasuhiko Yasuda
2006 conf
AINA (2)
Junya Yamashita, Ryohei Suzuki, Kei Sawai, Hiroki Saito, Tsuyoshi Suzuki, Yoshito Tobe, Niwat Thepvilojanapong, Kaoru Sezaki
2006 C conf
ISPDC
Xinfa Wei, Kaoru Sezaki
2006 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Shojiro Takeuchi, Kaoru Sezaki, Yasuhiko Yasuda
2006 B conf
WCNC
Shojiro Takeuchi, Kaoru Sezaki, Yasuhiko Yasuda
2006 conf
SPC
Leping Huang, Hiroshi Yamane, Kanta Matsuura, Kaoru Sezaki
2006 conf
NETGAMES
Kenji Hikichi, Yasuhiko Yasuda, Akifumi Fukuda, Kaoru Sezaki
2006 conf
AMT
Masatoshi Arikawa, Toru Hayashi, Kaoru Sezaki
2005 J jnl
IEICE Trans. Commun.
Sugang Xu, Kaoru Sezaki, Yoshiaki Tanaka
2005 Misc conf
SenSys
Kenji Sasaki, Yuichi Uehara, Yoshihiro Kanazawa, Tomohiro Uchiyama, Kazunori Makimura, Niwat Thepvilojanapong, Hiroki Saito, Kaoru Sezaki, Yoshito Tobe
2005 J jnl
IEICE Trans. Commun.
Shojiro Takeuchi, Kaoru Sezaki, Yasuhiko Yasuda
2005 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Yoshito Tobe, Niwat Thepvilojanapong, Kaoru Sezaki
2005 B conf
WCNC
Leping Huang, Kanta Matsuura, Hiroshi Yamane, Kaoru Sezaki
2005 conf
SAINT
Niwat Thepvilojanapong, Yoshito Tobe, Kaoru Sezaki
2005 Misc conf
SenSys
Niwat Thepvilojanapong, Yoshito Tobe, Kaoru Sezaki
2005 J jnl
Int. J. Pervasive Comput. Commun.
Leping Huang, Hongyuan Chen, T. V. L. N. Sivakumar, Tsuyoshi Kashima, Kaoru Sezaki
2005 conf
ICIP (2)
Kunitoshi Komatsu, Kaoru Sezaki
2005 conf
ICDCS Workshops
Hajime Sogawa, Niwat Thepvilojanapong, Hiroki Saito, Kaoru Sezaki, Yoshito Tobe
2005 conf
ISCAS (1)
Niwat Thepvilojanapong, Yoshito Tobe, Kaoru Sezaki
2005 J jnl
IEICE Trans. Commun.
Niwat Thepvilojanapong, Yoshito Tobe, Kaoru Sezaki
2005 A conf
Privacy Enhancing Technologies
Leping Huang, Hiroshi Yamane, Kanta Matsuura, Kaoru Sezaki
2004 B conf
GLOBECOM
Shojiro Takeuchi, Kosuke Yamazaki, Kaoru Sezaki, Yasuhiko Yasuda
2004 B conf
ICIP
Kunitoshi Komatsu, Kaoru Sezaki
2004 B conf
ICCCN
Leping Huang, Hongyuan Chen, T. V. L. N. Sivakumar, Kaoru Sezaki
2004 C conf
EUC
Leping Huang, Hongyuan Chen, T. V. L. N. Sivakumar, Tsuyoshi Kashima, Kaoru Sezaki
2003 B conf
AINA
Niwat Thepvilojanapong, Yoshito Tobe, Kaoru Sezaki
2003 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Kunitoshi Komatsu, Kaoru Sezaki
2002 B conf
GLOBECOM
Kenji Hikichi, Hironao Morino, Isamu Arimoto, Kaoru Sezaki, Yasuhiko Yasuda
2001 conf
ICIP (3)
Kunitoshi Komatsu, Kaoru Sezaki
2001 A conf
ICME
Kenji Hikichi, Hironao Morino, Ichiro Fukuda, Soju Matsumoto, Yasuhiko Yasuda, Isamu Arimoto, Mitsuharu Iijima, Kaoru Sezaki
2001 Misc conf
ICASSP
Kunitoshi Komatsu, Kaoru Sezaki
2000 Misc conf
ICASSP
Kunitoshi Komatsu, Kaoru Sezaki
1999 Misc conf
ICASSP
Kunitoshi Komatsu, Kaoru Sezaki
1998 Misc conf
ICASSP
Kunitoshi Komatsu, Kaoru Sezaki
1995 J jnl
Syst. Comput. Jpn.
Kunitoshi Komatsu, Kaoru Sezaki, Yasuhiko Yasuda
1990 J jnl
Comput. Networks ISDN Syst.
Kaoru Sezaki, Yoshiaki Tanaka, Minoru Akiyama
tests/integration/test_js_extractors.py
← Index tests/integration/test_js_extractors.py python
"""
Integration tests for JavaScript-specific extractors.
"""
import sys
import types
from unittest.mock import MagicMock

# Mock the oscrypto/signify/certvalidator chain before any redb imports.
# oscrypto fails on environments with OpenSSL 3.x due to version detection.
# JS extractors don't use PE signatures so this is safe.
_MOCK_MODULES = [
    # oscrypto/signify chain (PE signature analysis)
    "oscrypto", "oscrypto.errors", "oscrypto._openssl",
    "oscrypto._openssl._libcrypto", "oscrypto._openssl._libcrypto_ctypes",
    "oscrypto._openssl.util", "oscrypto.util", "oscrypto.kdf",
    "oscrypto._asymmetric", "oscrypto.asymmetric",
    "certvalidator", "certvalidator.validate",
    "signify", "signify.fingerprinter", "signify.authenticode",
    "signify.authenticode.signed_pe", "signify.authenticode.authroot",
    "signify.pkcs7", "signify.pkcs7.signeddata", "signify.pkcs7.signerinfo",
    "signify.x509", "signify.x509.certificates", "signify.x509.context",
    # hashing libs used by HashExtractor
    "ppdeep", "tlsh",
    # binary format libs (not needed for JS)
    "dotnetfile", "lief",
    "flare_floss", "yara_x",
    "machofile",
]
for _mod in _MOCK_MODULES:
    if _mod not in sys.modules:
        try:
            __import__(_mod)
        except (ImportError, Exception):
            sys.modules[_mod] = MagicMock()

import pytest
from pathlib import Path
from unittest.mock import patch

pytestmark = [pytest.mark.integration, pytest.mark.js]

TEST_FILES_DIR = Path(__file__).parent.parent.parent / "test_files"

JS_MALICIOUS_PATH = str(TEST_FILES_DIR / "test_malicious.js")


@pytest.fixture
def js_malicious_path():
    path = TEST_FILES_DIR / "test_malicious.js"
    if not path.exists():
        pytest.skip(f"Test file not found: {path}")
    return str(path)


# ============================================================================
# JSFeaturesExtractor Tests
# ============================================================================

class TestJSFeaturesExtractor:

    def test_extract_valid_js(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor
        from redb.models.dataclasses import JSFeatures

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result is not None
            assert isinstance(result, JSFeatures)

    def test_entropy_is_reasonable(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result.text_entropy > 0.0
            assert result.text_entropy < 8.0

    def test_detects_obfuscation(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            # The fixture is dense `\xHH` hex escapes (>5% of source) plus
            # text entropy >5.0 — two strong signals, which clears the
            # strong-signal gate the heuristic now requires.
            assert result.is_likely_obfuscated is True
            assert result.obfuscation_score >= 40
            assert len(result.obfuscation_techniques) > 0
            # `obfuscator_name` is None when js-x-ray isn't installed in the
            # test environment, and a recognised family name when it is.
            assert result.obfuscator_name is None or isinstance(
                result.obfuscator_name, str
            )

    def test_detects_eval_usage(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result.eval_count >= 1
            assert result.fromcharcode_count >= 1
            assert result.atob_count >= 1

    def test_detects_wscript_environment(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result.detected_environment == "wscript"
            assert result.script_type == "wscript"

    def test_line_metrics(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result.line_count > 0
            assert result.char_count > 0
            assert result.max_line_length > 0
            assert result.avg_line_length > 0

    def test_export_data_format(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            extractor.extract()
            export = extractor.prepare_export_data('ClickHouseExporter')

            assert export is not None
            data, col_names, col_types = export
            assert len(col_names) == len(col_types)
            assert len(data) == 1
            assert len(data[0]) == len(col_names)

    def test_clickhouse_table_name(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(js_malicious_path, mock_logger)
            assert extractor.get_clickhouse_table() == "redb_js_features"

    def test_empty_file_returns_none(self, tmp_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        empty_js = tmp_path / "empty.js"
        empty_js.write_text("")

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(empty_js), mock_logger)
            result = extractor.extract()
            assert result is None

    def test_pattern_counts_are_case_insensitive(self, tmp_path, mock_logger):
        """eval_count / atob_count come from js_patterns.PATTERNS, which are
        compiled with re.IGNORECASE. Capitalised forms must still be counted."""
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        mixed_case_js = tmp_path / "mixed.js"
        mixed_case_js.write_text(
            'EVAL("1+1");\n'
            'eval("2+2");\n'
            'ATOB("YWJj");\n'
        )

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(mixed_case_js), mock_logger)
            result = extractor.extract()
            assert result is not None
            # Both `EVAL(` and `eval(` should be counted.
            assert result.eval_count == 2
            assert result.atob_count == 1

    # ----- script_type / detected_environment classification ----------------
    # First-match-wins; ordering is what makes the HTA-vs-WScript split work,
    # so we pin one positive case per value and one orthogonality case.

    @pytest.mark.parametrize("source,expected_type", [
        ("#@~^abcdef==^#~@", "jse"),
        ("<?xml version='1.0'?>\n<job><script language='JScript'>x=1;</script></job>", "wsf"),
        ("<html><head><hta:application id='x'/></head><script>x=1</script></html>", "hta"),
        ("<html><body><script>alert(1)</script></body></html>", "embedded_html"),
        ("var s = new ActiveXObject('WScript.Shell');", "wscript"),
        ("import {foo} from 'bar';\nexport const x = 1;", "esm"),
        ("import 'side-effect.js';\nconst x = 1;", "esm"),
        ("const fs = require('fs');\nmodule.exports = {};", "node_module"),
        ("function add(a, b) { return a + b; }", "standalone"),
    ])
    def test_script_type_classification(self, tmp_path, mock_logger, source, expected_type):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        js_file = tmp_path / "sample.js"
        js_file.write_text(source)
        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(js_file), mock_logger, source=source)
            result = extractor.extract()
            assert result is not None
            assert result.script_type == expected_type

    @pytest.mark.parametrize("source,expected_env", [
        ("var s = new ActiveXObject('WScript.Shell');", "wscript"),
        ("chrome.runtime.onMessage.addListener(()=>{}); chrome.tabs.query({});", "browser_extension"),
        ("self.addEventListener('fetch', e => e.respondWith(caches.match(e.request)));", "service_worker"),
        ("const text = await Deno.readTextFile('a.txt');", "deno"),
        ("const fs = require('fs'); process.env.X;", "node"),
        ("document.getElementById('x'); window.location;", "browser"),
        ("function add(a, b) { return a + b; }", "unknown"),
    ])
    def test_environment_classification(self, tmp_path, mock_logger, source, expected_env):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        js_file = tmp_path / "sample.js"
        js_file.write_text(source)
        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(js_file), mock_logger, source=source)
            result = extractor.extract()
            assert result is not None
            assert result.detected_environment == expected_env

    def test_hta_with_wscript_keeps_format_and_runtime_orthogonal(self, tmp_path, mock_logger):
        """HTA droppers commonly call WScript APIs. script_type captures the
        file format (`hta`) and detected_environment captures the runtime
        surface (`wscript`); both axes must survive a single sample."""
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        source = (
            "<html><head><hta:application id='x'/></head>"
            "<script>new ActiveXObject('WScript.Shell').Run('cmd.exe');</script></html>"
        )
        js_file = tmp_path / "sample.hta"
        js_file.write_text(source)
        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(js_file), mock_logger, source=source)
            result = extractor.extract()
            assert result.script_type == "hta"
            assert result.detected_environment == "wscript"

    def test_bare_exports_property_is_not_node_module(self, tmp_path, mock_logger):
        """Regression: `module.exports` and `require(` mark a CommonJS module,
        but a bare `exports.` substring (e.g. `obj.exports.foo`) should not."""
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        source = "var x = obj.exports.foo;\nfunction f() { return 1; }\n"
        js_file = tmp_path / "sample.js"
        js_file.write_text(source)
        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSFeaturesExtractor(str(js_file), mock_logger, source=source)
            result = extractor.extract()
            assert result.script_type == "standalone"


# ============================================================================
# JSSuspiciousAPIsExtractor Tests
# ============================================================================

class TestJSSuspiciousAPIsExtractor:

    def test_extract_finds_apis(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result is not None
            assert len(result) > 0

    def test_detects_eval(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            api_names = [f['api_name'] for f in result]
            assert "eval" in api_names

    def test_detects_wscript_apis(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            api_names = [f['api_name'] for f in result]
            assert "WScript.CreateObject" in api_names
            assert "WScript.Shell" in api_names

    def test_detects_network_apis(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            api_names = [f['api_name'] for f in result]
            assert "XMLHttpRequest" in api_names
            assert "fetch" in api_names

    def test_detects_registry_access(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            api_names = [f['api_name'] for f in result]
            assert "RegWrite" in api_names

    def test_categories_are_valid(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor, SUSPICIOUS_APIS

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            valid_categories = set(SUSPICIOUS_APIS.keys())
            for finding in result:
                assert finding['api_category'] in valid_categories
                assert finding['call_count'] > 0
                assert len(finding['line_numbers']) > 0

    def test_export_data_format(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            extractor.extract()
            export = extractor.prepare_export_data('ClickHouseExporter')

            assert export is not None
            data, col_names, col_types = export
            assert len(col_names) == len(col_types)
            assert len(data) > 0
            assert len(data[0]) == len(col_names)

    def test_clickhouse_table_name(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(js_malicious_path, mock_logger)
            assert extractor.get_clickhouse_table() == "redb_js_suspicious_apis"

    def test_benign_js_has_no_findings(self, tmp_path, mock_logger):
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        benign_js = tmp_path / "benign.js"
        benign_js.write_text("function add(a, b) { return a + b; }\nconsole.log(add(1, 2));")

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(str(benign_js), mock_logger)
            result = extractor.extract()
            assert result is None

    def test_pattern_match_is_case_insensitive(self, tmp_path, mock_logger):
        """Patterns in js_patterns.PATTERNS are compiled with re.IGNORECASE.
        A capitalised `EVAL(` is not actually valid JS at runtime but obfuscated
        droppers occasionally use stringly-built names; verifying the match
        survives capitalisation guards against an accidental flag regression.
        """
        from redb.extractors.js_extractors.js_suspicious_apis import JSSuspiciousAPIsExtractor

        upper_js = tmp_path / "upper.js"
        upper_js.write_text('var x = EVAL("1+1");\nFETCH("http://evil.test");')

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSSuspiciousAPIsExtractor(str(upper_js), mock_logger)
            result = extractor.extract()
            assert result is not None
            api_names = {f['api_name'] for f in result}
            assert "eval" in api_names
            assert "fetch" in api_names


# ============================================================================
# JSStringsExtractor Tests
# ============================================================================

class TestJSStringsExtractor:

    def test_extract_finds_strings(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            assert result is not None
            assert len(result) > 0

    def test_decodes_hex_strings(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            hex_decoded = [s for s in result if s['string_encoding'] == 'hex']
            assert len(hex_decoded) > 0

    def test_decodes_charcode(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            charcode_decoded = [s for s in result if s['string_encoding'] == 'charcode']
            assert len(charcode_decoded) > 0
            assert any('powershell' in s['string'] for s in charcode_decoded)

    def test_decodes_base64(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            b64_decoded = [s for s in result if s['string_encoding'] == 'base64']
            assert len(b64_decoded) > 0

    def test_reconstructs_concatenated_strings(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            concat = [s for s in result if s['string_encoding'] == 'concat']
            assert len(concat) > 0

    def test_strings_have_standard_fields(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            for s in result:
                assert s['string_entropy'] >= 0
                assert s['string_length'] > 0
                assert s['string_raw_length'] > 0
                assert s['string_offset'] > 0
                assert len(s['string']) > 0
                assert len(s['string_raw']) > 0

    def test_export_data_format(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(js_malicious_path, mock_logger)
            extractor.extract()
            export = extractor.prepare_export_data('ClickHouseExporter')

            assert export is not None
            data, col_names, col_types = export
            assert len(col_names) == len(col_types)
            assert len(data) > 0
            assert len(data[0]) == len(col_names)

    def test_writes_to_shared_strings_table(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(str(js_malicious_path), mock_logger)
            assert extractor.get_clickhouse_table() == "code_binja_strings_raw"

    def test_line_numbers_correct_in_multiline_source(self, tmp_path, mock_logger):
        """Pin the bisect-based _find_line_number against a multi-line source.

        Builds a JS file where each encoded literal sits on a known line, then
        verifies the `string_offset` (1-indexed line number) the extractor
        emits matches the planted line. Regression guard for the swap from the
        historical O(N) `source[:start].count("\\n")` to O(log L) bisect.
        """
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        # 800 filler lines, then a known hex-escape literal on line 801
        # (1-indexed), then 100 more filler, then a charcode call on line 902.
        prefix = "\n".join([f"function f{i}() {{ return {i}; }}" for i in range(800)])
        line_801 = 'var h = "\\x68\\x74\\x74\\x70\\x73\\x3a\\x2f\\x2f"'
        middle = "\n".join([f"var v{i} = {i};" for i in range(100)])
        line_902 = 'var c = String.fromCharCode(112, 111, 119, 101, 114)'
        source = "\n".join([prefix, line_801, middle, line_902]) + "\n"

        js_path = tmp_path / "multiline.js"
        js_path.write_text(source)

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSStringsExtractor(str(js_path), mock_logger)
            result = extractor.extract()

            assert result is not None
            by_encoding = {}
            for f in result:
                by_encoding.setdefault(f['string_encoding'], []).append(f)

            assert any(f['string_offset'] == 801 for f in by_encoding.get('hex', [])), (
                f"hex literal should be reported on line 801; got "
                f"{[f['string_offset'] for f in by_encoding.get('hex', [])]}"
            )
            assert any(f['string_offset'] == 902 for f in by_encoding.get('charcode', [])), (
                f"charcode call should be reported on line 902; got "
                f"{[f['string_offset'] for f in by_encoding.get('charcode', [])]}"
            )

    def test_publishes_findings_to_context(self, js_malicious_path, mock_logger):
        """JSStringsExtractor must mirror its findings onto JSContext.decoded_strings
        so post-loop consumers (the IOC plumbing in workers.py) can read the
        decoded strings without holding a reference to this extractor."""
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor
        from redb.extractors.js_extractors.js_context import JSContext

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            assert ctx.decoded_strings is None  # not populated until extract runs

            extractor = JSStringsExtractor(js_malicious_path, mock_logger, context=ctx)
            findings = extractor.extract()

            assert ctx.decoded_strings is findings, (
                "ctx.decoded_strings should reference the same list returned "
                "by extract(), not a copy"
            )


# ============================================================================
# JSDeobfuscationExtractor Tests
# ============================================================================

class TestJSDeobfuscationExtractor:

    def test_extract_with_jsbeautifier_fallback(self, js_malicious_path, mock_logger):
        """Should fall back to jsbeautifier when webcrack is not installed."""
        from redb.extractors.js_extractors.js_deobfuscation import JSDeobfuscationExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSDeobfuscationExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            if result is not None:
                assert result['deobfuscator_used'] is not None
                assert result['deobfuscation_successful'] is True
                assert result['original_size'] > 0
                assert result['deobfuscated_size'] > 0
                assert len(result['deobfuscated_sha256']) == 64

    def test_export_data_format(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_deobfuscation import JSDeobfuscationExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSDeobfuscationExtractor(js_malicious_path, mock_logger)
            result = extractor.extract()

            if result is not None:
                export = extractor.prepare_export_data('ClickHouseExporter')
                assert export is not None
                data, col_names, col_types = export
                assert len(col_names) == len(col_types)
                assert len(data[0]) == len(col_names)

    def test_clickhouse_table_name(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_deobfuscation import JSDeobfuscationExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSDeobfuscationExtractor(js_malicious_path, mock_logger)
            assert extractor.get_clickhouse_table() == "redb_js_deobfuscation"

    def test_runs_deobfuscator_without_separate_probe(self, js_malicious_path, mock_logger):
        """The deobfuscator binary is invoked directly — no separate
        `--help` probe per sample. When the binary is missing, the
        FileNotFoundError is swallowed silently (no error log) and the
        extractor falls through to jsbeautifier.
        """
        from redb.extractors.js_extractors import js_deobfuscator as deob_helper
        from redb.extractors.js_extractors.js_deobfuscation import (
            JSDeobfuscationExtractor,
        )

        # _check_tool_available must be gone entirely.
        assert not hasattr(JSDeobfuscationExtractor, "_check_tool_available")

        popen_calls = []

        def fake_popen(*args, **kwargs):
            popen_calls.append(args[0] if args else kwargs.get("args"))
            raise FileNotFoundError(2, "No such file or directory", "fake-deobfuscator")

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(deob_helper.subprocess, 'Popen', fake_popen):
            extractor = JSDeobfuscationExtractor(js_malicious_path, mock_logger)
            extractor.extract()

        # Exactly one Popen attempt — the actual run. No extra probe call.
        assert len(popen_calls) == 1, (
            f"expected one Popen call (the deobfuscator run), got "
            f"{len(popen_calls)}: {popen_calls}"
        )
        # FileNotFoundError must not have been logged at error level.
        for call in mock_logger.error.call_args_list:
            joined = " ".join(str(a) for a in call.args)
            assert "not found" not in joined.lower(), (
                f"missing-binary case must not log at error level; got {call}"
            )


# ============================================================================
# JSContentExtractor Tests
# ============================================================================

class TestJSContentExtractor:
    """JSContentExtractor persists raw + normalised text into code_text_content."""

    def test_extract_populates_both_columns(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_content import JSContentExtractor
        from redb.extractors.js_extractors.js_context import JSContext

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            extractor = JSContentExtractor(js_malicious_path, mock_logger, context=ctx)
            result = extractor.extract()

            assert result is not None
            assert result['content_type'] == 'javascript'
            assert result['text_raw']
            # jsbeautifier should normalise even non-obfuscated input, so
            # against the bundled test sample we expect a normalised text.
            assert result['text_normalized'] is not None
            assert result['normalizer_used'] is not None

    def test_normalized_is_null_when_deobfuscator_returns_nothing(
        self, js_malicious_path, mock_logger
    ):
        """When neither webcrack nor jsbeautifier produces output the
        normalized columns must be NULL — distinguishes failure from a
        legitimate empty result."""
        from redb.extractors.js_extractors.js_content import JSContentExtractor
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors import js_deobfuscator as deob_helper

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(deob_helper, 'deobfuscate', lambda src, log: (None, None)):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            extractor = JSContentExtractor(js_malicious_path, mock_logger, context=ctx)
            result = extractor.extract()

            assert result is not None
            assert result['text_raw']
            assert result['text_normalized'] is None
            assert result['normalizer_used'] is None

    def test_does_not_run_its_own_deobfuscation(self, js_malicious_path, mock_logger):
        """JSContentExtractor reads from JSContext.deobfuscated, which is a
        cached_property. Two extractors sharing one context must trigger
        deobfuscation at most once across both extracts."""
        from redb.extractors.js_extractors.js_content import JSContentExtractor
        from redb.extractors.js_extractors.js_deobfuscation import (
            JSDeobfuscationExtractor,
        )
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors import js_deobfuscator as deob_helper

        calls = {"n": 0}

        def counting_deobfuscate(source, log):
            calls["n"] += 1
            return None, None

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(deob_helper, 'deobfuscate', counting_deobfuscate):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            JSDeobfuscationExtractor(js_malicious_path, mock_logger, context=ctx).extract()
            JSContentExtractor(js_malicious_path, mock_logger, context=ctx).extract()

            assert calls["n"] == 1, (
                f"deobfuscate() must be called once across both extractors "
                f"sharing a context, got {calls['n']}"
            )

    def test_export_data_format(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_content import JSContentExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSContentExtractor(js_malicious_path, mock_logger)
            extractor.extract()
            data, col_names, col_types = extractor.prepare_export_data(
                'ClickHouseExporter'
            )
            assert col_names == [
                'sha256', 'content_type', 'text_raw', 'text_normalized',
                'normalizer_used', 'analysis_date',
            ]
            assert len(col_names) == len(col_types) == len(data[0])
            assert data[0][1] == 'javascript'

    def test_clickhouse_table_name(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_content import JSContentExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            extractor = JSContentExtractor(js_malicious_path, mock_logger)
            assert extractor.get_clickhouse_table() == 'code_text_content'


# ============================================================================
# Source Sharing Tests
# ============================================================================

class TestSourceSharing:
    """Test that source text can be shared across extractors."""

    def test_shared_source_produces_same_results(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            with open(js_malicious_path, 'r') as f:
                source = f.read()

            ext_shared = JSFeaturesExtractor(js_malicious_path, mock_logger, source=source)
            ext_fresh = JSFeaturesExtractor(js_malicious_path, mock_logger)

            r_shared = ext_shared.extract()
            r_fresh = ext_fresh.extract()

            assert r_shared.text_entropy == r_fresh.text_entropy
            assert r_shared.eval_count == r_fresh.eval_count
            assert r_shared.obfuscation_score == r_fresh.obfuscation_score


# ============================================================================
# JSContext Sharing Tests
# ============================================================================

class TestJSContextSharing:
    """Verify the shared JSContext threads through every extractor cleanly:
    same outputs as independent runs, expensive work computed exactly once."""

    def test_shared_context_produces_same_results(self, js_malicious_path, mock_logger):
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor
        from redb.extractors.js_extractors.js_suspicious_apis import (
            JSSuspiciousAPIsExtractor,
        )

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)

            shared_features = JSFeaturesExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()
            shared_apis = JSSuspiciousAPIsExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()

            fresh_features = JSFeaturesExtractor(
                js_malicious_path, mock_logger
            ).extract()
            fresh_apis = JSSuspiciousAPIsExtractor(
                js_malicious_path, mock_logger
            ).extract()

            # Features dataclass must compare equal across paths.
            assert shared_features == fresh_features
            # Suspicious-API findings: same set of api_name/category pairs.
            shared_pairs = {(f['api_name'], f['api_category']) for f in shared_apis}
            fresh_pairs = {(f['api_name'], f['api_category']) for f in fresh_apis}
            assert shared_pairs == fresh_pairs

    def test_context_scan_is_computed_once(self, js_malicious_path, mock_logger):
        """Two extractors sharing one JSContext must each trigger their backing
        scan at most once.

        The context exposes two cached scan results: `scan` (raw source) and
        `scan_deobfuscated` (deobfuscated text, populated only by the dual-pass
        consumers in JSSuspiciousAPIsExtractor / JSDeobfuscationExtractor).
        Both are cached_property — repeating the access from a second extractor
        must not re-run scan_source. Whether the deobfuscated scan happens at
        all depends on the deobfuscator producing a different text for this
        sample; we accept either outcome and only fail if the per-cache
        single-run invariant is broken.
        """
        from redb.extractors.js_extractors import js_context as js_ctx_module
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors.js_features import JSFeaturesExtractor
        from redb.extractors.js_extractors.js_suspicious_apis import (
            JSSuspiciousAPIsExtractor,
        )

        real_scan = js_ctx_module.scan_source
        call_count = {"n": 0}

        def counting_scan(*args, **kwargs):
            call_count["n"] += 1
            return real_scan(*args, **kwargs)

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(js_ctx_module, 'scan_source', counting_scan):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)

            JSFeaturesExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()
            JSSuspiciousAPIsExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()
            after_first_pair = call_count["n"]

            # The raw scan and (optionally) the deobfuscated scan are the only
            # two scan_source calls allowed for this sample.
            assert 1 <= after_first_pair <= 2, (
                f"scan_source must run at most twice (raw + deobf) when context "
                f"is shared, got {after_first_pair}"
            )

            # Re-running the same extractors must not re-trigger any scan: both
            # cached_property results are pinned by the first access.
            JSFeaturesExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()
            JSSuspiciousAPIsExtractor(
                js_malicious_path, mock_logger, context=ctx
            ).extract()

            assert call_count["n"] == after_first_pair, (
                f"cached scans re-ran across extractor instances: "
                f"{after_first_pair} -> {call_count['n']}"
            )

    def test_context_deobfuscation_runs_once(self, js_malicious_path, mock_logger):
        """Multiple consumers reading JSContext.deobfuscated must trigger the
        external deobfuscator at most once. cached_property guarantees this;
        the test pins the contract so a future regression fails loudly."""
        from redb.extractors.js_extractors import js_context as js_ctx_module
        from redb.extractors.js_extractors.js_context import JSContext

        deob_calls = {"n": 0}
        real_helper = None

        def counting_deobfuscate(source, log):
            deob_calls["n"] += 1
            return None, None  # cheap stub: no need to actually run webcrack

        # Patch the import that JSContext.deobfuscated does lazily inside the
        # cached_property. We monkeypatch the helper module directly.
        from redb.extractors.js_extractors import js_deobfuscator as deob_helper

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(deob_helper, 'deobfuscate', counting_deobfuscate):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            _ = ctx.deobfuscated
            _ = ctx.deobfuscated  # second access must hit the cache
            _ = ctx.deobfuscated  # third access too

            assert deob_calls["n"] == 1, (
                f"deobfuscate() must be called once per context, got "
                f"{deob_calls['n']}"
            )

    def test_context_ast_parsed_once(self, js_malicious_path, mock_logger):
        """Repeated access to JSContext.ast must invoke pyjsparser.parse at
        most once. cached_property guarantees this; the test pins the
        contract so a future regression (e.g. dropping cached_property)
        fails loudly."""
        from redb.extractors.js_extractors.js_context import JSContext

        try:
            import pyjsparser  # noqa: F401
        except ImportError:
            pytest.skip("pyjsparser not installed")

        parse_calls = {"n": 0}
        real_parse = pyjsparser.parse

        def counting_parse(source):
            parse_calls["n"] += 1
            return real_parse(source)

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(pyjsparser, 'parse', counting_parse):
            ctx = JSContext.from_path(js_malicious_path, log=mock_logger)
            _ = ctx.ast
            _ = ctx.ast  # second access: must hit the cache
            _ = ctx.ast  # third access: still cached

            # parse may legitimately be 0 if the source can't be parsed and
            # the exception was swallowed; otherwise it must be exactly 1.
            assert parse_calls["n"] <= 1, (
                f"pyjsparser.parse called {parse_calls['n']} times for one "
                "context — cached_property must hold the parsed AST"
            )


# ============================================================================
# JS IOC Surface Tests (end-to-end)
# ============================================================================

class TestJSIOCSurfaces:
    """workers.py-style assembly of analysis_results: text_raw, text_normalized,
    and decoded strings. End-to-end through IOCExtractorFromResults."""

    def _build_analysis_results(self, ctx, raw_source, sha256):
        """Mirror the dict workers.py builds in the JS IOC block. Kept in a
        helper so the test exercises the same shape the production code uses
        without dragging the full workers.py dispatch into the test harness."""
        import hashlib

        text_raw_entries = [{"content": raw_source, "content_hash": sha256}]
        text_normalized_entries = []
        deobf_text, _ = ctx.deobfuscated
        if deobf_text and deobf_text != raw_source:
            text_normalized_entries.append({
                "content": deobf_text,
                "content_hash": hashlib.sha256(
                    deobf_text.encode("utf-8")
                ).hexdigest(),
            })
        decoded_strings = ctx.decoded_strings or []
        strings_entries = [
            {
                "string": s.get("string", ""),
                "string_offset": s.get("string_offset", 0),
            }
            for s in decoded_strings
        ]
        return {
            "strings": strings_entries,
            "text_raw": text_raw_entries,
            "text_normalized": text_normalized_entries,
        }

    def test_decoded_string_url_lands_via_strings_surface(self, tmp_path, mock_logger):
        """A URL hidden inside a String.fromCharCode(...) call is decoded by
        JSStringsExtractor into a plaintext string; running IOC extraction
        over the decoded `strings` surface picks it up with source_type=string.
        Without the broadened plumbing the URL would be invisible to IOC
        extraction (regex doesn't see it through the charcode encoding in
        the raw source)."""
        from redb.extractors.ioc_extractor.ioc_extractor import IOCExtractorFromResults
        from redb.extractors.ioc_extractor.standalone_ioc_extractor import (
            IOCType, SourceType,
        )
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors.js_strings import JSStringsExtractor

        # String.fromCharCode of "http://hidden.example.com" (25 chars, codes
        # built inline below).
        url = "http://hidden.example.com"
        codes = ", ".join(str(ord(c)) for c in url)
        source = (
            'var benign = "harmless string here";\n'
            f'var u = String.fromCharCode({codes});\n'
        )
        sample_path = tmp_path / "charcode.js"
        sample_path.write_text(source)

        sha256 = "b" * 64

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            ctx = JSContext.from_path(str(sample_path), log=mock_logger)
            JSStringsExtractor(str(sample_path), mock_logger, context=ctx).extract()

            analysis_results = self._build_analysis_results(ctx, ctx.source, sha256)
            iocs = IOCExtractorFromResults(
                analysis_results=analysis_results,
                sha256=sha256,
                log=mock_logger,
            ).extract()

            url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
            assert any(
                "hidden.example.com" in i.ioc_value
                and i.source_type == SourceType.STRING
                for i in url_iocs
            ), (
                f"charcode-decoded URL must be picked up via the strings "
                f"surface; got {[(i.ioc_value, i.source_type) for i in url_iocs]}"
            )

    def test_raw_url_lands_via_text_raw(self, tmp_path, mock_logger):
        """A plain URL in the raw source lands with source_type=text_raw, the
        new universal surface (was source_type=decompiled_function before #C)."""
        from redb.extractors.ioc_extractor.ioc_extractor import IOCExtractorFromResults
        from redb.extractors.ioc_extractor.standalone_ioc_extractor import (
            IOCType, SourceType,
        )
        from redb.extractors.js_extractors.js_context import JSContext

        source = 'fetch("https://plain.example.com/c2");\n'
        sample_path = tmp_path / "plain.js"
        sample_path.write_text(source)
        sha256 = "c" * 64

        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'):
            ctx = JSContext.from_path(str(sample_path), log=mock_logger)
            analysis_results = self._build_analysis_results(ctx, ctx.source, sha256)
            iocs = IOCExtractorFromResults(
                analysis_results=analysis_results,
                sha256=sha256,
                log=mock_logger,
            ).extract()

            url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
            assert any(
                "plain.example.com" in i.ioc_value
                and i.source_type == SourceType.TEXT_RAW
                for i in url_iocs
            )

    def test_no_text_normalized_entry_when_deobf_equals_raw(self, tmp_path, mock_logger):
        """If the deobfuscator returns text byte-identical to the raw source
        (or returns nothing), the analysis_results dict must not add a
        text_normalized entry — avoids double-scraping the same content."""
        from redb.extractors.js_extractors.js_context import JSContext
        from redb.extractors.js_extractors import js_deobfuscator as deob_helper

        source = 'fetch("https://noop.example.com/");\n'
        sample_path = tmp_path / "noop.js"
        sample_path.write_text(source)
        sha256 = "d" * 64

        # Force the deobfuscator to return identical text — exercises the
        # equality check in workers.py / _build_analysis_results.
        with patch('redb.settings.ELASTIC_BINARIES_COLLECTION', 'test'), \
                patch.object(deob_helper, 'deobfuscate',
                             lambda src, log: (src, "stub")):
            ctx = JSContext.from_path(str(sample_path), log=mock_logger)
            analysis_results = self._build_analysis_results(ctx, ctx.source, sha256)

            assert analysis_results["text_normalized"] == [], (
                "text_normalized must be empty when deobfuscation produced "
                "byte-identical output"
            )