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.
173 lines
4.7 KiB
JavaScript
173 lines
4.7 KiB
JavaScript
#!/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);
|
|
});
|