├── .bundle └── config ├── .circleci └── config.yml ├── .env.sample ├── .gitignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── Rakefile ├── app.json ├── app.rb ├── config.ru ├── controllers ├── base.rb ├── basic.rb ├── helpers │ ├── authorization.rb │ └── validation.rb ├── message.rb ├── room.rb └── user.rb ├── docker-compose.yml ├── models ├── room.rb └── user.rb ├── mongoid.yml ├── newrelic.yml ├── services └── message_service.rb └── spec ├── factories ├── room.rb └── user.rb ├── models ├── room_model_spec.rb └── user_model_spec.rb ├── requests ├── basic_test_spec.rb ├── message_test_spec.rb ├── room_test_spec.rb └── user_test_spec.rb └── spec_helper.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_JOBS: "4" 3 | BUNDLE_PATH: "vendor/path" 4 | BUNDLE_DISABLE_SHARED_GEMS: "true" 5 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: true 5 | steps: 6 | - checkout 7 | - run: 8 | name: install docker-compose 9 | command: | 10 | curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose 11 | chmod +x ~/docker-compose 12 | sudo mv ~/docker-compose /usr/local/bin/docker-compose 13 | - run: 14 | name: build and run tests 15 | command: | 16 | mkdir ~/persistence 17 | docker-compose up -d 18 | docker-compose run app bundle exec rspec spec --format progress 19 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | ADMIN_PASS=superduper 2 | 3 | RACK_ENV= 4 | 5 | MONGODB_URI= 6 | 7 | CORS_ALLOWED_ORIGINS= 8 | 9 | SESSION_SECRET= 10 | MEMCACHEDCLOUD_SERVERS= 11 | MEMCACHEDCLOUD_USERNAME= 12 | MEMCACHEDCLOUD_PASSWORD= 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dump.rdb 2 | log.txt 3 | .env 4 | .rspec 5 | .ruby-version 6 | 7 | tmp/ 8 | log/ 9 | vendor/* 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6.6 2 | 3 | # throw errors if Gemfile has been modified since Gemfile.lock 4 | RUN bundle config --global frozen 1 5 | 6 | WORKDIR /usr/src/app 7 | 8 | COPY Gemfile Gemfile.lock ./ 9 | RUN gem install bundler 10 | RUN bundle install 11 | 12 | COPY . . 13 | 14 | CMD bundle exec thin start -p 3000 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby "2.6.6" 3 | 4 | gem 'sinatra', '~> 2.0.2' 5 | gem 'sinatra-errorcodes' 6 | gem 'sinatra-rocketio' 7 | gem 'async_sinatra' 8 | 9 | gem 'dry-validation' 10 | 11 | gem 'shotgun' 12 | gem 'thin' 13 | 14 | gem 'mongoid' 15 | 16 | gem 'sysrandom' 17 | gem 'bcrypt' 18 | gem 'dotenv' 19 | 20 | gem 'dalli' 21 | gem 'rack-cache' 22 | gem 'rack-health' 23 | gem 'rack-cors' 24 | gem 'rack-contrib' 25 | 26 | gem 'rake' 27 | 28 | gem 'parallel' 29 | gem 'promise' 30 | 31 | # For security reasons 32 | gem 'rack', '>= 2.0.6' 33 | 34 | group :development, :test do 35 | gem 'rb-readline' 36 | gem 'pry', require: true 37 | gem 'pry-byebug' 38 | 39 | gem 'awesome_print' 40 | end 41 | 42 | group :test do 43 | gem 'rspec' 44 | gem 'mongoid-rspec' 45 | gem 'rack-test' 46 | 47 | gem 'database_cleaner' 48 | gem 'factory_girl' 49 | gem 'faker' 50 | end 51 | 52 | group :production do 53 | gem 'rack-ssl-enforcer' 54 | end 55 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (6.0.3.1) 5 | activesupport (= 6.0.3.1) 6 | activesupport (6.0.3.1) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 0.7, < 2) 9 | minitest (~> 5.1) 10 | tzinfo (~> 1.1) 11 | zeitwerk (~> 2.2, >= 2.2.2) 12 | async_sinatra (1.3.0) 13 | rack (>= 2.0.0) 14 | sinatra (>= 1.4.8) 15 | awesome_print (1.8.0) 16 | backports (3.17.1) 17 | bcrypt (3.1.13) 18 | bson (4.8.2) 19 | byebug (11.1.3) 20 | coderay (1.1.2) 21 | concurrent-ruby (1.1.6) 22 | daemons (1.3.1) 23 | dalli (2.7.10) 24 | database_cleaner (1.8.4) 25 | diff-lcs (1.3) 26 | dotenv (2.7.5) 27 | dry-configurable (0.11.5) 28 | concurrent-ruby (~> 1.0) 29 | dry-core (~> 0.4, >= 0.4.7) 30 | dry-equalizer (~> 0.2) 31 | dry-container (0.7.2) 32 | concurrent-ruby (~> 1.0) 33 | dry-configurable (~> 0.1, >= 0.1.3) 34 | dry-core (0.4.9) 35 | concurrent-ruby (~> 1.0) 36 | dry-equalizer (0.3.0) 37 | dry-inflector (0.2.0) 38 | dry-initializer (3.0.3) 39 | dry-logic (1.0.6) 40 | concurrent-ruby (~> 1.0) 41 | dry-core (~> 0.2) 42 | dry-equalizer (~> 0.2) 43 | dry-schema (1.5.0) 44 | concurrent-ruby (~> 1.0) 45 | dry-configurable (~> 0.8, >= 0.8.3) 46 | dry-core (~> 0.4) 47 | dry-equalizer (~> 0.2) 48 | dry-initializer (~> 3.0) 49 | dry-logic (~> 1.0) 50 | dry-types (~> 1.4) 51 | dry-types (1.4.0) 52 | concurrent-ruby (~> 1.0) 53 | dry-container (~> 0.3) 54 | dry-core (~> 0.4, >= 0.4.4) 55 | dry-equalizer (~> 0.3) 56 | dry-inflector (~> 0.1, >= 0.1.2) 57 | dry-logic (~> 1.0, >= 1.0.2) 58 | dry-validation (1.5.0) 59 | concurrent-ruby (~> 1.0) 60 | dry-container (~> 0.7, >= 0.7.1) 61 | dry-core (~> 0.4) 62 | dry-equalizer (~> 0.2) 63 | dry-initializer (~> 3.0) 64 | dry-schema (~> 1.5) 65 | em-websocket (0.5.1) 66 | eventmachine (>= 0.12.9) 67 | http_parser.rb (~> 0.6.0) 68 | event_emitter (0.2.6) 69 | eventmachine (1.2.7) 70 | factory_girl (4.9.0) 71 | activesupport (>= 3.0.0) 72 | faker (2.11.0) 73 | i18n (>= 1.6, < 2) 74 | hashie (4.1.0) 75 | http_parser.rb (0.6.0) 76 | httparty (0.18.0) 77 | mime-types (~> 3.0) 78 | multi_xml (>= 0.5.2) 79 | i18n (1.8.2) 80 | concurrent-ruby (~> 1.0) 81 | json (2.3.0) 82 | method_source (1.0.0) 83 | mime-types (3.3.1) 84 | mime-types-data (~> 3.2015) 85 | mime-types-data (3.2020.0425) 86 | minitest (5.14.1) 87 | mongo (2.12.1) 88 | bson (>= 4.8.2, < 5.0.0) 89 | mongoid (7.1.1) 90 | activemodel (>= 5.1, < 6.1) 91 | mongo (>= 2.7.0, < 3.0.0) 92 | mongoid-compatibility (0.5.1) 93 | activesupport 94 | mongoid (>= 2.0) 95 | mongoid-rspec (4.0.1) 96 | activesupport (>= 3.0.0) 97 | mongoid (>= 3.1) 98 | mongoid-compatibility (>= 0.5.1) 99 | rspec (~> 3.3) 100 | multi_json (1.14.1) 101 | multi_xml (0.6.0) 102 | mustermann (1.1.1) 103 | ruby2_keywords (~> 0.0.1) 104 | parallel (1.19.1) 105 | promise (0.3.1) 106 | pry (0.13.1) 107 | coderay (~> 1.1) 108 | method_source (~> 1.0) 109 | pry-byebug (3.9.0) 110 | byebug (~> 11.0) 111 | pry (~> 0.13.0) 112 | rack (2.2.2) 113 | rack-cache (1.11.1) 114 | rack (>= 0.4) 115 | rack-contrib (2.2.0) 116 | rack (~> 2.0) 117 | rack-cors (1.1.1) 118 | rack (>= 2.0.0) 119 | rack-health (0.1.2) 120 | rack (>= 1.2.0) 121 | rack-protection (2.0.8.1) 122 | rack 123 | rack-ssl-enforcer (0.2.9) 124 | rack-test (1.1.0) 125 | rack (>= 1.0, < 3) 126 | rake (13.0.1) 127 | rb-readline (0.5.5) 128 | rspec (3.9.0) 129 | rspec-core (~> 3.9.0) 130 | rspec-expectations (~> 3.9.0) 131 | rspec-mocks (~> 3.9.0) 132 | rspec-core (3.9.2) 133 | rspec-support (~> 3.9.3) 134 | rspec-expectations (3.9.1) 135 | diff-lcs (>= 1.2.0, < 2.0) 136 | rspec-support (~> 3.9.0) 137 | rspec-mocks (3.9.1) 138 | diff-lcs (>= 1.2.0, < 2.0) 139 | rspec-support (~> 3.9.0) 140 | rspec-support (3.9.3) 141 | ruby2_keywords (0.0.2) 142 | shotgun (0.9.2) 143 | rack (>= 1.0) 144 | sinatra (2.0.8.1) 145 | mustermann (~> 1.0) 146 | rack (~> 2.0) 147 | rack-protection (= 2.0.8.1) 148 | tilt (~> 2.0) 149 | sinatra-cometio (0.6.0) 150 | event_emitter (>= 0.2.5) 151 | eventmachine (>= 1.0.0) 152 | httparty 153 | json (>= 1.7.0) 154 | sinatra (>= 1.3.0) 155 | sinatra-contrib (>= 1.3.2) 156 | sinatra-contrib (2.0.8.1) 157 | backports (>= 2.8.2) 158 | multi_json 159 | mustermann (~> 1.0) 160 | rack-protection (= 2.0.8.1) 161 | sinatra (= 2.0.8.1) 162 | tilt (~> 2.0) 163 | sinatra-errorcodes (0.4.2) 164 | sinatra (~> 2.0.2) 165 | sinatra-rocketio (0.3.3) 166 | event_emitter (>= 0.2.5) 167 | eventmachine (>= 1.0.0) 168 | hashie 169 | sinatra 170 | sinatra-cometio (>= 0.5.9) 171 | sinatra-websocketio (>= 0.4.0) 172 | sinatra-websocketio (0.4.1) 173 | em-websocket (>= 0.5.0) 174 | event_emitter (>= 0.2.4) 175 | eventmachine (>= 1.0.0) 176 | json (>= 1.7.0) 177 | sinatra (>= 1.3.0) 178 | sinatra-contrib (>= 1.3.2) 179 | websocket-client-simple 180 | sysrandom (1.0.5) 181 | thin (1.7.2) 182 | daemons (~> 1.0, >= 1.0.9) 183 | eventmachine (~> 1.0, >= 1.0.4) 184 | rack (>= 1, < 3) 185 | thread_safe (0.3.6) 186 | tilt (2.0.10) 187 | tzinfo (1.2.7) 188 | thread_safe (~> 0.1) 189 | websocket (1.2.8) 190 | websocket-client-simple (0.3.0) 191 | event_emitter 192 | websocket 193 | zeitwerk (2.3.0) 194 | 195 | PLATFORMS 196 | ruby 197 | 198 | DEPENDENCIES 199 | async_sinatra 200 | awesome_print 201 | bcrypt 202 | dalli 203 | database_cleaner 204 | dotenv 205 | dry-validation 206 | factory_girl 207 | faker 208 | mongoid 209 | mongoid-rspec 210 | parallel 211 | promise 212 | pry 213 | pry-byebug 214 | rack (>= 2.0.6) 215 | rack-cache 216 | rack-contrib 217 | rack-cors 218 | rack-health 219 | rack-ssl-enforcer 220 | rack-test 221 | rake 222 | rb-readline 223 | rspec 224 | shotgun 225 | sinatra (~> 2.0.2) 226 | sinatra-errorcodes 227 | sinatra-rocketio 228 | sysrandom 229 | thin 230 | 231 | RUBY VERSION 232 | ruby 2.6.6p146 233 | 234 | BUNDLED WITH 235 | 2.1.4 236 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec thin -R config.ru -p $PORT -e $RACK_ENV start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat API Server 2 | [![build status](https://circleci.com/gh/IzumiSy/chat-api-server.svg?style=shield&circle-token=a8ab869724415d9d09f918fa716bf41a8ea45188)](https://circleci.com/gh/IzumiSy/chat-api-server) 3 | [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 4 | 5 | > Chat backend server. 6 | 7 | Sample front-end app: [IzumiSy/chat-frontend](https://github.com/IzumiSy/chat-frontend) 8 | 9 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 10 | 11 | ## Table of Contents 12 | - [Setup](#Setup) 13 | - [Run](#Run) 14 | - [Test](#Test) 15 | - [License](#License) 16 | 17 | ## Setup 18 | ```bash 19 | $ cp .env.sample .env 20 | $ vi .env 21 | ... 22 | ``` 23 | 24 | ## Run 25 | ```bash 26 | $ docker-compose build 27 | $ docker-compose run app bundle exec rake seed 28 | $ docker-compose up 29 | ``` 30 | 31 | ## Test 32 | ```bash 33 | $ docker-compose run app bundle exec rspec 34 | ``` 35 | 36 | ## License 37 | MIT © IzumiSy 38 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require_relative './app' 3 | 4 | include Rake::DSL 5 | 6 | desc 'Run console' 7 | task :console do 8 | exec 'pry -r ./app.rb' 9 | end 10 | 11 | namespace :rooms do 12 | desc 'Create rooms' 13 | task :seed do 14 | Room.create!(name: "Lobby") 15 | Room.create!(name: "Michelle") 16 | Room.create!(name: "Blankey") 17 | Room.create!(name: "Number") 18 | end 19 | 20 | desc 'Delete all rooms' 21 | task :drop do 22 | Room.delete_all 23 | end 24 | end 25 | 26 | namespace :users do 27 | desc 'Delete all users' 28 | task :drop do 29 | User.delete_all 30 | Room.all.each do |room| 31 | room.update_attribute(:users_count, 0); 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-api-server", 3 | "description": "Chat API server", 4 | "keywords": ["chat", "server"], 5 | "repository": "https://github.com/IzumiSy/chat-api-server", 6 | "addons": [ 7 | "mongolab:sandbox", 8 | "heroku-redis:hobby-dev" 9 | ], 10 | "scripts": { 11 | "postdeploy": "bundle exec rake db:seed_rooms" 12 | }, 13 | "env": { 14 | "ADMIN_PASS": { 15 | "description": "The password to use restricted API endpoints", 16 | "generator": "secret" 17 | }, 18 | "RACK_ENV": { 19 | "description": "This must be \"production\"", 20 | "value": "production" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/namespace' 3 | require 'sinatra/base' 4 | require 'sinatra/rocketio' 5 | require 'sinatra/errorcodes' 6 | require 'sinatra/async' 7 | 8 | require 'sysrandom/securerandom' 9 | require 'digest/md5' 10 | 11 | require 'mongoid' 12 | 13 | require 'parallel' 14 | require 'promise' 15 | 16 | require 'dotenv' 17 | require 'pry' if development? or test? 18 | 19 | require 'rack-ssl-enforcer' 20 | require 'rack-health' 21 | require 'rack-cache' 22 | require 'rack/session/dalli' 23 | require 'rack/cors' 24 | require 'rack/contrib' 25 | 26 | require_relative 'controllers/basic' 27 | require_relative 'controllers/room' 28 | require_relative 'controllers/message' 29 | require_relative 'controllers/user' 30 | 31 | require_relative 'models/room' 32 | require_relative 'models/user' 33 | 34 | Dotenv.load 35 | Mongoid.load!('mongoid.yml', ENV['RACK_ENV']) 36 | 37 | class Application < Sinatra::Base 38 | configure do 39 | use BasicRoutes 40 | use UserRoutes 41 | use RoomRoutes 42 | use MessageRoutes 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | 4 | require './app.rb' 5 | run Application 6 | -------------------------------------------------------------------------------- /controllers/base.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers/authorization' 2 | require_relative 'helpers/validation' 3 | 4 | class RouteBase < Sinatra::Base 5 | configure do 6 | register Sinatra::Errorcodes 7 | register Sinatra::Namespace 8 | register Sinatra::RocketIO 9 | register Sinatra::Async 10 | 11 | set :rocketio, websocket: false, comet: true 12 | 13 | disable :raise_errors 14 | disable :show_exceptions 15 | 16 | enable :halt_with_errors 17 | enable :logging 18 | 19 | memcached_servers = 20 | ENV.fetch('MEMCACHEDCLOUD_SERVERS', '127.0.0.1:11211') 21 | 22 | use Rack::Session::Dalli, 23 | key: 'rack.session', 24 | cache: Dalli::Client.new( 25 | memcached_servers, 26 | username: ENV['MEMCACHEDCLOUD_USERNAME'], 27 | password: ENV['MEMCACHEDCLOUD_PASSWORD'] 28 | ) 29 | 30 | use Rack::Cache, 31 | verbose: true, 32 | metastore: "memcached://#{memcached_servers}", 33 | entitystore: "memcached://#{memcached_servers}" 34 | 35 | use Rack::Health, path: '/healthcheck' 36 | use Rack::SslEnforcer, except_environments: ['development', 'test'] 37 | use Mongoid::QueryCache::Middleware 38 | 39 | use Rack::JSONBodyParser 40 | use Rack::Cors do 41 | allow do 42 | origins ENV.fetch('CORS_ALLOWED_ORIGINS', 'http://localhost:8000') 43 | resource '*', 44 | headers: :any, 45 | methods: [:get, :post, :put, :patch, :delete, :options, :head], 46 | credentials: true 47 | end 48 | end 49 | end 50 | 51 | before do 52 | content_type :json 53 | end 54 | 55 | error do |e| 56 | status case e 57 | when Mongoid::Errors::Validations 58 | HTTPError::BadRequest::CODE 59 | when Mongoid::Errors::DocumentNotFound 60 | HTTPError::NotFound::CODE 61 | when Mongoid::Errors::MongoidError 62 | HTTPError::InternalServerError::CODE 63 | end 64 | end 65 | 66 | helpers Authorization, Validation 67 | 68 | def empty_json! 69 | body {}.to_json 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /controllers/basic.rb: -------------------------------------------------------------------------------- 1 | require_relative "./base" 2 | 3 | class BasicRoutes < RouteBase 4 | # Update is_admin flag of the given user to true 5 | # in order to allow call of admin restricted API 6 | post '/api/admin/auth' do 7 | validates do 8 | schema do 9 | required(:auth_hash).filled(:string) 10 | required(:user_id).filled(:string) 11 | end 12 | end 13 | 14 | admin_pass = ENV['ADMIN_PASS'] 15 | if admin_pass.empty? 16 | raise HTTPError::InternalServerError, "ADMIN_PASS is not set" 17 | end 18 | 19 | login_hash = params[:auth_hash] 20 | user_id = params[:user_id] 21 | 22 | password_hash = Digest::MD5.hexdigest(admin_pass) 23 | unless password_hash == login_hash 24 | raise HTTPError::Unauthorized 25 | end 26 | 27 | user = User.find_by!(id: user_id) 28 | user.update_attribute(:is_admin, true) 29 | body user.to_json(only: User::USER_DATA_LIMITS.dup << :is_admin) 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /controllers/helpers/authorization.rb: -------------------------------------------------------------------------------- 1 | module Authorization 2 | def login(user) 3 | session[:user_id] = user.id 4 | end 5 | 6 | def logout 7 | session[:user_id] = nil 8 | end 9 | 10 | def current_user 11 | session[:user_id] && User.find(session[:user_id]) 12 | end 13 | 14 | def must_be_logged_in! 15 | raise HTTPError::Unauthorized unless current_user 16 | end 17 | 18 | def must_be_logged_in_as_admin! 19 | raise HTTPError::Unauthorized unless current_user && current_user.is_admin? 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /controllers/helpers/validation.rb: -------------------------------------------------------------------------------- 1 | require 'dry-validation' 2 | 3 | module Validation 4 | def validates(&block) 5 | schema = Class.new(Dry::Validation::Contract, &block).new 6 | validation = schema.call(params) 7 | raise HTTPError::BadRequest if validation.failure? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /controllers/message.rb: -------------------------------------------------------------------------------- 1 | require_relative "./base" 2 | require_relative "../services/message_service" 3 | 4 | class MessageRoutes < RouteBase 5 | # Message post API does not check if the room_id valid 6 | # or not, because it doesnt need to care its existence. 7 | post '/api/message/:room_id' do 8 | must_be_logged_in! 9 | 10 | validates do 11 | schema do 12 | required(:room_id).filled(:string) 13 | required(:content).filled(:string) 14 | end 15 | end 16 | 17 | room_id = params[:room_id] 18 | content = params[:content] 19 | 20 | data = { user_id: current_user.id, content: content, created_at: Time.now, user: current_user } 21 | MessageService.broadcast_message(room_id, data) 22 | 23 | empty_json! 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /controllers/room.rb: -------------------------------------------------------------------------------- 1 | require_relative "./base" 2 | require_relative "../services/message_service" 3 | 4 | class RoomRoutes < RouteBase 5 | get '/api/rooms' do 6 | must_be_logged_in! 7 | 8 | # [NOTE] Performance tuning tips 9 | # - to_json calls find() internally, so it is called here only once. 10 | # - length property and count() calls a counting method internally 11 | # that is relatively slow, so to avoid that overhead, here use JSON conversion. 12 | room_all = Mongoid::QueryCache.cache { 13 | Room.all.limit(Room::ROOM_MAX).to_json(only: Room::ROOM_DATA_LIMITS) 14 | } 15 | 16 | if JSON.parse(room_all).length <= 0 17 | puts "[INFO] Server seems to have no room. Needed to execute \"rake db:seed_rooms\"." 18 | end 19 | 20 | body room_all 21 | end 22 | 23 | post '/api/room/new' do 24 | must_be_logged_in_as_admin! 25 | 26 | validates do 27 | schema do 28 | required(:name).filled(:string) 29 | end 30 | end 31 | 32 | room_name = params[:name] 33 | room = Room.new(name: room_name) 34 | room.save! 35 | 36 | room.to_json(only: Room::ROOM_DATA_LIMITS) 37 | end 38 | 39 | get '/api/room/:id' do 40 | must_be_logged_in! 41 | 42 | validates do 43 | schema do 44 | required(:id).filled(:string) 45 | end 46 | end 47 | 48 | room_id = params[:id] 49 | 50 | Room.find_by!(id: room_id).to_json(only: ROOM_DATA_LIMITS) 51 | end 52 | 53 | get '/api/room/:id/users' do 54 | must_be_logged_in! 55 | 56 | validates do 57 | schema do 58 | required(:id).filled(:string) 59 | end 60 | end 61 | 62 | room_id = params[:id] 63 | 64 | Room.only(:users).find_by!(id: room_id) 65 | .users.asc(:name).to_json(only: User::USER_DATA_LIMITS) 66 | end 67 | 68 | post '/api/room/:id/enter' do 69 | must_be_logged_in! 70 | 71 | validates do 72 | schema do 73 | required(:id).filled(:string) 74 | end 75 | end 76 | 77 | room_id = params[:id] 78 | Room.transaction_enter(room_id, current_user) 79 | 80 | empty_json! 81 | end 82 | 83 | post '/api/room/:id/leave' do 84 | must_be_logged_in! 85 | 86 | validates do 87 | schema do 88 | required(:id).filled(:string) 89 | end 90 | end 91 | 92 | room_id = params[:id] 93 | 94 | if room_id == 'all' 95 | User.user_deletion(current_user) 96 | else 97 | Room.transaction_leave(room_id, current_user) 98 | end 99 | 100 | empty_json! 101 | end 102 | end 103 | 104 | -------------------------------------------------------------------------------- /controllers/user.rb: -------------------------------------------------------------------------------- 1 | require_relative "./base" 2 | 3 | class UserRoutes < RouteBase 4 | # This user creation port does not need to use slice 5 | # to limite user data to return. 6 | post '/api/user/new' do 7 | validates do 8 | schema do 9 | required(:name).filled(:string) 10 | optional(:face) 11 | end 12 | end 13 | 14 | client_ip = request.ip 15 | client_name = params[:name] 16 | 17 | if client_ip.empty? || client_name.empty? 18 | raise HTTPError::BadRequest 19 | end 20 | 21 | create_user_param = { 22 | name: client_name, ip: client_ip 23 | } 24 | 25 | if (params[:face]) 26 | create_user_param[:face] = params[:face] 27 | end 28 | 29 | unless lobby_room = Room.find_lobby() 30 | raise HTTPError::InternalServerError, "No Lobby Room" 31 | end 32 | 33 | _create_new_user = promise { 34 | create_user_param[:room_id] = lobby_room.id 35 | user = User.new(create_user_param) 36 | user.save! 37 | user 38 | } 39 | _increment_lobby = promise { 40 | Room.increment_counter(:users_count, lobby_room.id) 41 | lobby_room 42 | } 43 | 44 | # If room_id is specified, it means that user enters into 45 | # the room with room_id, so it makes a broadcasting. 46 | if lobby_room 47 | MessageService.broadcast_enter_msg(_create_new_user, _increment_lobby) 48 | end 49 | 50 | user = _create_new_user 51 | login(user) 52 | 53 | body user.to_json(only: User::USER_DATA_LIMITS.dup << :room_id) 54 | end 55 | 56 | get '/api/user/:id' do 57 | must_be_logged_in! 58 | 59 | validates do 60 | schema do 61 | required(:id).filled(:string) 62 | end 63 | end 64 | 65 | user_id = params[:id] 66 | body User.fetch_user_data(user_id, :USER) 67 | end 68 | 69 | get '/api/user/:id/room' do 70 | must_be_logged_in! 71 | 72 | validates do 73 | schema do 74 | required(:id).filled(:string) 75 | end 76 | end 77 | 78 | user_id = params[:id] 79 | body User.fetch_user_data(user_id, :ROOM) 80 | end 81 | 82 | # TODO Need to write test here 83 | # Notes: somehow PUT is not working well in this port 84 | # so I decided to use POST instead for updating user's data 85 | post '/api/user/:id' do 86 | must_be_logged_in! 87 | 88 | validates do 89 | schema do 90 | required(:id).filled(:string) 91 | required(:data).hash do 92 | optional(:name).filled(:string) 93 | end 94 | end 95 | end 96 | 97 | user_id = params[:id] 98 | data = params[:data] 99 | user = User.find_by!(id: user_id); 100 | user.update_attributes!(data); 101 | user.save 102 | 103 | body user.to_json(only: User::USER_DATA_LIMITS) 104 | end 105 | 106 | # TODO: need test 107 | delete '/api/user/:id' do 108 | must_be_logged_in! 109 | 110 | validates do 111 | schema do 112 | required(:id).filled(:string) 113 | end 114 | end 115 | 116 | user_id = params[:id] 117 | user = User.find_by!(id: user_id); 118 | 119 | User.user_deletion(user) 120 | 121 | empty_json! 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "3000:3000" 7 | environment: 8 | - MONGODB_URI=mongodb://mongo:27017 9 | - MEMCACHEDCLOUD_SERVERS=memcached 10 | depends_on: 11 | - memcached 12 | - mongodb 13 | memcached: 14 | image: "memcached:latest" 15 | ports: 16 | - "11211:11211" 17 | mongodb: 18 | image: "mongo:3.6.17" 19 | ports: 20 | - "27017:27017" 21 | volumes: 22 | - ./persistence:/data/db 23 | -------------------------------------------------------------------------------- /models/room.rb: -------------------------------------------------------------------------------- 1 | require_relative "../services/message_service" 2 | 3 | class Room 4 | include Mongoid::Document 5 | include Mongoid::Timestamps 6 | 7 | has_many :users 8 | 9 | LOBBY_ROOM_NAME = "Lobby" 10 | 11 | ROOM_TITLE_LENGTH_MAX = 64 12 | ROOM_MAX = 100 13 | ROOM_DATA_LIMITS = [:_id, :name, :users_count] 14 | 15 | field :name, type: String 16 | field :users_count, type: Integer, default: 0 17 | field :status, type: Integer, default: 0 18 | 19 | validates :name, presence: true, uniqueness: true, 20 | absence: false, length: { 21 | minimum: 0, 22 | maximum: self::ROOM_TITLE_LENGTH_MAX 23 | } 24 | 25 | public 26 | 27 | class << self 28 | def find_lobby 29 | Mongoid::QueryCache.cache { Room.find_by(name: LOBBY_ROOM_NAME) } 30 | end 31 | 32 | def transaction_enter(new_room_id, user) 33 | if user.room 34 | current_room_id = user.room.id 35 | Room.decrement_counter(:users_count, current_room_id) 36 | end 37 | 38 | _room = promise { 39 | Room.increment_counter(:users_count, new_room_id) 40 | Mongoid::QueryCache.cache { Room.find_by!(id: new_room_id) } 41 | } 42 | _user = promise { 43 | user.update_attributes!(room_id: new_room_id) 44 | user 45 | } 46 | 47 | MessageService.broadcast_enter_msg(_user, _room) 48 | end 49 | 50 | def transaction_leave(current_room_id, user) 51 | is_user_exist_in_room = 52 | current_room_id == user.room_id.to_s ? true : false 53 | 54 | return unless is_user_exist_in_room 55 | 56 | Thread.new { user.update_attributes!(room_id: nil) } 57 | Room.decrement_counter(:users_count, current_room_id) 58 | 59 | MessageService.broadcast_leave_msg(user) 60 | end 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /models/user.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include Mongoid::Document 3 | include Mongoid::Timestamps 4 | 5 | USER_NAME_LENGTH_MAX = 64 6 | USER_DATA_LIMITS = [:_id, :name, :face] 7 | 8 | belongs_to :room, foreign_key: 'room_id', optional: true 9 | 10 | FACE_ID_BASE = 144995 11 | FACE_IDS = [ 12 | 1867, 1870, 1874, 1898, 1900, 1968, 1973 13 | ].freeze 14 | 15 | field :name, type: String 16 | field :face, type: String, default: ->{ faceid_gen() } 17 | 18 | field :ip, type: String 19 | field :session, type: String 20 | 21 | STATUS = [ 22 | STATUS_NEUTRAL = 0 23 | ].freeze 24 | 25 | field :status, type: Integer, default: self::STATUS_NEUTRAL 26 | field :is_admin, type: Boolean, default: false 27 | 28 | validates :name, presence: true, uniqueness: true, 29 | absence: false, length: { 30 | minimum: 0, 31 | maximum: self::USER_NAME_LENGTH_MAX 32 | } 33 | validates :face, absence: false 34 | validates_inclusion_of :face, in: ->(_) do 35 | FACE_IDS.map { |f| "#{FACE_ID_BASE}#{f}" } 36 | end 37 | 38 | public 39 | 40 | class << self 41 | def get_name_availability(name) 42 | !!User.where(name: name).exists? 43 | end 44 | 45 | def find_user_by_session(session) 46 | Mongoid::QueryCache.cache { User.find_by(session: session) } 47 | end 48 | 49 | def find_user_by_ip(ip) 50 | Mongoid::QueryCache.cache { User.find_by(ip: ip) } 51 | end 52 | 53 | def fetch_user_data(user_id, fetch_type) 54 | return case fetch_type 55 | when :USER then 56 | User.only(:id, :name, :face).find_by!(id: user_id).to_json(only: USER_DATA_LIMITS) 57 | when :ROOM then 58 | User.only(:room).find_by!(id: user_id).room.to_json(only: Room::ROOM_DATA_LIMITS) 59 | else 60 | raise HTTPError::InternalServerError 61 | end 62 | end 63 | 64 | def trigger_disconnection_resolver(client) 65 | Thread.new { 66 | if user = User.find_user_by_session(client.session) 67 | User.user_deletion(user) 68 | end 69 | } 70 | end 71 | 72 | def user_deletion(user) 73 | return unless user 74 | Room.transaction_leave(user.room.id, user) if user.room 75 | user.delete 76 | end 77 | end 78 | 79 | private 80 | 81 | # Set random face id 82 | def faceid_gen 83 | faceid = FACE_IDS[rand(FACE_IDS.length)] 84 | "#{FACE_ID_BASE}#{faceid}" 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mongoid.yml: -------------------------------------------------------------------------------- 1 | 2 | options: &options 3 | raise_not_found_error: false 4 | 5 | client_options: &client_options 6 | connect: :direct 7 | 8 | default: &default 9 | default: 10 | <<: *client_options 11 | uri: <%= ENV['MONGODB_URI'] %> 12 | 13 | test: 14 | options: 15 | <<: *options 16 | clients: 17 | default: 18 | <<: *client_options 19 | database: test 20 | hosts: 21 | - mongodb:27017 22 | 23 | development: 24 | options: 25 | <<: *options 26 | clients: 27 | default: 28 | <<: *client_options 29 | database: development 30 | hosts: 31 | - mongodb:27017 32 | 33 | production: 34 | options: 35 | <<: *options 36 | clients: 37 | <<: *default 38 | -------------------------------------------------------------------------------- /newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Generated April 10, 2016 3 | # 4 | # For full documentation of agent configuration options, please refer to 5 | # https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration 6 | 7 | common: &default_settings 8 | app_name: OpenChat-Server 9 | log_level: info 10 | 11 | # To disable the agent regardless of other settings, uncomment the following: 12 | # agent_enabled: false 13 | 14 | 15 | # Environment-specific settings are in this section. 16 | # RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. 17 | # If your application has other named environments, configure them here. 18 | development: 19 | <<: *default_settings 20 | app_name: OpenChat-Server (Development) 21 | developer_mode: true 22 | 23 | test: 24 | <<: *default_settings 25 | monitor_mode: false 26 | 27 | staging: 28 | <<: *default_settings 29 | app_name: OpenChat-Server (Staging) 30 | 31 | production: 32 | <<: *default_settings 33 | -------------------------------------------------------------------------------- /services/message_service.rb: -------------------------------------------------------------------------------- 1 | 2 | module MessageService 3 | @io = Sinatra::RocketIO 4 | 5 | @io.once :start do 6 | puts "[ROCKET.IO] Started." 7 | end 8 | 9 | @io.on :connect do |client| 10 | puts "[INFO] New client: #{client.session}, #{client.address}" 11 | if user = User.find_user_by_ip(client.address) 12 | user.update_attributes!(session: client.session) 13 | end 14 | end 15 | 16 | @io.on :disconnect do |client| 17 | puts "[INFO] Client disconnected: #{client.session}, #{client.address}" 18 | User.trigger_disconnection_resolver(client) 19 | end 20 | 21 | @io.on :error do |client| 22 | puts "[INFO] Client error: #{client.session}, #{client.address}" 23 | User.trigger_disconnection_resolver(client) 24 | end 25 | 26 | # Pseudo enum 27 | module SYSTEM_LOG_TYPE 28 | USER_ENTER = 0 29 | USER_LEAVE = 1 30 | end 31 | 32 | class << self 33 | def broadcast_message(room_id, params) 34 | @io.push :newMessage, params.to_json, { channel: room_id } 35 | end 36 | 37 | def broadcast_enter_msg(user, room) 38 | broadcast_system_log(SYSTEM_LOG_TYPE::USER_ENTER, user.name, room.id) 39 | Thread.new { broadcast_members_update(room) } 40 | Thread.new { broadcast_room_update() } 41 | end 42 | 43 | def broadcast_leave_msg(user) 44 | return unless user.room 45 | broadcast_system_log(SYSTEM_LOG_TYPE::USER_LEAVE, user.name, user.room.id) 46 | Thread.new { broadcast_members_update(user.room) } 47 | Thread.new { broadcast_room_update() } 48 | end 49 | 50 | private 51 | 52 | def broadcast_system_log(type, user_name, room_id) 53 | case type 54 | when SYSTEM_LOG_TYPE::USER_ENTER then 55 | # @io.push userEnter, user_name, { channel: room_id } 56 | when SYSTEM_LOG_TYPE::USER_LEAVE then 57 | # @io.push userLeave, user_name, { channel: room_id } 58 | else 59 | # TODO kinda exception handling is needed here 60 | end 61 | end 62 | 63 | def broadcast_room_update() 64 | params = Room.all.to_json(only: Room::ROOM_DATA_LIMITS) 65 | @io.push :updateRooms, params 66 | end 67 | 68 | def broadcast_members_update(room) 69 | users = room.users.asc(:name).to_json(only: User::USER_DATA_LIMITS) 70 | @io.push :updateMembers, users, { channel: room.id } 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/factories/room.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :Lobby, class: Room do 3 | name { "Lobby" } 4 | end 5 | 6 | factory :room, class: Room do 7 | name { "TestRoom" } 8 | end 9 | 10 | factory :SuperRoom, class: Room do 11 | name { "SuperRoom" } 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence :name do Faker::Name.name end 3 | sequence :ip do Faker::Internet.ip_v4_address end 4 | end 5 | 6 | FactoryGirl.define do 7 | factory :user, class: User do 8 | name { generate :name } 9 | ip { generate :ip } 10 | end 11 | 12 | factory :user2, class: User do 13 | name { generate :name } 14 | ip { generate :ip } 15 | end 16 | 17 | factory :Jonathan, class: User do 18 | name { "Jonathan" } 19 | ip { generate :ip } 20 | end 21 | 22 | factory :admin, class: User do 23 | name { generate :name } 24 | ip { generate :ip } 25 | is_admin { true } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/models/room_model_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe Room do 4 | it { is_expected.to have_many(:users).with_foreign_key(:room_id) } 5 | 6 | it { is_expected.to validate_presence_of(:name) } 7 | it { is_expected.to validate_uniqueness_of(:name) } 8 | it { is_expected.to validate_length_of(:name).within(0..Room::ROOM_TITLE_LENGTH_MAX) } 9 | 10 | it { is_expected.to have_field(:name).of_type(String) } 11 | it { is_expected.to have_field(:status).of_type(Integer) } 12 | it { is_expected.to have_timestamps } 13 | end 14 | -------------------------------------------------------------------------------- /spec/models/user_model_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe User do 4 | it { is_expected.to belong_to(:room) } 5 | 6 | it { is_expected.to validate_presence_of(:name) } 7 | it { is_expected.to validate_uniqueness_of(:name) } 8 | it { is_expected.to validate_length_of(:name).within(0..User::USER_NAME_LENGTH_MAX) } 9 | 10 | it { is_expected.to have_fields(:name, :face).of_type(String) } 11 | it { is_expected.to have_fields(:ip, :session).of_type(String) } 12 | it { is_expected.to have_field(:is_admin) 13 | .of_type(Mongoid::Boolean).with_default_value_of(false) } 14 | it { is_expected.to have_field(:status) 15 | .of_type(Integer).with_default_value_of(0) } 16 | it { is_expected.to have_timestamps } 17 | end 18 | -------------------------------------------------------------------------------- /spec/requests/basic_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | ENV['ADMIN_PASS'] = 'testpass' 4 | 5 | describe "POST /api/admin/auth" do 6 | let(:user) { create(:user) } 7 | 8 | let(:success_param) { 9 | { auth_hash: Digest::MD5.hexdigest('testpass'), 10 | user_id: user.id } 11 | } 12 | let(:undefined_user_param) { 13 | { auth_hash: Digest::MD5.hexdigest('testpass'), 14 | user_id: BSON::ObjectId.new } 15 | } 16 | let(:error_param) { 17 | { auth_hash: Digest::MD5.hexdigest('testpassee'), 18 | user_id: user.id } 19 | } 20 | 21 | it "should get auth_token" do 22 | post 'api/admin/auth', success_param 23 | is_admin = JSON.parse(last_response.body)["is_admin"] 24 | expect(last_response.status).to eq(200) 25 | expect(is_admin).to eq(true) 26 | end 27 | 28 | it "should NOT update an user with undefined user_id" do 29 | post 'api/admin/auth', undefined_user_param 30 | expect(last_response.status).to eq(404) 31 | end 32 | 33 | it "should NOT get auth_token with incorrect password" do 34 | post 'api/admin/auth', error_param 35 | expect(last_response.status).to eq(401) 36 | end 37 | 38 | it "should NOT get auth_token with empty parameters" do 39 | post 'api/admin/auth' 40 | expect(last_response.status).to eq(400) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/requests/message_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe "POST /api/message/:room_id" do 4 | let(:user) { create(:user) } 5 | let(:room) { create(:room) } 6 | let(:invalid_message) { { content: "Hello" } } 7 | 8 | it "should get an error when no params" do 9 | post "/api/message/#{room.id}", {}, 'rack.session' => { user_id: user.id } 10 | expect(last_response.status).to eq(400) 11 | end 12 | 13 | # it "should get an error in posting a message to unexisted room" do 14 | # post "/api/message/12345", invalid_message, 'rack.session' => { user_id: user.id } 15 | # expect(last_response.status).to eq(404) 16 | # end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/requests/room_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe "POST /api/room/new" do 4 | let(:user) { create(:user) } 5 | let(:admin) { create(:admin) } 6 | let(:success_room) { { name: "RoomSuccess" } } 7 | let(:error_room) { { name: "RoomError" } } 8 | let(:duplicated_room) { { name: "SuperRoom" } } 9 | let!(:SuperRoom) { create(:SuperRoom) } 10 | 11 | it "should NOT create a room without parameters" do 12 | post "/api/room/new", {}, 'rack.session' => { user_id: user.id } 13 | expect(last_response.status).to eq(401) 14 | end 15 | 16 | it "should NOT create a room by non-admin user" do 17 | post "/api/room/new", error_room, 'rack.session' => { user_id: user.id } 18 | expect(last_response.status).to eq(401) 19 | end 20 | 21 | it "should NOT create a room because of name duplication" do 22 | post "/api/room/new", duplicated_room, 'rack.session' => { user_id: admin.id } 23 | expect(last_response.status).to eq(400) 24 | end 25 | 26 | it "should craete a room successfully" do 27 | post "/api/room/new", success_room, 'rack.session' => { user_id: admin.id } 28 | expect(last_response.status).to eq(200) 29 | end 30 | end 31 | 32 | # TODO Need implementation 33 | describe "GET /api/room" do 34 | it "should get messages of the room" do 35 | 36 | end 37 | end 38 | 39 | describe "GET /api/room/:id/users" do 40 | let(:user) { create(:user) } 41 | let(:admin) { create(:admin) } 42 | let(:room) { create(:room) } 43 | 44 | before do 45 | enter_room(room.id, user) 46 | enter_room(room.id, admin) 47 | end 48 | 49 | it "should have 2 users in the room" do 50 | get "/api/room/#{room.id}/users", {}, 'rack.session' => { user_id: user.id } 51 | users = JSON.parse(last_response.body).collect { |user| user["_id"]["$oid"] } 52 | expect(last_response.status).to eq(200) 53 | expect(users).to include(user.id.to_s, admin.id.to_s) 54 | end 55 | end 56 | 57 | describe "POST /api/room/enter" do 58 | let(:room) { create(:room) } 59 | let(:user) { create(:user) } 60 | 61 | it "should get an 404 error with invalid room id" do 62 | post "/api/room/12345/enter", {}, 'rack.session' => { user_id: user.id } 63 | expect(last_response.status).to eq(404) 64 | end 65 | 66 | # TODO Implement room check if the user successfully entered 67 | it "should have an user enter the room" do 68 | post "/api/room/#{room.id}/enter", {}, 'rack.session' => { user_id: user.id } 69 | expect(last_response.status).to eq(200) 70 | expect(room.users.count).to eq(1) 71 | expect(room.users.first.id).to eq(user.id) 72 | end 73 | 74 | it "should get 500 error with invalid type" do 75 | post "/api/room/#{room.id}/nothing", {}, 'rack.session' => { user_id: user.id } 76 | expect(last_response.status).to eq(404) 77 | end 78 | end 79 | 80 | describe "POST /api/room/:id/leave" do 81 | let(:room) { create(:room) } 82 | let(:user) { create(:user) } 83 | let(:admin) { create(:admin) } 84 | 85 | # TODO Implement room check if the user successfully leaved 86 | it "should have an user leave the room" do 87 | enter_room(room.id, user) 88 | enter_room(room.id, admin) 89 | post "/api/room/#{room.id}/leave", {}, 'rack.session' => { user_id: user.id } 90 | expect(last_response.status).to eq(200) 91 | expect(room.users.first).to eq(admin) 92 | expect(room.users.count).to eq(1) 93 | end 94 | 95 | # TODO Implement room check if the user successfully leaved 96 | it "should have an user leave the room with 'all' for :id" do 97 | enter_room(room.id, user) 98 | enter_room(room.id, admin) 99 | post "/api/room/all/leave", {}, 'rack.session' => { user_id: user.id } 100 | expect(last_response.status).to eq(200) 101 | expect(room.users.first).to eq(admin) 102 | expect(room.users.count).to eq(1) 103 | end 104 | end 105 | 106 | -------------------------------------------------------------------------------- /spec/requests/user_test_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper.rb" 2 | 3 | describe "POST /api/user/new" do 4 | let(:user) { { name: "test1" } } 5 | let(:error_user) { { name: "Jonathan" } } 6 | let!(:Lobby) { create(:Lobby) } 7 | let!(:Jonathan) { create(:Jonathan) } 8 | 9 | it "should NOT create a new user without name" do 10 | post "/api/user/new" 11 | expect(last_response.status).to eq(400) 12 | end 13 | 14 | it "should create a new user" do 15 | post "/api/user/new", user 16 | expect(last_response.status).to eq(200) 17 | end 18 | 19 | it "should NOT create a new user" do 20 | post "/api/user/new", error_user 21 | expect(last_response.status).to eq(400) 22 | end 23 | end 24 | 25 | describe "GET /api/user/:id" do 26 | let(:user) { create(:user) } 27 | 28 | it "should get an error when invalid user id" do 29 | get "/api/user/12345", {}, 'rack.session' => { user_id: user.id } 30 | expect(last_response.status).to eq(404) 31 | end 32 | 33 | it "should get data of the user with valid user id" do 34 | get "/api/user/#{user.id}", {}, 'rack.session' => { user_id: user.id } 35 | body = JSON.parse(last_response.body) 36 | expect(last_response.status).to eq(200) 37 | expect(body["name"]).to eq(user.name); 38 | end 39 | end 40 | 41 | describe "GET /api/user/:id/room" do 42 | let(:user) { create(:user) } 43 | let(:room) { create(:room) } 44 | 45 | it "should get room data the user belongs to" do 46 | enter_room(room.id, user) 47 | 48 | get "/api/user/#{user.id}/room", {}, 'rack.session' => { user_id: user.id } 49 | expect(last_response.status).to eq(200) 50 | end 51 | 52 | it "should get 404 error with an invalid type" do 53 | get "/api/user/#{user.id}/nothing", {}, 'rack.session' => { user_id: user.id } 54 | expect(last_response.status).to eq(404) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | require 'rspec' 4 | require 'json' 5 | require 'faker' 6 | require 'rack/test' 7 | require 'mongoid-rspec' 8 | require 'database_cleaner' 9 | require 'factory_girl' 10 | 11 | require_relative '../app.rb' 12 | 13 | require_relative './factories/user.rb' 14 | require_relative './factories/room.rb' 15 | 16 | module Helpers 17 | def enter_room(room_id, user) 18 | user = User.find(user.id) 19 | user.update_attributes!(room_id: room_id) 20 | end 21 | end 22 | 23 | RSpec.configure do |config| 24 | include Rack::Test::Methods 25 | include FactoryGirl::Syntax::Methods 26 | include Helpers 27 | 28 | config.include RSpec::Matchers 29 | config.include Mongoid::Matchers 30 | 31 | def app() 32 | Application.new 33 | end 34 | 35 | config.before(:suite) do 36 | DatabaseCleaner.strategy = :truncation 37 | DatabaseCleaner.orm = "mongoid" 38 | 39 | begin 40 | DatabaseCleaner.start 41 | FactoryGirl.lint 42 | ensure 43 | DatabaseCleaner.clean 44 | end 45 | end 46 | 47 | config.after(:each) do 48 | DatabaseCleaner.clean 49 | end 50 | 51 | config.profile_examples = 5 52 | end 53 | --------------------------------------------------------------------------------