fix: test email/SMS senders, all SMS delivery, join-key install UI (v1.30.1)
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m43s
Pull Request Tests / Run Tests (20.x) (push) Failing after 30s
Pull Request Tests / Run Tests (22.x) (push) Failing after 31s
Pull Request Tests / Test Summary (push) Failing after 4s

Test Email always failed with "Email.send is not a function":
models/email.js exports {Mail}, and the handler required the module and
called .send on it directly. Every other caller destructures it.

Test SMS failed with "Unexpected token '<'": it POSTed to
https://api.voip.ms/v1.0/sms/send with Basic auth, an endpoint that does
not exist. VoIP.ms's REST API is a GET against voip.ms/api/v1/rest.php
with api_username/api_password and method=sendSMS, so the fabricated URL
returned HTML and response.json() threw.

Worse, ALL SMS delivery was broken. models/sms.js called
PluginInstance.find({...}) but the ORM has no find -- the query method is
list({where}) -- so it threw on every send, before it could even fall
back to the direct VoIP.ms path. OTP-by-SMS and notifications were dead.

Both test endpoints now send through the same senders every real message
uses. A test that reimplements delivery proves nothing about whether real
delivery works, which is how two broken paths went unnoticed. Failures
report as 400 with the underlying reason rather than an opaque 500.

Adds a guard suite that fails the build on any call to a non-existent ORM
static, on requiring models/email without destructuring {Mail}, and on
any reference to the bogus api.voip.ms host.

Also: the Install Agent modal now leads with the join-key flow. v1.30.0
shipped join keys in the API and documented the modal as the place to get
one, but the modal still only did the pre-register flow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 11:51:37 -04:00
parent 03605267bc
commit f178f1a972
7 changed files with 306 additions and 37 deletions
+40 -31
View File
@@ -136,15 +136,25 @@ router.post('/test-email', async (req, res, next) => {
return res.status(400).json({ error: 'Recipient email address is required' });
}
// Use the email model to send the test message
const Email = require('../models/email');
// Send through the SAME sender every other feature uses (password reset,
// invites, OTP-by-email, notifications). A "test" that reimplements
// delivery proves nothing about whether real mail works.
//
// models/email.js exports `{Mail}`; requiring the module and calling
// `.send` on it directly -- as this did -- always threw
// "Email.send is not a function", so the button could never succeed.
const { Mail } = require('../models/email');
const testSubject = subject || 'SSO Manager Test Email';
const testBody = body || `<p>This is a test email from SSO Manager.</p><p>If you received this, your SMTP configuration is working correctly.</p><p>Sent at: ${new Date().toISOString()}</p>`;
await Email.send(to, testSubject, testBody);
await Mail.send(to, testSubject, testBody);
res.json({ success: true, message: `Test email sent to ${to}` });
} catch(err) {
next(err);
// A failed test is almost always a misconfiguration (wrong host, refused
// connection, bad credentials) -- the operator's to fix, and something the
// UI should be able to show them. Surfacing it as a 400 with the reason
// beats an opaque 500 carrying a raw stack-trace name.
return res.status(400).json({ error: err.message || 'Failed to send test email' });
}
});
@@ -156,38 +166,37 @@ router.post('/test-sms', async (req, res, next) => {
return res.status(400).json({ error: 'Recipient phone number is required' });
}
// Send through models/sms.js -- the same path every real SMS takes. It
// prefers a configured messaging plugin and falls back to VoIP.ms, and it
// normalizes the destination to E.164 digits.
//
// This used to POST to `https://api.voip.ms/v1.0/sms/send` with Basic auth.
// No such endpoint exists: VoIP.ms's REST API is a GET against
// `https://voip.ms/api/v1/rest.php` with `api_username`/`api_password` and
// `method=sendSMS`. The fabricated URL returned an HTML page, so
// `response.json()` threw `Unexpected token '<', "<!DOCTYPE "...` and the
// button reported that as the failure. It could never have sent anything.
const { SMS } = require('../models/sms');
const { PluginInstance } = require('../models/plugin_instance');
// A messaging plugin, when present, supplies its own credentials -- so
// requiring conf.voipms unconditionally would block a perfectly working
// setup from testing itself.
const messagingPlugins = await PluginInstance.list({ where: { category: 'messaging', enabled: true } }).catch(() => []);
const voipmsConf = conf.voipms || {};
if (!voipmsConf.username || !voipmsConf.password || !voipmsConf.did) {
return res.status(400).json({ error: 'VoIP.ms credentials not configured. Please configure username, DID, and password in the SMS tab.' });
if (!messagingPlugins.length && (!voipmsConf.username || !voipmsConf.password || !voipmsConf.did)) {
return res.status(400).json({ error: 'No messaging plugin is loaded and VoIP.ms credentials are not configured. Set username, DID and password in the SMS tab, or load a messaging plugin.' });
}
const testMessage = message || `SSO Manager Test SMS: This is a test message from ${conf.name}. If you received this, your VoIP.ms configuration is working correctly.`;
const testMessage = message || `SSO Manager Test SMS: This is a test message from ${conf.name}. If you received this, your SMS configuration is working correctly.`;
// VoIP.ms SMS API endpoint
const voipmsApiUrl = 'https://api.voip.ms/v1.0';
const authHeader = Buffer.from(`${voipmsConf.username}:${voipmsConf.password}`).toString('base64');
const response = await fetch(`${voipmsApiUrl}/sms/send`, {
method: 'POST',
headers: {
'Authorization': `Basic ${authHeader}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
did: voipmsConf.did,
to: to,
message: testMessage
})
});
const result = await response.json();
if (result.status === 'success') {
res.json({ success: true, message: `Test SMS sent to ${to}` });
} else {
res.status(400).json({ error: `VoIP.ms API error: ${result.message || 'Unknown error'}` });
}
await SMS.send(to, testMessage);
res.json({ success: true, message: `Test SMS sent to ${to}` });
} catch(err) {
next(err);
// The sender rejects with a useful reason (`VoIP.ms error: <status>`, or a
// plugin's own error). Surface it as a 400 the UI can display rather than
// an opaque 500 -- a misconfiguration is the operator's to fix, not a bug.
return res.status(400).json({ error: err.message || 'Failed to send test SMS' });
}
});