63323c1ca4
CI / test (push) Failing after 3m46s
CI / push-image (push) Has been skipped
CI / deploy-ift (push) Has been skipped
CI / ci-done (push) Failing after 0s
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped
Refs EventHub/EventHubFrontAdmin#40 Refs EventHub/EventHubFrontAdmin#41
399 lines
14 KiB
TypeScript
399 lines
14 KiB
TypeScript
import type { Route } from '@playwright/test';
|
|
import type { ContentState } from './contentSeed';
|
|
|
|
type JsonFn = (
|
|
route: Route,
|
|
status: number,
|
|
body: unknown,
|
|
headers?: Record<string, string>
|
|
) => Promise<void>;
|
|
|
|
function listSlice<T>(items: T[], url: URL): { rows: T[]; total: number } {
|
|
const limit = Number(url.searchParams.get('limit') || 50);
|
|
const offset = Number(url.searchParams.get('offset') || 0);
|
|
const q = (url.searchParams.get('q') || '').toLowerCase();
|
|
let rows = items;
|
|
if (q) {
|
|
rows = items.filter((item) => JSON.stringify(item).toLowerCase().includes(q));
|
|
}
|
|
const status = url.searchParams.get('status');
|
|
if (status && rows.length && typeof (rows[0] as { status?: string }).status === 'string') {
|
|
rows = rows.filter((item) => (item as { status?: string }).status === status);
|
|
}
|
|
const plan = url.searchParams.get('plan');
|
|
if (plan) {
|
|
rows = rows.filter((item) => (item as { plan?: string }).plan === plan);
|
|
}
|
|
const type = url.searchParams.get('type');
|
|
if (type) {
|
|
rows = rows.filter((item) => (item as { type?: string }).type === type);
|
|
}
|
|
return { rows: rows.slice(offset, offset + limit), total: rows.length };
|
|
}
|
|
|
|
/** Returns true if the request was handled. */
|
|
export async function tryHandleContentApi(
|
|
route: Route,
|
|
method: string,
|
|
path: string,
|
|
url: URL,
|
|
content: ContentState,
|
|
json: JsonFn,
|
|
postData: () => unknown
|
|
): Promise<boolean> {
|
|
// ---- users ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/users/stats')) {
|
|
await json(route, 200, {
|
|
total_users: content.users.length,
|
|
users_by_role: {},
|
|
users_by_status: {},
|
|
pending_users: 0,
|
|
});
|
|
return true;
|
|
}
|
|
if (method === 'GET' && path.endsWith('/v1/admin/users')) {
|
|
const page = listSlice(content.users, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/users\/([^/]+)$/);
|
|
if (m) {
|
|
const id = decodeURIComponent(m[1]);
|
|
const idx = content.users.findIndex((u) => u.id === id);
|
|
if (idx < 0) {
|
|
await json(route, 404, { error: 'not_found' });
|
|
return true;
|
|
}
|
|
if (method === 'GET') {
|
|
await json(route, 200, content.users[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'PUT') {
|
|
const patch = (postData() || {}) as Record<string, unknown>;
|
|
delete patch.id;
|
|
content.users[idx] = { ...content.users[idx], ...patch, id };
|
|
await json(route, 200, content.users[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'DELETE') {
|
|
content.users.splice(idx, 1);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- calendars ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/calendars/stats')) {
|
|
await json(route, 200, {
|
|
total_calendars: content.calendars.length,
|
|
calendars_by_type: {},
|
|
calendars_by_status: {},
|
|
top_calendars_by_reviews: [],
|
|
top_calendars_by_positive_reviews: [],
|
|
top_calendars_by_negative_reviews: [],
|
|
top_calendars_by_rating: [],
|
|
});
|
|
return true;
|
|
}
|
|
if (method === 'GET' && path.endsWith('/v1/admin/calendars')) {
|
|
const page = listSlice(content.calendars, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/calendars\/([^/]+)$/);
|
|
if (m) {
|
|
const id = decodeURIComponent(m[1]);
|
|
const idx = content.calendars.findIndex((c) => c.id === id);
|
|
if (idx < 0) {
|
|
await json(route, 404, { error: 'not_found' });
|
|
return true;
|
|
}
|
|
if (method === 'GET') {
|
|
await json(route, 200, content.calendars[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'PUT') {
|
|
const patch = (postData() || {}) as Record<string, unknown>;
|
|
delete patch.id;
|
|
content.calendars[idx] = { ...content.calendars[idx], ...patch, id };
|
|
await json(route, 200, content.calendars[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'DELETE') {
|
|
content.calendars.splice(idx, 1);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- events ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/events/stats')) {
|
|
await json(route, 200, {
|
|
total_events: content.events.length,
|
|
events_by_type: {},
|
|
events_by_status: {},
|
|
top_events_by_rating: [],
|
|
});
|
|
return true;
|
|
}
|
|
if (method === 'GET' && path.endsWith('/v1/admin/events')) {
|
|
const page = listSlice(content.events, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/events\/([^/]+)$/);
|
|
if (m) {
|
|
const id = decodeURIComponent(m[1]);
|
|
const idx = content.events.findIndex((e) => e.id === id);
|
|
if (idx < 0) {
|
|
await json(route, 404, { error: 'not_found' });
|
|
return true;
|
|
}
|
|
if (method === 'GET') {
|
|
await json(route, 200, content.events[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'PUT') {
|
|
const patch = (postData() || {}) as Record<string, unknown>;
|
|
delete patch.id;
|
|
content.events[idx] = { ...content.events[idx], ...patch, id };
|
|
await json(route, 200, content.events[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'DELETE') {
|
|
content.events.splice(idx, 1);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- subscriptions ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/subscriptions/stats')) {
|
|
await json(route, 200, {
|
|
total_subscriptions: content.subscriptions.length,
|
|
subscriptions_by_plan: {},
|
|
subscriptions_by_status: {},
|
|
trial_subscriptions: 0,
|
|
ending_paid_subscriptions: [],
|
|
});
|
|
return true;
|
|
}
|
|
if (method === 'GET' && path.endsWith('/v1/admin/subscriptions')) {
|
|
const page = listSlice(content.subscriptions, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/subscriptions\/([^/]+)$/);
|
|
if (m) {
|
|
const id = decodeURIComponent(m[1]);
|
|
const idx = content.subscriptions.findIndex((s) => s.id === id);
|
|
if (idx < 0) {
|
|
await json(route, 404, { error: 'not_found' });
|
|
return true;
|
|
}
|
|
if (method === 'GET') {
|
|
await json(route, 200, content.subscriptions[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'PUT') {
|
|
const patch = (postData() || {}) as Record<string, unknown>;
|
|
delete patch.id;
|
|
content.subscriptions[idx] = { ...content.subscriptions[idx], ...patch, id };
|
|
await json(route, 200, content.subscriptions[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'DELETE') {
|
|
content.subscriptions.splice(idx, 1);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- banned words ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/banned-words')) {
|
|
const page = listSlice(
|
|
content.bannedWords.map((w) => ({ ...w, id: w.word })),
|
|
url
|
|
);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
if (method === 'POST' && path.endsWith('/v1/admin/banned-words')) {
|
|
const payload = postData() as { word?: string };
|
|
const word = (payload.word || '').trim();
|
|
if (word) content.bannedWords.push({ word, added_by: 'admin-e2e-1', added_at: '2026-07-14T12:00:00Z' });
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/banned-words\/([^/]+)$/);
|
|
if (m && method === 'DELETE') {
|
|
const word = decodeURIComponent(m[1]);
|
|
content.bannedWords = content.bannedWords.filter((w) => w.word !== word);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// ---- automod ----
|
|
if (!('_automod' in content)) {
|
|
(content as { _automod?: { settings: Record<string, unknown>; hits: Record<string, unknown>[] } })._automod = {
|
|
settings: {
|
|
keyword_action: 'flag',
|
|
match_mode: 'word_boundary',
|
|
report_threshold: 3,
|
|
updated_at: '2026-07-14T12:00:00Z',
|
|
updated_by: 'admin-e2e-1',
|
|
},
|
|
hits: [
|
|
{
|
|
id: 'hit-1',
|
|
entity_type: 'event',
|
|
entity_id: 'evt-1',
|
|
trigger: 'keyword',
|
|
matched_words: ['spam'],
|
|
action_taken: 'flag',
|
|
status: 'open',
|
|
created_at: '2026-07-14T12:00:00Z',
|
|
resolved_at: null,
|
|
resolved_by: '',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
const automod = (content as { _automod: { settings: Record<string, unknown>; hits: Record<string, unknown>[] } })._automod;
|
|
if (method === 'GET' && path.endsWith('/v1/admin/automod/settings')) {
|
|
await json(route, 200, automod.settings);
|
|
return true;
|
|
}
|
|
if (method === 'PUT' && path.endsWith('/v1/admin/automod/settings')) {
|
|
const payload = postData() as Record<string, unknown>;
|
|
automod.settings = { ...automod.settings, ...payload };
|
|
await json(route, 200, automod.settings);
|
|
return true;
|
|
}
|
|
if (method === 'GET' && path.endsWith('/v1/admin/automod/hits')) {
|
|
const status = url.searchParams.get('status');
|
|
const rows = status
|
|
? automod.hits.filter((h) => h.status === status)
|
|
: automod.hits;
|
|
await json(route, 200, rows);
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/automod\/hits\/([^/]+)$/);
|
|
if (m && method === 'PUT') {
|
|
const id = decodeURIComponent(m[1]);
|
|
const payload = postData() as { status?: string };
|
|
const idx = automod.hits.findIndex((h) => h.id === id);
|
|
if (idx >= 0 && payload.status) {
|
|
automod.hits[idx] = {
|
|
...automod.hits[idx],
|
|
status: payload.status,
|
|
resolved_at: '2026-07-14T13:00:00Z',
|
|
resolved_by: 'admin-e2e-1',
|
|
};
|
|
await json(route, 200, automod.hits[idx]);
|
|
return true;
|
|
}
|
|
await json(route, 404, { error: 'not found' });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// ---- admins ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/admins')) {
|
|
const page = listSlice(content.admins, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
if (method === 'POST' && path.endsWith('/v1/admin/admins')) {
|
|
const payload = postData() as Record<string, unknown>;
|
|
const created = {
|
|
id: `admin-new-${content.admins.length + 1}`,
|
|
email: String(payload.email || 'new@eventhub.local'),
|
|
nickname: String(payload.nickname || 'New Admin'),
|
|
role: String(payload.role || 'admin'),
|
|
language: 'ru',
|
|
phone: null,
|
|
avatar_url: null,
|
|
timezone: 'UTC',
|
|
created_at: '2026-07-14T12:00:00Z',
|
|
updated_at: '2026-07-14T12:00:00Z',
|
|
last_login: null,
|
|
};
|
|
content.admins.push(created);
|
|
await json(route, 200, created);
|
|
return true;
|
|
}
|
|
{
|
|
const m = path.match(/\/v1\/admin\/admins\/([^/]+)$/);
|
|
if (m) {
|
|
const id = decodeURIComponent(m[1]);
|
|
const idx = content.admins.findIndex((a) => a.id === id);
|
|
if (idx < 0) {
|
|
await json(route, 404, { error: 'not_found' });
|
|
return true;
|
|
}
|
|
if (method === 'GET') {
|
|
await json(route, 200, content.admins[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'PUT') {
|
|
content.admins[idx] = { ...content.admins[idx], ...(postData() as object) };
|
|
await json(route, 200, content.admins[idx]);
|
|
return true;
|
|
}
|
|
if (method === 'DELETE') {
|
|
content.admins.splice(idx, 1);
|
|
await json(route, 200, { ok: true });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- audit ----
|
|
if (method === 'GET' && path.endsWith('/v1/admin/audit')) {
|
|
const page = listSlice(content.audit, url);
|
|
await json(route, 200, page.rows, { 'X-Total-Count': String(page.total) });
|
|
return true;
|
|
}
|
|
|
|
// ---- monitoring ----
|
|
if (method === 'GET' && path.includes('/v1/admin/nodes/metrics')) {
|
|
await json(route, 200, [
|
|
{
|
|
active_sessions: 3,
|
|
io_in: 10,
|
|
io_out: 20,
|
|
memory_atom: 1,
|
|
memory_binary: 2,
|
|
memory_ets: 3,
|
|
memory_processes: 4,
|
|
memory_total: 2048,
|
|
mnesia_commits: 1,
|
|
mnesia_failures: 0,
|
|
node: 'node-a',
|
|
process_count: 100,
|
|
run_queue: 0,
|
|
table_sizes: { users: 10 },
|
|
timestamp: '2026-07-14T12:00:00Z',
|
|
ws_connections: 5,
|
|
cpu_utilization: 12,
|
|
memory_available: 1024,
|
|
},
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|