@@ -197,37 +197,71 @@ def test_password_not_logged_when_line_invalid(caplog: pytest.LogCaptureFixture)
197197 with caplog .at_level (logging .DEBUG , logger = logger .name ):
198198 valid , invalid = TSC .UserItem .CSVImport .validate_file_for_import (test_data , logger )
199199 assert valid == 0
200- assert len (invalid ) == 1
201- assert secret not in invalid [0 ], f"Password leaked into returned invalid_lines: { invalid [0 ]!r} "
200+ assert invalid == ["jsmith,***,***,not-a-real-license,site,yes,jsmith@example.com" ]
202201 combined = "\n " .join (record .getMessage () for record in caplog .records )
203202 assert secret not in combined , f"Password leaked into logs on invalid row: { combined !r} "
204203
205204
206- def test_password_with_comma_is_fully_redacted (caplog : pytest .LogCaptureFixture ) -> None :
207- """A quoted comma in a password must not leak from invalid row output or logs ."""
205+ def test_quoted_comma_password_not_logged (caplog : pytest .LogCaptureFixture ) -> None :
206+ """A quoted comma in a password must not reach the logs; the invalid row is returned as written ."""
208207 line = 'jsmith,"hunter2,SECRETTAIL",John Smith,not-a-real-license,site,yes,jsmith@example.com'
209208 test_data = _mock_file_content ([line ])
210209 with caplog .at_level (logging .DEBUG , logger = logger .name ):
211210 valid , invalid = TSC .UserItem .CSVImport .validate_file_for_import (test_data , logger )
212211 assert valid == 0
213- assert len (invalid ) == 1
214- assert "hunter2" not in invalid [0 ]
215- assert "SECRETTAIL" not in invalid [0 ]
212+ assert invalid == ["jsmith,***,***,not-a-real-license,site,yes,jsmith@example.com" ]
216213 combined = "\n " .join (record .getMessage () for record in caplog .records )
217214 assert "hunter2" not in combined
218215 assert "SECRETTAIL" not in combined
219216 assert _mask_present (caplog .records )
220217
221218
222- def test_malformed_csv_row_is_not_returned_unredacted (caplog : pytest .LogCaptureFixture ) -> None :
223- secret = "SECRETTAIL"
224- test_data = _mock_file_content ([f'jsmith,"{ secret } ' ])
219+ def test_include_passwords_returns_invalid_lines_verbatim () -> None :
220+ line = "jsmith,hunter2,John Smith,not-a-real-license,site,yes,jsmith@example.com\n "
221+ valid , invalid = TSC .UserItem .CSVImport .validate_file_for_import (
222+ _mock_file_content ([line ]), logger , include_passwords = True
223+ )
224+ assert valid == 0
225+ assert invalid == [line ]
226+
227+
228+ def test_mask_password_columns_helper () -> None :
229+ mask = TSC .UserItem .CSVImport ._mask_password_columns
230+ assert mask ("jsmith,hunter2,fname,creator\n " ) == "jsmith,***,***,creator\n "
231+ assert mask ("jsmith,hunter2,fname,creator\r \n " ) == "jsmith,***,***,creator\r \n "
232+ assert mask ("jsmith,hunter2,fname" ) == "jsmith,***,***"
233+ assert mask ("jsmith,hunter2\r \n " ) == "jsmith,***\r \n "
234+ assert mask ("jsmith,,fname" ) == "jsmith,***,***"
235+ assert mask ("jsmith," ) == "jsmith,***"
236+ assert mask ('jsmith,"pass,word",fname,creator' ) == "jsmith,***,***,creator"
237+ # No password column: returned unchanged
238+ assert mask ("jsmith" ) == "jsmith"
239+ assert mask ("jsmith\n " ) == "jsmith\n "
240+
241+
242+ def test_unmatched_quote_row_still_parses () -> None :
243+ """A row that is not well-formed CSV is split on commas instead of aborting the import."""
244+ user = TSC .UserItem .CSVImport .create_user_from_line ('username,pa"ss,fname,creator,none,yes,email' )
245+ assert user is not None
246+ assert user .fullname == "fname"
247+ user = TSC .UserItem .CSVImport .create_user_from_line ('username,"unterminated,fname,creator,none,yes,email' )
248+ assert user is not None
249+ assert user .fullname == "fname"
250+
251+
252+ def test_shifted_password_fragment_not_logged (caplog : pytest .LogCaptureFixture ) -> None :
253+ """An unquoted comma shifts password fragments into later columns; they must not reach the logs."""
254+ line = "jsmith,hunter2,SECRETTAIL,TAILFRAGMENT,site,yes,jsmith@example.com"
255+ test_data = _mock_file_content ([line ])
225256 with caplog .at_level (logging .DEBUG , logger = logger .name ):
226257 valid , invalid = TSC .UserItem .CSVImport .validate_file_for_import (test_data , logger )
227258 assert valid == 0
228- assert invalid == ["<invalid CSV row redacted>" ]
229- combined = "\n " .join (record .getMessage () for record in caplog .records )
230- assert secret not in combined
259+ # Only two columns are masked, so a fragment past an unquoted second comma stays in the returned row.
260+ assert invalid == ["jsmith,***,***,TAILFRAGMENT,site,yes,jsmith@example.com" ]
261+ combined = " " .join (record .getMessage () for record in caplog .records ).lower ()
262+ assert "tailfragment" not in combined
263+ assert "secrettail" not in combined
264+ assert "column license" in combined
231265
232266
233267def test_password_not_in_validate_attribute_value_error (
@@ -269,27 +303,6 @@ def spy(item: str, possible_values: list, column_type) -> None:
269303 assert secret not in combined , f"Password leaked into logs: { combined !r} "
270304
271305
272- def test_redact_password_column_helper () -> None :
273- """Unit-level coverage for _redact_password_column across newline and edge cases."""
274- redact = TSC .UserItem .CSVImport ._redact_password_column
275- # LF-terminated
276- assert redact ("jsmith,hunter2,fname\n " ) == "jsmith,***,fname\n "
277- # CRLF-terminated (the \r rides with the last field, ending is preserved)
278- assert redact ("jsmith,hunter2,fname\r \n " ) == "jsmith,***,fname\r \n "
279- # CRLF where password IS the last field: the \r must not be silently
280- # dropped when the password value is replaced.
281- assert redact ("jsmith,hunter2\r \n " ) == "jsmith,***\r \n "
282- # No trailing newline
283- assert redact ("jsmith,hunter2,fname" ) == "jsmith,***,fname"
284- # Empty password field: still replaced (unconditional mask)
285- assert redact ("jsmith,,fname" ) == "jsmith,***,fname"
286- # Trailing comma with nothing after: column 1 exists as empty string, gets masked
287- assert redact ("jsmith," ) == "jsmith,***"
288- # Single column: no password to redact; return line unchanged
289- assert redact ("jsmith" ) == "jsmith"
290- assert redact ("jsmith\n " ) == "jsmith\n "
291-
292-
293306def test_validate_mixed_case_license () -> None :
294307 # Regression: issue #1809 - 'Viewer' (capital V) was rejected by case-sensitive check
295308 TSC .UserItem .CSVImport ._validate_import_line_or_throw ("username, pword, fname, Viewer, None, no, email" , logger )
0 commit comments