Ariweb Suite 0.1.1 기반: 공통 라이브러리(db·http·세션·작업 엔진·할당/예약·알림·감사·보관소), 포털 뼈대, Webmaker/AriMail 도입 소스와 manifest
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
9ca4cee341
commit
c7aea8d419
@@ -0,0 +1,152 @@
|
||||
'use strict';
|
||||
// v1.6.31 (요구 30) 자동 검증 — 서비스 단위(빈 데이터로 서버를 띄워 API 로 확인). 사용자 간 격리 · CSRF/출처 · 가입 제한 · 손상 파일 · 설정 토큰.
|
||||
const test = require('node:test'); const assert = require('node:assert/strict');
|
||||
const fs = require('fs'); const path = require('path'); const os = require('os'); const { spawn } = require('child_process');
|
||||
const ROOT = path.join(__dirname, '..'); const PORT = 3900 + Math.floor(Math.random() * 90);
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wm-srv-')); const B = `http://127.0.0.1:${PORT}`;
|
||||
let child; const jar = {};
|
||||
const api = async (p, init = {}, who = 'a') => { const headers = { 'Content-Type': 'application/json', ...(init.headers || {}) }; if (jar[who]) headers.cookie = jar[who]; if (init.referer !== null) headers.referer = init.referer || `${B}/`; const r = await fetch(B + p, { ...init, headers, redirect: 'manual' }); const sc = r.headers.get('set-cookie'); if (sc && init.keep !== false) jar[who] = sc.split(';')[0]; let b = {}; try { b = await r.json(); } catch (_) {} return { status: r.status, b, headers: r.headers }; };
|
||||
test.before(async () => {
|
||||
fs.mkdirSync(tmp, { recursive: true }); fs.writeFileSync(path.join(tmp, 'setup.token'), 'TESTTOKEN0123456789abcdefghij\n', { mode: 0o600 });
|
||||
child = spawn('node', [path.join(ROOT, 'server.js')], { env: { ...process.env, PORT: String(PORT), HOST: '127.0.0.1', DATA_ROOT: tmp, DATA_DIR: path.join(tmp, 'sites'), SITES_ROOT: path.join(tmp, 'www'), PUBLIC_BASE_URL: B, WEBMAKER_SETUP_PORT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let out = ''; child.stdout.on('data', (c) => { out += c; }); child.stderr.on('data', (c) => { out += c; });
|
||||
for (let i = 0; i < 100; i++) { if (/listening on/.test(out)) break; await new Promise((r) => setTimeout(r, 100)); }
|
||||
assert.match(out, /listening on/); assert.doesNotMatch(out, /admin \/ admin/, '기동 때 admin/admin 을 만들지 않음');
|
||||
});
|
||||
test.after(() => { try { child.kill('SIGTERM'); } catch (_) {} });
|
||||
test('기본 관리자 없음 · 가입·로그인·세션', async () => {
|
||||
const info = await api('/api/auth/info'); assert.equal(info.b.setupRequired, true, '관리자가 없으니 설치 점검 미완');
|
||||
assert.equal((await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ id: 'admin', password: 'admin' }) }, 'x')).status, 401, 'admin/admin 로그인 불가');
|
||||
const a = await api('/api/auth/signup', { method: 'POST', body: JSON.stringify({ id: 'alice', password: 'Passw0rd!', name: 'Alice' }) }, 'a'); assert.equal(a.status, 200);
|
||||
const b = await api('/api/auth/signup', { method: 'POST', body: JSON.stringify({ id: 'bob', password: 'Passw0rd!', name: 'Bob' }) }, 'b'); assert.equal(b.status, 200);
|
||||
assert.equal((await api('/api/auth/me', {}, 'a')).b.user.id, 'alice');
|
||||
});
|
||||
test('설정 토큰: 관리자가 생기기 전에는 토큰으로, 생긴 뒤에는 토큰 거절', async () => {
|
||||
const tok = fs.readFileSync(path.join(tmp, 'setup.token'), 'utf8').trim();
|
||||
const s1 = await api('/api/setup/login', { method: 'POST', body: JSON.stringify({ token: tok }) }, 's'); assert.equal(s1.status, 200);
|
||||
const cr = await api('/api/setup/admin/create', { method: 'POST', body: JSON.stringify({ id: 'root1', name: 'Root', password: 'RootPassw0rd!' }) }, 's'); assert.ok(cr.status < 300, JSON.stringify(cr.b));
|
||||
const s2 = await api('/api/setup/login', { method: 'POST', body: JSON.stringify({ token: tok }) }, 's2'); assert.equal(s2.status, 403, '쓸 수 있는 관리자가 생기면 토큰 로그인 불가');
|
||||
const lg = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ id: 'root1', password: 'RootPassw0rd!' }) }, 'r'); assert.equal(lg.status, 200);
|
||||
const ov = await api('/api/admin/overview', {}, 'r'); assert.equal(ov.status, 200);
|
||||
const au = await api('/api/admin/audit', {}, 'r'); assert.equal(au.status, 200); assert.ok((au.b.items || []).some((x) => x.action === 'auth.login'));
|
||||
assert.ok(!JSON.stringify(au.b).includes('RootPassw0rd'), '작업 기록에 비밀번호가 없음');
|
||||
const rc = await api('/api/admin/recovery', {}, 'r'); assert.equal(rc.status, 200); assert.ok(Array.isArray(rc.b.items));
|
||||
const dns = await api('/api/admin/dns', {}, 'r'); assert.equal(dns.status, 200, 'v1.6.33: /api/admin/dns 가 404 가 아니어야 함 (1.5.36~1.6.32 회귀)'); assert.ok(dns.b.catalog && Array.isArray(dns.b.domains));
|
||||
const zi = await api('/api/admin/dns/zones', { method: 'POST', body: JSON.stringify({ domain: 'example.com', provider: 'route53', import: true }) }, 'r'); assert.equal(zi.status, 503, '공급자 접속 정보 없음 → 503 안내 (not found 아님): ' + JSON.stringify(zi.b)); assert.match(zi.b.error || '', /접속 정보/);
|
||||
});
|
||||
test('관리자가 alice 에게 캐시 지급 → 작업 기록에 남음', async () => {
|
||||
const r = await api('/api/admin/credits/alice', { method: 'POST', body: JSON.stringify({ type: 'add', amount: 50000, memo: 'test' }) }, 'r'); assert.equal(r.status, 200, JSON.stringify(r.b));
|
||||
const au = await api('/api/admin/audit?action=credits.', {}, 'r'); assert.ok(au.b.items.some((x) => x.action === 'credits.add' && x.target === 'alice'));
|
||||
});
|
||||
test('사용자 간 격리: 다른 사용자의 사이트 ID 로 접근 불가', async () => {
|
||||
const mk = await api('/api/sites', { method: 'POST', body: JSON.stringify({ title: 'A site', purpose: 'p', content: 'c' }) }, 'a'); assert.ok(mk.status < 300, 'alice 사이트 생성 ' + mk.status + ' ' + JSON.stringify(mk.b).slice(0, 100));
|
||||
const id = (mk.b.site && mk.b.site.id) || mk.b.id; assert.ok(id);
|
||||
assert.equal((await api(`/api/sites/${id}`, {}, 'a')).status, 200);
|
||||
assert.equal((await api(`/api/sites/${id}`, {}, 'b')).status, 404, 'bob 은 404');
|
||||
assert.equal((await api(`/api/sites/${id}`, { method: 'PATCH', body: JSON.stringify({ title: 'hacked' }) }, 'b')).status, 404);
|
||||
assert.equal((await api(`/api/sites/${id}/events`, {}, 'b')).status, 404);
|
||||
assert.equal((await api('/api/admin/overview', {}, 'b')).status, 403, '일반 사용자는 관리자 API 403');
|
||||
global.__siteId = id;
|
||||
});
|
||||
test('생성 사이트 출처·CSRF: 세션 쿠키만으로는 API 사용 불가', async () => {
|
||||
const id = global.__siteId;
|
||||
assert.equal((await api('/api/auth/me', { referer: `${B}/${id}/index.html` }, 'a')).status, 401, '사이트 페이지 출처 → 세션 무시');
|
||||
assert.equal((await api('/api/auth/me', { referer: null, headers: { 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty' } }, 'a')).status, 401, 'Referer 없는 브라우저 fetch → 세션 무시');
|
||||
assert.ok([401, 403].includes((await api('/api/auth/csrf', { referer: `${B}/${id}/` }, 'a')).status), '사이트 페이지는 CSRF 토큰을 못 받음');
|
||||
const t = await api('/api/auth/csrf', {}, 'a'); assert.equal(t.status, 200);
|
||||
assert.equal((await api('/api/auth/me', { referer: null, headers: { 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'x-wm-csrf': t.b.csrf } }, 'a')).status, 200, 'CSRF 헤더가 있으면 가능');
|
||||
assert.equal((await api('/api/auth/me', { headers: { 'sec-fetch-mode': 'navigate', 'sec-fetch-dest': 'document' }, referer: null }, 'a')).status, 200, '화면 이동은 세션 사용');
|
||||
});
|
||||
test('v1.6.35 도메인 탭 API: 장바구니·외부 도메인 경로가 있고 거부 이유를 안내한다', async () => {
|
||||
const d = await api('/api/domains', {}, 'a'); assert.equal(d.status, 200); assert.ok(Array.isArray(d.b.cart)); assert.equal(typeof d.b.platform.allowSubdomain, 'boolean'); assert.equal(typeof d.b.dnsReady, 'boolean');
|
||||
// 경로가 있고(404 "not found" 가 아님) 이유를 말한다: 서브 도메인 계정이면 403, 자체 도메인 계정인데 등록 업체 접속 정보가 없으면 503 "도메인 서비스가 아직 준비되지 않았습니다"
|
||||
const ct = await api('/api/domains/cart', { method: 'POST', body: JSON.stringify({ domain: 'x.example' }) }, 'a'); assert.ok([403, 503].includes(ct.status), JSON.stringify(ct.b)); assert.match(ct.b.error, /서브 도메인 계정|준비되지 않았습니다/);
|
||||
// 외부 도메인 등록: 소유 확인(owned) 없이는 DNS 를 보기 전에 409 (서브 도메인 계정이면 403)
|
||||
const ex = await api('/api/domains/external', { method: 'POST', body: JSON.stringify({ domain: 'x.example', mode: 'ns', owned: false }) }, 'a'); assert.ok([403, 409].includes(ex.status), JSON.stringify(ex.b)); if (ex.status === 409) assert.match(ex.b.error, /이미 구매\(소유\)했는지/);
|
||||
assert.equal((await api('/api/domains/external', { method: 'POST', body: JSON.stringify({ domain: 'x.example', mode: 'ns', owned: true }) }, 'x')).status, 401, '로그인 없이는 401');
|
||||
assert.equal((await api('/api/domains/0123456789abcdef/status', {}, 'a')).status, 404, '남의/없는 도메인 상태는 404');
|
||||
// 관리자 설정: 서브 도메인 등록 금지 → 중복 확인이 403 이유로 끝난다
|
||||
const sv = await api('/api/admin/settings', { method: 'PUT', body: JSON.stringify({ domains: { allowSubdomain: false, platformDomain: 'example.com' } }) }, 'r'); assert.equal(sv.status, 200, JSON.stringify(sv.b).slice(0, 200)); assert.equal(sv.b.domainsConfig.allowSubdomain, false);
|
||||
const ck = await api('/api/domains/subdomain/check', { method: 'POST', body: JSON.stringify({ name: 'mystore' }) }, 'a'); assert.equal(ck.status, 403); assert.match(ck.b.error, /서브 도메인 등록을 받지 않습니다/);
|
||||
await api('/api/admin/settings', { method: 'PUT', body: JSON.stringify({ domains: { allowSubdomain: true } }) }, 'r');
|
||||
});
|
||||
test('v1.6.38 메일 서버 등록부 API · AI 제공자 저장이 등급 목록 포함 여부를 알려 준다', async () => {
|
||||
// 요구 2: 메일 서버 등록부 — 경로가 있고(관리자 전용) 검증 문구가 나오며 비밀번호는 응답·작업 기록에 없다
|
||||
assert.equal((await api('/api/admin/mail-servers', {}, 'a')).status, 403, '일반 회원은 403');
|
||||
const l0 = await api('/api/admin/mail-servers', {}, 'r'); assert.equal(l0.status, 200, 'v1.6.38: mail-servers 가 관리자 경로 목록에 있어야 함'); assert.ok(Array.isArray(l0.b.servers));
|
||||
const bad = await api('/api/admin/mail-servers', { method: 'POST', body: JSON.stringify({ nickname: '테스트', url: '', domain: 'mail.test', password: 'x' }) }, 'r'); assert.equal(bad.status, 400); assert.match(bad.b.error, /Arimail 주소를 입력하세요/);
|
||||
const ok = await api('/api/admin/mail-servers', { method: 'POST', body: JSON.stringify({ nickname: '테스트 메일', ip: '10.9.8.7', url: 'http://127.0.0.1:1', domain: 'mail.test', adminLogin: 'wm', password: 'SecretPw!9' }) }, 'r'); assert.equal(ok.status, 201, JSON.stringify(ok.b)); assert.equal(ok.b.server.hasPassword, true); assert.ok(!JSON.stringify(ok.b).includes('SecretPw'));
|
||||
const mail = await api('/api/admin/mail', {}, 'r'); assert.equal(mail.status, 200); assert.ok(mail.b.servers.some((x) => x.id === ok.b.server.id && x.ip === '10.9.8.7'), '메일 시스템 화면 응답에 서버 목록');
|
||||
const t = await api(`/api/admin/mail-servers/${ok.b.server.id}/test`, { method: 'POST', body: '{}' }, 'r'); assert.equal(t.status, 200); assert.equal(t.b.ok, false); assert.match(t.b.detail, /연결할 수 없습니다/);
|
||||
const au = await api('/api/admin/audit?action=mailserver.', {}, 'r'); assert.ok(au.b.items.some((x) => x.action === 'mailserver.save')); assert.ok(!JSON.stringify(au.b).includes('SecretPw'), '작업 기록에 비밀번호 없음');
|
||||
const del = await api(`/api/admin/mail-servers/${ok.b.server.id}`, { method: 'DELETE' }, 'r'); assert.equal(del.status, 200); assert.ok(!del.b.servers.some((x) => x.id === ok.b.server.id));
|
||||
// 요구 1: 키 없이 "모델 조회" 는 거절, 키 없이 저장하면 등급 목록에서 빠지는 이유를 응답으로
|
||||
const ft = await api('/api/admin/providers/openrouter/test', { method: 'POST', body: JSON.stringify({ apiKey: '' }) }, 'r'); assert.equal(ft.status, 200); assert.equal(ft.b.ok, false); assert.match(ft.b.error, /API 키가 필요합니다/);
|
||||
const sv = await api('/api/admin/providers/openrouter', { method: 'PUT', body: JSON.stringify({ enabled: true, models: [{ id: 'x/y', label: 'y' }] }) }, 'r'); assert.equal(sv.status, 200); assert.equal(sv.b.usable, false); assert.match(sv.b.unavailableReason, /API 키가 없습니다/);
|
||||
const lp = await api('/api/admin/providers', {}, 'r'); const orp = lp.b.providers.find((x) => x.id === 'openrouter'); assert.equal(orp.usable, false); assert.match(orp.unavailableReason, /API 키/);
|
||||
const st = await api('/api/admin/settings', {}, 'r'); assert.ok(st.b.unavailableProviders.some((x) => x.id === 'openrouter'));
|
||||
await api('/api/admin/providers/openrouter', { method: 'PUT', body: JSON.stringify({ enabled: false }) }, 'r');
|
||||
});
|
||||
test('v1.6.39 메일 서버 비밀번호 보기·되돌리기·초기화 경로 · Route 53 안내 문구', async () => {
|
||||
// 요구 4: [보기] 는 관리자만, 호출은 작업 기록에 남되 비밀번호는 기록에 없다
|
||||
const ok = await api('/api/admin/mail-servers', { method: 'POST', body: JSON.stringify({ nickname: '비밀 테스트', url: 'http://127.0.0.1:1', domain: 'pw.test', adminLogin: 'wm', password: 'FirstPw!1' }) }, 'r'); assert.equal(ok.status, 201, JSON.stringify(ok.b)); const id = ok.b.server.id;
|
||||
assert.equal(ok.b.server.hasPrevPassword, false); assert.equal(ok.b.server.lockedUntil, null); assert.equal(ok.b.server.adminUrl, 'http://127.0.0.1:1/admin');
|
||||
assert.equal((await api(`/api/admin/mail-servers/${id}/password`, {}, 'a')).status, 403, '일반 회원은 비밀번호를 볼 수 없다');
|
||||
const rv = await api(`/api/admin/mail-servers/${id}/password`, {}, 'r'); assert.equal(rv.status, 200); assert.equal(rv.b.password, 'FirstPw!1'); assert.equal(rv.b.adminLogin, 'wm');
|
||||
const au = await api('/api/admin/audit?action=mailserver.reveal', {}, 'r'); assert.ok(au.b.items.some((x) => x.action === 'mailserver.reveal' && x.target === id)); assert.ok(!JSON.stringify(au.b).includes('FirstPw'));
|
||||
// 요구 2: 되돌릴 이전 값이 없으면 404 안내, 비밀번호를 바꾸면 hasPrevPassword, 되돌리면 첫 값으로
|
||||
assert.equal((await api(`/api/admin/mail-servers/${id}/revert-password`, { method: 'POST', body: '{}' }, 'r')).status, 404);
|
||||
const ch = await api(`/api/admin/mail-servers/${id}`, { method: 'PUT', body: JSON.stringify({ nickname: '비밀 테스트', url: 'http://127.0.0.1:1', domain: 'pw.test', adminLogin: 'wm', password: 'SecondPw!2' }) }, 'r'); assert.equal(ch.status, 200); assert.equal(ch.b.server.hasPrevPassword, true);
|
||||
assert.equal((await api(`/api/admin/mail-servers/${id}/password`, {}, 'r')).b.password, 'SecondPw!2');
|
||||
const rev = await api(`/api/admin/mail-servers/${id}/revert-password`, { method: 'POST', body: '{}' }, 'r'); assert.equal(rev.status, 200); assert.equal(rev.b.server.hasPrevPassword, true, '맞바꾸기라 다시 되돌릴 수 있다');
|
||||
assert.equal((await api(`/api/admin/mail-servers/${id}/password`, {}, 'r')).b.password, 'FirstPw!1');
|
||||
// 요구 4: 초기화는 Arimail 에 닿아야 한다 — 닿지 않는 주소면 오류(비밀번호는 바뀌지 않음), via 비밀번호는 기록에 없다
|
||||
const rs = await api(`/api/admin/mail-servers/${id}/reset-password`, { method: 'POST', body: JSON.stringify({ via: { login: 'root', password: 'RootSecret!' } }) }, 'r'); assert.ok(rs.status >= 400, JSON.stringify(rs.b)); assert.match(rs.b.error, /Arimail/);
|
||||
assert.equal((await api(`/api/admin/mail-servers/${id}/password`, {}, 'r')).b.password, 'FirstPw!1');
|
||||
const au2 = await api('/api/admin/audit?action=mailserver.reset-password', {}, 'r'); assert.ok(au2.b.items.some((x) => x.action === 'mailserver.reset-password' && x.ok === false)); assert.ok(!JSON.stringify(au2.b).includes('RootSecret'));
|
||||
// 연결 확인 응답에 잠금 필드가 있다 (닿지 않는 서버라 locked:false)
|
||||
const t = await api(`/api/admin/mail-servers/${id}/test`, { method: 'POST', body: '{}' }, 'r'); assert.equal(t.b.locked, false); assert.equal(t.b.lockedUntil, null);
|
||||
await api(`/api/admin/mail-servers/${id}`, { method: 'DELETE' }, 'r');
|
||||
// 요구 1: Route 53 접속 정보가 없을 때 무엇이 빠졌는지 말한다 (비밀값 없음)
|
||||
assert.match(require('../lib/dns/route53').missingMessage(), /Route 53 접속 정보가 없습니다 — .*ACCESS_KEY_ID=/);
|
||||
});
|
||||
test('v1.6.40 캐시·충전: 상품 관리(관리자) · 사용자 주문 접수 · 입금 확인 적립 · 새 화면 파일', async () => {
|
||||
assert.equal((await api('/api/admin/prepaid/packages', { method: 'POST', body: JSON.stringify({ name: 'x', amount: 10000 }) }, 'a')).status, 403, '일반 사용자는 상품을 만들 수 없음');
|
||||
const mk = await api('/api/admin/prepaid/packages', { method: 'POST', body: JSON.stringify({ name: '스타터', amount: 20000, bonusType: 'percent', bonusValue: 15, note: '첫 충전' }) }, 'r'); assert.equal(mk.status, 200, JSON.stringify(mk.b)); assert.equal(mk.b.package.credit, 23000);
|
||||
const bad = await api('/api/admin/prepaid/packages', { method: 'POST', body: JSON.stringify({ name: '', amount: 20000 }) }, 'r'); assert.equal(bad.status, 400); assert.match(bad.b.error, /이름/);
|
||||
const pk = await api('/api/credits/packages', {}, 'b'); assert.equal(pk.status, 200); assert.equal(pk.b.pgReady, false, 'PG 미연동 표시'); assert.equal(pk.b.packages.length, 1); assert.equal(pk.b.packages[0].credit, 23000);
|
||||
const before = (await api('/api/me/credit', {}, 'b')).b.balance;
|
||||
const od = await api('/api/credits/orders', { method: 'POST', body: JSON.stringify({ packageId: mk.b.package.id }) }, 'b'); assert.equal(od.status, 201, JSON.stringify(od.b)); assert.equal(od.b.order.status, 'pending');
|
||||
assert.equal((await api('/api/me/credit', {}, 'b')).b.balance, before, '주문 접수만으로는 적립되지 않음');
|
||||
const adm = await api('/api/admin/prepaid?status=pending', {}, 'r'); assert.equal(adm.status, 200); assert.equal(adm.b.summary.pendingOrders, 1); assert.equal(adm.b.orders[0].userId, 'bob');
|
||||
assert.equal((await api(`/api/admin/prepaid/orders/${od.b.order.id}`, { method: 'POST', body: JSON.stringify({ action: 'paid', memo: '계좌이체' }) }, 'a')).status, 403, '일반 사용자는 적립 불가');
|
||||
const paid = await api(`/api/admin/prepaid/orders/${od.b.order.id}`, { method: 'POST', body: JSON.stringify({ action: 'paid', memo: '계좌이체 확인' }) }, 'r'); assert.equal(paid.status, 200, JSON.stringify(paid.b)); assert.equal(paid.b.balance, before + 23000, '결제 금액 + 인센티브 적립');
|
||||
assert.equal((await api(`/api/admin/prepaid/orders/${od.b.order.id}`, { method: 'POST', body: JSON.stringify({ action: 'paid' }) }, 'r')).status, 409, '같은 주문 두 번 적립 불가');
|
||||
assert.equal((await api(`/api/credits/orders/${od.b.order.id}/cancel`, { method: 'POST', body: '{}' }, 'b')).status, 409, '적립된 주문은 취소 불가');
|
||||
const mine = await api('/api/credits/orders', {}, 'b'); assert.equal(mine.b.orders[0].status, 'paid'); assert.equal((await api('/api/credits/orders', {}, 'a')).b.orders.length, 0, '다른 사용자 주문은 보이지 않음');
|
||||
const au = await api('/api/admin/audit?action=prepaid.', {}, 'r'); assert.ok(au.b.items.some((x) => x.action === 'prepaid.order.paid' && x.detail && x.detail.credit === 23000));
|
||||
// 새 사용자 화면: 레일 내비게이션(홈·배포·DNS·메일·캐시)과 새 화면 파일이 서빙된다
|
||||
const page = await fetch(`${B}/`, { headers: { cookie: jar.b, referer: `${B}/` }, redirect: 'manual' }); const html = await page.text(); assert.equal(page.status, 200);
|
||||
for (const t of ['data-tab="home"', 'data-tab="deploy"', 'data-tab="dns"', 'data-tab="mail"', 'data-tab="credits"', 'id="deployChecklist"', 'id="creditsBody"', '/flow-ui.js', '/credits-ui.js']) assert.ok(html.includes(t), '화면에 ' + t);
|
||||
for (const f of ['/flow-ui.js', '/credits-ui.js']) { const r = await fetch(B + f, { headers: { cookie: jar.b } }); assert.equal(r.status, 200, f); }
|
||||
});
|
||||
test('v1.7.3 메일 사용량 경로: 사이트(메일 꺼짐 → 이유) · 통계에 mailUsage 없음 · 관리자 서버 사용량/키 경로', async () => {
|
||||
const mk = await api('/api/sites', { method: 'POST', body: JSON.stringify({ title: 'Usage site', purpose: 'p', content: 'c' }) }, 'a'); assert.ok(mk.status < 300, JSON.stringify(mk.b).slice(0, 120)); const id = (mk.b.site && mk.b.site.id) || mk.b.id;
|
||||
const mu = await api(`/api/sites/${id}/mailusage`, {}, 'a'); assert.equal(mu.status, 200); assert.equal(mu.b.ok, false); assert.equal(mu.b.code, 'mail_off');
|
||||
assert.equal((await api(`/api/sites/${id}/mailusage`, {}, 'b')).status, 404, '다른 사용자는 404');
|
||||
const st = await api(`/api/sites/${id}/stats`, {}, 'a'); assert.equal(st.status, 200); assert.equal(st.b.disk.mail, null, '메일 꺼진 사이트는 disk.mail null'); assert.equal(st.b.mailUsage, undefined);
|
||||
const us = await api('/api/admin/mail-servers/nope/usage', {}, 'r'); assert.equal(us.status, 200); assert.equal(us.b.ok, false); assert.equal(us.b.error.code, 'no_server');
|
||||
const uk = await api('/api/admin/mail-servers/nope/usage-key', { method: 'POST', body: JSON.stringify({ action: 'issue' }) }, 'r'); assert.equal(uk.status, 404);
|
||||
assert.equal((await api('/api/admin/mail-servers/nope/usage-key', { method: 'POST', body: JSON.stringify({ action: 'bogus' }) }, 'r')).status, 400);
|
||||
assert.equal((await api('/api/admin/mail-servers/nope/usage', {}, 'a')).status, 403, '일반 사용자는 관리자 경로 403');
|
||||
for (const f of ['/site-info.js', '/domains-ui.js', '/admin.js']) { const r = await fetch(B + f); assert.equal(r.status, 200); const t = await r.text(); assert.ok(/mailusage|usage-key|mailUsage/.test(t), f + ' 에 사용량 코드 포함'); }
|
||||
});
|
||||
test('로그인 실패 잠금·속도 제한', async () => {
|
||||
let last; for (let i = 0; i < 11; i++) last = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ id: 'alice', password: 'wrong' }) }, 'z');
|
||||
assert.equal(last.status, 429, '10회 실패 뒤 계정 잠금');
|
||||
});
|
||||
test('손상 파일 감지: credits.json 이 깨지면 운영 복구에 보인다', async () => {
|
||||
try { fs.unlinkSync(path.join(tmp, 'credits.json.bak')); } catch (_) {} // v1.6.40: 앞 테스트(선불 적립)가 원장을 다시 써 .bak 이 생긴다 — 이 테스트는 "백업도 없는" 손상을 본다
|
||||
fs.writeFileSync(path.join(tmp, 'credits.json'), '{oops'); const r = await api('/api/admin/credits', {}, 'r'); assert.ok(r.status >= 500, '빈 원장으로 숨기지 않음: ' + r.status);
|
||||
const rc = await api('/api/admin/recovery', {}, 'r'); assert.ok(rc.b.items.some((x) => x.kind === 'data'));
|
||||
});
|
||||
Reference in New Issue
Block a user