From 99908d758b412232de98ec4d74901ae4323d4686 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 21 Feb 2024 23:30:42 -0800 Subject: [PATCH] session management progress (#2) - feat(session.delete): added - test(session): add lifecycle tests - config(session): ttl set to 1 hour - lib/mysql: DRY - init-mysql: added 'drop' CLI arg - ci: custom publish, b/c build test has SQL setup steps - test(user): remove duplicate setup code - test(user): add invalid user & pass tests - doc(README): added install section - better align object function names with http verbs --- .github/workflows/ci.yml | 15 ++---- .github/workflows/publish.yml | 53 ++++++++++++++++++-- README.md | 30 +++++++++++- conf.d/mysql.yml | 5 +- conf.d/session.yml | 7 +-- lib/group.js | 7 ++- lib/mysql.js | 42 ++++------------ lib/session.js | 32 ++++++------ lib/user.js | 91 +++++++++++++++++++---------------- lib/util.js | 1 + package.json | 1 + routes/index.js | 27 +++++------ routes/user.js | 68 +++++++++++++------------- sql/init-mysql.sh | 36 ++++++++------ test/config.js | 2 + test/fixtures/.setup.js | 4 +- test/fixtures/run.sh | 2 +- test/mysql.js | 5 ++ test/routes.js | 77 +++++++++++++++++++++++++---- test/user.js | 72 +++++++++++++++------------ test/util.js | 13 ++--- 21 files changed, 366 insertions(+), 224 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a5c4a0..18e9650 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,15 +51,12 @@ jobs: node-version: ${{ fromJson(needs.get-lts.outputs.active) }} fail-fast: false steps: - - name: Start MySQL - run: sudo /etc/init.d/mysql start + - run: sudo /etc/init.d/mysql start - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} with: node-version: ${{ matrix.node-version }} - - name: Initialize MySQL - run: sh sql/init-mysql.sh + - run: sh sql/init-mysql.sh - run: npm install - run: npm test @@ -81,8 +78,7 @@ jobs: name: Node ${{ matrix.node-version }} on ${{ matrix.os }} with: node-version: ${{ matrix.node-version }} - - name: Initialize MySQL - run: sh sql/init-mysql.sh + - run: sh sql/init-mysql.sh - run: npm install - run: npm test @@ -101,10 +97,9 @@ jobs: choco install mysql - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} + name: Node ${{ matrix.node-version }} with: node-version: ${{ matrix.node-version }} - - name: Initialize MySQL - run: sh sql/init-mysql.sh + - run: sh sql/init-mysql.sh - run: npm install - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f79da52..a09a62c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,53 @@ env: CI: true jobs: - publish: - uses: NicTool/.github/.github/workflows/publish.yml@main - secrets: inherit + build: + runs-on: ubuntu-latest + steps: + - run: sudo /etc/init.d/mysql start + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - run: sh sql/init-mysql.sh + - run: npm install + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v4 + name: Node ${{ env.node-version }} + with: + node-version: ${{ env.node-version }} + registry-url: https://registry.npmjs.org/ + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + # fetch-depth 0 needed by GitHub Release + + - name: publish to NPM + run: npm publish --access=public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + + - name: GitHub Release + uses: justincy/github-action-npm-release@2.0.1 + id: release + + publish-gpr: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.node-version }} + registry-url: https://npm.pkg.github.com/ + scope: '@nictool' + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 005f1d8..27d02ff 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,34 @@ # nt-api -nictool api +nictool api v3 +## Install + +1. Install [Node.js](https://nodejs.org/en/download/) on your system +2. Download the NicTool v3 API + + +``` +git clone https://github.com/NicTool/api.git nictool-api +cd nictool-api +npm install +``` + +## Configure + +Edit the files in conf.d to reflect your local settings. Each config file has a default section which lists all available config settings. Below the `default` section are optional deployment environments such as `production`, `development`, and `test`. When a config file is loaded, the environment variable `NODE_ENV` is checked and if defined, any overrides in the matching deployment section are applied. + +## Start the service + +Running one of these commands: + +`npm run start (production)` + +or + +`npm run develop (development)` + +will start up the HTTP service on the port specified in conf.d/http.yml. + diff --git a/conf.d/mysql.yml b/conf.d/mysql.yml index d60b3c1..ba5e7ac 100644 --- a/conf.d/mysql.yml +++ b/conf.d/mysql.yml @@ -1,4 +1,4 @@ - +# default settings apply to EVERY deployment default: host: 127.0.0.1 port: 3306 @@ -10,14 +10,17 @@ default: - 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 diff --git a/conf.d/session.yml b/conf.d/session.yml index 5ae7901..3a85808 100644 --- a/conf.d/session.yml +++ b/conf.d/session.yml @@ -4,15 +4,16 @@ default: # https://hapi.dev/module/cookie/api/?v=12.0.1 name: sid-nictool password: af1b926a5e21f535c4f5b6c42941c4cf - # ttl: + ttl: 3600000 # 1 hour # domain: path: / - # clearInvalid: false + clearInvalid: true isSameSite: Strict isSecure: true isHttpOnly: true keepAlive: false # redirectTo: + # group: NicTool production: cookie: @@ -27,4 +28,4 @@ test: development: cookie: isSecure: false - password: ^NicTool.Is,The#Best_Dns-Manager$ \ No newline at end of file + password: ^NicTool.Is,The#Best_Dns-Manager$ diff --git a/lib/group.js b/lib/group.js index 4bc2e7e..a6be753 100644 --- a/lib/group.js +++ b/lib/group.js @@ -6,11 +6,10 @@ class Group { constructor() {} async create(args) { - // console.log(`create`) const { error } = validate.group.validate(args) if (error) console.error(error) - const g = await this.read({ nt_group_id: args.nt_group_id }) + const g = await this.get({ nt_group_id: args.nt_group_id }) if (g.length) { // console.log(g) return g[0].nt_group_id @@ -20,12 +19,12 @@ class Group { return groupId } - async read(args) { + async get(args) { return await mysql.select(`SELECT * FROM nt_group WHERE`, args) } async destroy(args) { - const g = await this.read({ nt_group_id: args.nt_group_id }) + const g = await this.get({ nt_group_id: args.nt_group_id }) // console.log(g) if (g.length === 1) { await mysql.execute(`DELETE FROM nt_group WHERE nt_group_id=?`, [ diff --git a/lib/mysql.js b/lib/mysql.js index 4a56462..35516b4 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -14,24 +14,24 @@ class MySQL { // if (this.dbh && this.dbh?.connection?.connectionId) return this.dbh; const cfg = await config.get('mysql') - if (config.debug) console.log(cfg) + if (this._debug) console.log(cfg) this.dbh = await mysql.createConnection(cfg) - if (config.debug) + if (this._debug) console.log(`MySQL connection id ${this.dbh.connection.connectionId}`) return this.dbh } async execute(query, paramsArray) { if (!this.dbh || this.dbh?.connection?._closing) { - if (config.debug) console.log(`(re)connecting to MySQL`) + if (this._debug) console.log(`(re)connecting to MySQL`) this.dbh = await this.connect() } - // console.log(query) - // console.log(paramsArray) + if (this._debug) console.log(query) + if (this._debug) console.log(paramsArray) const [rows, fields] = await this.dbh.execute(query, paramsArray) - if (this.debug()) { + if (this._debug) { if (fields) console.log(fields) console.log(rows) } @@ -42,30 +42,11 @@ class MySQL { } async insert(query, params = {}) { - if (!this.dbh || this.dbh?.connection?._closing) { - if (config.debug) console.log(`(re)connecting to MySQL`) - this.dbh = await this.connect() - } - query += `(${Object.keys(params).join(',')}) VALUES(${Object.keys(params).map(() => '?')})` - - // console.log(query) - // console.log(Object.values(params)) - const [rows, fields] = await this.dbh.execute(query, Object.values(params)) - if (this.debug()) { - if (fields) console.log(fields) - console.log(rows) - } - - return rows.insertId + return await this.execute(query, Object.values(params)) } async select(query, params = {}) { - if (!this.dbh || this.dbh?.connection?._closing) { - if (config.debug) console.log(`(re)connecting to MySQL`) - this.dbh = await this.connect() - } - let paramsArray = [] if (Array.isArray(params)) { paramsArray = [...params] @@ -80,17 +61,12 @@ class MySQL { } } - const [rows, fields] = await this.dbh.execute(query, paramsArray) - if (this.debug()) { - if (fields) console.log(fields) - console.log(rows) - } - return rows + return await this.execute(query, paramsArray) } async disconnect(dbh) { const d = dbh || this.dbh - if (config.debug) + if (this._debug) console.log(`MySQL connection id ${d.connection.connectionId}`) await d.end() } diff --git a/lib/session.js b/lib/session.js index da6ba2e..963d9f1 100644 --- a/lib/session.js +++ b/lib/session.js @@ -4,40 +4,42 @@ class Session { constructor() {} async create(args) { - const r = await this.read({ nt_user_session: args.nt_user_session }) - if (r) return r + const r = await this.get(args) + if (r) return r.nt_user_session_id - const query = `INSERT INTO nt_user_session` - - const id = await Mysql.insert(query, { + const id = await Mysql.insert(`INSERT INTO nt_user_session`, { nt_user_id: args.nt_user_id, nt_user_session: args.nt_user_session, last_access: parseInt(Date.now() / 1000, 10), }) - return id } - async read(args) { + async get(args) { let query = `SELECT s.* FROM nt_user_session s LEFT JOIN nt_user u ON s.nt_user_id = u.nt_user_id WHERE u.deleted=0` const params = [] - if (args.id) { - query += ` AND s.nt_user_session_id = ?` - params.push(args.id) - } - if (args.nt_user_session) { - query += ` AND s.nt_user_session = ?` - params.push(args.nt_user_session) + for (const f of ['nt_user_session_id', 'nt_user_id', 'nt_user_session']) { + if (args[f] !== undefined) { + query += ` AND s.${f} = ?` + params.push(args[f]) + } } const sessions = await Mysql.execute(query, params) - // console.log(sessions) return sessions[0] } + + async delete(args) { + const r = await Mysql.execute( + `DELETE FROM nt_user_session WHERE nt_user_session_id=?`, + [args.nt_user_session_id], + ) + return r.affectedRows === 1 + } } module.exports = new Session() diff --git a/lib/user.js b/lib/user.js index da41c55..bcc7666 100644 --- a/lib/user.js +++ b/lib/user.js @@ -1,7 +1,8 @@ const crypto = require('node:crypto') const validate = require('@nictool/nt-validate') -const mysql = require('./mysql') +const Config = require('./config') +const Mysql = require('./mysql') class User { constructor(args) { @@ -11,17 +12,29 @@ class User { async authenticate(authTry) { if (this.debug) console.log(authTry) let [username, group] = authTry.username.split('@') - if (!group) group = 'NicTool' - - const query = `SELECT nt_user.*, nt_group.name AS groupname - FROM nt_user, nt_group - WHERE nt_user.nt_group_id = nt_group.nt_group_id - AND nt_group.deleted=0 - AND nt_user.deleted=0 - AND nt_user.username = ? - AND nt_group.name = ?` + if (!group) { + this.cfg = await Config.get('session') + group = this.cfg.group ?? 'NicTool' + } - for (const u of await mysql.execute(query, [username, group])) { + const query = `SELECT u.nt_user_id + , u.nt_group_id + , u.first_name + , u.last_name + , u.username + , u.password + , u.pass_salt + , u.email + , u.is_admin + , g.name AS groupname + FROM nt_user u, nt_group g + WHERE u.nt_group_id = g.nt_group_id + AND g.deleted=0 + AND u.deleted=0 + AND u.username = ? + AND g.name = ?` + + for (const u of await Mysql.execute(query, [username, group])) { if ( await this.validPassword( authTry.password, @@ -29,8 +42,12 @@ class User { authTry.username, u.pass_salt, ) - ) + ) { + for (const f of ['password', 'pass_salt']) { + delete u[f] // SECURITY: no longer needed + } return u + } } } @@ -38,7 +55,7 @@ class User { const { error } = validate.user.validate(args) if (error) console.error(error) - const u = await this.read({ + const u = await this.get({ nt_user_id: args.nt_user_id, nt_group_id: args.nt_group_id, }) @@ -52,12 +69,12 @@ class User { args.password = await this.hashAuthPbkdf2(args.password, args.pass_salt) } - const userId = await mysql.insert(`INSERT INTO nt_user`, args) + const userId = await Mysql.insert(`INSERT INTO nt_user`, args) return userId } - async read(args) { - return await mysql.select( + async get(args) { + return await Mysql.select( `SELECT email, first_name, last_name, nt_group_id, nt_user_id, username, email, deleted FROM nt_user WHERE`, args, @@ -65,9 +82,9 @@ class User { } async delete(args, val) { - const u = await this.read({ nt_user_id: args.nt_user_id }) + const u = await this.get({ nt_user_id: args.nt_user_id }) if (u.length === 1) { - await mysql.execute(`UPDATE nt_user SET deleted=? WHERE nt_user_id=?`, [ + await Mysql.execute(`UPDATE nt_user SET deleted=? WHERE nt_user_id=?`, [ val ?? 1, u[0].nt_user_id, ]) @@ -75,23 +92,23 @@ class User { } async destroy(args) { - const u = await this.read({ nt_user_id: args.nt_user_id }) + const u = await this.get({ nt_user_id: args.nt_user_id }) if (u.length === 1) { - await mysql.execute(`DELETE FROM nt_user WHERE nt_user_id=?`, [ + await Mysql.execute(`DELETE FROM nt_user WHERE nt_user_id=?`, [ u[0].nt_user_id, ]) } } - async get_perms(user_id) { - return await mysql.execute( - ` - SELECT ${getPermFields()} FROM nt_perm - WHERE deleted=0 - AND nt_user_id = ?`, - [user_id], - ) - } + // async get_perms(user_id) { + // return await Mysql.execute( + // ` + // SELECT ${getPermFields()} FROM nt_perm + // WHERE deleted=0 + // AND nt_user_id = ?`, + // [user_id], + // ) + // } generateSalt(length = 16) { const chars = Array.from({ length: 87 }, (_, i) => @@ -135,23 +152,12 @@ class User { return false } - - async getSession(sessionId) { - let query = `SELECT s.* - FROM nt_user_session s - LEFT JOIN nt_user u ON s.nt_user_id = u.nt_user_id - WHERE u.deleted=0 - AND s.nt_user_session = ?` - - const session = await mysql.execute(query, [sessionId]) - if (this.debug) console.log(session) - return session[0] - } } module.exports = new User() -module.exports._mysql = mysql +module.exports._mysql = Mysql +/* function getPermFields() { return ( `nt_perm.` + @@ -183,3 +189,4 @@ function getPermFields() { ].join(`, nt_perm.`) ) } +*/ \ No newline at end of file diff --git a/lib/util.js b/lib/util.js index 2b43f29..9a29cd7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,7 @@ exports.setEnv = () => { if (process.env.NODE_ENV !== undefined) return + /* c8 ignore next 9 */ switch (require('os').hostname()) { case 'mbp.simerson.net': case 'imac27.simerson.net': diff --git a/package.json b/package.json index 4f6e5ff..786a8c8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "prettier": "npx prettier *.js lib routes test html --check", "prettier:fix": "npm run prettier -- --write", "start": "NODE_ENV=production node ./server", + "develop": "NODE_ENV=development node ./server", "test": "test/fixtures/run.sh", "versions": "npx dependency-version-checker check", "watch": "npm run test -- --watch" diff --git a/routes/index.js b/routes/index.js index 218dd7d..5c651a3 100644 --- a/routes/index.js +++ b/routes/index.js @@ -9,14 +9,15 @@ const qs = require('qs') const util = require('../lib/util') util.setEnv() -const config = require('../lib/config') -const user = require('../lib/user') +const Config = require('../lib/config') +const Session = require('../lib/session') +const User = require('../lib/user') const UserRoutes = require('./user') let server const setup = async () => { - const httpCfg = await config.get('http') + const httpCfg = await Config.get('http') server = hapi.server({ port: httpCfg.port, @@ -34,20 +35,17 @@ const setup = async () => { await server.register(require('@hapi/basic')) await server.register(require('@hapi/cookie')) await server.register(require('@hapi/inert')) - const sessionCfg = await config.get('session') + const sessionCfg = await Config.get('session') server.auth.strategy('session', 'cookie', { cookie: sessionCfg.cookie, - redirectTo: '/login', - validate: async (request, session) => { - // console.log(`validate session: ${session}`) - const account = await session.read({ nt_user_session: session }) - - if (!account) return { isValid: false } // invalid cookie + const s = await Session.get({ nt_user_session_id: session.session_id }) + if (!s) return { isValid: false } // invalid cookie - return { isValid: true, credentials: account } + // const account = await User.get({ nt_user_id: s.nt_user_id }) + return { isValid: true } // , credentials: account } }, }) @@ -56,10 +54,9 @@ const setup = async () => { server.route({ method: 'GET', path: '/', - handler: (request) => { - return `Hello World! ${request?.auth?.credentials?.name}` + handler: (request, h) => { + return h.response({ joy: true }).code(200) }, - // options: {}, }) UserRoutes(server) @@ -73,7 +70,7 @@ const setup = async () => { }) server.events.on('stop', () => { - user._mysql.disconnect() + User._mysql.disconnect() }) } diff --git a/routes/user.js b/routes/user.js index 783f1d2..3776ab5 100644 --- a/routes/user.js +++ b/routes/user.js @@ -1,57 +1,59 @@ -const schema = require('@nictool/nt-validate') +const validate = require('@nictool/nt-validate') const User = require('../lib/user') +const Session = require('../lib/session') module.exports = (server) => { server.route([ { method: 'GET', - path: '/login', + path: '/user', options: { - auth: { mode: 'try' }, - plugins: { - cookie: { - redirectTo: false, - }, - }, - handler: async (request, h) => { - if (request.auth.isAuthenticated) { - return h.redirect('/') - } + // auth: { mode: 'try' }, + }, + handler: async (request, h) => { + // console.log(request.auth) + if (request.auth.isAuthenticated) { + return h.response('You ARE logged in!').code(200) + } - return 'You need to log in!' - }, + return h.response('You are NOT logged in!').code(401) }, }, { method: 'POST', - path: '/login', + path: '/session', options: { auth: { mode: 'try' }, - handler: async (request, h) => { - const account = await User.authenticate(request.payload) - if (!account) return 'Invalid authentication' - - // TODO: generate session + validate: { payload: validate.login }, + }, + handler: async (request, h) => { + const account = await User.authenticate(request.payload) + if (!account) { + return h.response('Invalid authentication').code(401) + } - // console.log(account) + const sessId = await Session.create({ + nt_user_id: account.nt_user_id, + nt_user_session: '12345', + }) - request.cookieAuth.set({ id: account.nt_user_id }) - return h.redirect('/') - }, - validate: { - payload: schema.login, - }, + request.cookieAuth.set({ + nt_user_id: account.nt_user_id, + nt_session_id: sessId.nt_user_session_id, + }) + return h.response(`SUCCESS: you are logged in`).code(200) }, }, { - method: 'GET', - path: '/logout', - options: { - handler: (request, h) => { + method: 'DELETE', + path: '/session', + handler: (request, h) => { + if (request.auth.isAuthenticated) { request.cookieAuth.clear() - return h.redirect('/') - }, + return h.response('You are logged out').code(200) + } + return h.response('You are NOT logged in!').code(401) }, }, ]) diff --git a/sql/init-mysql.sh b/sql/init-mysql.sh index 0a43f1f..3c3f454 100755 --- a/sql/init-mysql.sh +++ b/sql/init-mysql.sh @@ -1,26 +1,32 @@ #!/bin/sh -# configure MySQL in the GitHub runners -case "$(uname -s)" in - Linux*) - export MYSQL_PWD=root - ;; - Darwin*) - mysqladmin --user=root --password='' --protocol=tcp password 'root' - export MYSQL_PWD="root" - ;; - CYGWIN*|MINGW*|MINGW32*|MSYS*) - export MYSQL_PWD="" - ;; -esac +if [ "$MYSQL_PWD" = "" ]; +then + export MYSQL_PWD=root + + # configure MySQL in the GitHub workflow runners + case "$(uname -s)" in + Linux*) + ;; + Darwin*) + mysqladmin --user=root --password='' --protocol=tcp password 'root' + ;; + CYGWIN*|MINGW*|MINGW32*|MSYS*) + export MYSQL_PWD="" + ;; + esac +fi # AUTH="--defaults-extra-file=./sql/my-gha.cnf" -# mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1 +if [ "$1" = "drop" ]; then + mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1 +fi mysql --user=root -e 'CREATE DATABASE nictool;' || exit 1 -for f in './sql/*.sql'; +for f in ./sql/*.sql; do + echo "cat $f | mysql nictool" cat $f | mysql --user=root nictool || exit 1 done diff --git a/test/config.js b/test/config.js index 92e6451..bf1ac0c 100644 --- a/test/config.js +++ b/test/config.js @@ -57,12 +57,14 @@ const mysqlTestCfg = { const sessCfg = { cookie: { + clearInvalid: true, isHttpOnly: true, isSameSite: 'Strict', isSecure: false, name: 'sid-nictool', password: '^NicTool.Is,The#Best_Dns-Manager$', path: '/', + ttl: 3600000, }, keepAlive: false, } diff --git a/test/fixtures/.setup.js b/test/fixtures/.setup.js index b2fc10b..6fc4d6e 100644 --- a/test/fixtures/.setup.js +++ b/test/fixtures/.setup.js @@ -17,14 +17,14 @@ const setup = async () => { setup() async function createTestGroup() { - let g = group.read({ nt_group_id: groupCase.nt_group_id }) + let g = group.get({ nt_group_id: groupCase.nt_group_id }) if (g.length === 1) return await group.create(groupCase) } async function createTestUser() { - let u = await user.read({ nt_user_id: userCase.nt_user_id }) + let u = await user.get({ nt_user_id: userCase.nt_user_id }) if (u.length === 1) return const instance = JSON.parse(JSON.stringify(userCase)) diff --git a/test/fixtures/run.sh b/test/fixtures/run.sh index 9c6fa25..3b1906a 100755 --- a/test/fixtures/run.sh +++ b/test/fixtures/run.sh @@ -1,5 +1,5 @@ #!/bin/sh node test/fixtures/.setup.js -node --test +node --test test/*.js node test/fixtures/.teardown.js \ No newline at end of file diff --git a/test/mysql.js b/test/mysql.js index f78a7bf..168366b 100644 --- a/test/mysql.js +++ b/test/mysql.js @@ -14,9 +14,14 @@ describe('mysql', () => { mysql.debug(true) await mysql.execute(`SHOW DATABASES`) await mysql.select(`SELECT * FROM nt_group`) + mysql.debug(false) }) } + it.todo('SQL: formats SELECT queries', async () => {}) + + it.todo('SQL: formats INSERT queries', async () => {}) + it('disconnects', async () => { assert.ok(this.dbh.connection.connectionId) await mysql.disconnect(this.dbh) diff --git a/test/routes.js b/test/routes.js index 866dae0..4f12811 100644 --- a/test/routes.js +++ b/test/routes.js @@ -12,29 +12,86 @@ after(async () => { await this.server.stop() }) +const parseCookie = (c) => { + return c.split(';')[0] +} + describe('routes', () => { - describe('GET /login', () => { - it('responds with 200', async () => { + const routes = [{ GET: '/' }, { GET: '/user' }, { DELETE: '/session' }] + + for (const r of routes) { + const key = Object.keys(r)[0] + const val = Object.values(r)[0] + describe(`${key} ${val}`, () => { + it('no session responds with 401', async () => { + const res = await this.server.inject({ + method: key, + url: val, + }) + assert.deepEqual(res.statusCode, 401) + }) + }) + } + + describe('POST /session', () => { + it('valid auth sets a cookie', async () => { const res = await this.server.inject({ - method: 'GET', - url: '/login', + method: 'POST', + url: '/session', + payload: { + username: `${userCase.username}@example.com`, + password: 'Wh@tA-Decent#P6ssw0rd', + }, }) - assert.deepEqual(res.statusCode, 200) + assert.ok(res.headers['set-cookie'][0]) + // console.log(res.headers['set-cookie'][0]) + this.sessionCookie = parseCookie(res.headers['set-cookie'][0]) + // console.log(this.sessionCookie) }) }) - describe('POST /login', () => { - it('responds with 302', async () => { + describe('with valid session, can retrieve URIs that require auth', () => { + before(async () => { const res = await this.server.inject({ method: 'POST', - url: '/login', + url: '/session', payload: { username: `${userCase.username}@example.com`, password: 'Wh@tA-Decent#P6ssw0rd', }, }) - // console.log(res.result) - assert.deepEqual(res.statusCode, 302) + assert.ok(res.headers['set-cookie'][0]) + this.sessionCookie = parseCookie(res.headers['set-cookie'][0]) }) + + after(async () => { + const res = await this.server.inject({ + method: 'DELETE', + url: '/session', + headers: { + Cookie: this.sessionCookie, + }, + }) + console.log(res.result) + }) + + const routes = [{ GET: '/' }, { GET: '/user' }, { DELETE: '/session' }] + + for (const r of routes) { + const key = Object.keys(r)[0] + const val = Object.values(r)[0] + it(`${key} ${val}`, async () => { + const res = await this.server.inject({ + method: key, + url: val, + headers: { + Cookie: this.sessionCookie, + }, + }) + assert.equal(res.request.auth.isAuthenticated, true) + assert.equal(res.statusCode, 200) + // console.log(res.result) + }) + } }) }) diff --git a/test/user.js b/test/user.js index cac8401..119e7dc 100644 --- a/test/user.js +++ b/test/user.js @@ -1,35 +1,20 @@ const assert = require('node:assert/strict') -const { describe, it, before, after } = require('node:test') +const { describe, it, after } = require('node:test') const session = require('../lib/session') const user = require('../lib/user') const userCase = require('./fixtures/user.json') -before(async () => { - this.sessionId = await session.create({ - nt_user_id: userCase.nt_user_id, - nt_user_session: 12345, - }) - - let users = await user.read({ nt_user_id: userCase.nt_user_id }) - if (users.length === 1) return - - const instance = JSON.parse(JSON.stringify(userCase)) - instance.password = 'Wh@tA-Decent#P6ssw0rd' - - await user.create(instance) -}) - after(async () => { // user._mysql.disconnect() session._mysql.disconnect() }) describe('user', function () { - describe('read', function () { + describe('get', function () { it('finds existing user by nt_user_id', async () => { - const u = await user.read({ nt_user_id: 4096 }) + const u = await user.get({ nt_user_id: 4096 }) // console.log(u) assert.deepEqual(u[0], { nt_group_id: 4096, @@ -43,7 +28,7 @@ describe('user', function () { }) it('finds existing user by username', async () => { - const u = await user.read({ username: 'unit-test' }) + const u = await user.get({ username: 'unit-test' }) // console.log(u) assert.deepEqual(u[0], { nt_group_id: 4096, @@ -58,10 +43,10 @@ describe('user', function () { it('deletes a user', async () => { await user.delete({ nt_user_id: 4096 }) - let u = await user.read({ nt_user_id: 4096 }) + let u = await user.get({ nt_user_id: 4096 }) assert.equal(u[0].deleted, 1) await user.delete({ nt_user_id: 4096 }, 0) // restore - u = await user.read({ nt_user_id: 4096 }) + u = await user.get({ nt_user_id: 4096 }) assert.equal(u[0].deleted, 0) }) }) @@ -139,9 +124,21 @@ describe('user', function () { }) describe('authenticate', () => { - it.todo('rejects invalid user', () => {}) + it('rejects invalid user', async () => { + const u = await user.authenticate({ + username: 'fake-test@example.com', + password: 'evilCrackerJack', + }) + assert.equal(u, undefined) + }) - it.todo('rejects invalid pass', () => {}) + it('rejects invalid pass', async () => { + const u = await user.authenticate({ + username: 'unit-test@example.com', + password: 'evilCrackerJack', + }) + assert.equal(u, undefined) + }) it('accepts a valid username & password', async () => { const u = await user.authenticate({ @@ -155,26 +152,41 @@ describe('user', function () { describe('session', function () { // session._mysql.debug(true) + let sessionId describe('create', () => { it('creates a login session', async () => { - const s = await session.create({ + sessionId = await session.create({ nt_user_id: userCase.nt_user_id, nt_user_session: 12345, }) - assert.ok(s) + assert.ok(sessionId) }) }) - describe('read', function () { + describe('get', () => { it('finds a session by ID', async () => { - const s = await session.read({ id: this.sessionId }) - assert.ok(s) + const s = await session.get({ nt_user_session_id: sessionId }) + // console.log(s) + assert.ok(s.nt_user_session_id) }) it('finds a session by session', async () => { - const s = await session.read({ nt_user_session: 12345 }) - assert.ok(s) + const s = await session.get({ nt_user_session: 12345 }) + assert.ok(s.nt_user_session_id) + }) + }) + + describe('delete', () => { + it('deletes a session by ID', async () => { + assert.ok(await session.delete({ nt_user_session_id: sessionId })) + }) + + it('does not find a deleted session', async () => { + assert.equal( + await session.get({ nt_user_session_id: sessionId }), + undefined, + ) }) }) }) diff --git a/test/util.js b/test/util.js index 3eeb4e0..92db289 100644 --- a/test/util.js +++ b/test/util.js @@ -4,13 +4,14 @@ const { describe, it } = require('node:test') const util = require('../lib/util') describe('util', function () { - describe('setEnv', function () { - it('sets process.env.NODE_ENV', async () => { - assert.equal(process.env.NODE_ENV, undefined) - util.setEnv() - assert.ok(process.env.NODE_ENV) + if (process.env.NODE_ENV === undefined) { + describe('setEnv', function () { + it('sets process.env.NODE_ENV', async () => { + util.setEnv() + assert.ok(process.env.NODE_ENV) + }) }) - }) + } describe('meta', () => { it('returns the package version', () => {