test_data_quality.py 4.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """Data quality API tests with two source files and overlapping row numbers."""
  2. import json
  3. import tempfile
  4. import unittest
  5. from datetime import datetime
  6. from pathlib import Path
  7. from unittest.mock import patch
  8. from fastapi import FastAPI
  9. from fastapi.testclient import TestClient
  10. from openpyxl import Workbook
  11. from step4_web import data_quality as quality
  12. class DataQualityTests(unittest.TestCase):
  13. def setUp(self):
  14. self.tmp = tempfile.TemporaryDirectory()
  15. self.root = Path(self.tmp.name)
  16. self.production = self.root / 'production'
  17. self.production.mkdir()
  18. self.snapshot = {'data_version': 'v1', 'production_dir': str(self.production)}
  19. self.report = {'data_version': 'v1', 'warnings': []}
  20. for name in ('人员证书', '项目信息'):
  21. wb = Workbook(); ws = wb.active; ws.title = '数据'
  22. ws.append(['编号', '名称', '日期', '数量', '备注'])
  23. ws.append([None, name + '<script>alert(1)</script>', datetime(2026, 9, 4), 0, None])
  24. ws.append(['valid', '正常数据不应暴露', None, 1, None])
  25. wb.save(self.production / (name + '.xlsx')); wb.close()
  26. self.report['warnings'].append({'type': 'missing_key', 'template': name,
  27. 'file': 'ignored-old-path', 'row': 2, 'fields': ['编号'], 'sheet': '数据'})
  28. self.save()
  29. self.mock = patch.object(quality, 'release_snapshot', return_value=self.snapshot)
  30. self.mock.start()
  31. app = FastAPI(); app.include_router(quality.router)
  32. self.client = TestClient(app)
  33. def tearDown(self):
  34. self.client.close(); self.mock.stop(); self.tmp.cleanup()
  35. def save(self):
  36. (self.root / 'update_report.json').write_text(json.dumps(self.report), encoding='utf-8')
  37. def test_summary_counts_files_without_loading_raw_records(self):
  38. with patch.object(quality, 'load_workbook', side_effect=AssertionError('summary opened Excel')):
  39. response = self.client.get('/api/data-quality')
  40. self.assertEqual(response.status_code, 200)
  41. self.assertEqual(response.headers['cache-control'], 'no-store')
  42. self.assertEqual(response.json(), {'data_version':'v1', 'total':2,
  43. 'datasets': {'人员证书':1, '项目信息':1}})
  44. def test_details_separate_same_row_number_and_include_every_field(self):
  45. response = self.client.get('/api/data-quality?details=true&data_version=v1')
  46. self.assertEqual(response.status_code, 200)
  47. records = response.json()['records']
  48. self.assertEqual([r['file'] for r in records], ['人员证书.xlsx', '项目信息.xlsx'])
  49. for record in records:
  50. self.assertEqual(record['row'], 2)
  51. self.assertEqual(len(record['data']), 5)
  52. self.assertIsNone(record['data']['编号'])
  53. self.assertIsNone(record['data']['备注'])
  54. self.assertEqual(record['data']['数量'], 0)
  55. self.assertEqual(record['data']['日期'], '2026-09-04T00:00:00')
  56. self.assertTrue(record['data']['名称'].startswith(record['template']))
  57. self.assertNotIn('正常数据', response.text)
  58. self.assertNotIn(str(self.production), response.text)
  59. def test_version_change_before_and_during_read_requires_refresh(self):
  60. self.assertEqual(self.client.get('/api/data-quality?details=true&data_version=old').status_code, 409)
  61. with patch.object(quality, 'release_snapshot', side_effect=[self.snapshot, {**self.snapshot, 'data_version':'v2'}]):
  62. self.assertEqual(self.client.get('/api/data-quality?details=true').status_code, 409)
  63. def test_missing_source_keeps_problem_and_other_file_data(self):
  64. (self.production / '人员证书.xlsx').unlink()
  65. records = self.client.get('/api/data-quality?details=true').json()['records']
  66. self.assertIn('data_error', records[0])
  67. self.assertIsNotNone(records[1]['data'])
  68. def test_no_issues_and_missing_report_are_different(self):
  69. self.report['warnings'] = []; self.save()
  70. self.assertEqual(self.client.get('/api/data-quality?details=true').json()['records'], [])
  71. (self.root / 'update_report.json').unlink()
  72. self.assertEqual(self.client.get('/api/data-quality').status_code, 503)
  73. def test_report_version_mismatch_and_outside_source_rejected(self):
  74. self.report['data_version'] = 'old'; self.save()
  75. self.assertEqual(self.client.get('/api/data-quality').status_code, 503)
  76. self.report['data_version'] = 'v1'
  77. self.report['warnings'][0]['template'] = '../outside'; self.save()
  78. self.assertEqual(self.client.get('/api/data-quality?details=true').status_code, 503)
  79. if __name__ == '__main__':
  80. unittest.main(verbosity=2)