max / makenotwork
1 file changed,
+74 insertions,
-0 deletions
| @@ -720,4 +720,78 @@ mod tests { | |||
| 720 | 720 | let payload = parse_csv(csv, &mapping).unwrap(); | |
| 721 | 721 | assert_eq!(payload.subscribers.len(), 2); | |
| 722 | 722 | } | |
| 723 | + | ||
| 724 | + | // ── Adversarial: row-error recovery, malformed input, boundaries ── | |
| 725 | + | ||
| 726 | + | #[test] | |
| 727 | + | fn parse_csv_malformed_row_skipped_good_rows_survive() { | |
| 728 | + | // Second data row has an unterminated quote → a row-level parse error. | |
| 729 | + | // parse_csv must record the error, skip that row, and still keep the | |
| 730 | + | // surrounding valid rows rather than aborting the whole import. | |
| 731 | + | let csv = b"email\nalice@test.com\nbob@\"broken\ncarol@test.com\n"; | |
| 732 | + | let mapping = ColumnMapping { email: Some(0), ..Default::default() }; | |
| 733 | + | let payload = parse_csv(csv, &mapping).unwrap(); | |
| 734 | + | let emails: Vec<&str> = payload.subscribers.iter().map(|s| s.email.as_str()).collect(); | |
| 735 | + | assert!(emails.contains(&"alice@test.com"), "good row before the bad one kept"); | |
| 736 | + | assert!(emails.contains(&"carol@test.com"), "good row after the bad one kept"); | |
| 737 | + | } | |
| 738 | + | ||
| 739 | + | #[test] | |
| 740 | + | fn parse_csv_crlf_line_endings() { | |
| 741 | + | // Windows-exported CSVs use \r\n; trim(Trim::All) + the csv reader must | |
| 742 | + | // handle them without leaking a trailing \r into the values. | |
| 743 | + | let csv = b"email,name\r\nalice@test.com,Alice\r\nbob@test.com,Bob\r\n"; | |
| 744 | + | let mapping = ColumnMapping { email: Some(0), name: Some(1), ..Default::default() }; | |
| 745 | + | let payload = parse_csv(csv, &mapping).unwrap(); | |
| 746 | + | assert_eq!(payload.subscribers.len(), 2); | |
| 747 | + | assert_eq!(payload.subscribers[0].name.as_deref(), Some("Alice")); | |
| 748 | + | assert_eq!(payload.subscribers[1].email, "bob@test.com"); | |
| 749 | + | } | |
| 750 | + | ||
| 751 | + | #[test] | |
| 752 | + | fn email_domain_dot_rules_enforced() { | |
| 753 | + | // Rows that must be rejected by the inline email filter, plus one valid | |
| 754 | + | // plus-addressed email that must survive. | |
| 755 | + | let csv = b"email\nno-dot@localhost\nleading@.com\ntrailing@dot.\n@nolocal.com\nvalid+tag@sub.example.com\n"; | |
| 756 | + | let mapping = ColumnMapping { email: Some(0), ..Default::default() }; | |
| 757 | + | let payload = parse_csv(csv, &mapping).unwrap(); | |
| 758 | + | assert_eq!(payload.subscribers.len(), 1, "only the well-formed email survives"); | |
| 759 | + | assert_eq!(payload.subscribers[0].email, "valid+tag@sub.example.com"); | |
| 760 | + | } | |
| 761 | + | ||
| 762 | + | #[test] | |
| 763 | + | fn parse_amount_negative_whole_number_is_negative_cents() { | |
| 764 | + | assert_eq!(parse_amount_cents("-500"), Some(-500)); | |
| 765 | + | assert_eq!(parse_amount_cents("-$1,000"), Some(-1000)); | |
| 766 | + | } | |
| 767 | + | ||
| 768 | + | #[test] | |
| 769 | + | fn parse_amount_overflow_returns_none() { | |
| 770 | + | // Beyond i64 range → parse fails → None, never a panic or wrapped value. | |
| 771 | + | assert_eq!(parse_amount_cents("99999999999999999999999"), None); | |
| 772 | + | assert_eq!(parse_amount_cents("999999999999999999999.99"), None); | |
| 773 | + | } | |
| 774 | + | ||
| 775 | + | #[test] | |
| 776 | + | fn parse_csv_duplicate_emails_all_retained() { | |
| 777 | + | // Deduplication is a downstream concern; the parser must faithfully emit | |
| 778 | + | // every valid row so the importer can decide how to merge. | |
| 779 | + | let csv = b"email\ndup@test.com\ndup@test.com\n"; | |
| 780 | + | let mapping = ColumnMapping { email: Some(0), ..Default::default() }; | |
| 781 | + | let payload = parse_csv(csv, &mapping).unwrap(); | |
| 782 | + | assert_eq!(payload.subscribers.len(), 2); | |
| 783 | + | } | |
| 784 | + | ||
| 785 | + | #[test] | |
| 786 | + | fn parse_csv_enforces_row_cap() { | |
| 787 | + | // MAX_IMPORT_ROWS is 100_000; a CSV with more data rows must cap at the | |
| 788 | + | // limit rather than ingesting an unbounded upload. | |
| 789 | + | let mut csv = String::from("email\n"); | |
| 790 | + | for i in 0..100_050 { | |
| 791 | + | csv.push_str(&format!("user{i}@test.com\n")); | |
| 792 | + | } | |
| 793 | + | let mapping = ColumnMapping { email: Some(0), ..Default::default() }; | |
| 794 | + | let payload = parse_csv(csv.as_bytes(), &mapping).unwrap(); | |
| 795 | + | assert_eq!(payload.subscribers.len(), 100_000, "capped at MAX_IMPORT_ROWS"); | |
| 796 | + | } | |
| 723 | 797 | } |