added on local at 2026-07-01 13:48:20
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # AffSoft messaging v1 -- smoke test. | |
| 4 | # Drives the same MODS::AffSoft::Messaging API the CGIs use, end-to-end. | |
| 5 | # Run via: perl _messaging_smoke.pl | |
| 6 | # Returns 0 if all 13 spec steps pass, non-zero otherwise. | |
| 7 | #====================================================================== | |
| 8 | use strict; | |
| 9 | use warnings; | |
| 10 | use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com'; | |
| 11 | ||
| 12 | use MODS::DBConnect; | |
| 13 | use MODS::AffSoft::Config; | |
| 14 | use MODS::AffSoft::Messaging; | |
| 15 | ||
| 16 | my $config = MODS::AffSoft::Config->new; | |
| 17 | my $db = MODS::DBConnect->new; | |
| 18 | my $msg = MODS::AffSoft::Messaging->new; | |
| 19 | my $DB = $config->settings('database_name'); | |
| 20 | my $dbh = $db->db_connect(); | |
| 21 | ||
| 22 | my @log; my $fail = 0; | |
| 23 | sub pass { my ($n, $t) = @_; push @log, " step $n PASS - $t"; } | |
| 24 | sub fail { my ($n, $t) = @_; push @log, " step $n FAIL - $t"; $fail++; } | |
| 25 | ||
| 26 | # Cleanup helper (also runs at start to handle re-runs). | |
| 27 | sub purge { | |
| 28 | for my $em ('msg-merchant@ai.acme.com','msg-aff-a@ai.acme.com','msg-aff-b@ai.acme.com','msg-rando@ai.acme.com') { | |
| 29 | my $u = $db->db_readwrite($dbh, | |
| 30 | qq~SELECT id FROM \`${DB}\`.users WHERE email='$em' LIMIT 1~, $0, __LINE__); | |
| 31 | next unless $u && $u->{id}; | |
| 32 | my $uid = $u->{id}; | |
| 33 | # programs owned | |
| 34 | my @progs = $db->db_readwrite_multiple($dbh, | |
| 35 | qq~SELECT id FROM \`${DB}\`.affiliate_programs WHERE merchant_user_id='$uid'~, $0, __LINE__); | |
| 36 | for my $p (@progs) { | |
| 37 | my $pid = $p->{id}; | |
| 38 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.affiliate_memberships WHERE program_id='$pid'~, $0, __LINE__); | |
| 39 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.announcements WHERE merchant_program_id='$pid'~, $0, __LINE__); | |
| 40 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.affiliate_programs WHERE id='$pid'~, $0, __LINE__); | |
| 41 | } | |
| 42 | # memberships as affiliate | |
| 43 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.affiliate_memberships WHERE affiliate_user_id='$uid'~, $0, __LINE__); | |
| 44 | # dm threads where they're the only/one of participants | |
| 45 | my @parts = $db->db_readwrite_multiple($dbh, | |
| 46 | qq~SELECT thread_id FROM \`${DB}\`.dm_thread_participants WHERE user_id='$uid'~, $0, __LINE__); | |
| 47 | for my $row (@parts) { | |
| 48 | my $tid = $row->{thread_id}; | |
| 49 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.dm_attachments WHERE message_id IN (SELECT id FROM \`${DB}\`.dm_messages WHERE thread_id='$tid')~, $0, __LINE__); | |
| 50 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.dm_messages WHERE thread_id='$tid'~, $0, __LINE__); | |
| 51 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.dm_thread_participants WHERE thread_id='$tid'~, $0, __LINE__); | |
| 52 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.dm_threads WHERE id='$tid'~, $0, __LINE__); | |
| 53 | } | |
| 54 | # announcements posted by them | |
| 55 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.announcement_reads WHERE user_id='$uid' OR announcement_id IN (SELECT id FROM \`${DB}\`.announcements WHERE posted_by_user_id='$uid')~, $0, __LINE__); | |
| 56 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.announcements WHERE posted_by_user_id='$uid'~, $0, __LINE__); | |
| 57 | # email queue rows targeted at them | |
| 58 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.email_outbound_queue WHERE recipient_user_id='$uid'~, $0, __LINE__); | |
| 59 | # user row | |
| 60 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.user_settings WHERE user_id='$uid'~, $0, __LINE__); | |
| 61 | $db->db_readwrite($dbh, qq~DELETE FROM \`${DB}\`.users WHERE id='$uid'~, $0, __LINE__); | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | print "[smoke] Pre-purge\n"; | |
| 66 | purge(); | |
| 67 | ||
| 68 | # -------------------------------------------------------------- | |
| 69 | # STEP 1 -- seed three test users (+ one non-eligible "rando") | |
| 70 | # -------------------------------------------------------------- | |
| 71 | my %u; | |
| 72 | for my $spec ( | |
| 73 | ['msg-merchant@ai.acme.com', 'Test Merchant'], | |
| 74 | ['msg-aff-a@ai.acme.com', 'Affiliate A'], | |
| 75 | ['msg-aff-b@ai.acme.com', 'Affiliate B'], | |
| 76 | ['msg-rando@ai.acme.com', 'Random Rando'], | |
| 77 | ) { | |
| 78 | my ($em, $nm) = @$spec; | |
| 79 | $db->db_readwrite($dbh, qq~ | |
| 80 | INSERT INTO \`${DB}\`.users (email, display_name, account_status, plan_tier, created_at, password_hash) | |
| 81 | VALUES ('$em', '$nm', 'active', 'free', NOW(), 'x') | |
| 82 | ~, $0, __LINE__); | |
| 83 | my $r = $db->db_readwrite($dbh, qq~SELECT LAST_INSERT_ID() AS id~, $0, __LINE__); | |
| 84 | $u{$em} = $r->{id}; | |
| 85 | } | |
| 86 | if ($u{'msg-merchant@ai.acme.com'} && $u{'msg-aff-a@ai.acme.com'} && $u{'msg-aff-b@ai.acme.com'} && $u{'msg-rando@ai.acme.com'}) { | |
| 87 | pass(1, "seeded 4 users: merchant=$u{'msg-merchant@ai.acme.com'}, A=$u{'msg-aff-a@ai.acme.com'}, B=$u{'msg-aff-b@ai.acme.com'}, rando=$u{'msg-rando@ai.acme.com'}"); | |
| 88 | } else { | |
| 89 | fail(1, "user seed missing ids"); | |
| 90 | } | |
| 91 | ||
| 92 | # -------------------------------------------------------------- | |
| 93 | # STEP 2 -- merchant creates a program; A + B enroll (approved) | |
| 94 | # -------------------------------------------------------------- | |
| 95 | my $merch = $u{'msg-merchant@ai.acme.com'}; | |
| 96 | my $aff_a = $u{'msg-aff-a@ai.acme.com'}; | |
| 97 | my $aff_b = $u{'msg-aff-b@ai.acme.com'}; | |
| 98 | my $rando = $u{'msg-rando@ai.acme.com'}; | |
| 99 | ||
| 100 | $db->db_readwrite($dbh, qq~ | |
| 101 | INSERT INTO \`${DB}\`.affiliate_programs (merchant_user_id, slug, name, status, created_at) | |
| 102 | VALUES ('$merch', 'smoke-prog', 'Smoke Program', 'active', NOW()) | |
| 103 | ~, $0, __LINE__); | |
| 104 | my $pr = $db->db_readwrite($dbh, qq~SELECT LAST_INSERT_ID() AS id~, $0, __LINE__); | |
| 105 | my $pid = $pr->{id}; | |
| 106 | ||
| 107 | for my $a ($aff_a, $aff_b) { | |
| 108 | $db->db_readwrite($dbh, qq~ | |
| 109 | INSERT INTO \`${DB}\`.affiliate_memberships | |
| 110 | (program_id, affiliate_user_id, status, approved_at, created_at) | |
| 111 | VALUES ('$pid', '$a', 'approved', NOW(), NOW()) | |
| 112 | ~, $0, __LINE__); | |
| 113 | } | |
| 114 | ||
| 115 | if ($pid) { pass(2, "program $pid created; A+B enrolled (approved)"); } else { fail(2, "program not created"); } | |
| 116 | ||
| 117 | # -------------------------------------------------------------- | |
| 118 | # STEP 3 -- 1:1 DM: merchant -> A | |
| 119 | # -------------------------------------------------------------- | |
| 120 | my ($tid_ma, $err_ma) = $msg->create_thread($db, $dbh, $DB, { | |
| 121 | creator_id => $merch, kind => 'direct', | |
| 122 | participant_ids => [$aff_a], | |
| 123 | }); | |
| 124 | if ($tid_ma) { | |
| 125 | my ($mid_ma, $merr) = $msg->send_message($db, $dbh, $DB, { | |
| 126 | thread_id => $tid_ma, sender_id => $merch, body => "Hi from merchant", | |
| 127 | }); | |
| 128 | my $tr = $msg->load_thread($db, $dbh, $DB, $tid_ma); | |
| 129 | my $pcount = scalar grep { !$_->{removed_at} } @{ $tr->{participants} }; | |
| 130 | if ($mid_ma && $pcount == 2) { | |
| 131 | pass(3, "1:1 DM created tid=$tid_ma, msg=$mid_ma, participants=$pcount"); | |
| 132 | } else { | |
| 133 | fail(3, "send/participants wrong (mid=$mid_ma, pcount=$pcount)"); | |
| 134 | } | |
| 135 | } else { | |
| 136 | fail(3, "create_thread failed: $err_ma"); | |
| 137 | } | |
| 138 | ||
| 139 | # -------------------------------------------------------------- | |
| 140 | # STEP 4 -- read receipt: affiliate A "views" the thread | |
| 141 | # -------------------------------------------------------------- | |
| 142 | $msg->mark_thread_read($db, $dbh, $DB, $tid_ma, $aff_a); | |
| 143 | my $part_a = $db->db_readwrite($dbh, | |
| 144 | qq~SELECT last_read_at FROM \`${DB}\`.dm_thread_participants | |
| 145 | WHERE thread_id='$tid_ma' AND user_id='$aff_a' LIMIT 1~, $0, __LINE__); | |
| 146 | if ($part_a && $part_a->{last_read_at}) { | |
| 147 | pass(4, "A.last_read_at bumped to $part_a->{last_read_at}"); | |
| 148 | } else { | |
| 149 | fail(4, "last_read_at not set"); | |
| 150 | } | |
| 151 | ||
| 152 | # -------------------------------------------------------------- | |
| 153 | # STEP 5 -- reply | |
| 154 | # -------------------------------------------------------------- | |
| 155 | my ($mid_reply, $err_reply) = $msg->send_message($db, $dbh, $DB, { | |
| 156 | thread_id => $tid_ma, sender_id => $aff_a, body => "Hi back!", | |
| 157 | }); | |
| 158 | if ($mid_reply) { pass(5, "A reply msg=$mid_reply"); } else { fail(5, "reply: $err_reply"); } | |
| 159 | ||
| 160 | # -------------------------------------------------------------- | |
| 161 | # STEP 6 -- group DM: merchant + A + B | |
| 162 | # -------------------------------------------------------------- | |
| 163 | my ($tid_g, $err_g) = $msg->create_thread($db, $dbh, $DB, { | |
| 164 | creator_id => $merch, kind => 'group', title => 'Smoke group', | |
| 165 | participant_ids => [$aff_a, $aff_b], | |
| 166 | }); | |
| 167 | if ($tid_g) { | |
| 168 | my ($mid_g, $merr_g) = $msg->send_message($db, $dbh, $DB, { | |
| 169 | thread_id => $tid_g, sender_id => $merch, body => "Hello group!", | |
| 170 | }); | |
| 171 | my $tr = $msg->load_thread($db, $dbh, $DB, $tid_g); | |
| 172 | my $pcount = scalar grep { !$_->{removed_at} } @{ $tr->{participants} }; | |
| 173 | # verify both A + B can see the message | |
| 174 | my $ok_a = $msg->is_participant($db, $dbh, $DB, $tid_g, $aff_a); | |
| 175 | my $ok_b = $msg->is_participant($db, $dbh, $DB, $tid_g, $aff_b); | |
| 176 | if ($mid_g && $pcount == 3 && $ok_a && $ok_b) { | |
| 177 | pass(6, "group tid=$tid_g, 3 participants, A+B both see it"); | |
| 178 | } else { | |
| 179 | fail(6, "group setup wrong (count=$pcount, mid=$mid_g, ok_a=$ok_a, ok_b=$ok_b)"); | |
| 180 | } | |
| 181 | } else { | |
| 182 | fail(6, "group create failed: $err_g"); | |
| 183 | } | |
| 184 | ||
| 185 | # -------------------------------------------------------------- | |
| 186 | # STEP 7 -- attachment: save + serve gate | |
| 187 | # -------------------------------------------------------------- | |
| 188 | # Write a tiny in-memory PNG to a temp file and feed it through save_attachment. | |
| 189 | # 1x1 transparent PNG: | |
| 190 | my $png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; | |
| 191 | require MIME::Base64; | |
| 192 | my $png = MIME::Base64::decode_base64($png_b64); | |
| 193 | my $tmp_path = "/tmp/_smoke_$$.png"; | |
| 194 | open(my $fh, '>', $tmp_path) or die "can't write tmp png: $!"; | |
| 195 | binmode $fh; print $fh $png; close $fh; | |
| 196 | open(my $rfh, '<', $tmp_path) or die; | |
| 197 | binmode $rfh; | |
| 198 | my ($aid, $a_err) = $msg->save_attachment($db, $dbh, $DB, { | |
| 199 | uid => $merch, file_name => 'smoke.png', mime => 'image/png', fh => $rfh, | |
| 200 | }); | |
| 201 | close $rfh; unlink $tmp_path; | |
| 202 | if ($aid) { | |
| 203 | # Send a message with that attachment in the merchant-A thread | |
| 204 | my ($mid_att, $merr) = $msg->send_message($db, $dbh, $DB, { | |
| 205 | thread_id => $tid_ma, sender_id => $merch, body => "With attachment", | |
| 206 | attachment_ids => [$aid], | |
| 207 | }); | |
| 208 | # Verify load_attachment_for_user works for both participants and 403s for rando | |
| 209 | my ($row_m, $path_m) = $msg->load_attachment_for_user($db, $dbh, $DB, $aid, $merch); | |
| 210 | my ($row_a, $path_a) = $msg->load_attachment_for_user($db, $dbh, $DB, $aid, $aff_a); | |
| 211 | my ($row_r, $path_r) = $msg->load_attachment_for_user($db, $dbh, $DB, $aid, $rando); | |
| 212 | if ($mid_att && $row_m && $row_a && !$row_r) { | |
| 213 | pass(7, "attachment aid=$aid msg=$mid_att; merchant+A allowed, rando denied"); | |
| 214 | } else { | |
| 215 | fail(7, "att gate wrong (mid=$mid_att row_m=" . ($row_m?'y':'n') . " row_a=" . ($row_a?'y':'n') . " row_r=" . ($row_r?'y':'n') . ")"); | |
| 216 | } | |
| 217 | } else { | |
| 218 | fail(7, "save_attachment failed: $a_err"); | |
| 219 | } | |
| 220 | ||
| 221 | # -------------------------------------------------------------- | |
| 222 | # STEP 8 -- cold-pitch denial: A -> rando | |
| 223 | # -------------------------------------------------------------- | |
| 224 | my ($tid_bad, $err_bad) = $msg->create_thread($db, $dbh, $DB, { | |
| 225 | creator_id => $aff_a, kind => 'direct', | |
| 226 | participant_ids => [$rando], | |
| 227 | }); | |
| 228 | if (!$tid_bad && $err_bad && $err_bad =~ /not allowed/) { | |
| 229 | pass(8, "cold-pitch denied: '$err_bad'"); | |
| 230 | } else { | |
| 231 | fail(8, "cold-pitch SHOULD have failed but didn't (tid=$tid_bad, err=$err_bad)"); | |
| 232 | } | |
| 233 | ||
| 234 | # -------------------------------------------------------------- | |
| 235 | # STEP 9 -- platform announcement: admin posts to 'affiliates' + send_email | |
| 236 | # -------------------------------------------------------------- | |
| 237 | my ($ann_id, $ann_err) = $msg->create_announcement($db, $dbh, $DB, { | |
| 238 | scope => 'platform', posted_by => $merch, # using merch as poster (smoke has no admin) | |
| 239 | title => 'Smoke platform announcement', | |
| 240 | body => 'Hello all affiliates.', | |
| 241 | audience_kind => 'affiliates', | |
| 242 | send_email => 1, publish_now => 1, | |
| 243 | }); | |
| 244 | if ($ann_id) { | |
| 245 | my $a = $msg->load_announcement($db, $dbh, $DB, $ann_id); | |
| 246 | my @rcp = $msg->resolve_announcement_recipients($db, $dbh, $DB, $a); | |
| 247 | my $n = $msg->queue_announcement_blast($db, $dbh, $DB, $a); | |
| 248 | # Verify queue rows for A + B exist | |
| 249 | my $q_a = $db->db_readwrite($dbh, | |
| 250 | qq~SELECT COUNT(*) AS n FROM \`${DB}\`.email_outbound_queue | |
| 251 | WHERE source_tag='announcement_blast' AND source_ref_id='$ann_id' | |
| 252 | AND recipient_user_id IN ('$aff_a','$aff_b')~, $0, __LINE__); | |
| 253 | if ($q_a && $q_a->{n} >= 2) { | |
| 254 | pass(9, "ann $ann_id posted; queued $n emails; A+B rows present ($q_a->{n})"); | |
| 255 | } else { | |
| 256 | fail(9, "queue rows missing (got " . ($q_a ? $q_a->{n} : '?') . ")"); | |
| 257 | } | |
| 258 | } else { | |
| 259 | fail(9, "ann create failed: $ann_err"); | |
| 260 | } | |
| 261 | ||
| 262 | # -------------------------------------------------------------- | |
| 263 | # STEP 10 -- merchant announcement to program affiliates | |
| 264 | # -------------------------------------------------------------- | |
| 265 | my ($mann_id, $mann_err) = $msg->create_announcement($db, $dbh, $DB, { | |
| 266 | scope => 'merchant', posted_by => $merch, | |
| 267 | merchant_program_id => $pid, | |
| 268 | title => 'Smoke merchant broadcast', | |
| 269 | body => 'Hi affiliates of my program.', | |
| 270 | audience_kind => 'program', | |
| 271 | send_email => 0, publish_now => 1, | |
| 272 | }); | |
| 273 | if ($mann_id) { | |
| 274 | # A and B should see; rando should not. | |
| 275 | my @for_a = $msg->list_announcements_for_user($db, $dbh, $DB, $aff_a, 'merchant'); | |
| 276 | my @for_r = $msg->list_announcements_for_user($db, $dbh, $DB, $rando, 'merchant'); | |
| 277 | my $a_sees = grep { $_->{id} eq $mann_id } @for_a; | |
| 278 | my $r_sees = grep { $_->{id} eq $mann_id } @for_r; | |
| 279 | if ($a_sees && !$r_sees) { | |
| 280 | pass(10, "merchant ann $mann_id visible to A, hidden from rando"); | |
| 281 | } else { | |
| 282 | fail(10, "visibility wrong (a=$a_sees, r=$r_sees)"); | |
| 283 | } | |
| 284 | } else { | |
| 285 | fail(10, "merchant ann create failed: $mann_err"); | |
| 286 | } | |
| 287 | ||
| 288 | # -------------------------------------------------------------- | |
| 289 | # STEP 11 -- mark-read on announcement | |
| 290 | # -------------------------------------------------------------- | |
| 291 | $msg->mark_announcement_read($db, $dbh, $DB, $mann_id, $aff_a) if $mann_id; | |
| 292 | my $rr = $db->db_readwrite($dbh, | |
| 293 | qq~SELECT read_at FROM \`${DB}\`.announcement_reads WHERE announcement_id='$mann_id' AND user_id='$aff_a' LIMIT 1~, | |
| 294 | $0, __LINE__); | |
| 295 | if ($rr && $rr->{read_at}) { | |
| 296 | pass(11, "announcement_reads row written for A: $rr->{read_at}"); | |
| 297 | } else { | |
| 298 | fail(11, "no read receipt row"); | |
| 299 | } | |
| 300 | ||
| 301 | # -------------------------------------------------------------- | |
| 302 | # STEP 12 -- sidebar badge -- DM unread count for B | |
| 303 | # -------------------------------------------------------------- | |
| 304 | # B hasn't read the group msg from step 6 yet, so unread should be >= 1 | |
| 305 | my $b_before = $msg->total_unread_dm_count($db, $dbh, $DB, $aff_b); | |
| 306 | $msg->mark_thread_read($db, $dbh, $DB, $tid_g, $aff_b); | |
| 307 | my $b_after = $msg->total_unread_dm_count($db, $dbh, $DB, $aff_b); | |
| 308 | if ($b_before >= 1 && $b_after == 0) { | |
| 309 | pass(12, "B unread before=$b_before, after mark_read=$b_after"); | |
| 310 | } else { | |
| 311 | fail(12, "unread count wrong (before=$b_before, after=$b_after)"); | |
| 312 | } | |
| 313 | ||
| 314 | # -------------------------------------------------------------- | |
| 315 | # STEP 13 -- cleanup (we re-run purge at end so the DB is clean) | |
| 316 | # -------------------------------------------------------------- | |
| 317 | purge(); | |
| 318 | # Verify users gone | |
| 319 | my $left = $db->db_readwrite($dbh, | |
| 320 | qq~SELECT COUNT(*) AS n FROM \`${DB}\`.users WHERE email LIKE 'msg-%\@ai.acme.com'~, | |
| 321 | $0, __LINE__); | |
| 322 | if ($left && $left->{n} == 0) { | |
| 323 | pass(13, "cleanup complete; no msg-* users remain"); | |
| 324 | } else { | |
| 325 | fail(13, "cleanup left " . ($left ? $left->{n} : '?') . " users"); | |
| 326 | } | |
| 327 | ||
| 328 | $db->db_disconnect($dbh); | |
| 329 | ||
| 330 | print "\n========== Messaging v1 smoke results ==========\n"; | |
| 331 | print "$_\n" for @log; | |
| 332 | print "================================================\n"; | |
| 333 | print $fail ? "FAILED ($fail steps)\n" : "ALL 13 STEPS PASSED\n"; | |
| 334 | exit($fail ? 1 : 0); |