wip-ruby 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/test_examples.rb ADDED
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "logger"
5
+ require "tempfile"
6
+ require "securerandom"
7
+ require "wip-ruby"
8
+ require 'chunky_png'
9
+
10
+ # -----------------------------------------------------------------------------
11
+ # Configuration
12
+ # -----------------------------------------------------------------------------
13
+
14
+ # Set your API key via the WIP_API_KEY environment variable.
15
+ Wip.configure do |config|
16
+ config.api_key = ENV.fetch("WIP_API_KEY") { raise "WIP_API_KEY environment variable is required" }
17
+ config.base_url = "https://api.wip.co"
18
+ config.logger = Logger.new($stdout)
19
+ config.logger.level = Logger::DEBUG
20
+ end
21
+
22
+ # Initialize the main API client.
23
+ client = Wip::Client.new
24
+
25
+ # -----------------------------------------------------------------------------
26
+ # Helper to run tests
27
+ # -----------------------------------------------------------------------------
28
+
29
+ def run_test(test_name)
30
+ puts "\n=== #{test_name} ==="
31
+ yield
32
+ puts "✅ #{test_name} passed"
33
+ rescue => e
34
+ puts "❌ #{test_name} failed: #{e.class} - #{e.message}"
35
+ e.backtrace.each { |line| puts " #{line}" }
36
+ end
37
+
38
+ # -----------------------------------------------------------------------------
39
+ # Test: Configuration Validation
40
+ # -----------------------------------------------------------------------------
41
+
42
+ run_test("Configuration Validation") do
43
+ # Test missing API key
44
+ invalid_config = Wip::Configuration.new
45
+ begin
46
+ Wip::Client.new(invalid_config)
47
+ raise "Expected ConfigurationError, but none was raised"
48
+ rescue Wip::Error::ConfigurationError
49
+ # Expected error
50
+ end
51
+
52
+ # Test invalid timeout
53
+ invalid_timeout_config = Wip::Configuration.new
54
+ invalid_timeout_config.api_key = "test_key"
55
+ invalid_timeout_config.timeout = 0
56
+ begin
57
+ Wip::Client.new(invalid_timeout_config)
58
+ raise "Expected ConfigurationError for invalid timeout"
59
+ rescue Wip::Error::ConfigurationError
60
+ # Expected error
61
+ end
62
+
63
+ # Test invalid max_retries
64
+ invalid_retries_config = Wip::Configuration.new
65
+ invalid_retries_config.api_key = "test_key"
66
+ invalid_retries_config.max_retries = -1
67
+ begin
68
+ Wip::Client.new(invalid_retries_config)
69
+ raise "Expected ConfigurationError for invalid max_retries"
70
+ rescue Wip::Error::ConfigurationError
71
+ # Expected error
72
+ end
73
+
74
+ # Test invalid logger
75
+ invalid_logger_config = Wip::Configuration.new
76
+ invalid_logger_config.api_key = "test_key"
77
+ invalid_logger_config.logger = Object.new # Object without logger methods
78
+ begin
79
+ Wip::Client.new(invalid_logger_config)
80
+ raise "Expected ConfigurationError for invalid logger"
81
+ rescue Wip::Error::ConfigurationError
82
+ # Expected error
83
+ end
84
+ end
85
+
86
+ # -----------------------------------------------------------------------------
87
+ # Test: Todos Endpoints
88
+ # -----------------------------------------------------------------------------
89
+
90
+ run_test("Todos Endpoints") do
91
+ # Create a todo with a simple body
92
+ todo_body = "Test todo from manual script at #{Time.now.to_i}"
93
+ new_todo = client.todos.create(body: todo_body, attachments: [])
94
+ raise "Todo creation failed" unless new_todo["id"]
95
+
96
+ # Fetch and verify the created todo
97
+ fetched_todo = client.todos.find(new_todo["id"])
98
+ raise "Fetched todo body mismatch" unless fetched_todo["body"] == todo_body
99
+ raise "Todo URL missing" unless fetched_todo["url"].is_a?(String)
100
+ raise "Todo creator_id missing" unless fetched_todo["creator_id"].is_a?(String)
101
+ raise "Todo projects not an array" unless fetched_todo["projects"].is_a?(Array)
102
+ raise "Todo attachments not an array" unless fetched_todo["attachments"].is_a?(Array)
103
+
104
+ # Test todo with markdown formatting
105
+ markdown_body = "# Test Todo\n\n- Item 1\n- Item 2\n\n**Bold** and *italic*"
106
+ markdown_todo = client.todos.create(body: markdown_body, attachments: [])
107
+ raise "Markdown todo creation failed" unless markdown_todo["id"]
108
+
109
+ # List todos with pagination
110
+ first_page = client.viewer.todos(limit: 5)
111
+ raise "First page data missing" unless first_page["data"].is_a?(Array)
112
+ raise "First page missing total_count" unless first_page["total_count"].is_a?(Integer)
113
+ raise "First page missing has_more" unless [true, false].include?(first_page["has_more"])
114
+
115
+ if first_page["has_more"]
116
+ second_page = client.viewer.todos(limit: 5, starting_after: first_page["data"].last["id"])
117
+ raise "Second page data missing" unless second_page["data"].is_a?(Array)
118
+ raise "Second page todos overlap with first page" if (second_page["data"].map { |t| t["id"] } & first_page["data"].map { |t| t["id"] }).any?
119
+ end
120
+
121
+ # Test todos for specific user
122
+ user_todos = client.users.todos("rameerez", limit: 5)
123
+ raise "User todos data missing" unless user_todos["data"].is_a?(Array)
124
+ puts "Found #{user_todos["data"].size} todos for user 'rameerez'"
125
+
126
+ # Test todos for specific project
127
+ if first_page["data"].any? && first_page["data"].first["projects"].any?
128
+ project_id = first_page["data"].first["projects"].first["id"]
129
+ project_todos = client.projects.todos(project_id, limit: 5)
130
+ raise "Project todos data missing" unless project_todos["data"].is_a?(Array)
131
+ puts "Found #{project_todos["data"].size} todos for project #{project_id}"
132
+ end
133
+ end
134
+
135
+ # -----------------------------------------------------------------------------
136
+ # Test: Users Endpoints
137
+ # -----------------------------------------------------------------------------
138
+
139
+ run_test("Users Endpoints") do
140
+ # Test finding a known user
141
+ user = client.users.find("rameerez")
142
+ raise "User not found" unless user["username"] == "rameerez"
143
+ raise "User ID missing" unless user["id"].is_a?(String)
144
+ raise "User streak not a number" unless user["streak"].is_a?(Integer)
145
+ raise "User todos_count not a number" unless user["todos_count"].is_a?(Integer)
146
+ raise "User avatar missing" unless user["avatar"].is_a?(Hash)
147
+ raise "User avatar URLs missing" unless user["avatar"]["small"].is_a?(String) &&
148
+ user["avatar"]["medium"].is_a?(String) &&
149
+ user["avatar"]["large"].is_a?(String)
150
+
151
+ # Test authenticated user endpoints
152
+ auth_user1 = client.viewer.me
153
+ auth_user2 = client.viewer.me
154
+ raise "Authenticated user mismatch" unless auth_user1["id"] == auth_user2["id"]
155
+ raise "Authenticated user missing username" unless auth_user1["username"].is_a?(String)
156
+
157
+ # Test user's projects with pagination
158
+ user_projects = client.users.projects("rameerez", limit: 5)
159
+ raise "User projects data missing" unless user_projects["data"].is_a?(Array)
160
+ if user_projects["has_more"]
161
+ next_projects = client.users.projects("rameerez",
162
+ limit: 5,
163
+ starting_after: user_projects["data"].last["id"])
164
+ raise "Next page projects data missing" unless next_projects["data"].is_a?(Array)
165
+ end
166
+
167
+ # Test user's todos with pagination
168
+ user_todos = client.users.todos("rameerez", limit: 5)
169
+ raise "User todos data missing" unless user_todos["data"].is_a?(Array)
170
+ if user_todos["has_more"]
171
+ next_todos = client.users.todos("rameerez",
172
+ limit: 5,
173
+ starting_after: user_todos["data"].last["id"])
174
+ raise "Next page todos data missing" unless next_todos["data"].is_a?(Array)
175
+ end
176
+
177
+ puts "User 'rameerez' has #{user_projects['data'].size} projects and #{user_todos['data'].size} todos"
178
+ end
179
+
180
+ # -----------------------------------------------------------------------------
181
+ # Test: Projects Endpoints
182
+ # -----------------------------------------------------------------------------
183
+
184
+ run_test("Projects Endpoints") do
185
+ # Test authenticated user's projects
186
+ my_projects = client.viewer.projects(limit: 5)
187
+ raise "My projects data missing" unless my_projects["data"].is_a?(Array)
188
+
189
+ if my_projects["data"].any?
190
+ project = my_projects["data"].first
191
+
192
+ # Test fetching single project
193
+ proj_details = client.projects.find(project["id"])
194
+ raise "Project name missing" unless proj_details["name"].is_a?(String)
195
+ raise "Project URL missing" unless proj_details["url"].is_a?(String)
196
+ raise "Project owner missing" unless proj_details["owner"].is_a?(Hash)
197
+ raise "Project makers not an array" unless proj_details["makers"].is_a?(Array)
198
+ raise "Project logo missing" unless proj_details["logo"].is_a?(Hash)
199
+
200
+ # Test project todos with pagination
201
+ proj_todos = client.projects.todos(project["id"], limit: 5)
202
+ raise "Project todos data missing" unless proj_todos["data"].is_a?(Array)
203
+ if proj_todos["has_more"]
204
+ next_todos = client.projects.todos(project["id"],
205
+ limit: 5,
206
+ starting_after: proj_todos["data"].last["id"])
207
+ raise "Next page project todos missing" unless next_todos["data"].is_a?(Array)
208
+ end
209
+ puts "Project '#{proj_details['name']}' has #{proj_todos['data'].size} todos"
210
+ else
211
+ puts "No projects found for authenticated user"
212
+ end
213
+
214
+ # Test listing projects for a specific user with pagination
215
+ user_projects = client.users.projects("rameerez", limit: 5)
216
+ raise "User projects data missing" unless user_projects["data"].is_a?(Array)
217
+ if user_projects["has_more"]
218
+ next_projects = client.users.projects("rameerez",
219
+ limit: 5,
220
+ starting_after: user_projects["data"].last["id"])
221
+ raise "Next page user projects missing" unless next_projects["data"].is_a?(Array)
222
+ end
223
+ puts "User 'rameerez' has #{user_projects['data'].size} projects"
224
+ end
225
+
226
+ # -----------------------------------------------------------------------------
227
+ # Test: Uploads Endpoints
228
+ # -----------------------------------------------------------------------------
229
+
230
+ run_test("Uploads Endpoints") do
231
+ # Test uploading different file types
232
+ [
233
+ # Test PNG image
234
+ {
235
+ ext: ".png",
236
+ content_type: "image/png",
237
+ create: ->(file) {
238
+ png = ChunkyPNG::Image.new(100, 100, ChunkyPNG::Color.rgb(255, 0, 0))
239
+ png.rect(0, 0, 99, 99, ChunkyPNG::Color.rgb(0, 0, 0), ChunkyPNG::Color.rgb(255, 0, 0))
240
+ png.save(file.path)
241
+ }
242
+ },
243
+ # Test text file
244
+ {
245
+ ext: ".md",
246
+ content_type: "text/markdown",
247
+ create: ->(file) {
248
+ file.write("# Test Markdown\n\nThis is a test file for wip-ruby uploads.")
249
+ }
250
+ }
251
+ ].each do |test_file|
252
+ Tempfile.create(["wip_test", test_file[:ext]]) do |file|
253
+ # Create test file
254
+ test_file[:create].call(file)
255
+ file.flush
256
+
257
+ # Upload file and verify signed ID
258
+ signed_id = client.uploads.upload(file.path)
259
+ raise "Upload did not return a signed_id" unless signed_id && !signed_id.empty?
260
+ puts "Successfully uploaded #{test_file[:ext]} file with signed_id: #{signed_id}"
261
+
262
+ # Create todo with attachment
263
+ todo_body = "Todo with #{test_file[:ext]} attachment from test at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
264
+ todo = client.todos.create(
265
+ body: todo_body,
266
+ attachments: [signed_id]
267
+ )
268
+ raise "Todo creation failed" unless todo["id"]
269
+
270
+ # Verify todo and attachment
271
+ fetched_todo = client.todos.find(todo["id"])
272
+ raise "Todo body mismatch" unless fetched_todo["body"] == todo_body
273
+ raise "Attachment missing" if fetched_todo["attachments"].empty?
274
+ raise "Attachment ID mismatch" unless fetched_todo["attachments"].any? { |a| a["signed_id"] == signed_id }
275
+ puts "Successfully verified todo has the correct #{test_file[:ext]} attachment"
276
+ end
277
+ end
278
+
279
+ # Test invalid file type
280
+ Tempfile.create(["wip_test", ".invalid"]) do |file|
281
+ file.write("Invalid file type test")
282
+ file.flush
283
+
284
+ begin
285
+ client.uploads.upload(file.path)
286
+ raise "Expected ValidationError for invalid file type"
287
+ rescue Wip::Error::ValidationError => e
288
+ puts "Caught expected ValidationError for invalid file type: #{e.message}"
289
+ end
290
+ end
291
+ end
292
+
293
+ # -----------------------------------------------------------------------------
294
+ # Test: Error Handling
295
+ # -----------------------------------------------------------------------------
296
+
297
+ run_test("Error Handling - Not Found") do
298
+ # Test non-existent user
299
+ non_existent_user = "nonexistent_user_#{SecureRandom.hex(4)}"
300
+ begin
301
+ client.users.find(non_existent_user)
302
+ raise "Expected NotFoundError but none was raised"
303
+ rescue Wip::Error::NotFoundError => e
304
+ puts "Caught expected NotFoundError: #{e.message}"
305
+ end
306
+
307
+ # Test non-existent project
308
+ begin
309
+ client.projects.find("non_existent_project_#{SecureRandom.hex(4)}")
310
+ raise "Expected NotFoundError but none was raised"
311
+ rescue Wip::Error::NotFoundError => e
312
+ puts "Caught expected NotFoundError for project: #{e.message}"
313
+ end
314
+
315
+ # Test non-existent todo
316
+ begin
317
+ client.todos.find("non_existent_todo_#{SecureRandom.hex(4)}")
318
+ raise "Expected NotFoundError but none was raised"
319
+ rescue Wip::Error::NotFoundError => e
320
+ puts "Caught expected NotFoundError for todo: #{e.message}"
321
+ end
322
+ end
323
+
324
+ run_test("Error Handling - Unauthorized") do
325
+ # Test invalid API key
326
+ bad_config = Wip::Configuration.new.tap { |c| c.api_key = "invalid_key" }
327
+ bad_client = Wip::Client.new(bad_config)
328
+
329
+ # Test different endpoints with invalid key
330
+ [
331
+ -> { bad_client.viewer.me },
332
+ -> { bad_client.viewer.todos },
333
+ -> { bad_client.viewer.projects },
334
+ -> { bad_client.viewer.me }
335
+ ].each do |endpoint|
336
+ begin
337
+ endpoint.call
338
+ raise "Expected UnauthorizedError but none was raised"
339
+ rescue Wip::Error::UnauthorizedError => e
340
+ puts "Caught expected UnauthorizedError: #{e.message}"
341
+ end
342
+ end
343
+ end
344
+
345
+ run_test("Error Handling - Validation") do
346
+ # Test creating todo without body
347
+ begin
348
+ client.todos.create(body: "", attachments: [])
349
+ raise "Expected ValidationError but none was raised"
350
+ rescue Wip::Error::ValidationError => e
351
+ puts "Caught expected ValidationError for empty todo body: #{e.message}"
352
+ end
353
+
354
+ # Test upload with invalid content type
355
+ begin
356
+ client.uploads.request_upload_url(
357
+ filename: "test.xyz",
358
+ byte_size: 100,
359
+ checksum: "invalid",
360
+ content_type: "application/invalid"
361
+ )
362
+ raise "Expected ValidationError but none was raised"
363
+ rescue Wip::Error::ValidationError => e
364
+ puts "Caught expected ValidationError for invalid content type: #{e.message}"
365
+ end
366
+ end
367
+
368
+ # -----------------------------------------------------------------------------
369
+ # Test: Pagination
370
+ # -----------------------------------------------------------------------------
371
+
372
+ run_test("Pagination") do
373
+ # Test todos pagination
374
+ page1 = client.viewer.todos(limit: 2)
375
+ raise "Page1 data missing" unless page1["data"].is_a?(Array)
376
+ puts "Page1 contains #{page1['data'].size} items"
377
+
378
+ if page1["has_more"] && page1["data"].any?
379
+ page2 = client.viewer.todos(limit: 2, starting_after: page1["data"].last["id"])
380
+ raise "Page2 data missing" unless page2["data"].is_a?(Array)
381
+ puts "Page2 contains #{page2['data'].size} items"
382
+
383
+ # Verify no overlap between pages
384
+ page1_ids = page1["data"].map { |t| t["id"] }
385
+ page2_ids = page2["data"].map { |t| t["id"] }
386
+ raise "Page IDs overlap" unless (page1_ids & page2_ids).empty?
387
+
388
+ if page2["has_more"] && page2["data"].any?
389
+ page3 = client.viewer.todos(limit: 2, starting_after: page2["data"].last["id"])
390
+ raise "Page3 data missing" unless page3["data"].is_a?(Array)
391
+ puts "Page3 contains #{page3['data'].size} items"
392
+ end
393
+ end
394
+
395
+ # Test projects pagination
396
+ projects_page1 = client.viewer.projects(limit: 2)
397
+ raise "Projects page1 data missing" unless projects_page1["data"].is_a?(Array)
398
+ puts "Projects page1 contains #{projects_page1['data'].size} items"
399
+
400
+ if projects_page1["has_more"] && projects_page1["data"].any?
401
+ projects_page2 = client.viewer.projects(
402
+ limit: 2,
403
+ starting_after: projects_page1["data"].last["id"]
404
+ )
405
+ raise "Projects page2 data missing" unless projects_page2["data"].is_a?(Array)
406
+ puts "Projects page2 contains #{projects_page2['data'].size} items"
407
+ end
408
+ end
409
+
410
+ # -----------------------------------------------------------------------------
411
+ # Test: Rate Limiting
412
+ # -----------------------------------------------------------------------------
413
+
414
+ run_test("Rate Limiting") do
415
+ rate_limit_triggered = false
416
+ requests_made = 0
417
+
418
+ # Make rapid requests to try to trigger rate limiting
419
+ 20.times do |i|
420
+ begin
421
+ client.viewer.me
422
+ requests_made += 1
423
+ sleep(0.1) # Small delay between requests
424
+ rescue Wip::Error::RateLimitError => e
425
+ puts "Rate limit triggered after #{requests_made} requests: #{e.message}"
426
+ rate_limit_triggered = true
427
+ break
428
+ end
429
+ end
430
+
431
+ puts "Rate limiting test: #{rate_limit_triggered ? 'Rate limit was triggered' : 'No rate limit encountered'}"
432
+ puts "Completed #{requests_made} requests"
433
+ end
434
+
435
+ puts "\nAll manual tests completed!"
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wip-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Javi R
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-02-03 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.12'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.12'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-retry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mime-types
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.5'
54
+ description: WIP.co API wrapper for Ruby on Rails apps. WIP (wip.co) is a community
55
+ of makers who share what they're working on. wip-ruby provides an elegant, intuitive,
56
+ no-frills interface for interacting with the WIP API.
57
+ email:
58
+ - rubygems@rameerez.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".simplecov"
64
+ - CHANGELOG.md
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/wip-ruby.rb
69
+ - lib/wip.rb
70
+ - lib/wip/client.rb
71
+ - lib/wip/configuration.rb
72
+ - lib/wip/error.rb
73
+ - lib/wip/http_client.rb
74
+ - lib/wip/models/base.rb
75
+ - lib/wip/models/collection.rb
76
+ - lib/wip/models/comment.rb
77
+ - lib/wip/models/concerns/reactable.rb
78
+ - lib/wip/models/project.rb
79
+ - lib/wip/models/reaction.rb
80
+ - lib/wip/models/todo.rb
81
+ - lib/wip/models/user.rb
82
+ - lib/wip/resources/base.rb
83
+ - lib/wip/resources/comments.rb
84
+ - lib/wip/resources/projects.rb
85
+ - lib/wip/resources/reactions.rb
86
+ - lib/wip/resources/todos.rb
87
+ - lib/wip/resources/uploads.rb
88
+ - lib/wip/resources/users.rb
89
+ - lib/wip/resources/viewer.rb
90
+ - lib/wip/version.rb
91
+ - sig/wip/ruby.rbs
92
+ - test_examples.rb
93
+ homepage: https://github.com/rameerez/wip-ruby
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ allowed_push_host: https://rubygems.org
98
+ homepage_uri: https://github.com/rameerez/wip-ruby
99
+ source_code_uri: https://github.com/rameerez/wip-ruby
100
+ changelog_uri: https://github.com/rameerez/wip-ruby/blob/main/CHANGELOG.md
101
+ rubygems_mfa_required: 'true'
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 3.1.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.6.2
117
+ specification_version: 4
118
+ summary: WIP.co API wrapper for Ruby / Rails.
119
+ test_files: []