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