From 7e5c3140006369ebbb023c6f8964fd3389a0566e Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Tue, 28 Oct 2025 19:18:18 -0400 Subject: [PATCH] feat(nostr): Added new route for deleteChat and deletePost models --- src/adapters/localdb/index.js | 4 + src/controllers/rest-api/index.js | 5 + src/controllers/rest-api/nostr/controller.js | 219 ++++++++++++++++++ src/controllers/rest-api/nostr/index.js | 81 +++++++ src/entities/deletedChat.js | 17 ++ src/entities/deletedPost.js | 17 ++ src/use-cases/index.js | 2 + src/use-cases/nostr-use-cases.js | 92 ++++++++ test/e2e/automated/a12-nostr.rest-e2e.js | 188 +++++++++++++++ .../nostr/nostr.rest.controller.unit.js | 201 ++++++++++++++++ .../rest-api/nostr/nostr.rest.router.unit.js | 108 +++++++++ test/unit/entities/deletedChat.entity.unit.js | 57 +++++ test/unit/entities/deletedPost.entity.unit.js | 57 +++++ test/unit/mocks/adapters/index.js | 26 ++- test/unit/mocks/use-cases/index.js | 17 ++ test/unit/use-cases/nostr.use-case.unit.js | 213 +++++++++++++++++ 16 files changed, 1303 insertions(+), 1 deletion(-) create mode 100644 src/controllers/rest-api/nostr/controller.js create mode 100644 src/controllers/rest-api/nostr/index.js create mode 100644 src/entities/deletedChat.js create mode 100644 src/entities/deletedPost.js create mode 100644 src/use-cases/nostr-use-cases.js create mode 100644 test/e2e/automated/a12-nostr.rest-e2e.js create mode 100644 test/unit/controllers/rest-api/nostr/nostr.rest.controller.unit.js create mode 100644 test/unit/controllers/rest-api/nostr/nostr.rest.router.unit.js create mode 100644 test/unit/entities/deletedChat.entity.unit.js create mode 100644 test/unit/entities/deletedPost.entity.unit.js create mode 100644 test/unit/use-cases/nostr.use-case.unit.js diff --git a/src/adapters/localdb/index.js b/src/adapters/localdb/index.js index df8df8e..3551b8b 100644 --- a/src/adapters/localdb/index.js +++ b/src/adapters/localdb/index.js @@ -9,6 +9,8 @@ import Order from './models/order.js' import Offer from './models/offer.js' import Usage from './models/usage.js' import SmAccount from './models/smAccount.js' +import DeletedChat from './models/deletedChat.js' +import DeletedPost from './models/deletedPost.js' class LocalDB { constructor () { @@ -19,6 +21,8 @@ class LocalDB { this.Offer = Offer this.Usage = Usage this.SmAccount = SmAccount + this.DeletedChat = DeletedChat + this.DeletedPost = DeletedPost } } diff --git a/src/controllers/rest-api/index.js b/src/controllers/rest-api/index.js index 7d90ab2..781adc9 100644 --- a/src/controllers/rest-api/index.js +++ b/src/controllers/rest-api/index.js @@ -19,6 +19,7 @@ import OrderRouter from './order/index.js' import P2WDBRouter from './p2wdb/index.js' import UsageRESTController from './usage/index.js' import SmAccountRouter from './smAccount/index.js' +import NostrRouter from './nostr/index.js' class RESTControllers { constructor (localConfig = {}) { @@ -90,6 +91,10 @@ class RESTControllers { const smAccountRouter = new SmAccountRouter(dependencies) smAccountRouter.attach(app) + + // Attach the REST API Controllers associated with the /nostr route + const nostrRouter = new NostrRouter(dependencies) + nostrRouter.attach(app) } } diff --git a/src/controllers/rest-api/nostr/controller.js b/src/controllers/rest-api/nostr/controller.js new file mode 100644 index 0000000..989ec4b --- /dev/null +++ b/src/controllers/rest-api/nostr/controller.js @@ -0,0 +1,219 @@ +/* + REST API Controller library for the /nostr route +*/ +import wlogger from '../../../adapters/wlogger.js' + +class NostrRESTControllerLib { + constructor (localConfig = {}) { + // Dependency Injection. + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating /nostr REST Controller.' + ) + } + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating /nostr REST Controller.' + ) + } + + this.createDeletedChat = this.createDeletedChat.bind(this) + this.getDeletedChats = this.getDeletedChats.bind(this) + this.createDeletedPost = this.createDeletedPost.bind(this) + this.getDeletedPosts = this.getDeletedPosts.bind(this) + this.handleError = this.handleError.bind(this) + } + + /** + * @api {post} /nostr/deletedChat Create Deleted Chat. + * @apiPermission admin + * @apiName CreateDeletedChat + * @apiGroup REST Nostr + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -H "Authorization: Bearer " -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedChat + * + * @apiSuccess {String} _id DB Model id + * @apiSuccess {String} eventId Event id + * @apiSuccess {String} bchAddr BCH Address + * @apiSuccess {String} npub Nostr Npub + * @apiSuccess {String} pubkey Nostr pubkey + * @apiSuccess {Date} createdAt Created at date. + * @apiSuccess {Date} updatedAt Updated at date. + * + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * { + * "_id": "6900f87f9cafee38028ea27e", + * "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8", + * "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", + * "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1", + * "createdAt":"2025-10-28T17:08:15.409Z", + * "updatedAt": "2025-10-28T17:08:15.409Z" + * } + * + * @apiUse TokenError + */ + async createDeletedChat (ctx) { + try { + const chat = ctx.request.body + + const result = await this.useCases.nostr.createDeletedChat(chat) + + ctx.body = { + deletedChat: result + } + } catch (err) { + wlogger.error('Error in createDeletedChat() REST API handler.') + this.handleError(ctx, err) + } + } + + /** + * @api {get} /nostr/deletedChat Get Deleted Chats. + * @apiPermission public + * @apiName GetDeletedChats + * @apiGroup REST Nostr + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedChat + * + * @apiSuccess {String} _id DB Model id + * @apiSuccess {String} eventId Event id + * @apiSuccess {String} bchAddr BCH Address + * @apiSuccess {String} npub Nostr Npub + * @apiSuccess {String} pubkey Nostr pubkey + * @apiSuccess {Date} createdAt Created at date. + * @apiSuccess {Date} updatedAt Updated at date. + * + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * [{ + * "_id": "6900f87f9cafee38028ea27e", + * "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8", + * "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", + * "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1", + * "createdAt":"2025-10-28T17:08:15.409Z", + * "updatedAt": "2025-10-28T17:08:15.409Z" + * }] + * + * */ + async getDeletedChats (ctx) { + try { + const deletedChats = await this.useCases.nostr.getDeletedChats() + + ctx.body = { deletedChats } + } catch (err) { + wlogger.error('Error in getDeletedChats() REST API handler.') + this.handleError(ctx, err) + } + } + + /** + * @api {post} /nostr/deletedPost Create Deleted Post. + * @apiPermission admin + * @apiName CreateDeletedPost + * @apiGroup REST Nostr + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -H "Authorization: Bearer " -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedPost + * + * @apiSuccess {String} _id DB Model id + * @apiSuccess {String} eventId Event id + * @apiSuccess {String} bchAddr BCH Address + * @apiSuccess {String} npub Nostr Npub + * @apiSuccess {String} pubkey Nostr pubkey + * @apiSuccess {Date} createdAt Created at date. + * @apiSuccess {Date} updatedAt Updated at date. + * + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * { + * "_id": "6900f87f9cafee38028ea27e", + * "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8", + * "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", + * "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1", + * "createdAt":"2025-10-28T17:08:15.409Z", + * "updatedAt": "2025-10-28T17:08:15.409Z" + * } + * + * @apiUse TokenError + */ + async createDeletedPost (ctx) { + try { + const post = ctx.request.body + + const result = await this.useCases.nostr.createDeletedPost(post) + + ctx.body = { + deletedPost: result + } + } catch (err) { + wlogger.error('Error in createDeletedPost() REST API handler.') + this.handleError(ctx, err) + } + } + + /** + * @api {get} /nostr/deletedPost Get Deleted Posts. + * @apiPermission public + * @apiName GetDeletedPosts + * @apiGroup REST Nostr + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedPost + * + * @apiSuccess {String} _id DB Model id + * @apiSuccess {String} eventId Event id + * @apiSuccess {String} bchAddr BCH Address + * @apiSuccess {String} npub Nostr Npub + * @apiSuccess {String} pubkey Nostr pubkey + * @apiSuccess {Date} createdAt Created at date. + * @apiSuccess {Date} updatedAt Updated at date. + * + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * [{ + * "_id": "6900f87f9cafee38028ea27e", + * "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8", + * "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", + * "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1", + * "createdAt":"2025-10-28T17:08:15.409Z", + * "updatedAt": "2025-10-28T17:08:15.409Z" + * }] + * + * */ + async getDeletedPosts (ctx) { + try { + const deletedPosts = await this.useCases.nostr.getDeletedPosts() + + ctx.body = { deletedPosts } + } catch (err) { + wlogger.error('Error in getDeletedPosts() REST API handler.') + this.handleError(ctx, err) + } + } + + // DRY error handler + handleError (ctx, err) { + // If an HTTP status is specified by the buisiness logic, use that. + if (err.status) { + if (err.message) { + ctx.throw(err.status, err.message) + } else { + ctx.throw(err.status) + } + } else { + // By default use a 422 error if the HTTP status is not specified. + ctx.throw(422, err.message) + } + } +} + +export default NostrRESTControllerLib diff --git a/src/controllers/rest-api/nostr/index.js b/src/controllers/rest-api/nostr/index.js new file mode 100644 index 0000000..f434c9f --- /dev/null +++ b/src/controllers/rest-api/nostr/index.js @@ -0,0 +1,81 @@ +/* + REST API library for /nostr route. +*/ + +// Public npm libraries. +import Router from 'koa-router' + +// Local libraries. +import NostrRESTControllerLib from './controller.js' + +import Validators from '../middleware/validators.js' + +import config from '../../../../config/index.js' + +class NostrRouter { + constructor (localConfig = {}) { + // Dependency Injection. + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating NostrRouter REST Controller.' + ) + } + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating NostrRouter REST Controller.' + ) + } + + const dependencies = { + adapters: this.adapters, + useCases: this.useCases + } + + // Encapsulate dependencies. + this.config = config + this.nostrRESTController = new NostrRESTControllerLib(dependencies) + this.validators = new Validators() + + // Instantiate the router and set the base route. + const baseUrl = '/nostr' + this.router = new Router({ prefix: baseUrl }) + + this.attach = this.attach.bind(this) + this.createDeletedChat = this.createDeletedChat.bind(this) + this.createDeletedPost = this.createDeletedPost.bind(this) + } + + attach (app) { + if (!app) { + throw new Error( + 'Must pass app object when attaching REST API controllers.' + ) + } + + // Define the routes and attach the controller. + this.router.post('/deletedChat', this.createDeletedChat) + this.router.get('/deletedChat', this.nostrRESTController.getDeletedChats) + this.router.post('/deletedPost', this.createDeletedPost) + this.router.get('/deletedPost', this.nostrRESTController.getDeletedPosts) + + // Attach the Controller routes to the Koa app. + app.use(this.router.routes()) + app.use(this.router.allowedMethods()) + } + + async createDeletedChat (ctx, next) { + await this.validators.ensureAdmin(ctx, next) + await this.nostrRESTController.createDeletedChat(ctx, next) + return true + } + + async createDeletedPost (ctx, next) { + await this.validators.ensureAdmin(ctx, next) + await this.nostrRESTController.createDeletedPost(ctx, next) + return true + } +} + +export default NostrRouter diff --git a/src/entities/deletedChat.js b/src/entities/deletedChat.js new file mode 100644 index 0000000..d3f946f --- /dev/null +++ b/src/entities/deletedChat.js @@ -0,0 +1,17 @@ +/* + DeletedChat Entity +*/ + +class DeletedChat { + validate (data = {}) { + const { eventId } = data + // Input Validation + if (!eventId || typeof eventId !== 'string') { + throw new Error("Property 'eventId' must be a string!") + } + + return data + } +} + +export default DeletedChat diff --git a/src/entities/deletedPost.js b/src/entities/deletedPost.js new file mode 100644 index 0000000..35fee96 --- /dev/null +++ b/src/entities/deletedPost.js @@ -0,0 +1,17 @@ +/* + DeletedPost Entity +*/ + +class DeletedPost { + validate (data = {}) { + const { eventId } = data + // Input Validation + if (!eventId || typeof eventId !== 'string') { + throw new Error("Property 'eventId' must be a string!") + } + + return data + } +} + +export default DeletedPost diff --git a/src/use-cases/index.js b/src/use-cases/index.js index ae123a3..3ff6290 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -11,6 +11,7 @@ import OfferUseCases from './offer/index.js' import OrderUseCases from './order.js' import { UsageUseCases } from './usage-use-cases.js' import SmAccountUseCases from './smAccount-use-cases.js' +import NostrUseCases from './nostr-use-cases.js' class UseCases { constructor (localConfig = {}) { @@ -29,6 +30,7 @@ class UseCases { this.offer = new OfferUseCases(localConfig) this.usage = new UsageUseCases(localConfig) this.smAccount = new SmAccountUseCases(localConfig) + this.nostr = new NostrUseCases(localConfig) } // Run any startup Use Cases at the start of the app. diff --git a/src/use-cases/nostr-use-cases.js b/src/use-cases/nostr-use-cases.js new file mode 100644 index 0000000..3026b76 --- /dev/null +++ b/src/use-cases/nostr-use-cases.js @@ -0,0 +1,92 @@ +/* + This library contains business-logic for dealing with deleted chats. Most of these + functions are called by the /nostr REST API endpoints. +*/ + +import wlogger from '../adapters/wlogger.js' +import DeletedChatEntity from '../entities/deletedChat.js' +import DeletedPostEntity from '../entities/deletedPost.js' + +class NostrUseCases { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of adapters must be passed in when instantiating Nostr Use Cases library.' + ) + } + // Encapsulate dependencies + this.DeletedChatEntity = new DeletedChatEntity() + this.DeletedPostEntity = new DeletedPostEntity() + this.DeletedChatModel = this.adapters.localdb.DeletedChat + this.DeletedPostModel = this.adapters.localdb.DeletedPost + + this.createDeletedChat = this.createDeletedChat.bind(this) + this.getDeletedChats = this.getDeletedChats.bind(this) + this.createDeletedPost = this.createDeletedPost.bind(this) + this.getDeletedPosts = this.getDeletedPosts.bind(this) + } + + // Create a new deleted chat model and add it to the Mongo database. + async createDeletedChat (deletedChatObj) { + try { + // Input Validation + + const deletedChatEntity = this.DeletedChatEntity.validate(deletedChatObj) + const deletedChat = new this.DeletedChatModel(deletedChatEntity) + + // Save the new model to the database. + await deletedChat.save() + return deletedChat + } catch (err) { + // console.log('createDeletedChat() error: ', err) + wlogger.error('Error in lib/nostr.js/createDeletedChat()') + throw err + } + } + + // Returns an array of all deleted chats models in the Mongo database. + async getDeletedChats () { + try { + // Get deleted chats models. + const deletedChats = await this.DeletedChatModel.find({}) + + return deletedChats + } catch (err) { + wlogger.error('Error in lib/users.js/getAllDeletedChat()') + throw err + } + } + + // Create a new deleted post model and add it to the Mongo database. + async createDeletedPost (deletedPostObj) { + try { + // Input Validation + + const deletedPostEntity = this.DeletedPostEntity.validate(deletedPostObj) + const deletedPost = new this.DeletedPostModel(deletedPostEntity) + + // Save the new model to the database. + await deletedPost.save() + return deletedPost + } catch (err) { + wlogger.error('Error in lib/users.js/createDeletedPost()') + throw err + } + } + + // Returns an array of all deleted posts models in the Mongo database. + async getDeletedPosts () { + try { + // Get deleted posts models. + const deletedPosts = await this.DeletedPostModel.find({}) + + return deletedPosts + } catch (err) { + wlogger.error('Error in lib/users.js/getDeletedPosts()') + throw err + } + } +} + +export default NostrUseCases diff --git a/test/e2e/automated/a12-nostr.rest-e2e.js b/test/e2e/automated/a12-nostr.rest-e2e.js new file mode 100644 index 0000000..d6a7ca9 --- /dev/null +++ b/test/e2e/automated/a12-nostr.rest-e2e.js @@ -0,0 +1,188 @@ +import testUtils from '../../utils/test-utils.js' +import { assert } from 'chai' +import config from '../../../config/index.js' +import axios from 'axios' +import sinon from 'sinon' +import util from 'util' + +util.inspect.defaultOptions = { depth: 1 } + +const LOCALHOST = `http://localhost:${config.port}` + +const context = {} +let sandbox + +// const mockContext = require('../../unit/mocks/ctx-mock').context + +if (!config.noMongo) { + describe('Nostr', () => { + before(async () => { + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + + // Create a second test user. + const userObj = { + email: 'test2@test.com', + password: 'pass2', + name: 'test2' + } + const testUser = await testUtils.createUser(userObj) + // console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`) + + context.user2 = testUser.user + context.token2 = testUser.token + context.id2 = testUser.user._id + + // Get the JWT used to log in as the admin 'system' user. + const adminJWT = await testUtils.getAdminJWT() + console.log(`adminJWT: ${adminJWT}`) + context.adminJWT = adminJWT + + // const admin = await testUtils.loginAdminUser() + // context.adminJWT = admin.token + + // const admin = await adminLib.loginAdmin() + // console.log(`admin: ${JSON.stringify(admin, null, 2)}`) + }) + + beforeEach(() => { + // const useCases = new UseCases({ adapters }) + // uut = new UserController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('POST /nostr/deletedChat - Create Deleted Chat', () => { + it('should not create deleted chat if the authorization header is missing', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedChat` + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + it('should not create deleted chat if the authorization header is invalid scheme', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedChat`, + headers: { + Authorization: 'Bearer invalid' + } + } + + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + it('should not create deleted chat if the authorization header is invalid token', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedChat`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + it('should not create deleted chat if token is not admin', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedChat`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + }) + + describe('POST /nostr/deletedPost - Create Deleted Post', () => { + it('should not create deleted post if the authorization header is missing', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedPost` + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not create deleted post if the authorization header is invalid scheme', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedPost`, + headers: { + Authorization: 'Bearer invalid' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + it('should not create deleted post if the authorization header is invalid token', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedPost`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + it('should not create deleted post if token is not admin', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/nostr/deletedPost`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + }) + }) +} diff --git a/test/unit/controllers/rest-api/nostr/nostr.rest.controller.unit.js b/test/unit/controllers/rest-api/nostr/nostr.rest.controller.unit.js new file mode 100644 index 0000000..7edbdbe --- /dev/null +++ b/test/unit/controllers/rest-api/nostr/nostr.rest.controller.unit.js @@ -0,0 +1,201 @@ +/* + Unit tests for the REST API handler for the /nostr endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import NostrController from '../../../../../src/controllers/rest-api/nostr/controller.js' + +import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js' +let uut +let sandbox +let ctx + +describe('#Nostr-REST-Controller', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new NostrController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new NostrController() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /nostr REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new NostrController({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /nostr REST Controller.' + ) + } + }) + }) + + describe('#POST /nostr/deletedChat', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.nostr, 'createDeletedChat').rejects(new Error('test error')) + await uut.createDeletedChat(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + ctx.request.body = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + await uut.createDeletedChat(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + assert.isObject(ctx.response.body) + }) + }) + + describe('#GET /nostr/deletedChat', () => { + it('should return 422 status on arbitrary biz logic error', async () => { + try { + // Force an error + sandbox + .stub(uut.useCases.nostr, 'getDeletedChats') + .rejects(new Error('test error')) + + await uut.getDeletedChats(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + await uut.getDeletedChats(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.isObject(ctx.response.body) + assert.property(ctx.response.body, 'deletedChats') + assert.isArray(ctx.response.body.deletedChats) + }) + }) + describe('#POST /nostr/deletedPost', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.nostr, 'createDeletedPost').rejects(new Error('test error')) + await uut.createDeletedPost(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + it('should return 200 status on success', async () => { + ctx.request.body = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + await uut.createDeletedPost(ctx) + + assert.equal(ctx.status, 200) + + assert.isObject(ctx.response.body) + assert.property(ctx.response.body, 'deletedPost') + assert.isObject(ctx.response.body.deletedPost) + }) + }) + describe('#GET /nostr/deletedPost', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.nostr, 'getDeletedPosts').rejects(new Error('test error')) + await uut.getDeletedPosts(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + it('should return 200 status on success', async () => { + await uut.getDeletedPosts(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + assert.isObject(ctx.response.body) + assert.property(ctx.response.body, 'deletedPosts') + assert.isArray(ctx.response.body.deletedPosts) + }) + }) + + describe('#handleError', () => { + it('should still throw error if there is no message', () => { + try { + const err = { + status: 404 + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) + + it('should throw error with message', () => { + try { + const err = { + status: 422, + message: 'test error' + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/nostr/nostr.rest.router.unit.js b/test/unit/controllers/rest-api/nostr/nostr.rest.router.unit.js new file mode 100644 index 0000000..97eb681 --- /dev/null +++ b/test/unit/controllers/rest-api/nostr/nostr.rest.router.unit.js @@ -0,0 +1,108 @@ +/* + Unit tests for the REST API handler for the /nostr endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' + +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' + +import UseCasesMock from '../../../mocks/use-cases/index.js' + +// const app = require('../../../mocks/app-mock') + +import NostrRouter from '../../../../../src/controllers/rest-api/nostr/index.js' + +let uut +let sandbox +// let ctx + +// const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#Nostr-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new NostrRouter({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + // ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new NostrRouter() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating NostrRouter REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new NostrRouter({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating NostrRouter REST Controller.' + ) + } + }) + }) + + describe('#attach', () => { + it('should throw an error if app is not passed in.', () => { + try { + uut.attach() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Must pass app object when attaching REST API controllers.' + ) + } + }) + }) + + describe('#createDeletedChat', () => { + it('should route to the createDeletedChat function', async () => { + // Stub functions + const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true) + sandbox.stub(uut.nostrRESTController, 'createDeletedChat').resolves(true) + + // Call function + const res = await uut.createDeletedChat() + assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called') + assert.isTrue(res) + }) + }) + + describe('#createDeletedPost', () => { + it('should route to the createDeletedPost function', async () => { + // Stub functions + const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true) + sandbox.stub(uut.nostrRESTController, 'createDeletedPost').resolves(true) + + // Call function + const res = await uut.createDeletedPost() + assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called') + assert.isTrue(res) + }) + }) +}) diff --git a/test/unit/entities/deletedChat.entity.unit.js b/test/unit/entities/deletedChat.entity.unit.js new file mode 100644 index 0000000..0082c0f --- /dev/null +++ b/test/unit/entities/deletedChat.entity.unit.js @@ -0,0 +1,57 @@ +/* + Unit tests for the DeletedChat entity library. +*/ + +import { assert } from 'chai' +import sinon from 'sinon' + +import DeletedChat from '../../../src/entities/deletedChat.js' + +let sandbox +let uut + +describe('#DeletedChat-Entity', () => { + before(async () => {}) + + beforeEach(() => { + uut = new DeletedChat() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('#validate', () => { + it('should throw an error if eventId is not provided', () => { + try { + uut.validate() + } catch (err) { + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should return a DeletedChat object', () => { + const inputData = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + const deletedChat = uut.validate(inputData) + // console.log('entry: ', entry) + + assert.property(deletedChat, 'eventId') + assert.equal(deletedChat.eventId, inputData.eventId) + + assert.property(deletedChat, 'npub') + assert.equal(deletedChat.npub, inputData.npub) + + assert.property(deletedChat, 'bchAddr') + assert.equal(deletedChat.bchAddr, inputData.bchAddr) + + assert.property(deletedChat, 'pubkey') + assert.equal(deletedChat.pubkey, inputData.pubkey) + }) + }) +}) diff --git a/test/unit/entities/deletedPost.entity.unit.js b/test/unit/entities/deletedPost.entity.unit.js new file mode 100644 index 0000000..056c487 --- /dev/null +++ b/test/unit/entities/deletedPost.entity.unit.js @@ -0,0 +1,57 @@ +/* + Unit tests for the DeletedPost entity library. +*/ + +import { assert } from 'chai' +import sinon from 'sinon' + +import DeletedPost from '../../../src/entities/deletedPost.js' + +let sandbox +let uut + +describe('#DeletedPost-Entity', () => { + before(async () => { }) + + beforeEach(() => { + uut = new DeletedPost() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('#validate', () => { + it('should throw an error if eventId is not provided', () => { + try { + uut.validate() + } catch (err) { + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should return a DeletedPost object', () => { + const inputData = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + const deletedPost = uut.validate(inputData) + // console.log('entry: ', entry) + + assert.property(deletedPost, 'eventId') + assert.equal(deletedPost.eventId, inputData.eventId) + + assert.property(deletedPost, 'npub') + assert.equal(deletedPost.npub, inputData.npub) + + assert.property(deletedPost, 'bchAddr') + assert.equal(deletedPost.bchAddr, inputData.bchAddr) + + assert.property(deletedPost, 'pubkey') + assert.equal(deletedPost.pubkey, inputData.pubkey) + }) + }) +}) diff --git a/test/unit/mocks/adapters/index.js b/test/unit/mocks/adapters/index.js index a0f42b4..d500699 100644 --- a/test/unit/mocks/adapters/index.js +++ b/test/unit/mocks/adapters/index.js @@ -165,7 +165,31 @@ const localdb = { async save () { return {} } - } + }, + DeletedChat: class DeletedChat { + constructor (obj) {} + + static findById () {} + static find () {} + static findOne () {} + static updateOne () {} + + async save () { + return {} + } + }, + DeletedPost: class DeletedPost { + constructor (obj) {} + + static findById () {} + static find () {} + static findOne () {} + static updateOne () {} + + async save () { + return {} + } + } } const bch = { diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index a30d32e..f4dead8 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -135,6 +135,22 @@ class SmAccountUseCaseMock { } } +class NostrUseCasesMock { + async createDeletedChat() { + return {} + } + async getDeletedChats() { + return [] + } + async createDeletedPost() { + return {} + } + async getDeletedPosts() { + return [] + } + +} + class UseCasesMock { constuctor(localConfig = {}) { // this.user = new UserUseCaseMock(localConfig) @@ -146,6 +162,7 @@ class UseCasesMock { order = new Order() usage = new UsageUseCaseMock() smAccount = new SmAccountUseCaseMock() + nostr = new NostrUseCasesMock() } export default UseCasesMock; diff --git a/test/unit/use-cases/nostr.use-case.unit.js b/test/unit/use-cases/nostr.use-case.unit.js new file mode 100644 index 0000000..5d50099 --- /dev/null +++ b/test/unit/use-cases/nostr.use-case.unit.js @@ -0,0 +1,213 @@ +/* + Unit tests for the Nostr Use Case library. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Unit under test (uut) +import NostrUseCases from '../../../src/use-cases/nostr-use-cases.js' +// Local support libraries +import adapters from '../mocks/adapters/index.js' + +describe('#nostr-use-cases', () => { + let uut + let sandbox + + before(async () => { + // Delete all previous users in the database. + // await testUtils.deleteAllUsers() + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + + uut = new NostrUseCases({ adapters }) + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new NostrUseCases() + + assert.fail('Unexpected code path') + console.log(uut) // linter + } catch (err) { + assert.include( + err.message, + 'Instance of adapters must be passed in when instantiating Nostr Use Cases library.' + ) + } + }) + }) + + describe('#createDeletedChat', () => { + it('should throw an error if no input is given', async () => { + try { + await uut.createDeletedChat() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + // assert.equal(err.status, 422) + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should throw an error if eventId is not provided', async () => { + try { + await uut.createDeletedChat({}) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should catch and throw DB errors', async () => { + try { + // Force an error with the database. + sandbox.stub(uut, 'DeletedChatModel').throws(new Error('test error')) + + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q' + } + + await uut.createDeletedChat(inObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should create a new deleted chat model with minimum inputs', async () => { + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q' + } + + const res = await uut.createDeletedChat(inObj) + + assert.isObject(res) + }) + it('should create a new deleted chat model with maximum inputs', async () => { + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + const res = await uut.createDeletedChat(inObj) + + assert.isObject(res) + }) + }) + + describe('#getDeletedChats', () => { + it('should return all deleted chats from the database', async () => { + sandbox.stub(uut.DeletedChatModel, 'find').resolves([]) + const res = await uut.getDeletedChats() + + assert.isArray(res) + }) + + it('should catch and throw an error', async () => { + try { + // Force an error. + sandbox.stub(uut.DeletedChatModel, 'find').rejects(new Error('test error')) + + await uut.getDeletedChats() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#createDeletedPost', () => { + it('should throw an error if no input is given', async () => { + try { + await uut.createDeletedPost() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should throw an error if eventId is not provided', async () => { + try { + await uut.createDeletedPost({}) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'eventId' must be a string!") + } + }) + + it('should catch and throw DB errors', async () => { + try { + // Force an error with the database. + sandbox.stub(uut, 'DeletedPostModel').throws(new Error('test error')) + + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q' + } + + await uut.createDeletedPost(inObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should create a new deleted post model with minimum inputs', async () => { + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q' + } + + const res = await uut.createDeletedPost(inObj) + + assert.isObject(res) + }) + it('should create a new deleted post model with maximum inputs', async () => { + const inObj = { + eventId: 'note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q', + npub: 'npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8', + bchAddr: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + pubkey: '6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1' + } + + const res = await uut.createDeletedPost(inObj) + + assert.isObject(res) + }) + }) + + describe('#getDeletedPosts', () => { + it('should return all deleted posts from the database', async () => { + sandbox.stub(uut.DeletedPostModel, 'find').resolves([]) + const res = await uut.getDeletedPosts() + + assert.isArray(res) + }) + }) + + it('should catch and throw an error', async () => { + try { + // Force an error. + sandbox.stub(uut.DeletedPostModel, 'find').rejects(new Error('test error')) + + await uut.getDeletedPosts() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) +})