116 lines
10 KiB
JavaScript
116 lines
10 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import {spawn} from 'node:child_process'
|
|
|
|
// Never part of test:all: full mode sends three messages; resume mode sends one intentional duplicate.
|
|
const label=process.env.MMAPP_LIVE_TELEGRAM_TEST_LABEL??'197'
|
|
assert(/^\d{3}$/.test(label))
|
|
const resume=process.env.MMAPP_LIVE_TELEGRAM_RESUME==='manual-retry-only'
|
|
const database=process.env.MMAPP_OWN_SQL_DB
|
|
assert(/^mm_accept_166_\d+$/.test(database??''),'An explicitly owned test database is required')
|
|
const url=new URL(process.env.DATABASE_URL)
|
|
assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
|
|
assert.equal(process.env.MMAPP_LIVE_TELEGRAM_SEND_ACK,'owned-test-chat')
|
|
assert.equal(process.env.MMAPP_LIVE_TELEGRAM_ACK_DUPLICATE,'yes')
|
|
const bot=JSON.parse(fs.readFileSync(process.env.MMAPP_LIVE_TELEGRAM_PRIVATE_FILE))
|
|
assert.equal(bot.chatId,process.env.MMAPP_LIVE_TELEGRAM_CHAT_ACK)
|
|
assert(/^\d+:[A-Za-z0-9_-]+$/.test(bot.token))
|
|
const portal=JSON.parse(fs.readFileSync(process.env.MMAPP_LIVE_PORTAL_PRIVATE_FILE))
|
|
assert.equal(process.env.AUTH_REQUIRED,'true');assert.equal(process.env.AUTH_JWT_SECRET,portal.jwtSecret)
|
|
|
|
const {pool,closePool}=await import('/app/backend/dist/db/index.js')
|
|
const {initDatabase}=await import('/app/backend/dist/db/bootstrap.js')
|
|
const {updateTelegramSettings}=await import('/app/backend/dist/services/alerts-service.js')
|
|
const {enqueueTelegramOutbox,dispatchPendingOutbox,getAlertsOutboxOverview}=await import('/app/backend/dist/services/alert-engine/outbox.js')
|
|
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database)
|
|
const permissions=(await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]
|
|
assert(Object.values(permissions).every(v=>v===false))
|
|
const root='/app/backend/acceptance',prefix='own-live'+label+'-'+Date.now(),ownedUsers=[],cases=[]
|
|
const report={date:'2026-10-13',block:'12.5.'+label,database,resumeOnly:resume,scope:'Production compiled outbox, owned PostgreSQL, actual Portal JWT and real Telegram provider',cases,providerSendCalls:0,requestTrace:[],providerAcceptanceIds:[],ownPortalUsersRemoved:false,passed:false}
|
|
let app,adminToken,originalFetch=globalThis.fetch
|
|
const portalUrl='http://portal:8080'
|
|
async function portalCall(path,method='GET',body,token){
|
|
const r=await originalFetch(portalUrl+path,{method,headers:{...(body!==undefined?{'content-type':'application/json'}:{}),...(token?{Authorization:'Bearer '+token}:{})},body:body===undefined?undefined:JSON.stringify(body)})
|
|
const text=await r.text();return {status:r.status,data:text?JSON.parse(text):null}
|
|
}
|
|
const payload=(number)=>({text:`TEST${label} №${number} — очередь Telegram. ${number===1?'Проверка dedupe и двух отправителей.':'Проверка unknown и намеренного ручного повтора.'}`,chatId:bot.chatId,
|
|
history:{id:prefix+'-history'+number,ruleId:null,groupId:null,ruleName:'Own TEST'+label+' №'+number,severity:'info',message:'Контролируемая приёмка очереди Telegram',firedAt:new Date().toISOString()}})
|
|
async function sender(){
|
|
const code=`import assert from 'node:assert/strict';import {pool,closePool} from '/app/backend/dist/db/index.js';import {dispatchPendingOutbox} from '/app/backend/dist/services/alert-engine/outbox.js';assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,process.env.MMAPP_OWN_SQL_DB);let calls=0,ids=[];const native=globalThis.fetch;globalThis.fetch=async(...args)=>{const r=await native(...args);if(new URL(String(args[0])).pathname.endsWith('/sendMessage')){calls++;const d=await r.clone().json();if(d.ok)ids.push(d.result.message_id)}return r};try{const result=await dispatchPendingOutbox(1);console.log(JSON.stringify({result,calls,ids}))}finally{await closePool()}`
|
|
return new Promise((resolve,reject)=>{
|
|
const child=spawn(process.execPath,['--input-type=module','-e',code],{env:process.env,stdio:['ignore','pipe','pipe']})
|
|
let output='',error='';child.stdout.on('data',b=>output+=b);child.stderr.on('data',b=>error+=b)
|
|
child.on('error',reject);child.on('exit',status=>{if(status!==0)return reject(Error('Owned sender failed: '+error));try{resolve(JSON.parse(output.trim()))}catch(e){reject(e)}})
|
|
})
|
|
}
|
|
let suppressReceipt=false
|
|
try{
|
|
await initDatabase();assert.equal(Number((await pool.query('SELECT count(*) AS n FROM alert_outbox')).rows[0].n),0)
|
|
await updateTelegramSettings({token:bot.token,chatId:bot.chatId,messageThreadId:null})
|
|
const login=await portalCall('/api/v1/auth/login','POST',{email:portal.email,password:portal.password});assert.equal(login.status,200);adminToken=login.data.access_token
|
|
const readerEmail=prefix+'@mmapp.test'
|
|
const created=await portalCall('/api/v1/admin/users','POST',{email:readerEmail,password:portal.password,name:'Own TEST'+label+' reader',apps:['mm'],permissions:['mm:alerts:read']},adminToken)
|
|
assert.equal(created.status,201);ownedUsers.push(created.data.id)
|
|
fs.writeFileSync(root+'/owned-portal-users-private.json',JSON.stringify(ownedUsers),{mode:0o600})
|
|
const readerLogin=await portalCall('/api/v1/auth/login','POST',{email:readerEmail,password:portal.password});assert.equal(readerLogin.status,200)
|
|
const {buildApp}=await import('/app/backend/dist/index.js');app=await buildApp({logger:false,startScheduler:false})
|
|
const id1=prefix+'-1',id2=prefix+'-2'
|
|
if(!resume){
|
|
assert.equal(await enqueueTelegramOutbox({id:id1,dedupeKey:id1,payload:payload(1)}),true)
|
|
assert.equal(await enqueueTelegramOutbox({id:id1+'-duplicate',dedupeKey:id1,payload:payload(1)}),false)
|
|
const senders=await Promise.all([sender(),sender()])
|
|
assert.equal(senders.reduce((n,s)=>n+s.result.sent,0),1);assert.equal(senders.reduce((n,s)=>n+s.calls,0),1)
|
|
report.providerSendCalls+=1;report.providerAcceptanceIds.push(...senders.flatMap(s=>s.ids))
|
|
let overview=await getAlertsOutboxOverview();assert.equal(overview.counts.sent,1)
|
|
assert.equal(Number((await pool.query('SELECT count(*) AS n FROM alert_history WHERE id=$1 AND sent_ok=true',[prefix+'-history1'])).rows[0].n),1)
|
|
cases.push({name:'dedupe and independent senders',senders:2,queuedRows:1,providerSends:1,sentHistoryRows:1})
|
|
}
|
|
let lastSendAt=Date.now()
|
|
globalThis.fetch=async(...args)=>{
|
|
const sending=new URL(String(args[0])).pathname.endsWith('/sendMessage')
|
|
if(sending){await new Promise(resolve=>setTimeout(resolve,Math.max(0,1500-(Date.now()-lastSendAt))));lastSendAt=Date.now();assert(report.providerSendCalls<(resume?1:3),'Live send budget exhausted');report.providerSendCalls++}
|
|
const trace=sending?{requestNumber:report.providerSendCalls,startedAt:new Date().toISOString()}:null
|
|
if(trace)report.requestTrace.push(trace)
|
|
let response
|
|
try{response=await originalFetch(...args)}catch(e){if(trace)trace.fetchFailed=true;throw e}
|
|
if(sending){
|
|
trace.httpStatus=response.status
|
|
const accepted=await response.clone().json().catch(()=>({ok:false}))
|
|
trace.providerAccepted=accepted.ok===true
|
|
if(accepted.ok!==true){report.providerRejectionCodes??=[];report.providerRejectionCodes.push(accepted.error_code??null);return response}
|
|
report.providerAcceptanceIds.push(accepted.result.message_id)
|
|
if(suppressReceipt){trace.receiptSuppressed=true;suppressReceipt=false;throw Error('Own TEST197 receipt suppressed after confirmed provider acceptance')}
|
|
}
|
|
return response
|
|
}
|
|
assert.equal(await enqueueTelegramOutbox({id:id2,dedupeKey:id2,payload:payload(2)}),true)
|
|
let outcome;let overview;
|
|
if(!resume){
|
|
suppressReceipt=true;outcome=await dispatchPendingOutbox(1);assert.equal(outcome.sent,0);assert.equal(outcome.held,1)
|
|
overview=await getAlertsOutboxOverview();assert.equal(overview.counts.unknown,1);assert.equal(overview.items[0].id,id2)
|
|
const before=report.providerSendCalls;await dispatchPendingOutbox(1);assert.equal(report.providerSendCalls,before)
|
|
cases.push({name:'controlled lost receipt',providerAccepted:true,status:'unknown',automaticResend:false,physicalDisconnect:false})
|
|
}else{await pool.query("UPDATE alert_outbox SET status='unknown',last_error='Own resume fixture: controlled unknown, no additional provider send' WHERE id=$1",[id2]);cases.push({name:'resume fixture',status:'unknown',providerSend:false,priorLiveEvidence:'telegram197-partial.json'})}
|
|
const retry='/api/alerts/outbox/'+id2+'/retry',ack={expectedVersion:0,acknowledgedDuplicateRisk:true}
|
|
const reader=await app.inject({method:'POST',url:retry,headers:{authorization:'Bearer '+readerLogin.data.access_token},payload:ack});assert.equal(reader.statusCode,403)
|
|
const missing=await app.inject({method:'POST',url:retry,headers:{authorization:'Bearer '+adminToken},payload:{expectedVersion:0}});assert.equal(missing.statusCode,400)
|
|
const approved=await app.inject({method:'POST',url:retry,headers:{authorization:'Bearer '+adminToken},payload:ack});assert.equal(approved.statusCode,200);assert.equal(approved.json().resolutionVersion,1)
|
|
const stale=await app.inject({method:'POST',url:retry,headers:{authorization:'Bearer '+adminToken},payload:ack});assert.equal(stale.statusCode,409)
|
|
outcome=await dispatchPendingOutbox(1);report.retryDiagnostic={outcome,rows:(await pool.query("SELECT status,retry_count,resolution_version,next_attempt_at,last_error FROM alert_outbox WHERE id=$1",[id2])).rows};assert.equal(outcome.sent,1)
|
|
overview=await getAlertsOutboxOverview();assert.equal(overview.counts.sent,resume?1:2);assert.equal(overview.counts.unknown,0)
|
|
assert.equal(Number((await pool.query('SELECT count(*) AS n FROM alert_history WHERE sent_ok=true')).rows[0].n),resume?1:2)
|
|
cases.push({name:'explicit retry with real Portal JWT',reader:403,missingAck:400,admin:200,staleVersion:409,resolutionVersion:1,intentionalDuplicate:true,sentHistoryRows:resume?1:2})
|
|
assert.equal(report.providerSendCalls,resume?1:3);assert.equal(report.providerAcceptanceIds.length,resume?1:3)
|
|
report.passed=true
|
|
}catch(e){
|
|
fs.writeFileSync(root+'/private-failure.log',String(e.stack??e),{mode:0o600});report.errorName=e.name
|
|
}finally{
|
|
globalThis.fetch=originalFetch
|
|
if(app)await app.close()
|
|
for(const id of ownedUsers){const deleted=await portalCall('/api/v1/admin/users/'+id,'DELETE',undefined,adminToken);assert.equal(deleted.status,204)}
|
|
report.ownPortalUsersRemoved=true
|
|
await closePool()
|
|
fs.writeFileSync(root+'/result.json',JSON.stringify(report,null,2));console.log(JSON.stringify({passed:report.passed,providerSendCalls:report.providerSendCalls,errorName:report.errorName,ownPortalUsersRemoved:report.ownPortalUsersRemoved}))
|
|
}
|
|
if(!report.passed)process.exitCode=1
|