test_alerter.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. # -*- coding:utf-8 -*-
  2. """dw_base.alerter.alerter 配置加载 + 企微推送单测(不发真消息,urlopen 走替身)。"""
  3. import json
  4. from unittest.mock import MagicMock
  5. import pytest
  6. from dw_base.alerter import alerter as alerter_mod
  7. from dw_base.alerter.alerter import Alerter
  8. BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key='
  9. def _write_conf(tmp_path, channels=None):
  10. """造一份临时 alerter.ini,返回路径。"""
  11. if channels is None:
  12. channels = {'default': 'real-key-0001'}
  13. lines = ['[wechat_work]', 'base_url = ' + BASE_URL, '', '[channels]']
  14. lines.extend('{0} = {1}'.format(name, key) for name, key in channels.items())
  15. path = tmp_path / 'alerter.ini'
  16. path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
  17. return str(path)
  18. def _stub_urlopen(captured, resp='{"errcode":0,"errmsg":"ok"}'):
  19. """替身 urlopen:把 Request 存进 captured,返回可 read 的响应对象。"""
  20. def _open(req, timeout=None):
  21. captured['req'] = req
  22. captured['timeout'] = timeout
  23. fake_resp = MagicMock()
  24. fake_resp.read.return_value = resp.encode('utf-8')
  25. return fake_resp
  26. return _open
  27. # ---------- 构造期的四类失败 ----------
  28. def test_conf_file_missing_raises(tmp_path):
  29. with pytest.raises(RuntimeError, match='告警配置文件不存在'):
  30. Alerter(conf_path=str(tmp_path / 'nope.ini'))
  31. def test_channel_not_registered_raises(tmp_path):
  32. conf = _write_conf(tmp_path, {'default': 'real-key-0001'})
  33. with pytest.raises(RuntimeError, match='告警通道未注册'):
  34. Alerter(channel='realtime', conf_path=conf)
  35. def test_placeholder_key_raises(tmp_path):
  36. conf = _write_conf(tmp_path, {'default': '<替换为企微机器人 key>'})
  37. with pytest.raises(RuntimeError, match='仍是占位符'):
  38. Alerter(conf_path=conf)
  39. def test_webhook_composed_from_base_and_key(tmp_path):
  40. conf = _write_conf(tmp_path, {'default': 'abc-123'})
  41. assert Alerter(conf_path=conf).webhook == BASE_URL + 'abc-123'
  42. def test_custom_channel_picks_its_own_key(tmp_path):
  43. conf = _write_conf(tmp_path, {'default': 'key-d', 'realtime': 'key-r'})
  44. assert Alerter(channel='realtime', conf_path=conf).webhook == BASE_URL + 'key-r'
  45. def test_key_whitespace_stripped(tmp_path):
  46. path = tmp_path / 'alerter.ini'
  47. path.write_text(
  48. '[wechat_work]\nbase_url = {0}\n\n[channels]\ndefault = abc-123 \n'.format(BASE_URL),
  49. encoding='utf-8')
  50. assert Alerter(conf_path=str(path)).webhook == BASE_URL + 'abc-123'
  51. # ---------- 发送 ----------
  52. def test_send_markdown_posts_expected_payload(tmp_path, monkeypatch):
  53. conf = _write_conf(tmp_path, {'default': 'abc-123'})
  54. captured = {}
  55. monkeypatch.setattr(alerter_mod.urllib.request, 'urlopen', _stub_urlopen(captured))
  56. Alerter(conf_path=conf).send_markdown('### 标题\n> 一行')
  57. req = captured['req']
  58. assert req.full_url == BASE_URL + 'abc-123'
  59. assert req.headers['Content-type'] == 'application/json'
  60. assert captured['timeout'] == alerter_mod._TIMEOUT_SEC
  61. body = json.loads(req.data.decode('utf-8'))
  62. assert body == {'msgtype': 'markdown', 'markdown': {'content': '### 标题\n> 一行'}}
  63. def test_send_markdown_keeps_chinese_unescaped(tmp_path, monkeypatch):
  64. """ensure_ascii=False:中文按 UTF-8 原样进 body,不是 \\uXXXX。"""
  65. conf = _write_conf(tmp_path)
  66. captured = {}
  67. monkeypatch.setattr(alerter_mod.urllib.request, 'urlopen', _stub_urlopen(captured))
  68. Alerter(conf_path=conf).send_markdown('槽空闲')
  69. raw = captured['req'].data
  70. assert '槽空闲'.encode('utf-8') in raw
  71. assert b'\\u' not in raw
  72. def test_send_markdown_raises_on_nonzero_errcode(tmp_path, monkeypatch):
  73. conf = _write_conf(tmp_path)
  74. monkeypatch.setattr(
  75. alerter_mod.urllib.request, 'urlopen',
  76. _stub_urlopen({}, resp='{"errcode":93000,"errmsg":"invalid webhook url"}'))
  77. with pytest.raises(RuntimeError, match='企微发送失败'):
  78. Alerter(conf_path=conf).send_markdown('x')
  79. def test_send_markdown_ok_on_zero_errcode(tmp_path, monkeypatch):
  80. conf = _write_conf(tmp_path)
  81. monkeypatch.setattr(alerter_mod.urllib.request, 'urlopen', _stub_urlopen({}))
  82. Alerter(conf_path=conf).send_markdown('x') # 不抛即通过