Gitea-Client herausgelöst, Konfiguration für Organisation und System-Konto, Test-Nachbauten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea71b3f1c3
commit
531ee2a02b
6 files changed
+824
-47
No files matched your search
@@ -0,0 +1,237 @@
|
||||
import http from 'node:http'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* Nachgebautes Appwrite fuer Tests: nur der REST-Ausschnitt, den `appwriteAdmin.js` und
|
||||
* `appwriteStaff.js` nutzen (Dokumente einer Sammlung, `/account` per JWT). Laeuft auf
|
||||
* 127.0.0.1 mit einem vom Betriebssystem vergebenen Port; `endpoint` endet auf `/v1`,
|
||||
* genau wie `config.appwrite.endpoint`.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {Record<string, object[]>} [options.seed]
|
||||
* Anfangsbestand je Sammlung (Sammlungsname -> Dokumente). Jedes Dokument darf `$id`
|
||||
* mitbringen (sonst wird eins erzeugt); `$createdAt`/`$updatedAt` werden sonst mit dem
|
||||
* Startzeitpunkt gefuellt.
|
||||
* @returns {Promise<{
|
||||
* endpoint: string,
|
||||
* all: (collection: string) => object[],
|
||||
* get: (collection: string, id: string) => (object|undefined),
|
||||
* calls: Array<{method: string, path: string, query: Record<string, string>, headers: Record<string, string>, body: any}>,
|
||||
* close: () => Promise<void>,
|
||||
* }>} `all`/`get` lesen den LIVE-Stand (nach create/patch/delete ueber HTTP). Die
|
||||
* Datenbank-ID im Pfad wird nicht geprueft - jede wird akzeptiert.
|
||||
*/
|
||||
export async function startFakeAppwrite(options = {}) {
|
||||
const calls = []
|
||||
/** @type {Map<string, Map<string, object>>} Sammlungsname -> (Dokument-ID -> Dokument). */
|
||||
const store = new Map()
|
||||
|
||||
function collectionOf(name) {
|
||||
let map = store.get(name)
|
||||
if (!map) {
|
||||
map = new Map()
|
||||
store.set(name, map)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function all(col) {
|
||||
return [...collectionOf(col).values()]
|
||||
}
|
||||
|
||||
function get(col, id) {
|
||||
return collectionOf(col).get(id)
|
||||
}
|
||||
|
||||
function send(res, status, body) {
|
||||
if (body === undefined) {
|
||||
res.writeHead(status)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const text = JSON.stringify(body)
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(text),
|
||||
})
|
||||
res.end(text)
|
||||
}
|
||||
|
||||
function parseQueries(searchParams) {
|
||||
return searchParams
|
||||
.getAll('queries[]')
|
||||
.map((s) => {
|
||||
try {
|
||||
return JSON.parse(s)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Routen --- */
|
||||
|
||||
function handleAccount(match, searchParams, body, headers, res) {
|
||||
if (headers['x-appwrite-jwt'] === 'jwt-admin') {
|
||||
return send(res, 200, { $id: 'staff1', labels: ['admin'] })
|
||||
}
|
||||
send(res, 401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
function handleListDocuments(match, searchParams, body, headers, res) {
|
||||
const docs = [...collectionOf(match[2]).values()]
|
||||
const queries = parseQueries(searchParams)
|
||||
|
||||
let result = docs.filter((doc) =>
|
||||
queries.every((q) => {
|
||||
if (q.method === 'equal') return q.values.includes(doc[q.attribute])
|
||||
if (q.method === 'greaterThan') return doc[q.attribute] > q.values[0]
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
const orderAsc = queries.find((q) => q.method === 'orderAsc')
|
||||
const orderDesc = queries.find((q) => q.method === 'orderDesc')
|
||||
const sortAttr = orderAsc?.attribute || orderDesc?.attribute
|
||||
if (sortAttr) {
|
||||
const dir = orderAsc ? 1 : -1
|
||||
result = [...result].sort((a, b) => {
|
||||
if (a[sortAttr] === b[sortAttr]) return 0
|
||||
return a[sortAttr] > b[sortAttr] ? dir : -dir
|
||||
})
|
||||
}
|
||||
|
||||
const total = result.length
|
||||
const limit = queries.find((q) => q.method === 'limit')
|
||||
if (limit) result = result.slice(0, Number(limit.values[0]))
|
||||
|
||||
send(res, 200, { total, documents: result })
|
||||
}
|
||||
|
||||
function handleCreateDocument(match, searchParams, body, headers, res) {
|
||||
const map = collectionOf(match[2])
|
||||
const documentId = body?.documentId || randomUUID()
|
||||
if (map.has(documentId)) {
|
||||
return send(res, 409, { message: 'Document with the requested ID already exists.' })
|
||||
}
|
||||
const now = new Date().toISOString()
|
||||
const doc = { $id: documentId, $createdAt: now, $updatedAt: now, ...(body?.data || {}) }
|
||||
map.set(documentId, doc)
|
||||
send(res, 201, doc)
|
||||
}
|
||||
|
||||
function handleGetDocument(match, searchParams, body, headers, res) {
|
||||
const doc = collectionOf(match[2]).get(match[3])
|
||||
if (!doc) return send(res, 404, { message: 'Document not found' })
|
||||
send(res, 200, doc)
|
||||
}
|
||||
|
||||
function handlePatchDocument(match, searchParams, body, headers, res) {
|
||||
const doc = collectionOf(match[2]).get(match[3])
|
||||
if (!doc) return send(res, 404, { message: 'Document not found' })
|
||||
Object.assign(doc, body?.data || {})
|
||||
doc.$updatedAt = new Date().toISOString()
|
||||
send(res, 200, doc)
|
||||
}
|
||||
|
||||
function handleDeleteDocument(match, searchParams, body, headers, res) {
|
||||
const map = collectionOf(match[2])
|
||||
if (!map.has(match[3])) return send(res, 404, { message: 'Document not found' })
|
||||
map.delete(match[3])
|
||||
send(res, 204)
|
||||
}
|
||||
|
||||
const ROUTES = [
|
||||
{ method: 'GET', re: /^\/v1\/account$/, handler: handleAccount },
|
||||
{
|
||||
method: 'GET',
|
||||
re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents$/,
|
||||
handler: handleListDocuments,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents$/,
|
||||
handler: handleCreateDocument,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/,
|
||||
handler: handleGetDocument,
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/,
|
||||
handler: handlePatchDocument,
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
re: /^\/v1\/databases\/([^/]+)\/collections\/([^/]+)\/documents\/([^/]+)$/,
|
||||
handler: handleDeleteDocument,
|
||||
},
|
||||
]
|
||||
|
||||
/* --------------------------------------------------------- Server --- */
|
||||
|
||||
const sockets = new Set()
|
||||
const server = http.createServer((req, res) => {
|
||||
const chunks = []
|
||||
req.on('data', (c) => chunks.push(c))
|
||||
req.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf8')
|
||||
const url = new URL(req.url, 'http://fake-appwrite.local')
|
||||
let parsedBody = null
|
||||
if (raw) {
|
||||
try {
|
||||
parsedBody = JSON.parse(raw)
|
||||
} catch {
|
||||
parsedBody = null
|
||||
}
|
||||
}
|
||||
calls.push({
|
||||
method: req.method,
|
||||
path: url.pathname,
|
||||
query: Object.fromEntries(url.searchParams),
|
||||
headers: { ...req.headers },
|
||||
body: parsedBody,
|
||||
})
|
||||
|
||||
const route = ROUTES.find((r) => r.method === req.method && r.re.test(url.pathname))
|
||||
if (!route) return send(res, 404, { message: 'Not Found' })
|
||||
const match = route.re.exec(url.pathname)
|
||||
try {
|
||||
route.handler(match, url.searchParams, parsedBody, req.headers, res)
|
||||
} catch (err) {
|
||||
send(res, 500, { message: err?.message || 'fakeAppwrite: interner Fehler' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
sockets.add(socket)
|
||||
socket.on('close', () => sockets.delete(socket))
|
||||
})
|
||||
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
const { port } = server.address()
|
||||
const endpoint = `http://127.0.0.1:${port}/v1`
|
||||
|
||||
/* --------------------------------------------------------- Seeding --- */
|
||||
|
||||
const seededAt = new Date().toISOString()
|
||||
for (const [col, docs] of Object.entries(options.seed || {})) {
|
||||
const map = collectionOf(col)
|
||||
for (const doc of docs || []) {
|
||||
const { $id, $createdAt, $updatedAt, ...fields } = doc
|
||||
const id = $id || randomUUID()
|
||||
map.set(id, { $id: id, $createdAt: $createdAt || seededAt, $updatedAt: $updatedAt || seededAt, ...fields })
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
for (const socket of sockets) socket.destroy()
|
||||
return new Promise((resolve) => server.close(() => resolve()))
|
||||
}
|
||||
|
||||
return { endpoint, all, get, calls, close }
|
||||
}
|
||||
Reference in new issue
Block a user