Testing the recursive resolver

Ronit PandaRonit Panda
7 min read

This will be very straightforward.

So without any delay head over to src/udp-server.test.ts

Initial setup

  1. Add all required imports

  2. Start up a udpSocket on udp4

  3. TEST_PORT & TEST_HOST signify the host and port on which our actual udp server is running, onto which we need to send DNS queries to get response back

  4. Make sure before running these tests to run the UDP server by running
    pnpm run dev

  5. Lastly, the afterAll block ensures the udpSocket is closed after all the tests in this file have run.

     import * as dgram from 'node:dgram';
     import { DNSBuilder } from './message/builder';
     import { dnsParser } from './message/parser';
     import {
         Bool,
         DNSObject,
         OPCODE,
         QRIndicator,
         RCode,
         RECORD_TYPE,
     } from './message/types';
     import { decodeRDATA } from './utils';
    
     const udpSocket = dgram.createSocket('udp4');
     const TEST_PORT = 2053;
     const TEST_HOST = '127.0.0.1';
    
     afterAll((done) => {
         udpSocket.close(done);
     });
    

Test 1

if you look into the test it's fairly simple

  1. Create a dns object with 1 question and request for an A record

  2. Convert the object to a buffer, and send it via UDP to our dns server

  3. On response (message event) we parse the result and fetch the answers of it, expecting it to be valid and containing the answer that we expect

test('valid DNS request with A record and sigle question', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'google.com',
                TYPE: RECORD_TYPE.A,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const { answers } = dnsParser.parse(data);

                expect(answers).toBeDefined();
                if (!answers) return;

                expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[0]?.CLASS).toEqual(1);
                expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.A);
                expect(answers[0]?.RDLENGTH).toEqual(4);
                expect(answers[0]?.RDATA).toBeDefined();

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});

It will be a waste of time if we go over all tests, given each test follows the same template as above, we are varying the record type and number of records etc to test as much as possible

So am attaching the whole test file below (src/udp-server.test.ts) and you can just take a short glance over all of them

import * as dgram from 'node:dgram';
import { DNSBuilder } from './message/builder';
import { dnsParser } from './message/parser';
import {
    Bool,
    DNSObject,
    OPCODE,
    QRIndicator,
    RCode,
    RECORD_TYPE,
} from './message/types';
import { decodeRDATA } from './utils';

const udpSocket = dgram.createSocket('udp4');
const TEST_PORT = 2053;
const TEST_HOST = '127.0.0.1';

afterAll((done) => {
    udpSocket.close(done);
});

test('valid DNS request with A record and sigle question', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'google.com',
                TYPE: RECORD_TYPE.A,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const { answers } = dnsParser.parse(data);

                expect(answers).toBeDefined();
                if (!answers) return;

                expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[0]?.CLASS).toEqual(1);
                expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.A);
                expect(answers[0]?.RDLENGTH).toEqual(4);
                expect(answers[0]?.RDATA).toBeDefined();

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});
test('valid DNS request with CNAME record and sigle question should resolve underlying A record if asked', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'www.ronit.dev',
                TYPE: RECORD_TYPE.A,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const { answers } = dnsParser.parse(data);

                expect(answers).toBeDefined();
                if (!answers) return;

                expect(answers).toBeDefined();
                expect(answers.length).toEqual(2);

                expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[0]?.CLASS).toEqual(1);
                expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.CNAME);
                expect(answers[0]?.RDLENGTH).toEqual(18);

                expect(answers[1]?.NAME).toEqual('hashnode.network');
                expect(answers[1]?.CLASS).toEqual(1);
                expect(answers[1]?.TYPE).toEqual(RECORD_TYPE.A);
                expect(answers[1]?.RDLENGTH).toEqual(4);

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});
test('valid DNS request with CNAME record explicit asking of CNAME record should resolve only that', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'www.ronit.dev',
                TYPE: RECORD_TYPE.CNAME,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const { answers } = dnsParser.parse(data);

                expect(answers).toBeDefined();
                if (!answers) return;
                expect(answers.length).toEqual(1);

                expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[0]?.CLASS).toEqual(1);
                expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.CNAME);
                expect(answers[0]?.RDLENGTH).toEqual(18);

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});
test('invalid domain should give an NXDOMAIN rcode in header', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'lambda.ronit.dev',
                TYPE: RECORD_TYPE.A,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const {
                    header: { RCODE, ANCOUNT, NSCOUNT },
                } = dnsParser.parse(data);
                expect(RCODE).toEqual(RCode.NXDOMAIN);
                expect(ANCOUNT).toEqual(0);
                expect(NSCOUNT).toEqual(1);

                const { authority, answers } = dnsParser.parse(data);

                expect(authority).toBeDefined();
                if (!authority) return;

                expect(authority).toBeDefined();
                expect(authority.length).toEqual(1);
                expect(authority[0]?.NAME).toEqual('ronit.dev');
                expect(authority[0]?.CLASS).toEqual(1);
                expect(authority[0]?.TYPE).toEqual(RECORD_TYPE.SOA);

                expect(answers).toBeDefined();
                if (!answers) return;
                expect(answers.length).toEqual(0);

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});
test('valid DNS request with NS record', async () => {
    const dnsRequestObject: DNSObject = {
        header: {
            ID: 1234,
            QR: QRIndicator.QUERY,
            OPCODE: OPCODE.QUERY,
            AA: Bool.FALSE,
            TC: Bool.FALSE,
            RD: Bool.TRUE,
            RA: Bool.FALSE,
            Z: 0,
            RCODE: RCode.NOERROR,
            QDCOUNT: 1,
            ANCOUNT: 0,
            NSCOUNT: 0,
            ARCOUNT: 0,
        },
        questions: [
            {
                NAME: 'ronit.dev',
                TYPE: RECORD_TYPE.NS,
                CLASS: 1,
            },
        ],
    };

    const dnsBuilder = new DNSBuilder(dnsRequestObject);
    const requestBuffer = dnsBuilder.toBuffer();

    // Setup a promise to wait for the UDP response
    const responsePromise = new Promise<void>((resolve, reject) => {
        udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
            if (err) {
                reject(err);
            }
        });

        udpSocket.on('message', (data: Buffer) => {
            try {
                const { answers } = dnsParser.parse(data);

                expect(answers).toBeDefined();
                if (!answers) return;

                expect(answers.length).toEqual(2);

                expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[0]?.CLASS).toEqual(1);
                expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.NS);
                expect(answers[0]?.RDLENGTH).toEqual(24);
                expect(answers[0]?.RDATA).toBeDefined();
                expect(decodeRDATA(answers[0]?.RDATA as Buffer)).toEqual(
                    'greg.ns.cloudflare.com',
                );

                expect(answers[1]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
                expect(answers[1]?.CLASS).toEqual(1);
                expect(answers[1]?.TYPE).toEqual(RECORD_TYPE.NS);
                expect(answers[1]?.RDLENGTH).toEqual(8);
                expect(answers[1]?.RDATA).toBeDefined();
                expect(decodeRDATA(answers[1]?.RDATA as Buffer)).toEqual('khloe'); // this is due to compression in the response

                resolve(); // Resolve the promise when all assertions pass
            } catch (error) {
                reject(error); // Reject with error if assertions fail
            }
        });
    });

    // Wait for the response promise to resolve or timeout after a certain period
    await responsePromise;
});

To run this test simply run:

pnpm run test

As said earlier make sure the UDP server is actually running on the given host and port before running the tests

pnpm run dev

if the test passes means we our classes are working just fine,
& trust me they will ๐Ÿ˜Ž

Now let's move to the next blog. Where we will be implementing the dns cache class to make requests to our UDP server faster.

0
Subscribe to my newsletter

Read articles from Ronit Panda directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ronit Panda
Ronit Panda

Founding full stack engineer at dimension.dev