diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 491038a..9054b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,6 @@ jobs: - run: npm test test-win: - # if: false needs: [ get-lts ] runs-on: windows-latest strategy: diff --git a/.gitignore b/.gitignore index 323e2c5..b7b20fa 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ dist .pnp.* package-lock.json +conf.d/*.pem diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..825faf6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +.release/ diff --git a/.release b/.release index bfcd8a1..e0a2d64 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit bfcd8a1217b915350b9959d2b1006fa75f0e2c03 +Subproject commit e0a2d645d6ee9da2588a72e6004c547289ecc381 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf93697..f2cab30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [3.0.0-alpha.10] - 2026-03-25 + +- config: replace .yaml with .toml +- zone_record can be empty, default 0 +- feat(zone records): create and delete + ### [3.0.0-alpha.9] - 2026-03-15 - feat(zone): use DataTable for list, added search/limit options @@ -64,3 +70,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). [3.0.0-alpha.7]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.7 [3.0.0-alpha.8]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8 [3.0.0-alpha.9]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.9 +[3.0.0-alpha.10]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.10 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b321d59..e7a2587 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,7 +2,7 @@ This handcrafted artisanal software is brought to you by: -|
msimerson (16)| +|
msimerson (18)| | :---: | this file is generated by [.release](https://github.com/msimerson/.release). diff --git a/README.md b/README.md index 89c71ac..8d0ef53 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # NicTool API v3 +A RESTful JSON web service that exposes DNS management functions to users. ## Install diff --git a/conf.d/http.toml b/conf.d/http.toml new file mode 100644 index 0000000..54f70cd --- /dev/null +++ b/conf.d/http.toml @@ -0,0 +1,18 @@ +host = "localhost" +port = 3000 +keepAlive = false +group = "NicTool" + +[jwt] +key = "af1b926a5e21f535c4f5b6c42941c4cf" + +[cookie] +# https://hapi.dev/module/cookie/api/?v=12.0.1 +name = "sid-nictool" +ttl = 3600000 # 1 hour +path = "/" +clearInvalid = true +isSameSite = "Strict" +isSecure = true +isHttpOnly = false +password = "" # hint: openssl rand -hex 16 diff --git a/conf.d/http.yml b/conf.d/http.yml deleted file mode 100644 index 4d2a673..0000000 --- a/conf.d/http.yml +++ /dev/null @@ -1,64 +0,0 @@ -default: - host: localhost - port: 3000 - tls: - key: null - cert: null - jwt: - key: 'af1b926a5e21f535c4f5b6c42941c4cf' - cookie: - # https://hapi.dev/module/cookie/api/?v=12.0.1 - name: sid-nictool - password: af1b926a5e21f535c4f5b6c42941c4cf - ttl: 3600000 # 1 hour - # domain: - path: / - clearInvalid: true - isSameSite: Strict - isSecure: true - isHttpOnly: false - keepAlive: false - # redirectTo: - group: NicTool - -production: - port: 8080 - cookie: - # Set your own secret password. hint: openssl rand -hex 16 - # password: - -test: - cookie: - isSecure: false - password: ^NicTool.Is,The#Best_Dns-Manager$ - -development: - host: box-under-my-desk.example.com - tls: - key: | - -----BEGIN PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwBx1Qt9309i89 - O9Y8bhHO9BqyWWzd0hXI1o3d8Zn4aT2lhwmeeu2oSQsczvny0cJSs6HYe6asI6XZ - - Ane1BnOJ6/E+7Clo463N++OS - -----END PRIVATE KEY----- - - cert: | - -----BEGIN CERTIFICATE----- - MIID9DCCAtygAwIBAgIUF+ziLgjIA3qCf95DmVskHqSNvLUwDQYJKoZIhvcNAQEL - BQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xGjAYBgNVBAoM - - juZxYqQoPYBpk+eG/sudGGFKKGow1RbGbbNUrqATYxJCqPrN0mZuNkAgATbQtBjS - vyvASCDueS0= - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - MIID2TCCAsGgAwIBAgIUF+ziLgjIA3qCf95DmVskHqSNvLEwDQYJKoZIhvcNAQEL - BQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xGjAYBgNVBAoM - - iMt4AE3zfKgj/OLyAeseUlqukbnBQYlTiMUuPLTTp6d7uBi8/VuXBTrZ9nafPvSZ - TqccpFMgxCeImsJCgO5hBJYUTELDNEmJS5Vgy3Y= - -----END CERTIFICATE----- - - cookie: - # isSecure: false - password: ^NicTool.Is,The#Best_Dns-Manager$ diff --git a/conf.d/mysql.toml b/conf.d/mysql.toml new file mode 100644 index 0000000..d582b07 --- /dev/null +++ b/conf.d/mysql.toml @@ -0,0 +1,9 @@ +host = "127.0.0.1" +port = 3306 +socketPath = "" +user = "nictool" +database = "nictool" +timezone = "+00:00" +dateStrings = ["DATETIME", "TIMESTAMP"] +decimalNumbers = true +password = "" diff --git a/conf.d/mysql.yml b/conf.d/mysql.yml deleted file mode 100644 index ff73d87..0000000 --- a/conf.d/mysql.yml +++ /dev/null @@ -1,30 +0,0 @@ -# default settings apply to EVERY deployment -default: - host: 127.0.0.1 - port: 3306 - user: nictool - database: nictool - timezone: +00:00 - dateStrings: - - DATETIME - - TIMESTAMP - decimalNumbers: true - -# settings below this line override default settings -production: - host: mysql - password: '********' - -# used for CI testing (GitHub Actions workflows) -test: - user: root - password: root - -# used by code coverage testing -cov: - user: root - password: root - -development: - password: StaySafeOutThere - # socketPath: /opt/local/var/run/mysql82/mysqld.sock diff --git a/eslint.config.mjs b/eslint.config.mjs index 3ed7310..b5b5ee3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,39 +1,23 @@ -import globals from "globals"; -import babelParser from "@babel/eslint-parser"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; +import globals from 'globals' +import js from '@eslint/js' +import prettier from 'eslint-config-prettier' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}); - -export default [...compat.extends("eslint:recommended"), { +export default [ + { + ignores: ['**/package-lock.json', 'node_modules/**', '.release/**'], + }, + js.configs.recommended, + prettier, + { languageOptions: { - globals: { - ...globals.node, - }, - - parser: babelParser, - ecmaVersion: "latest", - sourceType: "module", - - parserOptions: { - babelOptions: { - configFile: false, - plugins: ["@babel/plugin-syntax-import-attributes"], - }, - - requireConfigFile: false, - }, + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node + }, }, - rules: { - "no-unused-vars": "warn" + 'no-unused-vars': 'warn', }, -}]; + }, +] diff --git a/lib/config.js b/lib/config.js index d636954..fecf84c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,63 +1,90 @@ import fs from 'node:fs/promises' import fsSync from 'node:fs' +import path from 'node:path' -import YAML from 'yaml' - -import { setEnv } from './util.js' -setEnv() +import { parse } from 'smol-toml' class Config { - constructor(opts = {}) { + constructor() { this.cfg = {} - this.getEnv(opts) - } - - getEnv(opts = {}) { - this.env = process.env.NODE_ENV ?? opts.env ?? '' this.debug = Boolean(process.env.NODE_DEBUG) - if (this.debug) console.log(`debug: true, env: ${this.env}`) } - async get(name, env) { - this.getEnv() + async get(name) { + this.debug = Boolean(process.env.NODE_DEBUG) - const cacheKey = [name, env ?? this.env].join(':') - if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached + if (this.cfg[name]) return this.cfg[name] - const str = await fs.readFile(`./conf.d/${name}.yml`, 'utf8') - const cfg = YAML.parse(str) + const str = await fs.readFile(`./conf.d/${name}.toml`, 'utf8') + const cfg = parse(str) if (this.debug) console.debug(cfg) - this.cfg[cacheKey] = applyDefaults(cfg[env ?? this.env], cfg.default) - return this.cfg[cacheKey] + if (name === 'http') { + const tls = await loadPEM('./conf.d') + if (tls) cfg.tls = tls + } + + this.cfg[name] = cfg + return cfg } - getSync(name, env) { - this.getEnv() + getSync(name) { + this.debug = Boolean(process.env.NODE_DEBUG) - const cacheKey = [name, env ?? this.env].join(':') - if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached + if (this.cfg[name]) return this.cfg[name] - const str = fsSync.readFileSync(`./conf.d/${name}.yml`, 'utf8') - const cfg = YAML.parse(str) + const str = fsSync.readFileSync(`./conf.d/${name}.toml`, 'utf8') + const cfg = parse(str) if (this.debug) console.debug(cfg) - this.cfg[cacheKey] = applyDefaults(cfg[env ?? this.env], cfg.default) - return this.cfg[cacheKey] + if (name === 'http') { + const tls = loadPEMSync('./conf.d') + if (tls) cfg.tls = tls + } + + this.cfg[name] = cfg + return cfg } } -function applyDefaults(cfg = {}, defaults = {}) { - for (const d in defaults) { - /* c8 ignore next */ - if (d === '__proto__' || d === 'constructor') continue - if ([undefined, null].includes(cfg[d])) { - cfg[d] = defaults[d] - } else if (typeof cfg[d] === 'object' && typeof defaults[d] === 'object') { - cfg[d] = applyDefaults(cfg[d], defaults[d]) - } +async function loadPEM(dir) { + let entries + try { + entries = await fs.readdir(dir) + } catch { + return null + } + const pemFile = entries.find((f) => f.endsWith('.pem')) + if (!pemFile) return null + + const content = await fs.readFile(path.join(dir, pemFile), 'utf8') + return parsePEMBlocks(content) +} + +function loadPEMSync(dir) { + let entries + try { + entries = fsSync.readdirSync(dir) + } catch { + return null + } + const pemFile = entries.find((f) => f.endsWith('.pem')) + if (!pemFile) return null + + const content = fsSync.readFileSync(path.join(dir, pemFile), 'utf8') + return parsePEMBlocks(content) +} + +function parsePEMBlocks(content) { + const keyMatch = content.match(/-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )?PRIVATE KEY-----/) + const certMatches = [...content.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)] + + if (!keyMatch && !certMatches.length) return null + + return { + key: keyMatch ? keyMatch[0] + '\n' : null, + cert: certMatches.length ? certMatches.map((m) => m[0]).join('\n') + '\n' : null, } - return cfg } export default new Config() diff --git a/lib/config.test.js b/lib/config.test.js index ea7740c..78e8eaa 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -5,55 +5,63 @@ import Config from './config.js' describe('config', () => { describe('get', () => { - it(`loads mysql test config`, async () => { - const cfg = await Config.get('mysql', 'test') - assert.deepEqual(cfg, mysqlTestCfg) + it(`loads mysql config`, async () => { + const cfg = await Config.get('mysql') + delete cfg.password; delete cfg.user + assert.deepEqual(cfg, mysqlCfg) }) - it(`loads mysql test config syncronously`, () => { - const cfg = Config.getSync('mysql', 'test') - assert.deepEqual(cfg, mysqlTestCfg) + it(`loads mysql config synchronously`, () => { + const cfg = Config.getSync('mysql') + delete cfg.password; delete cfg.user }) - it(`loads mysql cov config`, async () => { - const cfg = await Config.get('mysql', 'cov') - assert.deepEqual(cfg, mysqlTestCfg) - }) - - it(`loads mysql cov config (from cache)`, async () => { + it(`loads mysql config (from cache)`, async () => { process.env.NODE_DEBUG = 1 - const cfg = await Config.get('mysql', 'cov') - assert.deepEqual(cfg, mysqlTestCfg) + const cfg = await Config.get('mysql') + delete cfg.password; delete cfg.user + assert.deepEqual(cfg, mysqlCfg) process.env.NODE_DEBUG = '' }) - it(`loads http test config`, async () => { - const cfg = await Config.get('http', 'test') - assert.deepEqual(cfg, httpCfg) + it(`loads http config`, async () => { + const cfg = await Config.get('http') + const { tls, ...rest } = cfg + delete rest.password + assert.deepEqual(rest, httpCfg) + }) + + it(`loads http config synchronously`, () => { + const cfg = Config.getSync('http') + const { tls, ...rest } = cfg + delete rest.password + assert.deepEqual(rest, httpCfg) }) - it(`loads http test config syncronously`, () => { - const cfg = Config.getSync('http', 'test') - assert.deepEqual(cfg, httpCfg) + it(`loads tls from conf.d/*.pem when present`, async () => { + const cfg = await Config.get('http') + delete cfg.password + if (!cfg.tls) return // no PEM on this host — skip + assert.match(cfg.tls.key, /-----BEGIN.*PRIVATE KEY-----/) + assert.match(cfg.tls.cert, /-----BEGIN CERTIFICATE-----/) }) it(`detects NODE_DEBUG env`, async () => { process.env.NODE_DEBUG = 1 - await Config.get('mysql', 'test') + await Config.get('mysql') assert.equal(Config.debug, true) process.env.NODE_DEBUG = '' - await Config.get('mysql', 'test') + await Config.get('mysql') assert.equal(Config.debug, false) }) }) }) -const mysqlTestCfg = { +const mysqlCfg = { host: '127.0.0.1', port: 3306, - user: 'root', - password: 'root', + socketPath: '', database: 'nictool', timezone: '+00:00', dateStrings: ['DATETIME', 'TIMESTAMP'], @@ -63,23 +71,19 @@ const mysqlTestCfg = { const httpCfg = { host: 'localhost', port: 3000, - cookie: { - clearInvalid: true, - isHttpOnly: false, - isSameSite: 'Strict', - isSecure: false, - name: 'sid-nictool', - password: '^NicTool.Is,The#Best_Dns-Manager$', - path: '/', - ttl: 3600000, - }, + keepAlive: false, + group: 'NicTool', jwt: { key: 'af1b926a5e21f535c4f5b6c42941c4cf', }, - tls: { - cert: null, - key: null, + cookie: { + name: 'sid-nictool', + ttl: 3600000, + path: '/', + clearInvalid: true, + isSameSite: 'Strict', + isSecure: true, + isHttpOnly: false, + password: '', }, - keepAlive: false, - group: 'NicTool', } diff --git a/lib/zone_record.js b/lib/zone_record.js index e6b391e..9955c38 100644 --- a/lib/zone_record.js +++ b/lib/zone_record.js @@ -19,7 +19,8 @@ class ZoneRecord { if (g.length === 1) return g[0].id } - new RR[args.type](args) + const rrArgs = args.ttl === undefined ? { ...args, default: { ttl: 0 } } : args + new RR[args.type](rrArgs) args = objectToDb(args) @@ -166,9 +167,9 @@ function unApplyMap(obj, map) { } if (obj.type === 'NSEC3') { const [algo, flags, iters, salt, bitmaps, next] = obj.address.slice(1, -1).split("','") - obj['hash algorithm'] = /^\d+$/.test(algo) ? parseInt(algo) : algo ?? '' - obj.flags = /^\d+$/.test(flags) ? parseInt(flags) : flags ?? '' - obj.iterations = /^\d+$/.test(iters) ? parseInt(iters) : iters ?? '' + obj['hash algorithm'] = /^\d+$/.test(algo) ? parseInt(algo) : (algo ?? '') + obj.flags = /^\d+$/.test(flags) ? parseInt(flags) : (flags ?? '') + obj.iterations = /^\d+$/.test(iters) ? parseInt(iters) : (iters ?? '') obj.salt = salt obj['type bit maps'] = bitmaps obj['next hashed owner name'] = next @@ -177,9 +178,9 @@ function unApplyMap(obj, map) { } if (obj.type === 'NSEC3PARAM') { const [algo, flags, iters, salt] = obj.address.slice(1, -1).split("','") - obj['hash algorithm'] = /^\d+$/.test(algo) ? parseInt(algo) : algo ?? '' - obj.flags = /^\d+$/.test(flags) ? parseInt(flags) : flags ?? '' - obj.iterations = /^\d+$/.test(iters) ? parseInt(iters) : iters ?? '' + obj['hash algorithm'] = /^\d+$/.test(algo) ? parseInt(algo) : (algo ?? '') + obj.flags = /^\d+$/.test(flags) ? parseInt(flags) : (flags ?? '') + obj.iterations = /^\d+$/.test(iters) ? parseInt(iters) : (iters ?? '') obj.salt = salt delete obj.address delete map.address diff --git a/lib/zone_record.test.js b/lib/zone_record.test.js index ee0a4ff..67820e1 100644 --- a/lib/zone_record.test.js +++ b/lib/zone_record.test.js @@ -12,6 +12,28 @@ after(async () => { }) describe('zone_record', function () { + it('CREATE accepts omitted ttl and stores 0', async () => { + const testCase = { + id: 60001, + zid: 1, + owner: 'missing-ttl.example.com.', + type: 'A', + address: '203.0.113.45', + } + + await ZoneRecord.destroy({ id: testCase.id }) + + try { + await ZoneRecord.create(testCase) + const zrs = await ZoneRecord.get({ id: testCase.id }) + assert.equal(zrs[0].ttl, 0) + assert.equal(zrs[0].owner, testCase.owner) + assert.equal(zrs[0].type, testCase.type) + } finally { + await ZoneRecord.destroy({ id: testCase.id }) + } + }) + for (const rrType of fs.readdirSync('./lib/test/rrs')) { // console.log(rrType) // if (rrType !== 'tlsa.json') continue diff --git a/package.json b/package.json index 0c1e6b0..0831d88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nictool/api", - "version": "3.0.0-alpha.9", + "version": "3.0.0-alpha.10", "description": "NicTool API", "main": "index.js", "type": "module", @@ -14,6 +14,7 @@ "server.js" ], "scripts": { + "clean": "rm -rf node_modules package-lock.json", "format": "npm run lint:fix && npm run prettier:fix", "lint": "npx eslint *.js **/*.js", "lint:fix": "npm run lint -- --fix", @@ -23,9 +24,10 @@ "develop": "NODE_ENV=development node --watch server.js ./server", "test": "./test.sh", "test:develop": "NODE_ENV=development ./test.sh", - "versions": "npx dependency-version-checker check", - "versions:fix": "npx dependency-version-checker update", - "watch": "./test.sh watch" + "versions": "npx npm-dep-mgr check", + "versions:fix": "npx npm-dep-mgr update", + "watch": "./test.sh watch", + "test:coverage": "npx c8 --reporter=text --reporter=text-summary npm test" }, "repository": { "type": "git", @@ -44,23 +46,24 @@ }, "homepage": "https://github.com/NicTool/api#readme", "devDependencies": { - "@babel/eslint-parser": "^7.27.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "eslint": "^9.24.0" + "@eslint/js": "^10.0.1", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.4.0" }, "dependencies": { "@hapi/cookie": "^12.0.1", - "@hapi/hapi": "^21.4.0", + "@hapi/hapi": "^21.4.7", "@hapi/hoek": "^11.0.7", "@hapi/inert": "^7.1.0", - "@hapi/jwt": "^3.2.0", + "@hapi/jwt": "^3.2.3", "@hapi/vision": "^7.0.3", - "@nictool/dns-resource-record": "^1.2.2", - "@nictool/validate": "^0.8.2", + "@nictool/dns-resource-record": "^1.5.0", + "@nictool/validate": "^0.8.8", "hapi-swagger": "^17.3.2", - "mysql2": "^3.14.0", - "qs": "^6.14.0", - "yaml": "^2.7.1" + "mysql2": "^3.20.0", + "qs": "^6.15.0", + "smol-toml": "^1.6.1" }, "prettier": { "printWidth": 110, @@ -68,4 +71,4 @@ "singleQuote": true, "trailingComma": "all" } -} +} \ No newline at end of file diff --git a/routes/zone_record.js b/routes/zone_record.js index 137dbb8..eafbc75 100644 --- a/routes/zone_record.js +++ b/routes/zone_record.js @@ -82,7 +82,7 @@ function ZoneRecordRoutes(server) { return h .response({ - zone_record: zrs[0], + zone_record: zrs, meta: { api: meta.api, msg: `the zone record was created`, @@ -120,15 +120,19 @@ function ZoneRecordRoutes(server) { .code(404) } - const r = await ZoneRecord.delete({ + await ZoneRecord.delete({ id: zrs[0].id, deleted: 1, }) - console.log(`deleted`, r) + + const deletedZrs = await ZoneRecord.get({ + id: zrs[0].id, + deleted: true, + }) return h .response({ - zone: zrs[0], + zone_record: deletedZrs, meta: { api: meta.api, msg: `I deleted that zone record`, diff --git a/routes/zone_record.test.js b/routes/zone_record.test.js new file mode 100644 index 0000000..e7d8c20 --- /dev/null +++ b/routes/zone_record.test.js @@ -0,0 +1,184 @@ +import assert from 'node:assert/strict' +import { describe, it, before, after } from 'node:test' + +import { init } from './index.js' +import Group from '../lib/group.js' +import User from '../lib/user.js' +import Zone from '../lib/zone.js' +import ZoneRecord from '../lib/zone_record.js' + +import groupCase from './test/group.json' with { type: 'json' } +import userCase from './test/user.json' with { type: 'json' } +import zoneCase from './test/zone.json' with { type: 'json' } + +let server +const createdZoneRecordIds = [] + +const testGroupId = 5094 +const testZoneId = 5095 +const testZoneRecordId = 5096 + +const testZone = { + ...zoneCase, + id: testZoneId, + gid: testGroupId, + zone: 'route-zr-delete.example.com', +} + +const testZoneRecord = { + id: testZoneRecordId, + zid: testZoneId, + owner: 'www.route-zr-delete.example.com.', + ttl: 300, + type: 'A', + address: '203.0.113.6', +} + +before(async () => { + await ZoneRecord.destroy({ id: testZoneRecordId }) + await Zone.destroy({ id: testZoneId }) + + const testGroup = { ...groupCase, id: testGroupId } + const testUser = { + ...userCase, + id: testGroupId, + gid: testGroupId, + email: 'route-zr-delete@example.com', + username: `route-zr-delete-${testGroupId}`, + } + + await Group.create(testGroup) + await User.create(testUser) + await Zone.create(testZone) + await ZoneRecord.create(testZoneRecord) + + server = await init() +}) + +after(async () => { + for (const id of createdZoneRecordIds) { + await ZoneRecord.destroy({ id }) + } + await ZoneRecord.destroy({ id: testZoneRecordId }) + await Zone.destroy({ id: testZoneId }) + await server.stop() +}) + +describe('zone_record routes', () => { + let auth = { headers: {} } + + it('POST /session establishes a session', async () => { + const res = await server.inject({ + method: 'POST', + url: '/session', + payload: { + username: `route-zr-delete-${testGroupId}@${groupCase.name}`, + password: userCase.password, + }, + }) + + assert.equal(res.statusCode, 200) + assert.ok(res.result.session.token) + auth.headers = { Authorization: `Bearer ${res.result.session.token}` } + }) + + it('POST /zone_record creates and returns array payload', async () => { + const res = await server.inject({ + method: 'POST', + url: '/zone_record', + headers: auth.headers, + payload: { + zid: testZoneId, + owner: 'new.route-zr-delete.example.com.', + ttl: 300, + type: 'A', + address: '203.0.113.7', + }, + }) + + assert.equal(res.statusCode, 201) + assert.ok(Array.isArray(res.result.zone_record)) + assert.equal(res.result.zone_record.length, 1) + assert.equal(res.result.zone_record[0].type, 'A') + assert.equal(res.result.zone_record[0].owner, 'new.route-zr-delete.example.com.') + + createdZoneRecordIds.push(res.result.zone_record[0].id) + }) + + it('POST /zone_record accepts omitted ttl and stores 0', async () => { + const res = await server.inject({ + method: 'POST', + url: '/zone_record', + headers: auth.headers, + payload: { + zid: testZoneId, + owner: 'default-ttl.route-zr-delete.example.com.', + type: 'A', + address: '203.0.113.8', + }, + }) + + assert.equal(res.statusCode, 201) + assert.ok(Array.isArray(res.result.zone_record)) + assert.equal(res.result.zone_record.length, 1) + assert.equal(res.result.zone_record[0].ttl, 0) + assert.equal(res.result.zone_record[0].owner, 'default-ttl.route-zr-delete.example.com.') + + createdZoneRecordIds.push(res.result.zone_record[0].id) + }) + + it(`DELETE /zone_record/${testZoneRecordId} soft-deletes record`, async () => { + const res = await server.inject({ + method: 'DELETE', + url: `/zone_record/${testZoneRecordId}`, + headers: auth.headers, + }) + + assert.equal(res.statusCode, 200) + assert.ok(Array.isArray(res.result.zone_record)) + assert.equal(res.result.zone_record[0].id, testZoneRecordId) + assert.equal(res.result.zone_record[0].deleted, true) + }) + + it(`GET /zone_record/${testZoneRecordId} hides deleted by default`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/zone_record/${testZoneRecordId}`, + headers: auth.headers, + }) + + assert.equal(res.statusCode, 200) + assert.deepEqual(res.result.zone_record, []) + }) + + it(`GET /zone_record/${testZoneRecordId}?deleted=true returns soft-deleted record`, async () => { + const res = await server.inject({ + method: 'GET', + url: `/zone_record/${testZoneRecordId}?deleted=true`, + headers: auth.headers, + }) + + assert.equal(res.statusCode, 200) + assert.equal(res.result.zone_record[0].id, testZoneRecordId) + assert.equal(res.result.zone_record[0].deleted, true) + }) + + it(`DELETE /zone_record/${testZoneRecordId} returns 404 when already deleted`, async () => { + const res = await server.inject({ + method: 'DELETE', + url: `/zone_record/${testZoneRecordId}`, + headers: auth.headers, + }) + + assert.equal(res.statusCode, 404) + }) + + it('DELETE /session', async () => { + const res = await server.inject({ + method: 'DELETE', + url: '/session', + headers: auth.headers, + }) + assert.equal(res.statusCode, 200) + }) +}) diff --git a/test.sh b/test.sh index a53ee00..55ab372 100755 --- a/test.sh +++ b/test.sh @@ -2,6 +2,11 @@ set -eu +if [ "${CI:-}" = "true" ]; then + sed -i.bak 's/^user[[:space:]]*=.*/user = "root"/' conf.d/mysql.toml + sed -i.bak 's/^password[[:space:]]*=.*/password = "root"/' conf.d/mysql.toml +fi + NODE="node --no-warnings=ExperimentalWarning" $NODE test-fixtures.js teardown $NODE test-fixtures.js setup