commit 6f4ae7de6554892ad1c2ccb82afaa66645c2ca0e
Author: Sam Therapy <sam@samtherapy.net>
Date:   Sat Jun 18 20:11:46 2022 +0200

    Initial commit
    
    Signed-off-by: Sam Therapy <sam@samtherapy.net>

diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..cdce2ac
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,13 @@
+kind: pipeline
+type: docker
+name: default
+steps:
+  - name: lint
+    image: denoland/deno:alpine
+    commands:
+      - deno fmt --check
+      - deno lint
+  - name: test
+    image: denoland/deno:alpine
+    commands:
+      - deno test -A
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c640434
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,136 @@
+# ---> Node
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+.dccache
+
+result.json
+*.vtt
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e40716f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+  "deno.enable": true,
+  "deno.lint": true,
+  "deno.unstable": true
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5ed684c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,5 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c34d388
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# awl: drill, writ small (and using deno)
+
+Install:
+`deno install --allow-net https://git.froth.zone/sam/awl/raw/branch/master/mod.ts`
diff --git a/deps.ts b/deps.ts
new file mode 100644
index 0000000..c0d035e
--- /dev/null
+++ b/deps.ts
@@ -0,0 +1,6 @@
+export { parse } from "https://deno.land/std@0.144.0/flags/mod.ts";
+export {
+  bold,
+  italic,
+  underline,
+} from "https://deno.land/std@0.139.0/fmt/colors.ts";
diff --git a/lib/query.ts b/lib/query.ts
new file mode 100644
index 0000000..a758d2d
--- /dev/null
+++ b/lib/query.ts
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MIT
+import { QueryResponse, ServerOptions } from "./utils.ts";
+
+/**
+ * @param domain Domain to query
+ * @param query
+ * @param server {@link utils.ts/ServerOptions}
+ * @returns
+ */
+export async function doQuery(
+  domain: string,
+  query: Deno.RecordType,
+  server?: ServerOptions,
+) {
+  const response: QueryResponse = {} as QueryResponse;
+  if (!server?.server) {
+    const t0 = performance.now();
+    await Deno.resolveDns(domain, query)
+      // If there's no error
+      .then((value) => {
+        const t1 = performance.now();
+        response.time = t1 - t0;
+        response.response = "NOERROR";
+        response.dnsResponse = value;
+      })
+      // If there is an error
+      .catch((e: Error) => {
+        const t1 = performance.now();
+        response.time = t1 - t0;
+        switch (e.name) {
+          case "NotFound":
+            response.response = "NXDOMAIN";
+            break;
+          default:
+            response.response = "SERVFAIL";
+        }
+      });
+  } else {
+    const t0 = performance.now();
+    await Deno.resolveDns(domain, query, {
+      nameServer: { "ipAddr": server.server, "port": server.port },
+    })
+      // If there's no error
+      .then((value) => {
+        const t1 = performance.now();
+        response.time = t1 - t0;
+        response.response = "NOERROR";
+        response.dnsResponse = value;
+      })
+      // If there is an error
+      .catch((e: Error) => {
+        const t1 = performance.now();
+        response.time = t1 - t0;
+        switch (e.name) {
+          case "NotFound":
+            response.response = "NXDOMAIN";
+            break;
+          default:
+            response.response = "SERVFAIL";
+        }
+      });
+  }
+  return response;
+}
diff --git a/lib/response.ts b/lib/response.ts
new file mode 100644
index 0000000..3ceb3ae
--- /dev/null
+++ b/lib/response.ts
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: MIT
+import {
+  isCAA,
+  isMX,
+  isNAPTR,
+  isSOA,
+  isSRV,
+  isTXT,
+  QueryResponse,
+} from "./utils.ts";
+
+/**
+ * @param res A DNS {@link QueryResponse}
+ * @param domain The domain (or IP, if doing it in reverse) queried
+ * @param query The DNS query that was queried
+ * @returns {string} The DNS response, put in canonical form
+ */
+export function parseResponse(
+  res: QueryResponse,
+  domain: string,
+  query: string,
+  short: boolean,
+): string[] {
+  const answer: string[] = [];
+  switch (query) {
+    case "A":
+    case "AAAA":
+    case "CNAME":
+    case "NS":
+    case "PTR":
+      res.dnsResponse.forEach((ip) => {
+        let dnsQuery = "";
+        if (!short) dnsQuery += `${domain}       IN    ${query}    `;
+        dnsQuery += `${ip}`;
+        answer.push(dnsQuery);
+      });
+      break;
+    case "MX":
+      if (isMX(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          if (!short) {
+            dnsQuery += `${domain}       IN    ${query}    `;
+          }
+          dnsQuery += `${record.preference} ${record.exchange}`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    case "CAA":
+      if (isCAA(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          if (!short) {
+            dnsQuery += `${domain}       IN    ${query}    `;
+          }
+          dnsQuery += `${
+            record.critical ? "1" : "0"
+          } ${record.tag} "${record.value}"`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    case "NAPTR":
+      if (isNAPTR(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          if (!short) dnsQuery += `${domain}       IN    ${query}    `;
+          dnsQuery +=
+            `${record.order} ${record.preference} "${record.flags}" "${record.services}" ${record.regexp} ${record.replacement}`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    case "SOA":
+      if (isSOA(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          if (!short) dnsQuery += `${domain}       IN    ${query}    `;
+          dnsQuery +=
+            `${record.mname} ${record.rname} ${record.serial} ${record.refresh} ${record.retry} ${record.expire} ${record.minimum}`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    case "SRV":
+      if (isSRV(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          if (!short) dnsQuery += `${domain}       IN    ${query}    `;
+          dnsQuery +=
+            `${record.priority} ${record.weight} ${record.port} ${record.target}`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    case "TXT":
+      if (isTXT(res.dnsResponse)) {
+        res.dnsResponse.forEach((record) => {
+          let dnsQuery = "";
+          let txt = "";
+          record.forEach((value) => {
+            txt += `"${value}"`;
+          });
+          if (!short) {
+            dnsQuery += `${domain}       IN    ${query}    `;
+          }
+          dnsQuery += `${txt}`;
+          answer.push(dnsQuery);
+        });
+      }
+      break;
+    default:
+      throw new Error("Not yet implemented");
+  }
+  return answer;
+}
diff --git a/lib/reverse.ts b/lib/reverse.ts
new file mode 100644
index 0000000..3170f64
--- /dev/null
+++ b/lib/reverse.ts
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: MIT
+
+export function parsePTR(ip: string) {
+  if (ip.includes(".")) {
+    // It's an IPv4 address
+    const ptr = ip.split(".");
+    let pop: string | undefined = "not undefined";
+    let domain = "";
+    do {
+      pop = ptr.pop();
+      if (pop) {
+        domain += `${pop}.`;
+      }
+    } while (pop !== undefined);
+    domain += "in-addr.arpa";
+    return domain;
+  } else if (ip.includes(":")) {
+    const parsedIP = parseIPv6(ip);
+    const ptr = parsedIP.split(":");
+    // It's an IPv6 address
+    let pop: string[] | undefined = ["e"];
+    let domain = "";
+    do {
+      pop = ptr.pop()?.split("").reverse();
+      if (pop) {
+        for (const part of pop) {
+          domain += `${part}.`;
+        }
+      }
+    } while (pop !== undefined);
+    domain += "ip6.arpa";
+    return domain;
+  } else {
+    // It's not an address
+    return "";
+  }
+}
+
+export function parseIPv6(addr: string) {
+  addr = addr.replace(/^:|:$/g, "");
+
+  const ipv6 = addr.split(":");
+
+  for (let i = 0; i < ipv6.length; i++) {
+    let hex: string | string[] = ipv6[i];
+    if (hex != "") {
+      // normalize leading zeros
+      // TODO: make this not deprecated
+      ipv6[i] = ("0000" + hex).substr(-4);
+    } else {
+      // normalize grouped zeros ::
+      hex = [];
+      for (let j = ipv6.length; j <= 8; j++) {
+        hex.push("0000");
+      }
+      ipv6[i] = hex.join(":");
+    }
+  }
+
+  return ipv6.join(":");
+}
+
+export function parseNAPTR(phNum: string) {
+  phNum = phNum.toString();
+  phNum = phNum.replace("+", "").replaceAll(" ", "").replaceAll("-", "");
+  const rev = phNum.split("").reverse();
+  let ptr = "";
+  rev.forEach((n) => {
+    ptr += `${n}.`;
+  });
+  ptr += "e164.arpa";
+  return ptr;
+}
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..93442f9
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: MIT
+
+/**
+ * A DNS response
+ */
+export type QueryResponse = {
+  dnsResponse:
+    | string[]
+    | Deno.CAARecord[]
+    | Deno.MXRecord[]
+    | Deno.NAPTRRecord[]
+    | Deno.SOARecord[]
+    | Deno.SRVRecord[]
+    | string[][];
+  response: string;
+  time: number;
+};
+/**
+ * Options for which DNS server to query
+ */
+export type ServerOptions = {
+  server: string;
+  port?: number;
+};
+
+/**
+ * Test if the DNS query is an MX record
+ * @param {QueryResponse["dnsResponse"]} record - DNS response
+ * @returns {boolean} - true if the record is an MX record
+ */
+export function isMX(
+  record: QueryResponse["dnsResponse"],
+): record is Deno.MXRecord[] {
+  return (record as unknown as Deno.MXRecord[])[0].exchange !== undefined;
+}
+/**
+ * Test if the DNS query is a CAA record
+ * @param {QueryResponse["dnsResponse"]} record - DNS response
+ * @returns {boolean} - true if the record is a CAA record
+ */
+export function isCAA(
+  record: QueryResponse["dnsResponse"],
+): record is Deno.CAARecord[] {
+  return (record as unknown as Deno.CAARecord[])[0].critical !== undefined;
+}
+
+/**
+ * Test if the DNS query is an NAPTR record
+ * @param {QueryResponse["dnsResponse"]} record - DNS response
+ * @returns {boolean} - true if the record is an NAPTR record
+ */
+export function isNAPTR(
+  record: QueryResponse["dnsResponse"],
+): record is Deno.NAPTRRecord[] {
+  return (record as unknown as Deno.NAPTRRecord[])[0].regexp !== undefined;
+}
+
+/**
+ * Test if the DNS query is an SOA record
+ * @param {QueryResponse["dnsResponse"]} record - DNS response
+ * @returns {boolean} - true if the record is an SOA record
+ */
+export function isSOA(
+  record: QueryResponse["dnsResponse"],
+): record is Deno.SOARecord[] {
+  return (record as unknown as Deno.SOARecord[])[0].rname !== undefined;
+}
+
+/**
+ * Test if the DNS query is an SRV record
+ * @param {QueryResponse["dnsResponse"]} record - DNS response
+ * @returns {boolean} - true if the record is an SRV record
+ */
+export function isSRV(
+  record: QueryResponse["dnsResponse"],
+): record is Deno.SRVRecord[] {
+  return (record as unknown as Deno.SRVRecord[])[0].port !== undefined;
+}
+
+export function isTXT(
+  record: QueryResponse["dnsResponse"],
+): record is string[][] {
+  return Array.isArray(record as unknown as Array<string>);
+}
diff --git a/main.ts b/main.ts
new file mode 100644
index 0000000..9bd0553
--- /dev/null
+++ b/main.ts
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: MIT
+import { bold, italic, parse, underline } from "./deps.ts";
+import { QueryResponse, ServerOptions } from "./lib/utils.ts";
+import { doQuery } from "./lib/query.ts";
+import { parseResponse } from "./lib/response.ts";
+import { parseNAPTR, parsePTR } from "./lib/reverse.ts";
+
+async function main() {
+  // Parse args
+  const args = parse(Deno.args, {
+    alias: {
+      h: "help",
+      p: "port",
+      s: "short",
+      x: "ptr",
+      V: "version",
+    },
+    boolean: ["help", "ptr", "short", "version"],
+    string: ["port"],
+    default: {
+      "port": "53",
+    },
+  });
+  if (args.help) args.version = true;
+
+  if (args.version) {
+    console.log(
+      `${
+        bold("awl")
+      } version 0.1.0 (running with deno ${Deno.version.deno}, TypeScript ${Deno.version.typescript}, on V8 ${Deno.version.v8})
+Written by (YOUR NAME GOES HERE)`,
+    );
+  }
+
+  if (args.help) {
+    console.log(
+      ` ${bold("Usage:")} awl name ${italic("type")} ${italic("@server")}
+       ${bold("<name>")}    domain name
+       ${bold("<type>")}    defaults to A
+       ${bold("<@server>")} defaults to your local resolver
+
+       arguments ${bold(underline("NEED TO BE"))} in this order\n
+      `,
+      `${underline("Options")}:
+        -p <port> use <port> for query, defaults to 53
+        -s Equivalent to dig +short, only return addresses
+        -x do a reverse lookup
+        -h print this helpful guide
+        -V get the version
+      `,
+    );
+  }
+
+  if (args.version) Deno.exit(0);
+
+  let domain = args._[0]?.toString();
+  domain ??= ".";
+
+  let query: Deno.RecordType =
+    (args._[1] ?? (args.ptr ? "PTR" : "A")) as Deno.RecordType;
+
+  if (domain === ".") query = "NS";
+
+  if (query === "PTR") {
+    // The "server" is an IP address, it needs to become a canonical domain
+    domain = parsePTR(domain);
+  } else if (query === "NAPTR") {
+    // The "server" is a phone number, it needs to become a canonical domain
+    domain = parseNAPTR(domain);
+  }
+
+  if (domain.charAt(domain.length - 1) !== ".") {
+    domain = domain.concat(".");
+  }
+
+  const server: ServerOptions = {
+    server: (args._[2] as string)?.split("@").pop() || "",
+    port: parseInt(args.port),
+  };
+
+  const response: QueryResponse = await doQuery(domain, query, server);
+
+  if (!args.short) {
+    console.log(
+      `;; ->>HEADER<<- opcode: QUERY, rcode: ${response.response}
+;; QUESTION SECTION:
+;; ${domain}    IN    ${query}
+  
+;; ANSWER SECTION:`,
+    );
+  }
+  if (response.response === "NOERROR") {
+    const res = parseResponse(response, domain, query, args.short);
+    res.forEach((answer) => {
+      console.log(answer);
+    });
+  }
+  if (!args.short) {
+    console.log(`
+;; ADDITONAL SECTION:
+
+;; Query time: ${response.time} msec
+;; SERVER: ${displayServer(server)}
+;; WHEN: ${new Date().toLocaleString()}
+        `);
+  }
+}
+
+/**
+ * A handler for displaying the server
+ * @param {ServerOptions} server - The DNS server used
+ * @returns {string} The string used
+ */
+function displayServer(server: ServerOptions): string {
+  let val = "";
+  if (server.server) {
+    val += server.server;
+  }
+  if (server.port != 53) {
+    val += `#${server.port}`;
+  }
+  if (!val) val = "System";
+  return val;
+}
+
+if (import.meta.main) {
+  await main();
+}
diff --git a/mod.ts b/mod.ts
new file mode 100644
index 0000000..2642365
--- /dev/null
+++ b/mod.ts
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT
+
+// Exports for ldawl, the library for awl
+export type { QueryResponse, ServerOptions } from "./lib/utils.ts";
+export { doQuery } from "./lib/query.ts";
+export { parseResponse } from "./lib/response.ts";
+export { parseIPv6, parseNAPTR, parsePTR } from "./lib/reverse.ts";
diff --git a/tests/query_test.ts b/tests/query_test.ts
new file mode 100644
index 0000000..aa9d95b
--- /dev/null
+++ b/tests/query_test.ts
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: MIT
+import { assertEquals } from "./testDeps.ts";
+import { doQuery } from "../lib/query.ts";
+
+Deno.test("Get localhost", async () => {
+  const res = await doQuery("localhost", "A");
+  assertEquals(res.dnsResponse, ["127.0.0.1"]);
+  assertEquals(res.response, "NOERROR");
+});
+
+Deno.test("Get localhost, external NS", async () => {
+  const res = await doQuery("localhost", "AAAA", { server: "1.1.1.1" });
+  assertEquals(res.dnsResponse, ["::1"]);
+  assertEquals(res.response, "NOERROR");
+});
+
+Deno.test("PTR localhost", async () => {
+  const res = await doQuery("1.0.0.127.in-addr.arpa.", "PTR");
+  assertEquals(res.dnsResponse, ["localhost."]);
+});
+
+// This test will fail if this random Bri ish phone number goes down
+// Deno.test("NAPTR, Remote",async () => {
+//   const res = await doQuery("4.4.2.2.3.3.5.6.8.1.4.4.e164.arpa.", "NAPTR");
+//   assertEquals(res.dnsResponse, [
+//     {
+//       order: 100,
+//       preference: 10,
+//       flags: "u",
+//       services: "E2U+sip",
+//       regexp: "!^\\+441865332(.*)$!sip:\\1@nominet.org.uk!",
+//       replacement: "."
+//     },
+//     {
+//       order: 100,
+//       preference: 20,
+//       flags: "u",
+//       services: "E2U+pstn:tel",
+//       regexp: "!^(.*)$!tel:\\1!",
+//       replacement: "."
+//     }
+//   ])
+// })
+
+Deno.test("Get invalid IP, regular NS", async () => {
+  const res = await doQuery("l", "A");
+  assertEquals(res.dnsResponse, undefined);
+  assertEquals(res.response, "NXDOMAIN");
+});
+
+// This isn't supposed to SERVFAIL but idk
+// It also takes forever
+
+// Deno.test("Get invalid IP, external NS", async () => {
+//   const res = await doQuery("b", "AAAA", { server: "1.1.1.1" });
+//   assertEquals(res.dnsResponse, undefined);
+//   assertEquals(res.response, "SERVFAIL");
+// });
diff --git a/tests/response_test.ts b/tests/response_test.ts
new file mode 100644
index 0000000..0313329
--- /dev/null
+++ b/tests/response_test.ts
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: MIT
+import { assertEquals, assertThrows } from "./testDeps.ts";
+import { parseResponse } from "../lib/response.ts";
+import { QueryResponse } from "../lib/utils.ts";
+
+const mockResponse: QueryResponse = {
+  dnsResponse: [],
+  response: "NOERROR",
+  time: 0,
+};
+let domain = "localhost.";
+
+Deno.test("A query", () => {
+  mockResponse.dnsResponse = ["127.0.0.1"];
+  assertEquals(parseResponse(mockResponse, domain, "A", false), [
+    "localhost.       IN    A    127.0.0.1",
+  ]);
+});
+
+Deno.test("AAAA query, short", () => {
+  mockResponse.dnsResponse = ["::1"];
+  assertEquals(parseResponse(mockResponse, domain, "AAAA", true), [
+    "::1",
+  ]);
+});
+
+Deno.test("MX query", () => {
+  mockResponse.dnsResponse = [{
+    exchange: "mail.localhost",
+    preference: 10,
+  }];
+  assertEquals(parseResponse(mockResponse, domain, "MX", false), [
+    "localhost.       IN    MX    10 mail.localhost",
+  ]);
+});
+
+Deno.test("CAA query", () => {
+  mockResponse.dnsResponse = [{
+    critical: false,
+    tag: "issue",
+    value: "pki.goog",
+  }];
+  assertEquals(parseResponse(mockResponse, domain, "CAA", false), [
+    'localhost.       IN    CAA    0 issue "pki.goog"',
+  ]);
+});
+
+Deno.test("NAPTR query", () => {
+  domain = "4.3.2.1.5.5.5.0.0.8.1.e164.arpa.";
+  mockResponse.dnsResponse = [{
+    flags: "u",
+    order: 100,
+    preference: 10,
+    services: "E2U+sip",
+    regexp: "!^.*$!sip:customer-service@example.com!",
+    replacement: ".",
+  }, {
+    flags: "u",
+    order: 102,
+    preference: 10,
+    services: "E2U+email",
+    regexp: "!^.*$!mailto:information@example.com!",
+    replacement: ".",
+  }];
+  assertEquals(parseResponse(mockResponse, domain, "NAPTR", false), [
+    `4.3.2.1.5.5.5.0.0.8.1.e164.arpa.       IN    NAPTR    100 10 "u" "E2U+sip" !^.*$!sip:customer-service@example.com! .`,
+    `4.3.2.1.5.5.5.0.0.8.1.e164.arpa.       IN    NAPTR    102 10 "u" "E2U+email" !^.*$!mailto:information@example.com! .`,
+  ]);
+});
+
+Deno.test("SOA query", () => {
+  domain = "cloudflare.com.";
+  mockResponse.dnsResponse = [{
+    mname: "ns3.cloudflare.com.",
+    rname: "dns.cloudflare.com.",
+    serial: 2280958559,
+    refresh: 10000,
+    retry: 2400,
+    expire: 604800,
+    minimum: 300,
+  }];
+  assertEquals(parseResponse(mockResponse, domain, "SOA", false), [
+    "cloudflare.com.       IN    SOA    ns3.cloudflare.com. dns.cloudflare.com. 2280958559 10000 2400 604800 300",
+  ]);
+});
+
+Deno.test("SRV query", () => {
+  domain = "localhost";
+  mockResponse.dnsResponse = [{
+    port: 22,
+    priority: 0,
+    target: "localhost",
+    weight: 10,
+  }];
+  assertEquals(parseResponse(mockResponse, domain, "SRV", false), [
+    "localhost       IN    SRV    0 10 22 localhost",
+  ]);
+});
+
+Deno.test("TXT query", () => {
+  mockResponse.dnsResponse = [["a"]];
+  assertEquals(parseResponse(mockResponse, domain, "TXT", false), [
+    'localhost       IN    TXT    "a"',
+  ]);
+});
+
+Deno.test("Invalid query", () => {
+  mockResponse.dnsResponse = [["a"]];
+  assertThrows((): void => {
+    parseResponse(mockResponse, domain, "E", true);
+  });
+});
diff --git a/tests/reverse_test.ts b/tests/reverse_test.ts
new file mode 100644
index 0000000..b556972
--- /dev/null
+++ b/tests/reverse_test.ts
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: MIT
+import { parseIPv6, parseNAPTR, parsePTR } from "../lib/reverse.ts";
+import { assertEquals } from "./testDeps.ts";
+
+Deno.test("IPv6 Parse, localhost", () => {
+  assertEquals(parseIPv6("::1"), "0000:0000:0000:0000:0000:0000:0000:0001");
+});
+
+Deno.test("IPv6 Parse, :: in middle of address", () => {
+  assertEquals(
+    parseIPv6("2001:4860:4860::8844"),
+    "2001:4860:4860:0000:0000:0000:0000:8844",
+  );
+});
+
+Deno.test("IPv4 PTR, localhost", () => {
+  assertEquals(parsePTR("127.0.0.1"), "1.0.0.127.in-addr.arpa");
+});
+
+Deno.test("IPv6 PTR, actual IP", () => {
+  assertEquals(
+    parsePTR("2606:4700:4700::1111"),
+    "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa",
+  );
+});
+
+Deno.test("PTR, Fallback lel", () => {
+  assertEquals(parsePTR("1367218g3a1"), "");
+});
+
+Deno.test("NAPTR, US number", () => {
+  assertEquals(
+    parseNAPTR("+1-800-555-1234"),
+    "4.3.2.1.5.5.5.0.0.8.1.e164.arpa",
+  );
+});
+
+Deno.test("NAPTR, non-US number", () => {
+  assertEquals(
+    parseNAPTR("44 186 533 2244"),
+    "4.4.2.2.3.3.5.6.8.1.4.4.e164.arpa",
+  );
+});
diff --git a/tests/testDeps.ts b/tests/testDeps.ts
new file mode 100644
index 0000000..6a92c1e
--- /dev/null
+++ b/tests/testDeps.ts
@@ -0,0 +1,4 @@
+export {
+  assertEquals,
+  assertThrows,
+} from "https://deno.land/std@0.144.0/testing/asserts.ts";
