chore: sync running docker state
Commit the app code currently running in Docker, including scheduler, store status, journal reminder, maintenance mode, and Foodsharing API check updates. Remove generated runtime logs from version control and ignore local runtime captures.
This commit is contained in:
172
scripts/check-foodsharing-endpoints.js
Normal file
172
scripts/check-foodsharing-endpoints.js
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const API_DOC_URL = 'https://foodsharing.de/api/doc';
|
||||
|
||||
const EXPECTED_ENDPOINTS = [
|
||||
{
|
||||
path: '/api/login',
|
||||
method: 'post',
|
||||
requestRef: '#/components/schemas/LoginRequest',
|
||||
responseCodes: ['200', '401', '403', '409']
|
||||
},
|
||||
{
|
||||
path: '/api/users/current/details',
|
||||
method: 'get',
|
||||
responseRef: '#/components/schemas/ProfileDetails',
|
||||
responseCodes: ['200', '401']
|
||||
},
|
||||
{
|
||||
path: '/api/users/{userId}/stores',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '400', '401', '403', '404']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/pickups',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401', '403']
|
||||
},
|
||||
{
|
||||
path: '/api/regions/{regionId}/stores',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401', '403']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/details',
|
||||
method: 'get',
|
||||
responseRef: '#/components/schemas/Store',
|
||||
responseCodes: ['200', '401', '403', '404']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/members',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401', '403', '404']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/regular-pickups',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401', '403', '404']
|
||||
},
|
||||
{
|
||||
path: '/api/users/{userId}/pickups/registered',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401', '403']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/pickups/{pickupDate}/eligibility',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401']
|
||||
},
|
||||
{
|
||||
path: '/api/stores/{storeId}/pickups/{pickupDate}/users/current',
|
||||
method: 'post',
|
||||
responseCodes: ['200', '401', '403']
|
||||
},
|
||||
{
|
||||
path: '/api/conversations',
|
||||
method: 'get',
|
||||
responseCodes: ['200', '401']
|
||||
}
|
||||
];
|
||||
|
||||
function fetchText(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(url, (res) => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => resolve(data));
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function extractSpec(html) {
|
||||
const match = html.match(/<script id="swagger-data" type="application\/json">([\s\S]*?)<\/script>/);
|
||||
if (!match) {
|
||||
throw new Error('Swagger spec konnte in /api/doc nicht gefunden werden.');
|
||||
}
|
||||
const payload = JSON.parse(match[1]);
|
||||
if (!payload?.spec?.paths) {
|
||||
throw new Error('OpenAPI-Spec in /api/doc ist unvollständig.');
|
||||
}
|
||||
return payload.spec;
|
||||
}
|
||||
|
||||
function schemaRefFromResponse(operation) {
|
||||
return operation?.responses?.['200']?.content?.['application/json']?.schema?.$ref || null;
|
||||
}
|
||||
|
||||
function schemaRefFromRequest(operation) {
|
||||
return operation?.requestBody?.content?.['application/json']?.schema?.$ref || null;
|
||||
}
|
||||
|
||||
function validateOperation(spec, expectation) {
|
||||
const operation = spec.paths?.[expectation.path]?.[expectation.method];
|
||||
if (!operation) {
|
||||
return [`Fehlt: ${expectation.method.toUpperCase()} ${expectation.path}`];
|
||||
}
|
||||
|
||||
const issues = [];
|
||||
|
||||
if (expectation.requestRef) {
|
||||
const actualRef = schemaRefFromRequest(operation);
|
||||
if (actualRef !== expectation.requestRef) {
|
||||
issues.push(
|
||||
`Unerwartetes Request-Schema bei ${expectation.method.toUpperCase()} ${expectation.path}: ${actualRef || 'keins'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.responseRef) {
|
||||
const actualRef = schemaRefFromResponse(operation);
|
||||
if (actualRef !== expectation.responseRef) {
|
||||
issues.push(
|
||||
`Unerwartetes 200-Schema bei ${expectation.method.toUpperCase()} ${expectation.path}: ${actualRef || 'keins'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const actualCodes = Object.keys(operation.responses || {}).sort();
|
||||
const expectedCodes = [...expectation.responseCodes].sort();
|
||||
if (actualCodes.join(',') !== expectedCodes.join(',')) {
|
||||
issues.push(
|
||||
`Unerwartete Response-Codes bei ${expectation.method.toUpperCase()} ${expectation.path}: erwartet ${expectedCodes.join(
|
||||
','
|
||||
)}, bekommen ${actualCodes.join(',')}`
|
||||
);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const html = await fetchText(API_DOC_URL);
|
||||
const spec = extractSpec(html);
|
||||
const issues = EXPECTED_ENDPOINTS.flatMap((endpoint) => validateOperation(spec, endpoint));
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.error('Foodsharing-API-Doku stimmt nicht mit den erwarteten Endpunkten ueberein:');
|
||||
issues.forEach((issue) => console.error(`- ${issue}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Geprueft: ${EXPECTED_ENDPOINTS.length} Foodsharing-Endpunkte laut ${API_DOC_URL}`);
|
||||
EXPECTED_ENDPOINTS.forEach((endpoint) => {
|
||||
console.log(`- ${endpoint.method.toUpperCase()} ${endpoint.path}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user