Files
AriWeb-Suite/test/domains.test.mjs

47 lines
6.1 KiB
JavaScript

/** test/domains.test.mjs — 도메인 영역 단위 테스트(서비스 직접 호출): 이름 정규화·중복·필요 DNS 값·메일 레코드 연동·해제·운영자 목록. 연결 작업 흐름은 test/sites.test.mjs 가 함께 검증한다. */
import { test, eq, ok, rejects, done } from '../lib/test.mjs';
import { makeDeps, seedGroup, appFor, call } from './helpers.mjs';
import { createFakeHal } from '../portal/sites/fake-hal.mjs';
import { createFake as fakeWebmaker } from '../portal/webmaker/adapter.mjs';
import { signHeaders } from '../lib/serviceauth.mjs';
import * as quota from '../lib/quota.mjs';
const setup = async () => { const deps = await makeDeps(); deps.hal = createFakeHal(); deps.webmaker = fakeWebmaker({ store: deps.store }); const who = seedGroup(deps); quota.grantEntitlement(deps.store, { groupId: who.group.id, resource: 'sites', amount: 1 }); const app = await appFor(deps, ['sites', 'domains'], who); return { deps, who, app, D: deps.domains }; };
await test('이름 정규화·형식 검사·용도·필요 DNS 값(복사용)·메일 레코드는 메일 영역에서 제공', async () => {
const { deps, who, app, D } = await setup();
eq(D.normalize(' HTTPS://Example.COM/path '), 'example.com');
await rejects(() => Promise.resolve().then(() => D.add({ group: who.group, user: who.user }, { name: 'bad_domain' })), 'bad_domain');
await rejects(() => Promise.resolve().then(() => D.add({ group: who.group, user: who.user }, { name: 'a.com', purpose: 'x' })), 'bad_purpose');
const d = D.add({ group: who.group, user: who.user }, { name: 'Mail.Example.org', purpose: 'both' }); eq(d.name, 'mail.example.org');
let recs = await D.requiredRecords(d); ok(recs.find((r) => r.type === 'TXT' && r.name === '_ariweb-verify.mail.example.org')); const a = recs.find((r) => r.type === 'A'); eq(a.value, null); ok(/관리형 배포/.test(a.note)); ok(recs.find((r) => /MX/.test(r.type) && /메일 서비스 화면/.test(r.note)));
deps.mailDns = { recordsFor: async (name) => [{ type: 'MX', name, value: `mail.ariweb.net`, prio: 10, state: 'pending' }, { type: 'TXT', name, value: 'v=spf1 include:_spf.ariweb.net ~all', purpose: 'SPF', state: 'pending' }] };
recs = await D.requiredRecords(d); ok(recs.find((r) => r.type === 'MX' && r.value === 'mail.ariweb.net')); ok(recs.find((r) => r.purpose === 'SPF'));
eq(D.setMailDnsState('mail.example.org', 'warn', { dkim: 'missing' }).mail_dns_state, 'warn'); eq(D.list(who.group.id)[0].mailDnsState, 'warn');
// 사이트에 연결된 관리형 도메인은 서버 공인 IP 를 A 값으로
const s = (await call(app, 'POST', '/api/sites', { name: 'S', projectMode: 'none', deployMode: 'managed' })).json.site; deps.sites.save(s.id, { server_id: 'srv_web2' });
const d2 = D.add({ group: who.group, user: who.user }, { name: 'web.example.org', purpose: 'web', siteId: s.id }); eq((await D.requiredRecords(d2)).find((r) => r.type === 'A').value, '203.0.113.20');
eq(D.detail(D.get(d2.id)).actionNeeded.code, 'ownership');
});
await test('중복·소속 검사·연결 대상 변경·해제·API 권한', async () => {
const { deps, who, app, D } = await setup();
const d = D.add({ group: who.group, user: who.user }, { name: 'dup.example', purpose: 'web' });
const e = await rejects(() => Promise.resolve().then(() => D.add({ group: who.group, user: who.user }, { name: 'dup.example' })), 'domain_taken'); ok(/이미 등록한/.test(e.message)); eq(e.detail.href, `/domains/${d.id}`);
const other = seedGroup(deps, { email: 'o@example.test' }); const e2 = await rejects(() => Promise.resolve().then(() => D.add({ group: other.group, user: other.user }, { name: 'dup.example' })), 'domain_taken'); ok(/다른 고객/.test(e2.message)); eq(e2.detail.href, null);
await rejects(() => Promise.resolve().then(() => D.add({ group: other.group, user: other.user }, { name: 'x.example', siteId: 'nope' })), 'not_found');
const s = (await call(app, 'POST', '/api/sites', { name: 'S', projectMode: 'none' })).json.site; const up = D.update(d, { siteId: s.id, purpose: 'both' }, who.user); eq(up.site_id, s.id); eq(up.purpose, 'both'); eq(up.dns_state, 'pending');
const r = await call(app, 'PATCH', `/api/domains/${d.id}`, { purpose: 'web' }); eq(r.status, 200); eq(r.json.domain.purpose, 'web');
// viewer 는 변경 불가
const now = new Date().toISOString(); deps.store.run('INSERT INTO users(id, email, display_name, created_at, updated_at) VALUES(?,?,?,?,?)', 'u_v', 'v@example.test', 'V', now, now); deps.store.run('INSERT INTO memberships(group_id, user_id, role, joined_at) VALUES(?,?,\'viewer\',?)', who.group.id, 'u_v', now);
const appV = await appFor(deps, ['sites', 'domains'], { user: deps.store.get('SELECT * FROM users WHERE id=\'u_v\''), group: who.group, membership: deps.store.get('SELECT * FROM memberships WHERE user_id=\'u_v\'') }); eq((await call(appV, 'DELETE', `/api/domains/${d.id}`)).status, 403); eq((await call(appV, 'GET', `/api/domains/${d.id}`)).status, 200);
eq((await call(app, 'DELETE', `/api/domains/${d.id}`)).json.released, true); eq(D.get(d.id), null); eq(deps.store.get('SELECT COUNT(*) AS n FROM audit_log WHERE action=\'domain.release\'').n, 1);
});
await test('운영자 목록: 서명 필요·반복 실패·만료 임박', async () => {
const { deps, who, app, D } = await setup(); deps.vault.set('service:hal', 'hal-secret-example');
const d = D.add({ group: who.group, user: who.user }, { name: 'ops.example', purpose: 'web' }); deps.store.run('UPDATE domains SET cert_state=\'valid\', cert_expires_at=? WHERE id=?', new Date(Date.now() + 3 * 86400000).toISOString(), d.id);
for (let i = 0; i < 3; i++) deps.store.run('INSERT INTO domain_checks(id, domain_id, kind, ok, checked_at) VALUES(?,?,\'dns\',0,?)', 'dc' + i, d.id, new Date().toISOString());
eq((await call(app, 'GET', '/internal/ops/domains')).status, 401);
const h = Object.fromEntries(Object.entries(signHeaders('hal', 'hal-secret-example', 'GET', '/internal/ops/domains', undefined)).map(([k, v]) => [k.toLowerCase(), v]));
const r = await call(app, 'GET', '/internal/ops/domains', undefined, h); eq(r.status, 200, r.text); eq(r.json.expiring.length, 1); eq(r.json.repeatedFailures.length, 1); eq(r.json.domains[0].recentFailures, 3);
});
done();