Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/_messaging_smoke.pl
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/_messaging_smoke.pl

added on local at 2026-07-01 13:48:20

Added
+334
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 9f1881bc0086
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
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#======================================================================
8use strict;
9use warnings;
10use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
11
12use MODS::DBConnect;
13use MODS::AffSoft::Config;
14use MODS::AffSoft::Messaging;
15
16my $config = MODS::AffSoft::Config->new;
17my $db = MODS::DBConnect->new;
18my $msg = MODS::AffSoft::Messaging->new;
19my $DB = $config->settings('database_name');
20my $dbh = $db->db_connect();
21
22my @log; my $fail = 0;
23sub pass { my ($n, $t) = @_; push @log, " step $n PASS - $t"; }
24sub fail { my ($n, $t) = @_; push @log, " step $n FAIL - $t"; $fail++; }
25
26# Cleanup helper (also runs at start to handle re-runs).
27sub 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
65print "[smoke] Pre-purge\n";
66purge();
67
68# --------------------------------------------------------------
69# STEP 1 -- seed three test users (+ one non-eligible "rando")
70# --------------------------------------------------------------
71my %u;
72for 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}
86if ($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# --------------------------------------------------------------
95my $merch = $u{'msg-merchant@ai.acme.com'};
96my $aff_a = $u{'msg-aff-a@ai.acme.com'};
97my $aff_b = $u{'msg-aff-b@ai.acme.com'};
98my $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__);
104my $pr = $db->db_readwrite($dbh, qq~SELECT LAST_INSERT_ID() AS id~, $0, __LINE__);
105my $pid = $pr->{id};
106
107for 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
115if ($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# --------------------------------------------------------------
120my ($tid_ma, $err_ma) = $msg->create_thread($db, $dbh, $DB, {
121 creator_id => $merch, kind => 'direct',
122 participant_ids => [$aff_a],
123});
124if ($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);
143my $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__);
146if ($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# --------------------------------------------------------------
155my ($mid_reply, $err_reply) = $msg->send_message($db, $dbh, $DB, {
156 thread_id => $tid_ma, sender_id => $aff_a, body => "Hi back!",
157});
158if ($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# --------------------------------------------------------------
163my ($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});
167if ($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:
190my $png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
191require MIME::Base64;
192my $png = MIME::Base64::decode_base64($png_b64);
193my $tmp_path = "/tmp/_smoke_$$.png";
194open(my $fh, '>', $tmp_path) or die "can't write tmp png: $!";
195binmode $fh; print $fh $png; close $fh;
196open(my $rfh, '<', $tmp_path) or die;
197binmode $rfh;
198my ($aid, $a_err) = $msg->save_attachment($db, $dbh, $DB, {
199 uid => $merch, file_name => 'smoke.png', mime => 'image/png', fh => $rfh,
200});
201close $rfh; unlink $tmp_path;
202if ($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# --------------------------------------------------------------
224my ($tid_bad, $err_bad) = $msg->create_thread($db, $dbh, $DB, {
225 creator_id => $aff_a, kind => 'direct',
226 participant_ids => [$rando],
227});
228if (!$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# --------------------------------------------------------------
237my ($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});
244if ($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# --------------------------------------------------------------
265my ($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});
273if ($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;
292my $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__);
295if ($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
305my $b_before = $msg->total_unread_dm_count($db, $dbh, $DB, $aff_b);
306$msg->mark_thread_read($db, $dbh, $DB, $tid_g, $aff_b);
307my $b_after = $msg->total_unread_dm_count($db, $dbh, $DB, $aff_b);
308if ($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# --------------------------------------------------------------
317purge();
318# Verify users gone
319my $left = $db->db_readwrite($dbh,
320 qq~SELECT COUNT(*) AS n FROM \`${DB}\`.users WHERE email LIKE 'msg-%\@ai.acme.com'~,
321 $0, __LINE__);
322if ($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
330print "\n========== Messaging v1 smoke results ==========\n";
331print "$_\n" for @log;
332print "================================================\n";
333print $fail ? "FAILED ($fail steps)\n" : "ALL 13 STEPS PASSED\n";
334exit($fail ? 1 : 0);
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help