mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-09 03:15:40 +00:00
ci/cd(backend): offline test suite + Dockerize + audit-grade deploy (b + c)
Closes the "backend has zero CI + manual SSH deploy" audit finding for the indexer. Part B — CI-runnable tests (the integration-tests/*.live.test.js need a live chain and are excluded, not stubbed): extract injectable cores (indexer.js: DB+HTTP+decode, no @pezkuwi import; council.js: createApp factory for the council/KYC routes). New node:test suite (19/19, fully offline) — in-memory sqlite, dependency-injected chain stub, an in-memory fake-supabase, and REAL @pezkuwi keyring signatures proving 401 bad-sig / 400 msg-mismatch / 200 valid / 409 dup / 403 non-member / threshold auto-execute. Backend job now runs npm ci + npm test and is in ci-gate (failing tests block merge). Fixed a latent bug: block indexing now awaits each insert (was fire-and-forget forEach). Added runtime deps server.js imported but were missing from package.json (@supabase/supabase-js, pino, pino-http) — server.js could not have `npm ci`-run before. Part C — Dockerfile (non-root, HEALTHCHECK /health, sqlite state on a /data VOLUME kept out of the image) + backend deploy pipeline mirroring the web one: build+push to GHCR, cosign keyless sign + verify, ssh deploy with the DB volume preserved across deploys, /health poll, auto-rollback to previous SHA, same Telegram CEO approval gate, main/tags only, never fork PRs. New secrets documented in backend/DEPLOY.md (BACKEND_VPS_HOST/USER/ SSH_KEY). DB_PATH env (default ./transactions.db) lets prod point at the volume. Note for owner: if server.js (council/KYC bootstrap) is legacy/unused, its newly-added deps can be dropped instead.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Minimal in-memory Supabase stand-in.
|
||||
//
|
||||
// Implements exactly the query-builder surface the council/KYC handlers use:
|
||||
// .from(t).insert(v)
|
||||
// .from(t).select(cols).eq(c,v).single()
|
||||
// .from(t).select('*', { count:'exact', head:true }).eq(...).eq(...)
|
||||
// .from(t).update(v).eq(c,v)
|
||||
// .from(t).select(cols).eq(c,v) (list)
|
||||
// Unique-violation (23505) is simulated on council_members.address and
|
||||
// kyc_proposals.user_address so the 409 paths are covered. Everything lives in
|
||||
// plain arrays — no network, no real DB.
|
||||
|
||||
export function makeFakeSupabase (initial = {}) {
|
||||
const tables = {
|
||||
council_members: [],
|
||||
kyc_proposals: [],
|
||||
votes: [],
|
||||
...structuredCloneSafe(initial)
|
||||
}
|
||||
|
||||
const UNIQUE = {
|
||||
council_members: 'address',
|
||||
kyc_proposals: 'user_address'
|
||||
}
|
||||
|
||||
function from (name) {
|
||||
const rows = tables[name] || (tables[name] = [])
|
||||
const builder = {
|
||||
_filters: [],
|
||||
_count: null,
|
||||
_head: false,
|
||||
_update: null,
|
||||
|
||||
insert (vals) {
|
||||
const arr = Array.isArray(vals) ? vals : [vals]
|
||||
for (const v of arr) {
|
||||
const uniqueCol = UNIQUE[name]
|
||||
if (uniqueCol && rows.some(r => r[uniqueCol] === v[uniqueCol])) {
|
||||
return Promise.resolve({ error: { code: '23505' }, data: null })
|
||||
}
|
||||
rows.push({ id: rows.length + 1, executed: false, created_at: new Date().toISOString(), ...v })
|
||||
}
|
||||
return Promise.resolve({ error: null, data: null })
|
||||
},
|
||||
|
||||
select (cols, opts) {
|
||||
if (opts) {
|
||||
this._count = opts.count || null
|
||||
this._head = !!opts.head
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
eq (col, val) {
|
||||
this._filters.push([col, val])
|
||||
return this
|
||||
},
|
||||
|
||||
neq () { return this },
|
||||
|
||||
update (vals) {
|
||||
this._update = vals
|
||||
return this
|
||||
},
|
||||
|
||||
_apply () {
|
||||
return rows.filter(r => this._filters.every(([c, v]) => r[c] === v))
|
||||
},
|
||||
|
||||
single () {
|
||||
const found = this._apply()
|
||||
if (found.length === 0) {
|
||||
return Promise.resolve({ data: null, error: { code: 'PGRST116', message: 'no rows' } })
|
||||
}
|
||||
return Promise.resolve({ data: found[0], error: null })
|
||||
},
|
||||
|
||||
// Thenable terminal for chains awaited without .single()
|
||||
then (resolve, reject) {
|
||||
try {
|
||||
const found = this._apply()
|
||||
if (this._update) {
|
||||
found.forEach(r => Object.assign(r, this._update))
|
||||
return resolve({ error: null, data: found })
|
||||
}
|
||||
if (this._count) {
|
||||
return resolve({ count: found.length, error: null, data: this._head ? null : found })
|
||||
}
|
||||
return resolve({ data: found, error: null })
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
return { from, _tables: tables }
|
||||
}
|
||||
|
||||
function structuredCloneSafe (obj) {
|
||||
const out = {}
|
||||
for (const [k, v] of Object.entries(obj)) out[k] = v.map(r => ({ ...r }))
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user