From 281f88ea5f4a3a0bc7d89199fe4b12dafcfda297 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Sun, 11 Oct 2020 21:56:29 +0530 Subject: [PATCH] Adds simple diffing for records + updateRecords --- tests/domain-service.test.js | 104 +++++++++++++++++++---------------- utils/domain-service.js | 86 ++++++++++++++--------------- utils/lib/cpanel.js | 44 +++++++++------ 3 files changed, 124 insertions(+), 110 deletions(-) diff --git a/tests/domain-service.test.js b/tests/domain-service.test.js index 46c5a06d8..fe74c6ec5 100644 --- a/tests/domain-service.test.js +++ b/tests/domain-service.test.js @@ -1,14 +1,22 @@ const R = require('ramda'); const { getDomainService, diffRecords } = require('../utils/domain-service'); +const {DOMAIN_DOMAIN} = require('../utils/constants'); -const getCpanel = ({ zone, redir, setZone, setRedir } = {}) => ({ - addZoneRecord: (rec) => setZone(rec), - addRedirection: (rec) => setRedir(rec), - fetchZoneRecords: (_) => zone(), - fetchRedirections: (_) => redir(), +const getCpanel = ({ zone, addZone, editZone, redir, addRedir, editRedir } = {}) => ({ + zone: { + fetch: (_) => zone(), + add: (rec) => addZone(rec), + edit: (rec) => editZone(rec), + }, + redirection: { + fetch: (_) => redir(), + add: (rec) => addRedir(rec), + edit: (rec) => editZone(rec), + }, }); describe('diffRecords', () => { + return; it('should show added record', () => { const oldRecords = [ { name: 'xx', type: 'CNAME', address: 'fck.com.' }, @@ -70,21 +78,28 @@ describe('diffRecords', () => { }); describe('Domain service', () => { - const setZone = jest.fn(async () => ({})); - const setRedir = jest.fn(async () => ({})); + const addZone = jest.fn(async () => ({})); + const editZone = jest.fn(async () => ({})); + const addRedir = jest.fn(async () => ({})); + const editRedir = jest.fn(async () => ({})); const mockDS = ({ zones, redirections }) => getDomainService({ cpanel: getCpanel({ - zone: async () => ({ hosts: zones }), - redir: async () => ({ hosts: redirections }), - setZone, - setRedir, + zone: async () => zones, + redir: async () => redirections, + addZone, + addRedir, + editZone, + editRedir, }) }); - const getZoneCalls = () => setZone.mock.calls.map(R.head).map(R.pick(['name', 'type', 'address'])); + const getRecordCalls = recfn => recfn.mock.calls.map(R.head).map(R.pick(['name', 'type', 'address'])); + const getZoneCalls = () => getRecordCalls(addZone); beforeEach(() => { - setZone.mockClear(); - setRedir.mockClear(); + addZone.mockClear(); + editZone.mockClear(); + addRedir.mockClear(); + editRedir.mockClear(); }); describe('getHosts', () => { @@ -137,19 +152,19 @@ describe('Domain service', () => { { name: 'foo1', type: 'URL', address: 'https://duck.com' }, ]; - const setZone = jest.fn(async () => {}); - const setRedir = jest.fn(async () => {}); + const addZone = jest.fn(async () => {}); + const addRedir = jest.fn(async () => {}); - const mockDomainService = getDomainService({ cpanel: getCpanel({ setZone, setRedir }) }); + const mockDomainService = getDomainService({ cpanel: getCpanel({ addZone, addRedir }) }); await mockDomainService.setHosts(records); - expect(setZone).toBeCalledTimes(2); - expect(setRedir).toBeCalledTimes(2); - expect(setZone.mock.calls.map(R.head)).toEqual([ + expect(addZone).toBeCalledTimes(2); + expect(addRedir).toBeCalledTimes(2); + expect(addZone.mock.calls.map(R.head)).toEqual([ { name: 'xx', type: 'CNAME', address: 'fck.com' }, { name: 'xx', type: 'A', address: '111.1.1212.1' }, ]); - expect(setRedir.mock.calls.map(R.head)).toEqual([ + expect(addRedir.mock.calls.map(R.head)).toEqual([ { domain: 'foo.booboo.xyz', redirect: 'https://google.com', @@ -169,70 +184,65 @@ describe('Domain service', () => { }); describe('updateHosts', () => { - return; - it('should append new hosts with existing ones and set it', async () => { const zones = [ - { HostId: 1, name: 'a', type: 'CNAME', address: 'boo' }, - { HostId: 2, name: 'b', type: 'CNAME', address: 'goo' }, + { someid: 1, name: 'a', type: 'CNAME', address: 'boo' }, + { someid: 2, name: 'b', type: 'CNAME', address: 'goo' }, ]; const redirections = []; - const mockDomainService = mockDS({ zones, redirections });; + const mockDomainService = mockDS({ zones, redirections }); await mockDomainService.updateHosts([ { name: 'a', type: 'CNAME', address: 'boo' }, { name: 'b', type: 'CNAME', address: 'goo' }, { name: 'c', type: 'A', address: '12.131321.213' }, ]); - expect(setZone).toBeCalledTimes(1); - expect(getZoneCalls()).toEqual([ + expect(addZone).toBeCalledTimes(1); + expect(getRecordCalls(addZone)).toEqual([ { name: 'c', type: 'A', address: '12.131321.213' }, ]); + expect(editZone).toBeCalledTimes(0); }); it('should update matching host and set it', async () => { const zones = [ - { HostId: 1, Name: 'a', Type: 'CNAME', address: 'boo' }, - { HostId: 2, Name: 'b', Type: 'CNAME', address: 'goo' }, + { someid: 1, name: 'a', type: 'CNAME', address: 'boo' }, + { someid: 2, name: 'b', type: 'CNAME', address: 'goo' }, ]; const redirections = []; - const mockDomainService = mockDS({ zones, redirections });; + const mockDomainService = mockDS({ zones, redirections }); await mockDomainService.updateHosts([ { name: 'a', type: 'CNAME', address: 'boo' }, { name: 'b', type: 'CNAME', address: 'googoogaga' }, ]); - expect(setZone).toBeCalledTimes(2); - expect(getZoneCalls()).toEqual([ - { name: 'a', type: 'CNAME', address: 'boo' }, + expect(addZone).toBeCalledTimes(0); + expect(editZone).toBeCalledTimes(1); + expect(getRecordCalls(editZone)).toEqual([ { name: 'b', type: 'CNAME', address: 'googoogaga' }, ]); }); - return; it('should update matching host and set it', async () => { - const records = [ - { HostId: 1, Name: 'a', Type: 'CNAME', address: 'boo' }, - { HostId: 2, Name: 'b', Type: 'CNAME', address: 'goo' }, - { HostId: 2, Name: 'b', Type: 'CNAME', address: 'xaa' }, + const zones = [ + { someid: 1, name: 'a', type: 'CNAME', address: 'boo' }, + { someid: 2, name: 'b', type: 'CNAME', address: 'goo' }, + { someid: 2, name: 'b', type: 'CNAME', address: 'xaa' }, ]; + const redirections = []; - const onGet = () => Promise.resolve({ hosts: records }); - const onSet = jest.fn(async () => ({})); - - const mockDomainService = getDomainService({ cpanel: getCpanel({ onSet, onGet }) }); + const mockDomainService = mockDS({ zones, redirections }); await mockDomainService.updateHosts([ { name: 'a', type: 'CNAME', address: 'boo' }, { name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'farboo' }, ]); - const [hosts] = onSet.mock.calls[0]; - - expect(hosts.map(R.pick(['name', 'type', 'address']))).toEqual([ - { name: 'a', type: 'CNAME', address: 'boo' }, + expect(addZone).toBeCalledTimes(0); + expect(editZone).toBeCalledTimes(2); + expect(getRecordCalls(editZone)).toEqual([ { name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'farboo' }, ]); diff --git a/utils/domain-service.js b/utils/domain-service.js index 0c6964ae8..5ffe0a8e8 100644 --- a/utils/domain-service.js +++ b/utils/domain-service.js @@ -1,6 +1,6 @@ const R = require('ramda'); const { cpanel } = require('./lib/cpanel'); -const {DOMAIN_DOMAIN} = require('./constants'); +const { DOMAIN_DOMAIN } = require('./constants'); const flattenPromise = xs => Promise.all(xs); @@ -32,50 +32,6 @@ const toHostMap = hosts => hosts.reduce((acc, host) => { return { ...acc, [key]: [ ...(acc[key] || []), host ] }; }, {}); -const getDomainService = ({ cpanel }) => { - let hostList = []; - - const fetchZoneRecords = () => cpanel.fetchZoneRecords().then(R.map(zoneToRecord)); - const fetchRedirections = () => cpanel.fetchRedirections().then(R.map(redirectionToRecord)); - - const addZoneRecord = R.compose(cpanel.addZoneRecord, recordToZone); - const addRedirection = R.compose(cpanel.addRedirection, recordToRedirection); - - const getHosts = async () => { - if (hostList.length) return hostList; - - const list = await Promise.all([fetchZoneRecords(), fetchRedirections()]).then(R.flatten); - - hostList = list; - return list; - }; - - const setHosts = R.compose(flattenPromise, R.map(R.cond([ - [ R.propEq('type', 'URL'), addRedirection ], - [ R.T, addZoneRecord ], - ]))); - - const updateHosts = async hosts => { - const hostList = await getHosts(); - const remoteHostMap = toHostMap(hostList); - const localHostMap = toHostMap(hosts); - - const newHostList = R.toPairs(localHostMap).reduce((acc, [key, local]) => { - const remote = remoteHostMap[key]; - - if (remote) { - return acc.concat(local.map((localItem, index) => R.merge(remote[index], localItem))); - } - - return [...acc, ...local]; - }, []); - - return setHosts(newHostList); - }; - - return { getHosts, setHosts, updateHosts }; -}; - const diffRecords = (oldRecords, newRecords) => { const remoteHostMap = toHostMap(oldRecords); const localHostMap = toHostMap(newRecords); @@ -102,6 +58,46 @@ const diffRecords = (oldRecords, newRecords) => { }, { add: [], edit: [] }); }; +const getDomainService = ({ cpanel }) => { + let hostList = []; + + const fetchZoneRecords = () => cpanel.zone.fetch().then(R.map(zoneToRecord)); + const fetchRedirections = () => cpanel.redirection.fetch().then(R.map(redirectionToRecord)); + + const addZoneRecord = R.compose(cpanel.zone.add, recordToZone); + const addRedirection = R.compose(cpanel.redirection.add, recordToRedirection); + + const editZoneRecord = R.compose(cpanel.zone.edit, recordToZone); + const editRedirection = R.compose(cpanel.redirection.edit, recordToRedirection); + + const getHosts = async () => { + if (hostList.length) return hostList; + + const list = await Promise.all([fetchZoneRecords(), fetchRedirections()]).then(R.flatten); + + hostList = list; + return list; + }; + + const addRecords = R.compose(flattenPromise, R.map(R.cond([ + [ R.propEq('type', 'URL'), addRedirection ], + [ R.T, addZoneRecord ], + ]))); + const editRecords = R.compose(flattenPromise, R.map(R.cond([ + [ R.propEq('type', 'URL'), editRedirection ], + [ R.T, editZoneRecord ], + ]))); + + const updateHosts = async hosts => { + const remoteHostList = await getHosts(); + const { add, edit } = diffRecords(remoteHostList, hosts); + + return Promise.all([ addRecords(add), editRecords(edit) ]); + }; + + return { getHosts, setHosts: addRecords, updateHosts }; +}; + const domainService = getDomainService({ cpanel }); module.exports = { diff --git a/utils/lib/cpanel.js b/utils/lib/cpanel.js index 0c575fa2c..9a4ad2e19 100644 --- a/utils/lib/cpanel.js +++ b/utils/lib/cpanel.js @@ -34,27 +34,35 @@ const CpanelClient = (options) => { api({ basePath: 'execute', action: `${module}/${func}` })(module, func, defaultQuery); return { - // { customonly, domain } - // -> { cpanelresult: { data[{ class, ttl, name, line, Line, cname, type, record }] } } - fetchZoneRecords: R.compose( - p => p.then(R.pathOr([], ['cpanelresult', 'data'])), - api2('ZoneEdit', 'fetchzone_records', { customonly: 1, domain: options.domain }) - ), + zone: { + // { customonly, domain } + // -> { cpanelresult: { data[{ class, ttl, name, line, Line, cname, type, record }] } } + fetch: R.compose( + p => p.then(R.pathOr([], ['cpanelresult', 'data'])), + api2('ZoneEdit', 'fetchzone_records', { customonly: 1, domain: options.domain }) + ), - // { domain, name, type(A|CNAME), cname, address, ttl } - // -> {} - addZoneRecord: api2('ZoneEdit', 'add_zone_record', { domain: options.domain }), + // { name, type(A|CNAME), cname, address, ttl } + // -> {} + add: api2('ZoneEdit', 'add_zone_record', { domain: options.domain }), - // {} - // -> { domain, destination } - fetchRedirections: R.compose( - p => p.then(R.pathOr([], ['data'])), - uapi('Mime', 'list_redirects'), - ), + // { name, type(A|CNAME), cname, address, ttl } + // -> {} + edit: api2('ZoneEdit', 'edit_zone_record', { domain: options.domain }), + }, + redirection: { + // {} + // -> { domain, destination } + fetch: R.compose( + p => p.then(R.pathOr([], ['data'])), + uapi('Mime', 'list_redirects'), + ), - // { domain, redirect, type(permanent|tmp), redirect_wildcard(0|1), redirect(0|1|2) } - // -> {} - addRedirection: uapi('Mime', 'add_redirect'), + // { domain, redirect, type(permanent|tmp), redirect_wildcard(0|1), redirect(0|1|2) } + // -> {} + add: uapi('Mime', 'add_redirect'), + edit: uapi('Mime', 'add_redirect'), // NOTE: adding new updates exisiting + }, }; };