From 1afea3491b2aad2fc9e40da9c27e3fbf86581e0e Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 10 Dec 2025 11:47:12 -0500 Subject: [PATCH] Add Phase 3 advanced commands with full test coverage Comment commands (lib/fizzy/commands/comment.rb): - list: List comments on a card with pagination support - show: Show a specific comment by ID - create: Create comment with --body or --body-file options - update: Update comment body - delete: Delete a comment Reaction commands (lib/fizzy/commands/reaction.rb): - list: List reactions on a comment - create: Add emoji reaction to a comment - delete: Remove a reaction Step commands (lib/fizzy/commands/step.rb): - show: Show a specific to-do item - create: Create step with --content and optional --completed flag - update: Update content or toggle completed state - delete: Delete a step Notification commands (lib/fizzy/commands/notification.rb): - list: List notifications with pagination support - read: Mark notification as read - unread: Mark notification as unread - read-all: Mark all notifications as read Card action commands (added to lib/fizzy/commands/card.rb): - close/reopen: Close or reopen a card - postpone: Mark card as "not now" - column: Move card into a column (triage) - untriage: Send card back to triage - assign: Toggle user assignment (uses assignee_id param) - tag: Toggle tag on card (uses tag_title param) - watch/unwatch: Toggle watching a card Test coverage: - 96 tests, 188 assertions, all passing - New test files for each command group --- lib/fizzy.rb | 4 + lib/fizzy/cli.rb | 12 ++ lib/fizzy/commands/card.rb | 77 ++++++++++ lib/fizzy/commands/comment.rb | 81 +++++++++++ lib/fizzy/commands/notification.rb | 47 ++++++ lib/fizzy/commands/reaction.rb | 39 +++++ lib/fizzy/commands/step.rb | 60 ++++++++ test/fizzy/commands/card_actions_test.rb | 177 +++++++++++++++++++++++ test/fizzy/commands/comment_test.rb | 171 ++++++++++++++++++++++ test/fizzy/commands/notification_test.rb | 111 ++++++++++++++ test/fizzy/commands/reaction_test.rb | 81 +++++++++++ test/fizzy/commands/step_test.rb | 157 ++++++++++++++++++++ 12 files changed, 1017 insertions(+) create mode 100644 lib/fizzy/commands/comment.rb create mode 100644 lib/fizzy/commands/notification.rb create mode 100644 lib/fizzy/commands/reaction.rb create mode 100644 lib/fizzy/commands/step.rb create mode 100644 test/fizzy/commands/card_actions_test.rb create mode 100644 test/fizzy/commands/comment_test.rb create mode 100644 test/fizzy/commands/notification_test.rb create mode 100644 test/fizzy/commands/reaction_test.rb create mode 100644 test/fizzy/commands/step_test.rb diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 7a798e9..d549368 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -18,4 +18,8 @@ require_relative "fizzy/commands/column" require_relative "fizzy/commands/user" require_relative "fizzy/commands/tag" +require_relative "fizzy/commands/comment" +require_relative "fizzy/commands/reaction" +require_relative "fizzy/commands/step" +require_relative "fizzy/commands/notification" require_relative "fizzy/cli" diff --git a/lib/fizzy/cli.rb b/lib/fizzy/cli.rb index 1885314..5662875 100644 --- a/lib/fizzy/cli.rb +++ b/lib/fizzy/cli.rb @@ -31,5 +31,17 @@ def self.exit_on_failure? desc "tag SUBCOMMAND", "Manage tags" subcommand "tag", Commands::Tag + + desc "comment SUBCOMMAND", "Manage comments" + subcommand "comment", Commands::Comment + + desc "reaction SUBCOMMAND", "Manage reactions" + subcommand "reaction", Commands::Reaction + + desc "step SUBCOMMAND", "Manage steps (to-do items)" + subcommand "step", Commands::Step + + desc "notification SUBCOMMAND", "Manage notifications" + subcommand "notification", Commands::Notification end end diff --git a/lib/fizzy/commands/card.rb b/lib/fizzy/commands/card.rb index d3d00f0..1c6b897 100644 --- a/lib/fizzy/commands/card.rb +++ b/lib/fizzy/commands/card.rb @@ -100,6 +100,83 @@ def delete(number) rescue Fizzy::Error => e output_error(e) end + + # Card Action Commands + + desc "close NUMBER", "Close a card" + def close(number) + result = client.post(client.account_path("/cards/#{number}/closure"), {}) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "reopen NUMBER", "Reopen a closed card" + def reopen(number) + result = client.delete(client.account_path("/cards/#{number}/closure")) + output(result || Response.success(data: { reopened: true })) + rescue Fizzy::Error => e + output_error(e) + end + + desc "postpone NUMBER", "Postpone a card (mark as not now)" + def postpone(number) + result = client.post(client.account_path("/cards/#{number}/not_now"), {}) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "column NUMBER", "Move a card into a column" + option :column, required: true, type: :string, desc: "Column ID to move into" + def column(number) + result = client.post(client.account_path("/cards/#{number}/triage"), { column_id: options[:column] }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "untriage NUMBER", "Send a card back to triage" + def untriage(number) + result = client.delete(client.account_path("/cards/#{number}/triage")) + output(result || Response.success(data: { untriaged: true })) + rescue Fizzy::Error => e + output_error(e) + end + + desc "assign NUMBER", "Toggle assignment for a user on a card" + option :user, required: true, type: :string, desc: "User ID to toggle assignment" + def assign(number) + result = client.post(client.account_path("/cards/#{number}/assignments"), { assignee_id: options[:user] }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "tag NUMBER", "Toggle a tag on a card (creates tag if needed)" + option :tag, required: true, type: :string, desc: "Tag title to toggle" + def tag(number) + result = client.post(client.account_path("/cards/#{number}/taggings"), { tag_title: options[:tag] }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "watch NUMBER", "Watch a card for notifications" + def watch(number) + result = client.post(client.account_path("/cards/#{number}/watch"), {}) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "unwatch NUMBER", "Stop watching a card" + def unwatch(number) + result = client.delete(client.account_path("/cards/#{number}/watch")) + output(result || Response.success(data: { unwatched: true })) + rescue Fizzy::Error => e + output_error(e) + end end end end diff --git a/lib/fizzy/commands/comment.rb b/lib/fizzy/commands/comment.rb new file mode 100644 index 0000000..7d492d1 --- /dev/null +++ b/lib/fizzy/commands/comment.rb @@ -0,0 +1,81 @@ +module Fizzy + module Commands + class Comment < Base + desc "list", "List comments on a card" + option :card, required: true, type: :string, desc: "Card number" + option :page, type: :numeric, desc: "Page number" + option :all, type: :boolean, default: false, desc: "Fetch all pages" + def list + params = {} + params[:page] = options[:page] if options[:page] + + result = if options[:all] + client.get_all(client.account_path("/cards/#{options[:card]}/comments"), params) + else + client.get(client.account_path("/cards/#{options[:card]}/comments"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "show ID", "Show a specific comment" + option :card, required: true, type: :string, desc: "Card number" + def show(id) + result = client.get(client.account_path("/cards/#{options[:card]}/comments/#{id}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Create a new comment" + option :card, required: true, type: :string, desc: "Card number" + option :body, type: :string, desc: "Comment body (supports rich text)" + option :body_file, type: :string, desc: "Read body from file" + def create + comment_params = {} + + if options[:body_file] + comment_params[:body] = File.read(options[:body_file]) + elsif options[:body] + comment_params[:body] = options[:body] + else + raise Fizzy::ValidationError, "Either --body or --body-file is required" + end + + result = client.post(client.account_path("/cards/#{options[:card]}/comments"), { comment: comment_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update ID", "Update a comment" + option :card, required: true, type: :string, desc: "Card number" + option :body, type: :string, desc: "Comment body (supports rich text)" + option :body_file, type: :string, desc: "Read body from file" + def update(id) + comment_params = {} + + if options[:body_file] + comment_params[:body] = File.read(options[:body_file]) + elsif options[:body] + comment_params[:body] = options[:body] + end + + result = client.put(client.account_path("/cards/#{options[:card]}/comments/#{id}"), { comment: comment_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete ID", "Delete a comment" + option :card, required: true, type: :string, desc: "Card number" + def delete(id) + result = client.delete(client.account_path("/cards/#{options[:card]}/comments/#{id}")) + output(result || Response.success(data: { deleted: true })) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/lib/fizzy/commands/notification.rb b/lib/fizzy/commands/notification.rb new file mode 100644 index 0000000..c741e0f --- /dev/null +++ b/lib/fizzy/commands/notification.rb @@ -0,0 +1,47 @@ +module Fizzy + module Commands + class Notification < Base + desc "list", "List notifications" + option :page, type: :numeric, desc: "Page number" + option :all, type: :boolean, default: false, desc: "Fetch all pages" + def list + params = {} + params[:page] = options[:page] if options[:page] + + result = if options[:all] + client.get_all(client.account_path("/notifications"), params) + else + client.get(client.account_path("/notifications"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "read ID", "Mark a notification as read" + def read(id) + result = client.post(client.account_path("/notifications/#{id}/reading"), {}) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "unread ID", "Mark a notification as unread" + def unread(id) + result = client.delete(client.account_path("/notifications/#{id}/reading")) + output(result || Response.success(data: { unread: true })) + rescue Fizzy::Error => e + output_error(e) + end + + map "read-all" => :read_all + desc "read-all", "Mark all notifications as read" + def read_all + result = client.post(client.account_path("/notifications/bulk_reading"), {}) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/lib/fizzy/commands/reaction.rb b/lib/fizzy/commands/reaction.rb new file mode 100644 index 0000000..6f8f236 --- /dev/null +++ b/lib/fizzy/commands/reaction.rb @@ -0,0 +1,39 @@ +module Fizzy + module Commands + class Reaction < Base + desc "list", "List reactions on a comment" + option :card, required: true, type: :string, desc: "Card number" + option :comment, required: true, type: :string, desc: "Comment ID" + def list + result = client.get(client.account_path("/cards/#{options[:card]}/comments/#{options[:comment]}/reactions")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Add a reaction to a comment" + option :card, required: true, type: :string, desc: "Card number" + option :comment, required: true, type: :string, desc: "Comment ID" + option :content, required: true, type: :string, desc: "Emoji (max 16 chars)" + def create + result = client.post( + client.account_path("/cards/#{options[:card]}/comments/#{options[:comment]}/reactions"), + { reaction: { content: options[:content] } } + ) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete ID", "Remove a reaction" + option :card, required: true, type: :string, desc: "Card number" + option :comment, required: true, type: :string, desc: "Comment ID" + def delete(id) + result = client.delete(client.account_path("/cards/#{options[:card]}/comments/#{options[:comment]}/reactions/#{id}")) + output(result || Response.success(data: { deleted: true })) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/lib/fizzy/commands/step.rb b/lib/fizzy/commands/step.rb new file mode 100644 index 0000000..7dc8d97 --- /dev/null +++ b/lib/fizzy/commands/step.rb @@ -0,0 +1,60 @@ +module Fizzy + module Commands + class Step < Base + desc "show ID", "Show a specific step (to-do item)" + option :card, required: true, type: :string, desc: "Card number" + def show(id) + result = client.get(client.account_path("/cards/#{options[:card]}/steps/#{id}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Create a new step (to-do item)" + option :card, required: true, type: :string, desc: "Card number" + option :content, required: true, type: :string, desc: "Step content" + option :completed, type: :boolean, default: false, desc: "Mark as completed" + def create + step_params = { + content: options[:content], + completed: options[:completed] + } + + result = client.post(client.account_path("/cards/#{options[:card]}/steps"), { step: step_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update ID", "Update a step" + option :card, required: true, type: :string, desc: "Card number" + option :content, type: :string, desc: "Step content" + option :completed, type: :boolean, desc: "Mark as completed" + option :not_completed, type: :boolean, desc: "Mark as not completed" + def update(id) + step_params = {} + step_params[:content] = options[:content] if options.key?(:content) + + if options[:not_completed] + step_params[:completed] = false + elsif options.key?(:completed) + step_params[:completed] = options[:completed] + end + + result = client.put(client.account_path("/cards/#{options[:card]}/steps/#{id}"), { step: step_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete ID", "Delete a step" + option :card, required: true, type: :string, desc: "Card number" + def delete(id) + result = client.delete(client.account_path("/cards/#{options[:card]}/steps/#{id}")) + output(result || Response.success(data: { deleted: true })) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/test/fizzy/commands/card_actions_test.rb b/test/fizzy/commands/card_actions_test.rb new file mode 100644 index 0000000..0ff84d6 --- /dev/null +++ b/test/fizzy/commands/card_actions_test.rb @@ -0,0 +1,177 @@ +require "test_helper" + +class Fizzy::Commands::CardActionsTest < Fizzy::TestCase + def setup + super + @original_home = ENV["HOME"] + @temp_dir = Dir.mktmpdir + ENV["HOME"] = @temp_dir + + config = Fizzy::Config.new + config.save!(token: "test_token", account: "test_account") + end + + def teardown + super + ENV["HOME"] = @original_home + FileUtils.rm_rf(@temp_dir) + end + + def test_close_card + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/closure") + .to_return( + status: 200, + body: '{"id": "100", "number": 42, "status": "closed"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:close, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_reopen_card + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/closure") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:reopen, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["reopened"] + end + + def test_postpone_card + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/not_now") + .to_return( + status: 200, + body: '{"id": "100", "number": 42, "status": "not_now"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:postpone, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_move_card_to_column + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/triage") + .with( + body: { column_id: "col1" }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 200, + body: '{"id": "100", "number": 42}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { column: "col1" }).invoke(:column, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_untriage_card + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/triage") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:untriage, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["untriaged"] + end + + def test_assign_user_to_card + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/assignments") + .with( + body: { assignee_id: "u1" }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 200, + body: '{"assigned": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { user: "u1" }).invoke(:assign, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_tag_card + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/taggings") + .with( + body: { tag_title: "urgent" }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 200, + body: '{"tagged": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { tag: "urgent" }).invoke(:tag, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_watch_card + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/watch") + .to_return( + status: 200, + body: '{"watching": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:watch, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_unwatch_card + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/watch") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:unwatch, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["unwatched"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end diff --git a/test/fizzy/commands/comment_test.rb b/test/fizzy/commands/comment_test.rb new file mode 100644 index 0000000..5f23e45 --- /dev/null +++ b/test/fizzy/commands/comment_test.rb @@ -0,0 +1,171 @@ +require "test_helper" + +class Fizzy::Commands::CommentTest < Fizzy::TestCase + def setup + super + @original_home = ENV["HOME"] + @temp_dir = Dir.mktmpdir + ENV["HOME"] = @temp_dir + + config = Fizzy::Config.new + config.save!(token: "test_token", account: "test_account") + end + + def teardown + super + ENV["HOME"] = @original_home + FileUtils.rm_rf(@temp_dir) + end + + def test_list_returns_comments + stub_request(:get, "https://app.fizzy.do/test_account/cards/42/comments") + .to_return( + status: 200, + body: '[{"id": "c1", "body": {"plain_text": "First comment"}}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42" }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 1, result["data"].length + assert_equal "First comment", result["data"][0]["body"]["plain_text"] + end + + def test_list_with_pagination + stub_request(:get, "https://app.fizzy.do/test_account/cards/42/comments") + .with(query: { "page" => "2" }) + .to_return( + status: 200, + body: '[{"id": "c2", "body": {"plain_text": "Page 2 comment"}}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42", page: 2 }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Page 2 comment", result["data"][0]["body"]["plain_text"] + end + + def test_show_returns_comment + stub_request(:get, "https://app.fizzy.do/test_account/cards/42/comments/c1") + .to_return( + status: 200, + body: '{"id": "c1", "body": {"plain_text": "Comment details"}}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42" }).invoke(:show, ["c1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "c1", result["data"]["id"] + end + + def test_create_comment + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/comments") + .with( + body: { comment: { body: "New comment" } }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 201, + body: '{"id": "c2", "body": {"plain_text": "New comment"}}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42", body: "New comment" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_create_comment_from_file + file_path = File.join(@temp_dir, "comment.txt") + File.write(file_path, "Comment from file") + + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/comments") + .with( + body: { comment: { body: "Comment from file" } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "c3"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42", body_file: file_path }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_create_comment_requires_body + output = capture_output do + begin + Fizzy::Commands::Comment.new([], { card: "42" }).invoke(:create, []) + rescue SystemExit + # Expected + end + end + + result = JSON.parse(output) + refute result["success"] + assert_equal "VALIDATION_ERROR", result["error"]["code"] + end + + def test_update_comment + stub_request(:put, "https://app.fizzy.do/test_account/cards/42/comments/c1") + .with( + body: { comment: { body: "Updated body" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "c1", "body": {"plain_text": "Updated body"}}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42", body: "Updated body" }).invoke(:update, ["c1"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_delete_comment + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/comments/c1") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Comment.new([], { card: "42" }).invoke(:delete, ["c1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["deleted"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end diff --git a/test/fizzy/commands/notification_test.rb b/test/fizzy/commands/notification_test.rb new file mode 100644 index 0000000..af80c9e --- /dev/null +++ b/test/fizzy/commands/notification_test.rb @@ -0,0 +1,111 @@ +require "test_helper" + +class Fizzy::Commands::NotificationTest < Fizzy::TestCase + def setup + super + @original_home = ENV["HOME"] + @temp_dir = Dir.mktmpdir + ENV["HOME"] = @temp_dir + + config = Fizzy::Config.new + config.save!(token: "test_token", account: "test_account") + end + + def teardown + super + ENV["HOME"] = @original_home + FileUtils.rm_rf(@temp_dir) + end + + def test_list_returns_notifications + stub_request(:get, "https://app.fizzy.do/test_account/notifications") + .to_return( + status: 200, + body: '[{"id": "n1", "title": "New comment", "read": false}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Notification.new.invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 1, result["data"].length + assert_equal "New comment", result["data"][0]["title"] + end + + def test_list_with_pagination + stub_request(:get, "https://app.fizzy.do/test_account/notifications") + .with(query: { "page" => "2" }) + .to_return( + status: 200, + body: '[{"id": "n2", "title": "Old notification"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Notification.new([], { page: 2 }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Old notification", result["data"][0]["title"] + end + + def test_read_notification + stub_request(:post, "https://app.fizzy.do/test_account/notifications/n1/reading") + .to_return( + status: 200, + body: '{"id": "n1", "read": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Notification.new.invoke(:read, ["n1"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_unread_notification + stub_request(:delete, "https://app.fizzy.do/test_account/notifications/n1/reading") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Notification.new.invoke(:unread, ["n1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["unread"] + end + + def test_read_all_notifications + stub_request(:post, "https://app.fizzy.do/test_account/notifications/bulk_reading") + .to_return( + status: 200, + body: '{"read_count": 5}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Notification.new.invoke(:read_all, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end diff --git a/test/fizzy/commands/reaction_test.rb b/test/fizzy/commands/reaction_test.rb new file mode 100644 index 0000000..89a9e52 --- /dev/null +++ b/test/fizzy/commands/reaction_test.rb @@ -0,0 +1,81 @@ +require "test_helper" + +class Fizzy::Commands::ReactionTest < Fizzy::TestCase + def setup + super + @original_home = ENV["HOME"] + @temp_dir = Dir.mktmpdir + ENV["HOME"] = @temp_dir + + config = Fizzy::Config.new + config.save!(token: "test_token", account: "test_account") + end + + def teardown + super + ENV["HOME"] = @original_home + FileUtils.rm_rf(@temp_dir) + end + + def test_list_returns_reactions + stub_request(:get, "https://app.fizzy.do/test_account/cards/42/comments/c1/reactions") + .to_return( + status: 200, + body: '[{"id": "r1", "content": "👍"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Reaction.new([], { card: "42", comment: "c1" }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 1, result["data"].length + assert_equal "👍", result["data"][0]["content"] + end + + def test_create_reaction + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/comments/c1/reactions") + .with( + body: { reaction: { content: "🎉" } }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 201, + body: '{"id": "r2", "content": "🎉"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Reaction.new([], { card: "42", comment: "c1", content: "🎉" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_delete_reaction + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/comments/c1/reactions/r1") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Reaction.new([], { card: "42", comment: "c1" }).invoke(:delete, ["r1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["deleted"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end diff --git a/test/fizzy/commands/step_test.rb b/test/fizzy/commands/step_test.rb new file mode 100644 index 0000000..e1ce899 --- /dev/null +++ b/test/fizzy/commands/step_test.rb @@ -0,0 +1,157 @@ +require "test_helper" + +class Fizzy::Commands::StepTest < Fizzy::TestCase + def setup + super + @original_home = ENV["HOME"] + @temp_dir = Dir.mktmpdir + ENV["HOME"] = @temp_dir + + config = Fizzy::Config.new + config.save!(token: "test_token", account: "test_account") + end + + def teardown + super + ENV["HOME"] = @original_home + FileUtils.rm_rf(@temp_dir) + end + + def test_show_returns_step + stub_request(:get, "https://app.fizzy.do/test_account/cards/42/steps/s1") + .to_return( + status: 200, + body: '{"id": "s1", "content": "Do this thing", "completed": false}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42" }).invoke(:show, ["s1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "s1", result["data"]["id"] + assert_equal "Do this thing", result["data"]["content"] + end + + def test_create_step + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/steps") + .with( + body: { step: { content: "New step", completed: false } }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 201, + body: '{"id": "s2", "content": "New step", "completed": false}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42", content: "New step" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_create_step_completed + stub_request(:post, "https://app.fizzy.do/test_account/cards/42/steps") + .with( + body: { step: { content: "Done step", completed: true } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "s3", "content": "Done step", "completed": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42", content: "Done step", completed: true }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_update_step_content + stub_request(:put, "https://app.fizzy.do/test_account/cards/42/steps/s1") + .with( + body: { step: { content: "Updated content" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "s1", "content": "Updated content"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42", content: "Updated content" }).invoke(:update, ["s1"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_update_step_mark_completed + stub_request(:put, "https://app.fizzy.do/test_account/cards/42/steps/s1") + .with( + body: { step: { completed: true } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "s1", "completed": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42", completed: true }).invoke(:update, ["s1"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_update_step_mark_not_completed + stub_request(:put, "https://app.fizzy.do/test_account/cards/42/steps/s1") + .with( + body: { step: { completed: false } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "s1", "completed": false}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42", not_completed: true }).invoke(:update, ["s1"]) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_delete_step + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42/steps/s1") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Step.new([], { card: "42" }).invoke(:delete, ["s1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["deleted"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end