"""Tests for FEED_FILE_TIMESTAMP behavior across all splitter functions. These tests cover the regression introduced in INT-2478 and fixed in INT-2649: FEED_FILE_TIMESTAMP is appended to output filenames after reading the Header row. All H/T validation must compare raw (pre-mutation) Snowflake filenames so that the timestamp suffix does not trigger a spurious ValueError. Covered code paths: - split_rows_to_files (US / GB / EX_US modes) - split_res_to_files (AGGREGATE / SAP_SETTLEMENT — STREAMING mode) FEED_FILE_TIMESTAMP absent: existing behaviour must be unchanged (no regression). FEED_FILE_TIMESTAMP present: - output filename is timestamped - H/T validation passes when raw names match - H/T validation still raises ValueError when raw names genuinely differ """ import os import pytest import feed_file_exporter as ffe from constants import database as db_consts TIMESTAMP = '20260224_023117' def _make_rows(raw_name): """Return a minimal H/B/T row list for the given raw filename.""" return [ { db_consts.RECORD_TYPE: 'H', db_consts.BODY_CONTENT: 'HEADER', db_consts.FILE_NAME: raw_name, }, { db_consts.RECORD_TYPE: 'N', db_consts.BODY_CONTENT: 'BODY', db_consts.FILE_NAME: raw_name, }, { db_consts.RECORD_TYPE: 'T', db_consts.BODY_CONTENT: 'TAIL', db_consts.FILE_NAME: raw_name, }, ] class _Row: """Minimal stand-in for a SQLAlchemy RowProxy.""" def __init__(self, d): self._d = d def keys(self): return list(self._d.keys()) def __iter__(self): return iter(self._d.values()) class _RP: """Minimal stand-in for a SQLAlchemy ResultProxy.""" def __init__(self, rows): self._rows = rows def fetchone(self): return _Row(self._rows[0]) def __iter__(self): for r in self._rows[1:]: yield _Row(r) # ── split_rows_to_files (US / GB / EX_US) ──────────────────────────────────── def test_split_rows_timestamp_applied_to_filename_with_extension( tmp_path, monkeypatch ): """Timestamp is inserted before the file extension (.txt → _TS.txt).""" monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('sony_stars_US_001.txt') count, upload_list, gen_list = ffe.split_rows_to_files(rows, str(tmp_path)) expected_name = f'sony_stars_US_001_{TIMESTAMP}.txt' assert count == 3 assert upload_list == [expected_name] assert len(gen_list) == 1 assert os.path.basename(gen_list[0]) == expected_name assert os.path.exists(gen_list[0]) def test_split_rows_timestamp_applied_to_filename_no_extension( tmp_path, monkeypatch ): """Timestamp is appended at the end when the filename has no extension.""" monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('sony_stars_US_001') _, upload_list, gen_list = ffe.split_rows_to_files(rows, str(tmp_path)) expected_name = f'sony_stars_US_001_{TIMESTAMP}' assert upload_list == [expected_name] assert os.path.basename(gen_list[0]) == expected_name def test_split_rows_ht_validation_passes_with_timestamp(tmp_path, monkeypatch): """H/T validation uses raw names — must NOT raise when timestamp is set. Regression test for INT-2649: the original bug raised ValueError on every production run because the mutated (timestamped) filename was compared against the raw Snowflake Tail filename. """ monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('sony_stars_US_001.txt') # Must not raise count, _, _ = ffe.split_rows_to_files(rows, str(tmp_path)) assert count == 3 def test_split_rows_ht_mismatch_still_raises_with_timestamp( tmp_path, monkeypatch ): """A genuine raw H/T name mismatch still raises ValueError with timestamp. Confirms the H/T guard still works correctly after the INT-2649 fix. """ monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('A.txt') rows[-1][db_consts.FILE_NAME] = 'B.txt' # raw tail differs from raw header with pytest.raises(ValueError, match='[Mm]ismatch'): ffe.split_rows_to_files(rows, str(tmp_path)) def test_split_rows_timestamp_absent_filename_unchanged(tmp_path, monkeypatch): """Without FEED_FILE_TIMESTAMP the filename is unchanged (no regression).""" monkeypatch.delenv('FEED_FILE_TIMESTAMP', raising=False) rows = _make_rows('sony_stars_US_001.txt') _, upload_list, gen_list = ffe.split_rows_to_files(rows, str(tmp_path)) assert upload_list == ['sony_stars_US_001.txt'] assert os.path.basename(gen_list[0]) == 'sony_stars_US_001.txt' # ── split_res_to_files (AGGREGATE / SAP_SETTLEMENT — STREAMING mode) ───────── def test_split_res_timestamp_applied_to_filename(tmp_path, monkeypatch): """STREAMING mode applies FEED_FILE_TIMESTAMP to the output filename.""" monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('aggregate_feed.txt') rp = _RP(rows) count, gen_list = ffe.split_res_to_files(rp, str(tmp_path)) expected_name = f'aggregate_feed_{TIMESTAMP}.txt' assert count == 3 assert len(gen_list) == 1 assert os.path.basename(gen_list[0]) == expected_name assert os.path.exists(gen_list[0]) def test_split_res_ht_validation_passes_with_timestamp(tmp_path, monkeypatch): """STREAMING mode H/T validation uses raw names — must NOT raise. Same root-cause regression as INT-2649 applied to the STREAMING path. """ monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('aggregate_feed.txt') rp = _RP(rows) count, _ = ffe.split_res_to_files(rp, str(tmp_path)) assert count == 3 def test_split_res_ht_mismatch_still_raises_with_timestamp( tmp_path, monkeypatch ): """STREAMING mode genuine raw H/T mismatch still raises ValueError.""" monkeypatch.setenv('FEED_FILE_TIMESTAMP', TIMESTAMP) rows = _make_rows('A.txt') rows[-1][db_consts.FILE_NAME] = 'B.txt' rp = _RP(rows) with pytest.raises(ValueError, match='[Mm]ismatch'): ffe.split_res_to_files(rp, str(tmp_path)) def test_split_res_timestamp_absent_filename_unchanged(tmp_path, monkeypatch): """Without FEED_FILE_TIMESTAMP STREAMING mode filename is unchanged.""" monkeypatch.delenv('FEED_FILE_TIMESTAMP', raising=False) rows = _make_rows('aggregate_feed.txt') rp = _RP(rows) _, gen_list = ffe.split_res_to_files(rp, str(tmp_path)) assert os.path.basename(gen_list[0]) == 'aggregate_feed.txt'