mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
fix(salary): validate salary data shape before lookup (#141)
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
"""Tests for salary_lookup.py — format_entry, match_score, and search_company."""
|
||||
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from pathlib import Path
|
||||
|
||||
import salary_lookup
|
||||
from salary_lookup import (
|
||||
format_entry,
|
||||
normalize,
|
||||
@@ -9,6 +14,7 @@ from salary_lookup import (
|
||||
extract_core_words,
|
||||
match_score,
|
||||
search_company,
|
||||
validate_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -165,6 +171,76 @@ class SearchCompanyTests(unittest.TestCase):
|
||||
self.assertEqual(results, [])
|
||||
|
||||
|
||||
class ValidateDataTests(unittest.TestCase):
|
||||
def assert_invalid_data(self, data, expected_message):
|
||||
stderr = io.StringIO()
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
with redirect_stderr(stderr):
|
||||
validate_data(data)
|
||||
|
||||
self.assertEqual(raised.exception.code, 1)
|
||||
self.assertIn("Error: invalid salary_data.json", stderr.getvalue())
|
||||
self.assertIn(expected_message, stderr.getvalue())
|
||||
self.assertIn("tools/README_SALARY_TOOL.md", stderr.getvalue())
|
||||
|
||||
def test_valid_minimal_data_is_returned(self):
|
||||
data = {"metadata": {}, "companies": [{"company": "Example Corp"}]}
|
||||
|
||||
self.assertIs(validate_data(data), data)
|
||||
|
||||
def test_top_level_value_must_be_object(self):
|
||||
self.assert_invalid_data([], "top-level JSON value must be an object")
|
||||
|
||||
def test_companies_must_be_list(self):
|
||||
self.assert_invalid_data({"companies": {"company": "Example Corp"}}, "'companies' must be a list")
|
||||
|
||||
def test_metadata_must_be_object_when_provided(self):
|
||||
self.assert_invalid_data(
|
||||
{"metadata": [], "companies": [{"company": "Example Corp"}]},
|
||||
"'metadata' must be an object when provided",
|
||||
)
|
||||
|
||||
def test_load_data_reports_json_parse_errors_without_traceback(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
data_file = Path(tmpdir) / "salary_data.json"
|
||||
data_file.write_text('{"companies": [', encoding="utf-8")
|
||||
|
||||
original_data_file = salary_lookup.DATA_FILE
|
||||
salary_lookup.DATA_FILE = data_file
|
||||
try:
|
||||
stderr = io.StringIO()
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
with redirect_stderr(stderr):
|
||||
salary_lookup.load_data()
|
||||
finally:
|
||||
salary_lookup.DATA_FILE = original_data_file
|
||||
|
||||
self.assertEqual(raised.exception.code, 1)
|
||||
self.assertIn("invalid JSON at line", stderr.getvalue())
|
||||
self.assertIn("tools/README_SALARY_TOOL.md", stderr.getvalue())
|
||||
|
||||
def test_company_entry_must_be_object(self):
|
||||
self.assert_invalid_data({"companies": ["Example Corp"]}, "companies[1] must be an object")
|
||||
|
||||
def test_company_name_is_required(self):
|
||||
self.assert_invalid_data({"companies": [{"city": "Aarhus"}]}, "companies[1].company must be a non-empty string")
|
||||
|
||||
def test_company_name_must_not_be_blank(self):
|
||||
self.assert_invalid_data({"companies": [{"company": " "}]}, "companies[1].company must be a non-empty string")
|
||||
|
||||
def test_city_must_be_string_when_provided(self):
|
||||
self.assert_invalid_data(
|
||||
{"companies": [{"company": "Example Corp", "city": 123}]},
|
||||
"companies[1].city must be a string when provided",
|
||||
)
|
||||
|
||||
def test_categories_must_be_object_when_provided(self):
|
||||
self.assert_invalid_data(
|
||||
{"companies": [{"company": "Example Corp", "categories": []}]},
|
||||
"companies[1].categories must be an object when provided",
|
||||
)
|
||||
|
||||
|
||||
class UtilityTests(unittest.TestCase):
|
||||
def test_normalize_strips_suffix_and_noise(self):
|
||||
self.assertEqual(normalize("Novo Nordisk A/S"), "novonordisk")
|
||||
|
||||
Reference in New Issue
Block a user