ribose 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +27 -0
  3. data/.rubocop.yml +21 -16
  4. data/CHANGELOG.md +20 -0
  5. data/README.md +99 -0
  6. data/lib/ribose/actions/create.rb +2 -1
  7. data/lib/ribose/actions/update.rb +2 -1
  8. data/lib/ribose/calendar.rb +2 -2
  9. data/lib/ribose/client.rb +27 -9
  10. data/lib/ribose/configuration.rb +13 -4
  11. data/lib/ribose/connection.rb +15 -0
  12. data/lib/ribose/conversation.rb +18 -0
  13. data/lib/ribose/event.rb +86 -0
  14. data/lib/ribose/file_uploader.rb +20 -2
  15. data/lib/ribose/file_version.rb +97 -0
  16. data/lib/ribose/request.rb +49 -10
  17. data/lib/ribose/session.rb +36 -22
  18. data/lib/ribose/space.rb +4 -0
  19. data/lib/ribose/space_category.rb +15 -0
  20. data/lib/ribose/space_file.rb +29 -0
  21. data/lib/ribose/user.rb +9 -5
  22. data/lib/ribose/version.rb +1 -1
  23. data/lib/ribose/version_uploader.rb +27 -0
  24. data/lib/ribose.rb +3 -0
  25. data/ribose.gemspec +3 -4
  26. data/spec/fixtures/calendar_event.json +48 -0
  27. data/spec/fixtures/conversation.json +1 -1
  28. data/spec/fixtures/event_created.json +21 -0
  29. data/spec/fixtures/event_updated.json +23 -0
  30. data/spec/fixtures/file_version.json +14 -0
  31. data/spec/fixtures/space_categories.json +150 -0
  32. data/spec/fixtures/space_file_icon.json +4 -0
  33. data/spec/ribose/connection_spec.rb +11 -0
  34. data/spec/ribose/conversation_spec.rb +14 -0
  35. data/spec/ribose/event_spec.rb +85 -0
  36. data/spec/ribose/file_uploader_spec.rb +15 -2
  37. data/spec/ribose/file_version_spec.rb +76 -0
  38. data/spec/ribose/session_spec.rb +2 -2
  39. data/spec/ribose/space_category_spec.rb +13 -0
  40. data/spec/ribose/space_file_spec.rb +44 -0
  41. data/spec/ribose/space_spec.rb +11 -0
  42. data/spec/support/fake_ribose_api.rb +93 -5
  43. data/spec/support/file_upload_stub.rb +21 -15
  44. metadata +32 -34
  45. data/.travis.yml +0 -5
@@ -1,5 +1,7 @@
1
1
  module Ribose
2
2
  class Request
3
+
4
+ DEFAULT_CONTENT_TYPE = "application/json"
3
5
  # Initialize a Request
4
6
  #
5
7
  # @param http_method [Symbol] HTTP verb as sysmbol
@@ -20,8 +22,17 @@ module Ribose
20
22
  # @return [Sawyer::Resource]
21
23
  #
22
24
  def request(options = {})
25
+ parsable = extract_config_option(:parse) != false
23
26
  options[:query] = extract_config_option(:query) || {}
24
- agent.call(http_method, api_endpoint, data, options).data
27
+
28
+ response = agent.call(http_method, api_endpoint, data, options)
29
+
30
+ # update client headers from response
31
+ client.client_id = response.headers['client']
32
+ client.uid = response.headers['uid']
33
+ client.access_token = response.headers['access-token']
34
+
35
+ parsable == true ? response.data : response
25
36
  end
26
37
 
27
38
  # Make a HTTP GET Request
@@ -68,7 +79,7 @@ module Ribose
68
79
  attr_reader :client, :data, :http_method
69
80
 
70
81
  def ribose_host
71
- Ribose.configuration.api_host
82
+ Ribose.configuration.api_host.host
72
83
  end
73
84
 
74
85
  def extract_config_option(key)
@@ -78,13 +89,22 @@ module Ribose
78
89
  end
79
90
 
80
91
  def find_suitable_client
81
- client = extract_config_option(:client) || Ribose::Client.new
82
- client.is_a?(Ribose::Client) ? client: raise(Ribose::Unauthorized)
92
+ # client = extract_config_option(:client) || Ribose::Client.new
93
+ client = extract_config_option(:client) ||
94
+ Ribose::Client.from_login(
95
+ email: Ribose.configuration.user_email,
96
+ password: Ribose.configuration.user_password,
97
+ api_token: Ribose.configuration.api_token
98
+ )
99
+ client.is_a?(Ribose::Client) ? client : raise(Ribose::Unauthorized)
83
100
  end
84
101
 
85
102
  def require_auth_headers?
86
- auth_header = extract_config_option(:auth_header)
87
- auth_header == false ? false : true
103
+ extract_config_option(:auth_header) != false
104
+ end
105
+
106
+ def custom_content_headers
107
+ extract_config_option(:headers) || {}
88
108
  end
89
109
 
90
110
  def api_endpoint
@@ -95,9 +115,17 @@ module Ribose
95
115
  end
96
116
 
97
117
  def sawyer_options
118
+ faraday_options = { builder: custom_rack_builder }
119
+ unless Ribose.configuration.verify_ssl?
120
+ faraday_options.merge!(ssl: Faraday::SSLOptions.new(
121
+ false, nil, nil, OpenSSL::SSL::VERIFY_NONE
122
+ )
123
+ )
124
+ end
125
+
98
126
  {
99
127
  links_parser: Sawyer::LinkParsers::Simple.new,
100
- faraday: Faraday.new(builder: custom_rack_builder),
128
+ faraday: Faraday.new(faraday_options),
101
129
  }
102
130
  end
103
131
 
@@ -107,14 +135,25 @@ module Ribose
107
135
  end
108
136
  end
109
137
 
138
+ def set_content_type(headers)
139
+ header = custom_content_headers
140
+
141
+ headers[:content_type] = DEFAULT_CONTENT_TYPE
142
+ headers[:accept] = header.fetch(:accept, DEFAULT_CONTENT_TYPE)
143
+ end
144
+
110
145
  def agent
111
146
  @agent ||= Sawyer::Agent.new(ribose_host, sawyer_options) do |http|
112
- http.headers[:accept] = "application/json"
113
- http.headers[:content_type] = "application/json"
147
+ set_content_type(http.headers)
148
+
149
+ # set headers for devise-token-auth
150
+ http.headers["access-token"] = client.access_token
151
+ http.headers["client"] = client.client_id
152
+ http.headers["uid"] = client.uid
114
153
 
115
154
  if require_auth_headers?
116
155
  http.headers["X-Indigo-Token"] = client.api_token
117
- http.headers["X-Indigo-Email"] = client.user_email
156
+ http.headers["X-Indigo-Email"] = client.api_email
118
157
  end
119
158
  end
120
159
  end
@@ -1,40 +1,54 @@
1
1
  require "json"
2
- require "mechanize"
2
+ require 'net/http'
3
+ require 'uri'
3
4
  require "ribose/config"
4
5
 
5
6
  module Ribose
6
7
  class Session
7
- def initialize(username, password)
8
- @username = username
9
- @password = password
8
+ def initialize(username, password, api_email, api_token)
9
+ @username = username
10
+ @password = password
11
+ @api_email = api_email
12
+ @api_token = api_token
10
13
  end
11
14
 
12
15
  def create
13
- JSON.parse(authenticate_user)
14
- rescue NoMethodError, JSON::ParserError
15
- raise Ribose::Unauthorized
16
+ authenticate_user
16
17
  end
17
18
 
18
- def self.create(username:, password:)
19
- new(username, password).create
19
+ def self.create(username:, password:, api_email:, api_token:)
20
+ new(username, password, api_email, api_token).create
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- attr_reader :username, :password
25
+ attr_reader :username, :password, :api_email, :api_token
25
26
 
26
27
  def authenticate_user
27
- page = agent.get(ribose_url_for("login"))
28
- find_and_submit_the_user_login_form(page)
29
- agent.get(ribose_url_for(["settings", "general", "info"])).body
30
- end
31
-
32
- def find_and_submit_the_user_login_form(page)
33
- login_form = page.form_with(id: "new_user")
34
- login_form.field_with(id: "loginEmail").value = username
35
- login_form.field_with(id: "loginPassword").value = password
36
-
37
- login_form.submit
28
+ uri = URI.parse(ribose_url_for("api/v2/auth/sign_in"))
29
+ response = Net::HTTP.start(
30
+ uri.host,
31
+ uri.port,
32
+ use_ssl: true,
33
+ verify_mode: Ribose.configuration.ssl_verification_mode
34
+ ) do |http|
35
+ request = Net::HTTP::Post.new(uri)
36
+ # set request headers
37
+ request['X-Indigo-Username'] = api_email
38
+ request['X-Indigo-Token'] = api_token
39
+ request['Content-Type'] = 'application/json'
40
+
41
+ # set form data
42
+ request.set_form_data(
43
+ 'username' => username,
44
+ 'password' => password
45
+ )
46
+ http.request(request)
47
+ end
48
+
49
+ # return response headers in hash if success
50
+ return response.each_header.to_h if response.is_a? Net::HTTPSuccess
51
+ nil
38
52
  end
39
53
 
40
54
  def agent
@@ -42,7 +56,7 @@ module Ribose
42
56
  end
43
57
 
44
58
  def ribose_url_for(*endpoint)
45
- [Ribose.configuration.web_url, *endpoint].join("/")
59
+ [Ribose.configuration.api_host, *endpoint].join("/")
46
60
  end
47
61
  end
48
62
  end
data/lib/ribose/space.rb CHANGED
@@ -15,6 +15,10 @@ module Ribose
15
15
  Ribose::Request.post("spaces/#{space_uuid}/freeze", options)
16
16
  end
17
17
 
18
+ def self.delete(space_uuid, confirmation:, **options)
19
+ remove(space_uuid, options.merge(password_confirmation: confirmation))
20
+ end
21
+
18
22
  private
19
23
 
20
24
  attr_reader :space
@@ -0,0 +1,15 @@
1
+ module Ribose
2
+ class SpaceCategory < Ribose::Base
3
+ include Ribose::Actions::All
4
+
5
+ private
6
+
7
+ def resources
8
+ nil
9
+ end
10
+
11
+ def resources_path
12
+ "space_categories"
13
+ end
14
+ end
15
+ end
@@ -34,6 +34,21 @@ module Ribose
34
34
  new(space_id: space_id, resource_id: file_id, **options).fetch
35
35
  end
36
36
 
37
+ # Download a space file
38
+ #
39
+ # @param space_id [UUID] The Space UUID
40
+ # @param file_id [Integer] The File Id
41
+ # @param options [Hash] Options as key and value pair.
42
+ #
43
+ # Two important keys are :version_id, and :output and
44
+ # if these are provided then it will use those otherwise
45
+ # it will do additional request to retirve those details
46
+ #
47
+ def self.download(space_id, file_id, options = {})
48
+ options[:version_id] ||= fetch(space_id, file_id).current_version_id
49
+ Ribose::FileVersion.download(space_id, file_id, **options)
50
+ end
51
+
37
52
  # Create a new file upload
38
53
  #
39
54
  # @param space_id [String] The Space UUID
@@ -66,6 +81,20 @@ module Ribose
66
81
  new(space_id: space_id, resource_id: file_id, **options).delete
67
82
  end
68
83
 
84
+ def fetch_icon
85
+ Ribose::Request.get([resource_path, "icon"].join("/"))
86
+ end
87
+
88
+ # Fetch a space file icon
89
+ #
90
+ # @param space_id [String] The Space UUID
91
+ # @param file_id [String] The space file ID
92
+ # @return [Sawyer::Resource]
93
+ #
94
+ def self.fetch_icon(space_id, file_id, options = {})
95
+ new(space_id: space_id, resource_id: file_id, **options).fetch_icon
96
+ end
97
+
69
98
  private
70
99
 
71
100
  attr_reader :space_id
data/lib/ribose/user.rb CHANGED
@@ -8,8 +8,12 @@ module Ribose
8
8
 
9
9
  def activate
10
10
  Ribose::Request.post(
11
- "signup.user",
12
- custom_option.merge(user: attributes, auth_header: false),
11
+ "api/v2/auth",
12
+ custom_option.merge(
13
+ user: attributes,
14
+ auth_header: false,
15
+ client: Ribose::Client.new
16
+ ),
13
17
  )
14
18
  end
15
19
 
@@ -17,12 +21,12 @@ module Ribose
17
21
  #
18
22
  # @param email [String] The registering user email
19
23
  # @param password [String] A strong password for login
20
- # @param otp [String] The OTP received via the email
24
+ # @param edata [String] The OTP received via the email
21
25
  # @param attributes [Hash] The other attributes as Hash.
22
26
  # @return [Sawyer::Resoruce] The newly activated user
23
27
  #
24
- def self.activate(email:, password:, otp:, **attributes)
25
- new(attributes.merge(email: email, password: password, otp: otp)).activate
28
+ def self.activate(email:, password:, edata:, **attributes)
29
+ new(attributes.merge(email: email, password: password, edata: edata)).activate
26
30
  end
27
31
 
28
32
  private
@@ -1,3 +1,3 @@
1
1
  module Ribose
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.1".freeze
3
3
  end
@@ -0,0 +1,27 @@
1
+ require "ribose/file_uploader"
2
+
3
+ module Ribose
4
+ class VersionUploader < Ribose::FileUploader
5
+ def initialize(space_id, file_id, file:, **attributes)
6
+ @file_id = file_id
7
+ super(space_id, file: file, **attributes)
8
+ end
9
+
10
+ def self.upload(space_id, file_id, file:, **attributes)
11
+ new(space_id, file_id, attributes.merge(file: file)).create
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :file_id
17
+
18
+ def notifiable_attributes(attributes, key)
19
+ attributes[:file_info_version] = attributes.delete(:file_info)
20
+ attributes.merge(key: key)
21
+ end
22
+
23
+ def space_file_path
24
+ ["spaces", space_id, "file", "files", file_id, "versions"].join("/")
25
+ end
26
+ end
27
+ end
data/lib/ribose.rb CHANGED
@@ -28,6 +28,9 @@ require "ribose/session"
28
28
  require "ribose/profile"
29
29
  require "ribose/wiki"
30
30
  require "ribose/member_role"
31
+ require "ribose/event"
32
+ require "ribose/space_category"
33
+ require "ribose/file_version"
31
34
 
32
35
  module Ribose
33
36
  def self.root
data/ribose.gemspec CHANGED
@@ -21,13 +21,12 @@ Gem::Specification.new do |spec|
21
21
  spec.required_ruby_version = Gem::Requirement.new(">= 2.1.9")
22
22
 
23
23
  spec.add_dependency "id_pack", "~> 1.0.1"
24
- spec.add_dependency "mechanize", "~> 2.7.5"
25
24
  spec.add_dependency "mime-types", "~> 3.1"
26
25
  spec.add_dependency "sawyer", "~> 0.8.1"
27
26
 
28
- spec.add_development_dependency "bundler", "~> 1.14"
29
- spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "bundler"
28
+ spec.add_development_dependency "rake"
30
29
  spec.add_development_dependency "rspec", "~> 3.0"
31
- spec.add_development_dependency "pry", "~> 0.10.4"
30
+ spec.add_development_dependency "pry"
32
31
  spec.add_development_dependency "webmock", "~> 2.0"
33
32
  end
@@ -0,0 +1,48 @@
1
+ {
2
+ "filter": {
3
+ "enabled": [
4
+ 123456789
5
+ ],
6
+ "userSetting": [0]
7
+ },
8
+ "cal_info": [
9
+ {
10
+ "id": 123456789,
11
+ "owner_type": "User",
12
+ "name": "johndoe",
13
+ "owner_id": "2970d105-5ccc-4a8c",
14
+ "owner_name": "Personal",
15
+ "display_name": "johndoe (Personal)",
16
+ "can_manage": true,
17
+ "can_create_event": true
18
+ }
19
+ ],
20
+ "spaces_permission": {
21
+ "268b0407-c3a3-4aad-8693-fdba789f7f0d": false,
22
+ "457e8438-1c6f-423f-8bf2-77b9d5102fb0": true,
23
+ "52e47e18-9a9d-4663-94c5-abcb18fa783a": true,
24
+ "a45387e2-a573-48ba-8df0-f1424d8a8ec9": false,
25
+ "a7f7b94e-a007-4457-868f-5af319706ad5": false
26
+ },
27
+ "event": {
28
+ "id": 456789,
29
+ "name": "Sample event",
30
+ "description": "",
31
+ "where": "",
32
+ "all_day": true,
33
+ "recurring_type": "not_repeat",
34
+ "my_note": null,
35
+ "old_head_id": null,
36
+ "calendar_id": 123456789,
37
+ "created_by": "2970d105-5ccc-4a8c-b0c4-ec32d539a00a",
38
+ "utc_start": "2018-03-13T12:00:00.000Z",
39
+ "utc_finish": "2018-03-13T13:00:00.000Z",
40
+ "utc_old_start": null,
41
+ "utc_old_finish": null,
42
+ "can_save": true,
43
+ "can_delete": true,
44
+ "timestamp": 1520690119
45
+ },
46
+ "head_event": null,
47
+ "recurring": []
48
+ }
@@ -7,7 +7,7 @@
7
7
  "name": "Trips to the Mars!",
8
8
  "tag_list": [],
9
9
  "published": false,
10
- "is_favorite": false,
10
+ "is_favorite": true,
11
11
  "is_read": true,
12
12
  "allow_publish": false,
13
13
  "number_of_messages": 1,
@@ -0,0 +1,21 @@
1
+ {
2
+ "events": {
3
+ "id": 789012345,
4
+ "name": "Sample Event",
5
+ "description": "Sample event",
6
+ "where": "Skype",
7
+ "all_day": false,
8
+ "recurring_type": "not_repeat",
9
+ "my_note": null,
10
+ "old_head_id": null,
11
+ "calendar_id": 18987,
12
+ "created_by": "2970d105-5ccc",
13
+ "utc_start": "2018-04-04T18:00:00.000Z",
14
+ "utc_finish": "2018-04-04T18:30:00.000Z",
15
+ "utc_old_start": null,
16
+ "utc_old_finish": null,
17
+ "can_save": true,
18
+ "can_delete": true,
19
+ "timestamp": 1521387260
20
+ }
21
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "events": [
3
+ {
4
+ "id": 345678901,
5
+ "name": "Sample event",
6
+ "description": "",
7
+ "where": "",
8
+ "all_day": true,
9
+ "recurring_type": "not_repeat",
10
+ "my_note": "",
11
+ "old_head_id": null,
12
+ "calendar_id": 123456789,
13
+ "created_by": "bdc6aced-f7d0-465d-9d8b",
14
+ "utc_start": "2017-11-07T12:00:00.000Z",
15
+ "utc_finish": "2017-11-07T13:00:00.000Z",
16
+ "utc_old_start": null,
17
+ "utc_old_finish": null,
18
+ "can_save": true,
19
+ "can_delete": true,
20
+ "timestamp": 1508401619
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "name": "sample.png",
4
+ "description": "",
5
+ "created_at": "2017-11-30T09:12:03.000+00:00",
6
+ "updated_at": "2017-11-30T09:12:05.000+00:00",
7
+ "position": 1,
8
+ "current_version_id": 789012,
9
+ "icon_path": "/spaces/456789/file/files/11570/versions/13235?type=usual",
10
+ "content_size": 81421,
11
+ "author": "John Doe",
12
+ "content_type": "image/png",
13
+ "file_info_id": 11570
14
+ }
@@ -0,0 +1,150 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "name": "animals"
5
+ },
6
+ {
7
+ "id": 2,
8
+ "name": "architecture"
9
+ },
10
+ {
11
+ "id": 3,
12
+ "name": "art"
13
+ },
14
+ {
15
+ "id": 4,
16
+ "name": "business"
17
+ },
18
+ {
19
+ "id": 5,
20
+ "name": "cars_and_motorcycles"
21
+ },
22
+ {
23
+ "id": 6,
24
+ "name": "common_interest"
25
+ },
26
+ {
27
+ "id": 7,
28
+ "name": "diy_and_crafts"
29
+ },
30
+ {
31
+ "id": 8,
32
+ "name": "design"
33
+ },
34
+ {
35
+ "id": 9,
36
+ "name": "education"
37
+ },
38
+ {
39
+ "id": 10,
40
+ "name": "entertainment"
41
+ },
42
+ {
43
+ "id": 11,
44
+ "name": "fashion"
45
+ },
46
+ {
47
+ "id": 12,
48
+ "name": "film_music_books"
49
+ },
50
+ {
51
+ "id": 13,
52
+ "name": "food_and_drink"
53
+ },
54
+ {
55
+ "id": 14,
56
+ "name": "gardening"
57
+ },
58
+ {
59
+ "id": 15,
60
+ "name": "geek"
61
+ },
62
+ {
63
+ "id": 16,
64
+ "name": "geography"
65
+ },
66
+ {
67
+ "id": 17,
68
+ "name": "groups_and_organizations"
69
+ },
70
+ {
71
+ "id": 18,
72
+ "name": "hair_and_beauty"
73
+ },
74
+ {
75
+ "id": 19,
76
+ "name": "health_and_fitness"
77
+ },
78
+ {
79
+ "id": 20,
80
+ "name": "history"
81
+ },
82
+ {
83
+ "id": 21,
84
+ "name": "holidays_and_events"
85
+ },
86
+ {
87
+ "id": 22,
88
+ "name": "home_decor"
89
+ },
90
+ {
91
+ "id": 23,
92
+ "name": "humor"
93
+ },
94
+ {
95
+ "id": 24,
96
+ "name": "illustrations_and_posters"
97
+ },
98
+ {
99
+ "id": 25,
100
+ "name": "kids"
101
+ },
102
+ {
103
+ "id": 26,
104
+ "name": "mens_fashion"
105
+ },
106
+ {
107
+ "id": 27,
108
+ "name": "outdoors"
109
+ },
110
+ {
111
+ "id": 28,
112
+ "name": "philosophy"
113
+ },
114
+ {
115
+ "id": 29,
116
+ "name": "photography"
117
+ },
118
+ {
119
+ "id": 30,
120
+ "name": "products"
121
+ },
122
+ {
123
+ "id": 31,
124
+ "name": "religion_and_spirituality"
125
+ },
126
+ {
127
+ "id": 32,
128
+ "name": "science_and_nature"
129
+ },
130
+ {
131
+ "id": 33,
132
+ "name": "sports"
133
+ },
134
+ {
135
+ "id": 34,
136
+ "name": "technology"
137
+ },
138
+ {
139
+ "id": 35,
140
+ "name": "travel"
141
+ },
142
+ {
143
+ "id": 36,
144
+ "name": "weddings"
145
+ },
146
+ {
147
+ "id": 37,
148
+ "name": "others"
149
+ }
150
+ ]
@@ -0,0 +1,4 @@
1
+ {
2
+ "icon_processed": true,
3
+ "icon_path": "/spaces/files/icon_path?type=usual"
4
+ }
@@ -23,4 +23,15 @@ RSpec.describe Ribose::Connection do
23
23
  expect(suggestions.first.name).to eq("Jennie Doe")
24
24
  end
25
25
  end
26
+
27
+ describe ".disconnect" do
28
+ it "disconnect with provided connection" do
29
+ connection_id = 123_456
30
+ stub_ribose_connection_delete_api(connection_id)
31
+
32
+ expect do
33
+ Ribose::Connection.disconnect(connection_id)
34
+ end.not_to raise_error
35
+ end
36
+ end
26
37
  end
@@ -59,6 +59,20 @@ RSpec.describe Ribose::Conversation do
59
59
  end
60
60
  end
61
61
 
62
+ describe ".mark_as_favorite" do
63
+ it "marks a conversation as favorite" do
64
+ space_id = 123_456_789
65
+ conversation_id = 456_789
66
+ stub_ribose_space_conversation_mafav_api(space_id, conversation_id)
67
+
68
+ conversation = Ribose::Conversation.mark_as_favorite(
69
+ space_id, conversation_id
70
+ )
71
+
72
+ expect(conversation.is_favorite).to eq(true)
73
+ end
74
+ end
75
+
62
76
  describe ".destroy" do
63
77
  it "remvoes a conversation from a space" do
64
78
  space_id = 123456789