Import tests: app/backend/tests/public-contract.test.mjs

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:20:51 +03:30
parent 66569baea5
commit aed2d9a740
+123
View File
@@ -0,0 +1,123 @@
import test from 'node:test';
import assert from 'node:assert/strict';
const BASE = process.env.TEST_BASE_URL || 'http://127.0.0.1:3010';
const TEST_ARTICLE_SLUG = process.env.TEST_ARTICLE_SLUG || 'agentic-ai-production-2026';
const REQUIRE_SERVER = process.env.TEST_REQUIRE_SERVER === '1';
async function serverAvailable() {
try {
const res = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(1500) });
return res.status < 500;
} catch {
return false;
}
}
const HAS_SERVER = await serverAvailable();
if (!HAS_SERVER && REQUIRE_SERVER) {
throw new Error(`TEST_REQUIRE_SERVER=1 but no backend is reachable at ${BASE}`);
}
const SERVER_SKIP = HAS_SERVER ? false : `no backend reachable at ${BASE}`;
async function request(path, options = {}) {
const headers = {
'content-type': 'application/json',
...(options.headers || {}),
};
const res = await fetch(`${BASE}${path}`, { ...options, headers });
const body = await res.json().catch(() => null);
return { res, body };
}
test('public content endpoints expose stable list payloads', { skip: SERVER_SKIP }, async () => {
const endpoints = [
['/api/v1/articles?limit=3', 'items'],
['/api/v1/categories', 'items'],
['/api/v1/tags', 'items'],
['/api/v1/authors', 'items'],
['/api/v1/series', 'items'],
['/api/v1/newsletter/archive', 'items'],
['/api/v1/jobs', 'items'],
['/api/v1/courses', 'items'],
];
for (const [path, key] of endpoints) {
const { res, body } = await request(path);
assert.equal(res.status, 200, `${path} should be reachable`);
assert.ok(Array.isArray(body?.[key]), `${path} should return ${key} array`);
}
});
test('article detail, comments, related, and qa endpoints keep their contracts', { skip: SERVER_SKIP }, async () => {
const article = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}`);
assert.equal(article.res.status, 200);
assert.ok(article.body?.article?.id);
assert.equal(article.body.article.slug, TEST_ARTICLE_SLUG);
const comments = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/comments`);
assert.equal(comments.res.status, 200);
assert.ok(Array.isArray(comments.body?.items));
const related = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/related`);
assert.equal(related.res.status, 200);
assert.ok(Array.isArray(related.body?.items));
const qa = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/qa`);
assert.equal(qa.res.status, 200);
assert.ok(Array.isArray(qa.body?.items));
});
test('missing public resources return 404 instead of leaking internals', { skip: SERVER_SKIP }, async () => {
const article = await request('/api/v1/articles/definitely-missing-article');
assert.equal(article.res.status, 404);
assert.equal(article.body?.error, 'not_found');
const profile = await request('/api/v1/profile/@definitely-missing-user');
assert.equal(profile.res.status, 404);
assert.equal(profile.body?.error, 'not_found');
});
test('authenticated surfaces reject anonymous requests consistently', { skip: SERVER_SKIP }, async () => {
const protectedGets = [
'/api/v1/auth/me',
'/api/v1/auth/lists',
'/api/v1/auth/history',
'/api/v1/auth/notifications',
'/api/v1/auth/notifications/preferences',
'/api/v1/auth/badges',
'/api/v1/membership/status',
'/api/v1/admin/dashboard',
'/api/v1/admin/errors',
'/api/v1/admin/articles',
];
for (const path of protectedGets) {
const { res, body } = await request(path);
assert.equal(res.status, 401, `${path} should reject anonymous access`);
assert.equal(body?.error, 'unauthenticated');
}
});
test('public mutating endpoints validate bad input without creating records', { skip: SERVER_SKIP }, async () => {
const newsletter = await request('/api/v1/newsletter', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' }),
});
assert.equal(newsletter.res.status, 400);
assert.equal(newsletter.body?.error, 'invalid_input');
const errorReport = await request('/api/v1/errors', {
method: 'POST',
body: JSON.stringify({ message: '' }),
});
assert.equal(errorReport.res.status, 400);
assert.equal(errorReport.body?.error, 'invalid_input');
const job = await request('/api/v1/jobs', {
method: 'POST',
body: JSON.stringify({ company: 'x', title: 'y', description: 'too short' }),
});
assert.equal(job.res.status, 400);
assert.equal(job.body?.error, 'invalid_input');
});