├── e2e ├── src │ ├── config.js │ ├── services │ │ ├── channel.js │ │ ├── api.js │ │ └── uiHelpers.js │ ├── content │ │ └── app.css │ └── app.js ├── package.json └── index.html ├── src ├── config.js ├── models │ └── Message.js ├── helpers │ └── index.js ├── routes │ └── rooms.js ├── app.js ├── namespaces │ └── index.js └── repositories │ └── channelRepository.js ├── .gitignore ├── package.json ├── LICENSE ├── test ├── helpers.test.js └── channelRepository.test.js └── README.md /e2e/src/config.js: -------------------------------------------------------------------------------- 1 | export const baseUrl = 'http://localhost:3000'; -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | exports.channels = [ 2 | 'random', 3 | 'pullrequests', 4 | ]; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Dependency files 5 | package-lock.json 6 | 7 | # parcel-bundler cache (https://parceljs.org/) 8 | .cache 9 | dist/ 10 | 11 | # IDE 12 | .vscode -------------------------------------------------------------------------------- /src/models/Message.js: -------------------------------------------------------------------------------- 1 | module.exports = class Message { 2 | constructor(userId, text, timestamp) { 3 | this.userId = userId; 4 | this.text = text; 5 | this.timestamp = timestamp; 6 | } 7 | } -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | exports.isDefine = (input) => !!input; 2 | 3 | const nonAnyWhiteSpace = /\S/; 4 | exports.isNotDefineOrWhiteSpace = (input) => ( 5 | !exports.isDefine(input) || 6 | !nonAnyWhiteSpace.test(input) 7 | ); -------------------------------------------------------------------------------- /e2e/src/services/channel.js: -------------------------------------------------------------------------------- 1 | import ioClient from 'socket.io-client'; 2 | // TODO: Discuss to move out of this service. 3 | export const messageFactory = (channel, user) => ({ 4 | compose: (text) => ({ 5 | channel, 6 | user, 7 | text, 8 | }), 9 | }); 10 | 11 | export const createSocket = ({url, channel, options}) => 12 | ioClient(`${url}/${channel}`, options); -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket_example_client", 3 | "version": "1.0.0", 4 | "description": "Test that environment works", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start:e2e": "node ../src/app.js | parcel index.html", 9 | "start:client": "parcel index.html" 10 | }, 11 | "author": "Jaime Salas", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "parcel": "^1.9.7" 15 | }, 16 | "dependencies": { 17 | "socket.io": "^2.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e/src/services/api.js: -------------------------------------------------------------------------------- 1 | import { baseUrl } from '../config'; 2 | 3 | const apiUrl = `${baseUrl}/api`; 4 | 5 | export const fetchRooms = () => ( 6 | fetch(`${apiUrl}/rooms`) 7 | .then((response) => { 8 | if (response.ok) { 9 | return response.json(); 10 | } 11 | }) 12 | ); 13 | 14 | export const canenrollRoom = (roomId, userId) => { 15 | return fetch(`${apiUrl}/rooms/canenroll/${roomId}/user`, { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | userId 19 | }), 20 | mode: 'cors', 21 | headers: { 'Content-Type': 'application/json' } 22 | }).then((response) => { 23 | if (response.ok) { 24 | return response.json(); 25 | } 26 | }); 27 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-chat-back", 3 | "version": "1.0.0", 4 | "description": "Backend for chat demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec", 8 | "start": "node src/app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Lemoncode/redux-chat-back.git" 13 | }, 14 | "author": "Lemoncode", 15 | "license": "MIT", 16 | "dependencies": { 17 | "body-parser": "^1.18.3", 18 | "cors": "^2.8.4", 19 | "express": "^4.16.3", 20 | "socket.io": "^2.1.1" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/Lemoncode/redux-chat-back/issues" 24 | }, 25 | "homepage": "https://github.com/Lemoncode/redux-chat-back#readme", 26 | "devDependencies": { 27 | "chai": "^4.1.2", 28 | "mocha": "^5.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/rooms.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | module.exports = (repository) => ( 5 | router 6 | .get('/', async (_, res, next) => { 7 | try { 8 | const rooms = await repository.channelList(); 9 | console.log(rooms); 10 | res.json(rooms); 11 | } catch (error) { 12 | next(error); 13 | } 14 | }) 15 | .post('/canenroll/:roomId/user', async (req, res, next) => { 16 | try { 17 | const { roomId } = req.params; 18 | const { userId } = req.body; 19 | const exists = await repository.existUser(roomId, userId); 20 | res.json(!exists); 21 | } catch (error) { 22 | next(error); 23 | } 24 | }) 25 | ); 26 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | app = express(), 3 | bodyParser = require('body-parser'), 4 | cors = require('cors'), 5 | repository = require('./repositories/channelRepository'), 6 | namespaceFactory = require('./namespaces'); 7 | 8 | const { channels } = require('./config'); 9 | 10 | const initializeChannels = async (io, channels, repository) => { 11 | await Promise.all( 12 | channels.map(c => repository.createChannel(c)) 13 | ); 14 | channels.map(c => ({ 15 | io, 16 | channel: c, 17 | repository 18 | })) 19 | .forEach(i => namespaceFactory(i)); 20 | }; 21 | 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | app.use(bodyParser.json()); 24 | app.use(cors()); 25 | 26 | const roomsRouter = require('./routes/rooms')(repository); 27 | app.use('/api/rooms', roomsRouter); 28 | 29 | const server = require('http').Server(app); 30 | const io = require('socket.io')(server); 31 | server.listen(3000); 32 | // TODO: Create env variable to initialize repository in memomory 33 | initializeChannels(io, channels, repository); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lemoncode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /e2e/src/services/uiHelpers.js: -------------------------------------------------------------------------------- 1 | export const clearMessageText = (element) => element.value = ''; 2 | 3 | export const addMessageText = (containerId, tag, message) => { 4 | const messageElement = document.createElement(tag); 5 | messageElement.innerText = `${message.user}: ${message.text}`; 6 | document.getElementById(containerId).appendChild(messageElement); 7 | }; 8 | 9 | const createRoomItem = (roomName) => { 10 | const roomItem = document.createElement('li'); 11 | roomItem.innerText = roomName; 12 | return roomItem; 13 | }; 14 | 15 | export const populateRooms = (rooms, elementId) => { 16 | const roomsContainer = document.getElementById(elementId); 17 | const roomItems = rooms.map(createRoomItem); 18 | roomItems.forEach((ri) => roomsContainer.appendChild(ri)); 19 | }; 20 | 21 | export const changeSelectedRoom = (selectedRoomId, value) => { 22 | document.getElementById(selectedRoomId).innerText = value; 23 | }; 24 | 25 | export const selectedRoomValue = (selectedRoomId) => ( 26 | document.getElementById(selectedRoomId).innerHTML 27 | ); 28 | 29 | export const userInputValue = (userId) => ( 30 | document.getElementById(userId).value 31 | ); -------------------------------------------------------------------------------- /src/namespaces/index.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ io, channel, repository }) => { 2 | const namespace = io.of(channel); 3 | 4 | namespace.on('connection', (socket) => { 5 | const { user } = socket.handshake.query; 6 | repository.addUser(channel, user) 7 | .then(() => { 8 | console.log(`Added user:${user} to channel: random`) 9 | }) 10 | .catch(() => socket.disconnect()); 11 | 12 | socket.on('messages', () => { 13 | console.log('messages request'); 14 | repository.getMessages(channel) 15 | .then((messages) => { 16 | socket.emit('messages', messages); 17 | }) 18 | .catch(e => console.log(e)); 19 | }); 20 | 21 | socket.on('message', (message) => { 22 | const { channel, user, text } = message; 23 | const serverMessage = { 24 | channelKey: channel, 25 | userId: user, 26 | text, 27 | }; 28 | 29 | repository.addMessage(serverMessage) 30 | .then(() => { 31 | namespace.emit('message', message); 32 | }) 33 | .catch(e => console.log(e)); 34 | }); 35 | }); 36 | }; -------------------------------------------------------------------------------- /e2e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | 31 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { isNotDefineOrWhiteSpace } = require('../src/helpers'); 4 | 5 | describe('helpers', () => { 6 | describe('isNotDefineOrWhiteSpace', () => { 7 | describe('when input is null', () => { 8 | it('returns true', () => { 9 | const input = null; 10 | const result = isNotDefineOrWhiteSpace(input); 11 | expect(result).true; 12 | }); 13 | }); 14 | 15 | describe('when input is undefined', () => { 16 | it('resturns true', () => { 17 | let input; 18 | const result = isNotDefineOrWhiteSpace(input); 19 | expect(result).true; 20 | }); 21 | }); 22 | 23 | describe('when input is empty', () => { 24 | it('returns true', () => { 25 | const input = ''; 26 | const result = isNotDefineOrWhiteSpace(input); 27 | expect(result).true; 28 | }); 29 | }); 30 | 31 | describe('when input is white space', () => { 32 | it('returns true', () => { 33 | const input = ' '; 34 | const result = isNotDefineOrWhiteSpace(input); 35 | expect(result).true; 36 | }); 37 | }); 38 | 39 | describe('when input has value', () => { 40 | it('returns false', () => { 41 | const input = 'v'; 42 | const result = isNotDefineOrWhiteSpace(input); 43 | expect(result).false; 44 | }); 45 | }); 46 | 47 | describe('when input starts with white sapace and has characters', () => { 48 | it('returns false', () => { 49 | const input = ' CA'; 50 | const result = isNotDefineOrWhiteSpace(input); 51 | expect(result).false; 52 | }); 53 | }); 54 | }); 55 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-chat-back 2 | 3 | ## Summary 4 | 5 | * This solution works with `express` in combination with `socket.io`. To run this app you will need node >= 10.x.x 6 | 7 | ## Start API 8 | 9 | * To start the main API on development mode from root solution folder. 10 | * The `API`, will run in `localhost`, `port:3000`. 11 | * The protocol for API is `http`. 12 | 13 | ```bash 14 | $ npm start 15 | ``` 16 | 17 | ## Unit Testing 18 | 19 | * To run unit tests from root folder: 20 | 21 | ```bash 22 | $ npm test 23 | ``` 24 | 25 | 26 | ## Funtcional Test API 27 | 28 | ### To test `echo` with server, from `e2e` as root folder run: 29 | 30 | ```bash 31 | $ npm run start:echo 32 | ``` 33 | 34 | * This will start up the `client` and the `API`. 35 | * Open the client on browser: `http://localhost:1234/` 36 | * Type and send message, in browser console must appear: 37 | - `/random-channel#79PVp22K8iHx6tTCAAAA` This identifies the socket.io namespace and socket id 38 | - `{channel: "random-channel", user: "pepe", text: "your text..."}` The message that we sent to server, and it's sending back to us. Just an echo. 39 | 40 | ### To test `API` with `client`, we need to open two separate node processes. First we have to run the server, from root folder: 41 | 42 | ```bash 43 | $ npm start 44 | ``` 45 | * The `API`, will run in `localhost`, `port:3000`. 46 | 47 | * Once server is up and running, we can launch client, from `e2e` folder run 48 | ```bash 49 | $ npm run start:client 50 | ``` 51 | 52 | * The `client`, will run in `localhost`, `port:1234` 53 | 54 | # About Basefactor + Lemoncode 55 | 56 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 57 | 58 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 59 | 60 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 61 | 62 | For the LATAM/Spanish audience check out [Online Devops Bootcamp](https://lemoncode.net/bootcamp-devops#bootcamp-devops/inicio) 63 | -------------------------------------------------------------------------------- /e2e/src/content/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Tahoma, sans-serif; 3 | } 4 | 5 | /* 6 | Main block 7 | */ 8 | .chat { 9 | display: grid; 10 | grid-template-columns: 1fr 1fr 1fr 1fr; 11 | grid-template-rows: auto auto auto; 12 | grid-column-gap: 16px; 13 | grid-template-areas: "main main main rooms" 14 | "main main main rooms" 15 | "sender sender sender room-selector"; 16 | } 17 | 18 | @media screen and (max-width: 800px) { 19 | /* Here we can use a flex conatiner */ 20 | .chat { 21 | display: grid; 22 | grid-template-columns: 1fr 1fr; 23 | grid-template-rows: auto auto auto; 24 | grid-template-areas: "main main" 25 | "main main" 26 | "sender sender"; 27 | } 28 | 29 | .chat__rooms { 30 | display: none; 31 | } 32 | 33 | .chat__room-selector { 34 | display: none; 35 | } 36 | } 37 | 38 | /* 39 | Block elements 40 | */ 41 | .chat__main { 42 | grid-area: main; 43 | } 44 | 45 | .chat__rooms { 46 | grid-area: rooms; 47 | } 48 | 49 | .chat__sender { 50 | grid-area: sender; 51 | } 52 | 53 | .chat__room-selector { 54 | grid-area: room-selector; 55 | } 56 | 57 | .enrolment { 58 | display: flex; 59 | justify-content: space-between; 60 | padding-bottom: 16px; 61 | padding-top: 16px; 62 | } 63 | 64 | .elronment__field { 65 | padding: 8px; 66 | margin-right: 16px; 67 | } 68 | 69 | .elronment__field-input { 70 | -moz-border-radius:2px; 71 | -webkit-border-radius:2px; 72 | border-radius:2px; 73 | border: 0.5px solid silver; 74 | } 75 | 76 | .enrolment__submit { 77 | background-color:#0dd117; 78 | -moz-border-radius:4px; 79 | -webkit-border-radius:4px; 80 | border-radius:4px; 81 | border:2px solid #0dd117; 82 | display:inline-block; 83 | cursor:pointer; 84 | color:#ffffff; 85 | font-size:9px; 86 | padding:8px 16px; 87 | text-decoration:none; 88 | /* text-shadow:0px 1px 0px #528ecc; */ 89 | } 90 | 91 | .rooms-container { 92 | list-style: none; 93 | } 94 | 95 | .messages-container { 96 | border: 1px solid silver; 97 | margin-bottom: 5px; 98 | padding: 4px; 99 | border-radius: 4px; 100 | min-height: 80vh; 101 | display: flex; 102 | flex-direction: column; 103 | } 104 | 105 | .message-sender { 106 | border: 1px solid black; 107 | padding: 4px; 108 | border-radius: 4px; 109 | display: flex; 110 | justify-content: space-between; 111 | } 112 | 113 | .message-sender__text { 114 | width: 90%; 115 | margin-right: 5px; 116 | resize: none; 117 | } 118 | 119 | .message-sender__submit { 120 | background-color:#79bbff; 121 | -moz-border-radius:4px; 122 | -webkit-border-radius:4px; 123 | border-radius:4px; 124 | border:2px solid #337bc4; 125 | display:inline-block; 126 | cursor:pointer; 127 | color:#ffffff; 128 | font-size:17px; 129 | padding:16px 31px; 130 | text-decoration:none; 131 | text-shadow:0px 1px 0px #528ecc; 132 | } 133 | 134 | .message-sender__submit--hover { 135 | background-color:#378de5; 136 | } 137 | 138 | .message-sender__submit--active { 139 | position:relative; 140 | top:1px; 141 | } 142 | -------------------------------------------------------------------------------- /src/repositories/channelRepository.js: -------------------------------------------------------------------------------- 1 | const Message = require('../models/Message'); 2 | const { isNotDefineOrWhiteSpace } = require('../helpers'); 3 | 4 | const channels = []; 5 | 6 | const createChannel = (channelKey) => { 7 | channels[channelKey] = { 8 | users: [], 9 | messages: [], 10 | }; 11 | }; 12 | 13 | exports.channelList = () => ( 14 | Promise.resolve(Object.keys(channels)) 15 | ); 16 | 17 | exports.existChannel = (channelKey) => ( 18 | new Promise((resolve, _) => { 19 | exports.channelList().then((channelList) => { 20 | resolve( 21 | !!channelList.find(c => c === channelKey) 22 | ); 23 | }); 24 | }) 25 | ); 26 | 27 | exports.createChannel = async (channelKey) => { 28 | const channelExist = await exports.existChannel(channelKey); 29 | return new Promise((resolve, reject) => { 30 | if (isNotDefineOrWhiteSpace(channelKey)) { 31 | reject('No value for channelKey.'); 32 | } 33 | if (channelExist) { 34 | reject(`Channel ${channelKey} already exist.`); 35 | } 36 | createChannel(channelKey); 37 | resolve(); 38 | }); 39 | }; 40 | 41 | exports.existUser = async (channelKey, userId) => { 42 | const existChannel = await exports.existChannel(channelKey); 43 | return new Promise((resolve, reject) => { 44 | if (!existChannel) { 45 | reject(`${channelKey} does not exist.`); 46 | }; 47 | resolve( 48 | !!channels[channelKey].users.find(u => u === userId) 49 | ); 50 | }); 51 | }; 52 | 53 | exports.addUser = async (channelKey, userId) => { 54 | const existChannel = await exports.existChannel(channelKey); 55 | const existUser = await exports.existUser(channelKey, userId); 56 | return new Promise((resolve, reject) => { 57 | if ( 58 | isNotDefineOrWhiteSpace(channelKey) || 59 | isNotDefineOrWhiteSpace(userId) 60 | ) { 61 | reject('No value for channelKey or userId'); 62 | } 63 | 64 | if (!existChannel) { 65 | reject(`${channelKey} does not exist.`); 66 | } 67 | if (!existUser) { 68 | channels[channelKey].users = [...channels[channelKey].users, userId]; 69 | resolve(); 70 | } 71 | reject(`${userId} already exists in this channel.`); 72 | }); 73 | }; 74 | 75 | const canAddMessage = async (channelKey, userId) => ( 76 | await exports.existChannel(channelKey) && 77 | await exports.existUser(channelKey, userId) 78 | ); 79 | 80 | exports.addMessage = async ({ channelKey, userId, text }) => { 81 | const canAddMessageResolved = await canAddMessage(channelKey, userId) 82 | return new Promise((resolve, reject) => { 83 | if (canAddMessageResolved) { 84 | channels[channelKey].messages = [ 85 | ...channels[channelKey].messages, 86 | new Message(userId, text, Date.now()) 87 | ]; 88 | resolve(); 89 | } 90 | reject(`Channel: ${channelKey} or User: ${userId} does not exist.`); 91 | }); 92 | }; 93 | 94 | exports.getMessages = async (channelKey) => { 95 | const existChannel = await exports.existChannel(channelKey); 96 | return new Promise((resolve, reject) => { 97 | if (existChannel) { 98 | resolve(channels[channelKey].messages); 99 | } 100 | reject(`${channelKey} does not exist.`); 101 | }); 102 | }; -------------------------------------------------------------------------------- /e2e/src/app.js: -------------------------------------------------------------------------------- 1 | import { createSocket, messageFactory } from './services/channel'; 2 | import { 3 | clearMessageText, 4 | addMessageText, 5 | populateRooms, 6 | changeSelectedRoom, 7 | userInputValue, 8 | selectedRoomValue 9 | } from './services/uiHelpers'; 10 | import { fetchRooms, canenrollRoom } from './services/api'; 11 | import { baseUrl } from './config'; 12 | 13 | const socketEventsHandler = (socket) => { 14 | socket.on('connect', () => { 15 | console.log(socket.id); 16 | socket.emit('messages'); 17 | }); 18 | socket.on('error', (err) => console.log(err)); 19 | socket.on('disconnect', () => console.log('disconnected')) 20 | 21 | socket.on('message', (msg) => { 22 | addMessageText('messages', 'p', msg); 23 | const textElement = document.getElementById('message'); 24 | clearMessageText(textElement); 25 | }); 26 | socket.on('messages', (msgs) => { 27 | // NOTE: Timestamp it's created on server not used on client yet. 28 | console.log('messages', msgs); 29 | msgs.map((ms) => ({ 30 | user: ms.userId, 31 | text: ms.text 32 | })).forEach((mc) => { 33 | addMessageText('messages', 'p', mc); 34 | }); 35 | }); 36 | }; 37 | 38 | const handleSelectedRoom = () => { 39 | // NOTE: Bear on mind to disconnect from socket channel. 40 | document.getElementById('rooms') 41 | .addEventListener('click', (evt) => { 42 | evt.stopPropagation(); 43 | changeSelectedRoom('selectedroom', evt.target.innerHTML); 44 | }); 45 | }; 46 | 47 | const handleEnrolment = (callback) => { 48 | // NOTE: Check selected room. 49 | // NOTE: Check input user. 50 | document.getElementById('enrolmentsubmit') 51 | .addEventListener('click', (evt) => { 52 | evt.stopPropagation(); 53 | evt.preventDefault(); 54 | const room = selectedRoomValue('selectedroom'); 55 | const userId = userInputValue('userid'); 56 | canenrollRoom( 57 | room, 58 | userId, 59 | ).then((canenroll) => { 60 | if (canenroll) { 61 | callback(null, room, userId); 62 | } else { 63 | callback('Can not enroll to this channel'); 64 | } 65 | } 66 | ).catch(e => console.log(e)); 67 | }); 68 | }; 69 | 70 | const handleSender = (socket, room, user) => { 71 | const _messageFactory = messageFactory(room, user); 72 | document.getElementById('sendbutton') 73 | .addEventListener('click', (evt) => { 74 | evt.stopPropagation(); 75 | const textElement = document.getElementById('message'); 76 | const message = _messageFactory.compose(textElement.value) 77 | socket.emit('message', message); 78 | }); 79 | }; 80 | 81 | document.addEventListener('DOMContentLoaded', () => { 82 | fetchRooms() 83 | .then((rooms) => { 84 | populateRooms(rooms, 'rooms'); 85 | handleSelectedRoom(); 86 | }) 87 | .catch(e => console.log(e)); 88 | 89 | handleEnrolment((err, room, userId) => { 90 | if (!err) { 91 | const socketParams = { 92 | url: baseUrl, 93 | channel: room, 94 | options: { 95 | query: `user=${userId}` 96 | }, 97 | }; 98 | const socket = createSocket(socketParams); 99 | socketEventsHandler(socket); 100 | handleSender(socket, room, userId); 101 | } 102 | }); 103 | }); 104 | 105 | -------------------------------------------------------------------------------- /test/channelRepository.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const channelRepository = require('../src/repositories/channelRepository'); 4 | 5 | describe('channelRepository', () => { 6 | describe('adding new channel with empty channelKey', () => { 7 | it('should avoid add channel', async () => { 8 | const channelKey = ''; 9 | try { 10 | await channelRepository.createChannel(channelKey); 11 | } catch (error) { 12 | expect(error).equals('No value for channelKey.'); 13 | } 14 | }); 15 | }); 16 | 17 | describe('adding new channel with null channelKey', () => { 18 | it('should avoid add channel', async () => { 19 | const channelKey = null; 20 | try { 21 | await channelRepository.createChannel(channelKey); 22 | } catch (error) { 23 | expect(error).equals('No value for channelKey.'); 24 | } 25 | }); 26 | }); 27 | 28 | describe('adding new channel with undefined channelKey', () => { 29 | it('should avoid add channel', async () => { 30 | let channelKey; 31 | try { 32 | await channelRepository.createChannel(channelKey); 33 | } catch (error) { 34 | expect(error).equals('No value for channelKey.'); 35 | } 36 | }); 37 | }); 38 | 39 | describe('adding new channel with white space channelKey', () => { 40 | it('should avoid add channel', async () => { 41 | const channelKey = ' '; 42 | try { 43 | await channelRepository.createChannel(channelKey); 44 | } catch (error) { 45 | expect(error).equals('No value for channelKey.'); 46 | } 47 | }); 48 | }); 49 | 50 | describe('adding new channel to empty repository', () => { 51 | it('should make repository to have elements', async () => { 52 | await channelRepository.createChannel('foo'); 53 | const channelList = await channelRepository.channelList(); 54 | expect(channelList.length).to.greaterThan(0); 55 | }); 56 | }); 57 | 58 | describe('adding twice a channel to repository', () => { 59 | it('should avoid duplicated channel', async () => { 60 | await channelRepository.createChannel('comics'); 61 | try { 62 | await channelRepository.createChannel('comics'); 63 | } catch (error) { 64 | expect(error).equals('Channel comics already exist.'); 65 | } 66 | }); 67 | }); 68 | 69 | describe('adding a user with empty userId', () => { 70 | it('should avoid insert user', async () => { 71 | const userId = ''; 72 | try { 73 | await channelRepository.addUser('comics', userId); 74 | } catch (error) { 75 | expect(error).equals('No value for channelKey or userId'); 76 | } 77 | }); 78 | }); 79 | 80 | describe('adding a user with null userId', () => { 81 | it('should avoid insert user', async () => { 82 | const userId = null; 83 | try { 84 | await channelRepository.addUser('comics', userId); 85 | } catch (error) { 86 | expect(error).equals('No value for channelKey or userId'); 87 | } 88 | }); 89 | }); 90 | 91 | describe('adding a user with undefined userId', () => { 92 | it('should avoid insert user', async () => { 93 | let userId; 94 | try { 95 | await channelRepository.addUser('comics', userId); 96 | } catch (error) { 97 | expect(error).equals('No value for channelKey or userId'); 98 | } 99 | }); 100 | }); 101 | 102 | describe('adding a user with white space userId', () => { 103 | it('should avoid insert user', async () => { 104 | const userId = ' '; 105 | try { 106 | await channelRepository.addUser('comics', userId); 107 | } catch (error) { 108 | expect(error).equals('No value for channelKey or userId'); 109 | } 110 | }); 111 | }); 112 | 113 | describe('adding twice a user to a channel repository', () => { 114 | it('should avoid duplicated user', async () => { 115 | await channelRepository.addUser('comics', 'jai'); 116 | try { 117 | await channelRepository.addUser('comics', 'jai'); 118 | } catch (error) { 119 | expect(error).equals('jai already exists in this channel.'); 120 | } 121 | }); 122 | }); 123 | 124 | describe('request messages from channel with messages', () => { 125 | it('should return messages', async () => { 126 | const message = { 127 | channelKey: 'comics', 128 | userId: 'jai', 129 | text: 'batman is awesome', 130 | }; 131 | await channelRepository.addMessage(message); 132 | const messages = await channelRepository.getMessages(message.channelKey); 133 | expect(messages.length).to.greaterThan(0); 134 | }); 135 | }); 136 | 137 | describe('add message to non existing channel', () => { 138 | it('should avoid add message', async () => { 139 | try { 140 | const message = { 141 | channelKey: 'no exists', 142 | userId: 'jai', 143 | text: 'plain text' 144 | }; 145 | await channelRepository.addMessage(message); 146 | } catch (error) { 147 | expect(error).equals('Channel: no exists or User: jai does not exist.'); 148 | } 149 | }); 150 | }); 151 | 152 | describe('not existing user adding a message to channel', () => { 153 | it('should avoid add message', async () => { 154 | try { 155 | const message = { 156 | channelKey: 'noexist', 157 | userId: 'no exists', 158 | text: 'plain text' 159 | }; 160 | await channelRepository.addMessage(message); 161 | } catch (error) { 162 | expect(error).equals('Channel: noexist or User: no exists does not exist.'); 163 | } 164 | }); 165 | }); 166 | }); --------------------------------------------------------------------------------