Files

68 lines
9.3 KiB
JavaScript

'use strict';
// v1.6.32 (요구 1) Route 53 Domains 등록 업체 — 모의 JSON 1.1 서버로 검색·가격·등록(비동기 완료)·갱신·네임서버·소유 확인·등록자 변환을 검사한다. 실제 AWS 호출·비용 없음.
const test = require('node:test'); const assert = require('node:assert/strict');
const http = require('http'); const fs = require('fs'); const path = require('path'); const os = require('os');
const ROOT = path.join(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wm-r53-')); process.env.DATA_ROOT = tmp; process.env.DATA_DIR = path.join(tmp, 'sites');
const calls = []; const state = { ops: {}, domains: {} }; let nextOp = 1;
const srv = http.createServer((req, res) => {
let b = ''; req.on('data', (c) => { b += c; }); req.on('end', () => {
const op = String(req.headers['x-amz-target'] || '').split('.').pop(); const j = b ? JSON.parse(b) : {}; calls.push({ op, j, auth: req.headers.authorization || '' });
const send = (code, o) => { res.writeHead(code, { 'Content-Type': 'application/x-amz-json-1.1' }); res.end(JSON.stringify(o)); };
if (!/^AWS4-HMAC-SHA256 Credential=AKIATEST\//.test(req.headers.authorization || '')) return send(403, { __type: 'InvalidSignatureException', message: 'bad signature' });
if (op === 'ListPrices') return j.Tld === 'com' ? send(200, { Prices: [{ Tld: 'com', RegistrationPrice: { Price: 14.0, Currency: 'USD' }, RenewalPrice: { Price: 15.0, Currency: 'USD' }, TransferPrice: { Price: 14.0, Currency: 'USD' } }] }) : send(200, { Prices: [] });
if (op === 'CheckDomainAvailability') return send(200, { Availability: j.DomainName === 'taken.com' ? 'UNAVAILABLE' : j.DomainName === 'odd.com' ? 'DONT_KNOW' : 'AVAILABLE' });
if (op === 'RegisterDomain') { if (!j.RegistrantContact || !/^\+82\.\d+$/.test(j.RegistrantContact.PhoneNumber)) return send(400, { __type: 'com.amazonaws.route53domains#InvalidInput', message: 'bad phone' }); const id = 'op-' + nextOp++; state.ops[id] = { Status: 'IN_PROGRESS', polls: 0, name: j.DomainName, years: j.DurationInYears }; return send(200, { OperationId: id }); }
if (op === 'GetOperationDetail') { const o = state.ops[j.OperationId]; if (!o) return send(400, { __type: 'InvalidInput', message: 'no op' }); o.polls++; if (o.polls >= 2 && o.years && o.name !== 'slow.com') { o.Status = 'SUCCESSFUL'; state.domains[o.name] = { exp: Math.floor(Date.now() / 1000) + o.years * 365 * 86400, ns: ['ns-1.awsdns-00.com', 'ns-2.awsdns-11.net'] }; } return send(200, { OperationId: j.OperationId, Status: o.Status, Type: 'REGISTER_DOMAIN', DomainName: o.name }); }
if (op === 'GetDomainDetail') { const d = state.domains[j.DomainName]; if (!d) return send(400, { __type: 'com.amazonaws.route53domains#InvalidInput', message: 'Domain ' + j.DomainName + ' not found in account' }); return send(200, { DomainName: j.DomainName, ExpirationDate: d.exp, Nameservers: d.ns.map((Name) => ({ Name })), StatusList: ['ok'], AutoRenew: false }); }
if (op === 'RenewDomain') { const d = state.domains[j.DomainName]; if (!d) return send(400, { __type: 'InvalidInput', message: 'not found' }); if (j.CurrentExpiryYear !== new Date(d.exp * 1000).getUTCFullYear()) return send(400, { __type: 'InvalidInput', message: 'wrong expiry year' }); d.exp += j.DurationInYears * 365 * 86400; const id = 'op-' + nextOp++; state.ops[id] = { Status: 'SUCCESSFUL', polls: 9, name: j.DomainName }; return send(200, { OperationId: id }); }
if (op === 'UpdateDomainNameservers') { const d = state.domains[j.DomainName]; if (!d) return send(400, { __type: 'InvalidInput', message: 'not found' }); d.ns = j.Nameservers.map((n) => n.Name); return send(200, { OperationId: 'op-ns' }); }
if (op === 'ListDomains') return send(200, { Domains: Object.entries(state.domains).map(([DomainName, d]) => ({ DomainName, Expiry: d.exp, AutoRenew: false })) });
return send(400, { __type: 'UnknownOperation', message: op });
});
});
let reg;
test.before(async () => { await new Promise((r) => srv.listen(0, '127.0.0.1', r)); process.env.ROUTE53DOMAINS_API_BASE = `http://127.0.0.1:${srv.address().port}`; require(ROOT + '/lib/vault').set({ kind: 'route53', secret: 'secret', settings: { ACCESS_KEY_ID: 'AKIATEST' } }); reg = require(ROOT + '/lib/registrars/route53'); });
test.after(() => srv.close());
const CONTACT = { firstName: 'Gildong', lastName: 'Hong', company: '', email: 'g@example.com', phoneCountry: '+82', phoneArea: '010', phoneNumber: '12345678', street: 'Teheran-ro', number: '123', zipcode: '06123', city: 'Seoul', state: '', country: 'KR' };
test('등록 업체 창구에 route53 이 있고 보관소 키로 준비된다', () => { const r = require(ROOT + '/lib/registrar'); assert.ok(r.IDS.includes('route53')); assert.equal(r.LABELS.route53, 'Amazon Route 53'); assert.ok(reg.ready()); assert.ok(r.catalog().find((x) => x.id === 'route53').needsContact); });
test('검색: 가능·불가·모름·미지원 확장자, 가격 USD', async () => {
const r = await reg.check(['free.com', 'taken.com', 'odd.com', 'nope.kr']);
assert.equal(r[0].status, 'free'); assert.deepEqual(r[0].price, { currency: 'USD', amount: 14 });
assert.equal(r[1].status, 'active'); assert.match(r[1].reason, /이미 등록/);
assert.equal(r[2].status, 'unknown'); assert.equal(r[3].status, 'unknown'); assert.match(r[3].reason, /다루지 않는 확장자/);
assert.equal((await reg.price('free.com', 'create', 2)).amount, 28); assert.equal((await reg.price('free.com', 'renew', 1)).amount, 15);
await assert.rejects(reg.price('x.kr'), /등록하지 않습니다/);
});
test('등록자 변환: 전화 +82.1012345678, PERSON/COMPANY, 빠진 값 거절', () => {
const c = reg.toContact(CONTACT); assert.equal(c.PhoneNumber, '+82.1012345678'); assert.equal(c.ContactType, 'PERSON'); assert.equal(c.AddressLine1, 'Teheran-ro 123'); assert.equal(c.CountryCode, 'KR');
assert.equal(reg.toContact({ ...CONTACT, company: 'Oz Group' }).OrganizationName, 'Oz Group');
assert.throws(() => reg.toContact({ ...CONTACT, email: '' }), /Email/); assert.throws(() => reg.toContact(null), /등록자 정보/);
});
test('등록: 비동기 작업을 기다려 ACT + 만료일, 갱신은 CurrentExpiryYear 를 읽어 보낸다, 네임서버 변경, 소유 확인', async () => {
const r = await reg.createDomain('free.com', { period: 2, contact: CONTACT });
assert.equal(r.status, 'ACT'); assert.equal(r.id, 'free.com'); assert.match(r.expiresAt, /^\d{4}-\d{2}-\d{2}$/);
const reg1 = calls.find((c) => c.op === 'RegisterDomain').j; assert.equal(reg1.DurationInYears, 2); assert.equal(reg1.AutoRenew, false); assert.equal(reg1.PrivacyProtectRegistrantContact, true);
const before = (await reg.getDomain('free.com')).expiresAt; const rn = await reg.renewDomain('free.com', 'free.com', 1); assert.equal(rn.status, 'ACT');
assert.ok((await reg.getDomain('free.com')).expiresAt > before, '만료일이 늘어남');
await reg.setNameServers('free.com', 'free.com', ['ns1.example.net.', 'ns2.example.net']); assert.deepEqual((await reg.getDomain('free.com')).nameServers, ['ns1.example.net', 'ns2.example.net']);
assert.ok(await reg.ownsDomain('free.com')); assert.equal(await reg.ownsDomain('other.com'), null, '계정에 없는 도메인은 null');
assert.equal((await reg.listDomains()).length, 1);
});
test('등록이 오래 걸리면 PENDING 으로 돌아오고, 구매 흐름은 영역을 미루고 finalizeRegistration 이 완료 뒤 채운다', async () => {
const r = await reg.createDomain('slow.com', { period: 1, contact: CONTACT }); assert.equal(r.status, 'PENDING'); assert.ok(r.operationId);
// 도메인 기록에 PENDING 으로 넣고 완료 확인 흐름을 돌린다 (registrar 설정 route53, DNS 수동)
const d = require(ROOT + '/lib/domains'); const fs2 = require('fs'); const f = path.join(tmp, 'domains.json'); const cur = (() => { try { return JSON.parse(fs2.readFileSync(f, 'utf8')); } catch (_) { return { domains: [], contacts: {}, purchases: [] }; } })();
cur.domains.push({ id: 'dom1', userId: 'u1', name: 'slow.com', type: 'registered', opId: 'slow.com', status: 'PENDING', period: 1, registrar: 'route53', createdAt: new Date().toISOString(), pending: { kind: 'registration', since: new Date().toISOString(), etaMinutes: 30, checks: [], operationId: r.operationId } }); fs2.writeFileSync(f, JSON.stringify(cur));
const f1 = await d.finalizeRegistration('dom1', 'admin'); assert.equal(f1.done, false, '아직 처리 중');
const p1 = await d.propagation('dom1'); assert.equal(p1.pending, true); assert.match(p1.message, /등록을 처리/);
state.ops[r.operationId].Status = 'SUCCESSFUL'; state.domains['slow.com'] = { exp: Math.floor(Date.now() / 1000) + 365 * 86400, ns: ['ns-a', 'ns-b'] };
const f2 = await d.finalizeRegistration('dom1', 'admin'); assert.equal(f2.done, true); assert.match(f2.detail, /등록 완료/);
const after = JSON.parse(fs2.readFileSync(f, 'utf8')).domains.find((x) => x.id === 'dom1'); assert.equal(after.status, 'ACT'); assert.ok(after.expiresAt); assert.match(after.zoneError || '', /DNS 공급자/, 'DNS 공급자 없음 → 영역 생략 안내');
assert.equal((await d.finalizeRegistration('dom1', 'admin')).done, true, '멱등');
});
test('잘못된 키는 403 메시지로 안내한다', async () => {
require(ROOT + '/lib/vault').set({ kind: 'route53', secret: 'secret', settings: { ACCESS_KEY_ID: 'AKIAWRONG' } });
await assert.rejects(reg.listDomains(), (e) => e.status === 403 && /InvalidSignatureException/.test(e.message));
});