From a77c5bda0271904456f64599af4988e277272be6 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 10 Dec 2025 11:04:20 -0500 Subject: [PATCH] Add Phase 2 core CRUD commands with pagination support Implement board, card, column, user, and tag commands with full CRUD operations. Add --page and --all flags for pagination across all list commands. --- README.md | 2 +- lib/fizzy.rb | 5 + lib/fizzy/cli.rb | 15 ++ lib/fizzy/client.rb | 20 +++ lib/fizzy/commands/base.rb | 2 + lib/fizzy/commands/board.rb | 78 ++++++++++ lib/fizzy/commands/card.rb | 105 ++++++++++++++ lib/fizzy/commands/column.rb | 74 ++++++++++ lib/fizzy/commands/tag.rb | 22 +++ lib/fizzy/commands/user.rb | 50 +++++++ test/fizzy/commands/board_test.rb | 219 +++++++++++++++++++++++++++++ test/fizzy/commands/card_test.rb | 177 +++++++++++++++++++++++ test/fizzy/commands/column_test.rb | 140 ++++++++++++++++++ test/fizzy/commands/tag_test.rb | 67 +++++++++ test/fizzy/commands/user_test.rb | 99 +++++++++++++ 15 files changed, 1074 insertions(+), 1 deletion(-) create mode 100644 lib/fizzy/commands/board.rb create mode 100644 lib/fizzy/commands/card.rb create mode 100644 lib/fizzy/commands/column.rb create mode 100644 lib/fizzy/commands/tag.rb create mode 100644 lib/fizzy/commands/user.rb create mode 100644 test/fizzy/commands/board_test.rb create mode 100644 test/fizzy/commands/card_test.rb create mode 100644 test/fizzy/commands/column_test.rb create mode 100644 test/fizzy/commands/tag_test.rb create mode 100644 test/fizzy/commands/user_test.rb diff --git a/README.md b/README.md index 4e54bc7..cb5b7d2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Fizzy CLI -A command-line interface for the [Fizzy](https://app.fizzy.do) API. +A command-line interface for the [Fizzy](https://fizzy.do) API. Read API [docs](https://github.com/basecamp/fizzy/blob/main/docs/API.md). ## Installation diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 031fe4d..7a798e9 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -13,4 +13,9 @@ require_relative "fizzy/commands/base" require_relative "fizzy/commands/auth" require_relative "fizzy/commands/identity" +require_relative "fizzy/commands/board" +require_relative "fizzy/commands/card" +require_relative "fizzy/commands/column" +require_relative "fizzy/commands/user" +require_relative "fizzy/commands/tag" require_relative "fizzy/cli" diff --git a/lib/fizzy/cli.rb b/lib/fizzy/cli.rb index abcbe5d..1885314 100644 --- a/lib/fizzy/cli.rb +++ b/lib/fizzy/cli.rb @@ -16,5 +16,20 @@ def self.exit_on_failure? desc "identity SUBCOMMAND", "Manage identity" subcommand "identity", Commands::Identity + + desc "board SUBCOMMAND", "Manage boards" + subcommand "board", Commands::Board + + desc "card SUBCOMMAND", "Manage cards" + subcommand "card", Commands::Card + + desc "column SUBCOMMAND", "Manage columns" + subcommand "column", Commands::Column + + desc "user SUBCOMMAND", "Manage users" + subcommand "user", Commands::User + + desc "tag SUBCOMMAND", "Manage tags" + subcommand "tag", Commands::Tag end end diff --git a/lib/fizzy/client.rb b/lib/fizzy/client.rb index 535e9a1..dfa813d 100644 --- a/lib/fizzy/client.rb +++ b/lib/fizzy/client.rb @@ -41,6 +41,26 @@ def account_path(path) "/#{@account}#{path}" end + def get_all(path, params = {}) + all_data = [] + current_params = params.dup + + loop do + result = get(path, current_params) + data = result[:data] + all_data.concat(Array(data)) + + pagination = result[:pagination] + break unless pagination && pagination[:has_next] && pagination[:next_url] + + next_uri = URI.parse(pagination[:next_url]) + next_params = URI.decode_www_form(next_uri.query || "").to_h + current_params = next_params.transform_keys(&:to_sym) + end + + { data: all_data, pagination: nil } + end + private def build_uri(path, params = {}) diff --git a/lib/fizzy/commands/base.rb b/lib/fizzy/commands/base.rb index e802e08..f0e4fb2 100644 --- a/lib/fizzy/commands/base.rb +++ b/lib/fizzy/commands/base.rb @@ -44,6 +44,8 @@ def verbose? def output(result) response = if result.is_a?(Response) result + elsif result.nil? + Response.success(data: nil) else Response.success(data: result[:data], pagination: result[:pagination]) end diff --git a/lib/fizzy/commands/board.rb b/lib/fizzy/commands/board.rb new file mode 100644 index 0000000..c3d198a --- /dev/null +++ b/lib/fizzy/commands/board.rb @@ -0,0 +1,78 @@ +module Fizzy + module Commands + class Board < Base + desc "list", "List all boards" + 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("/boards"), params) + else + client.get(client.account_path("/boards"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "show ID", "Show a specific board" + def show(id) + result = client.get(client.account_path("/boards/#{id}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Create a new board" + option :name, required: true, type: :string, desc: "Board name" + option :all_access, type: :boolean, default: true, desc: "Allow all users to access" + option :auto_postpone_period, type: :numeric, desc: "Auto-postpone period in days" + def create + body = { + board: { + name: options[:name], + all_access: options[:all_access], + auto_postpone_period: options[:auto_postpone_period] + }.compact + } + + result = client.post(client.account_path("/boards"), body) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update ID", "Update a board" + option :name, type: :string, desc: "Board name" + option :all_access, type: :boolean, desc: "Allow all users to access" + option :user_ids, type: :string, desc: "Comma-separated user IDs for access" + option :auto_postpone_period, type: :numeric, desc: "Auto-postpone period in days" + def update(id) + board_params = {} + board_params[:name] = options[:name] if options.key?(:name) + board_params[:all_access] = options[:all_access] if options.key?(:all_access) + board_params[:auto_postpone_period] = options[:auto_postpone_period] if options.key?(:auto_postpone_period) + + if options[:user_ids] + board_params[:user_ids] = options[:user_ids].split(",").map(&:strip) + end + + result = client.put(client.account_path("/boards/#{id}"), { board: board_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete ID", "Delete a board" + def delete(id) + result = client.delete(client.account_path("/boards/#{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/card.rb b/lib/fizzy/commands/card.rb new file mode 100644 index 0000000..d3d00f0 --- /dev/null +++ b/lib/fizzy/commands/card.rb @@ -0,0 +1,105 @@ +module Fizzy + module Commands + class Card < Base + desc "list", "List cards" + option :board, type: :string, desc: "Filter by board ID" + option :column, type: :string, desc: "Filter by column ID" + option :tag, type: :string, desc: "Filter by tag ID" + option :assignee, type: :string, desc: "Filter by assignee ID" + option :status, type: :string, desc: "Filter by status (published, closed, not_now)" + option :page, type: :numeric, desc: "Page number" + option :all, type: :boolean, default: false, desc: "Fetch all pages" + def list + params = {} + params[:board_id] = options[:board] if options[:board] + params[:column_id] = options[:column] if options[:column] + params[:tag_id] = options[:tag] if options[:tag] + params[:assignee_id] = options[:assignee] if options[:assignee] + params[:status] = options[:status] if options[:status] + params[:page] = options[:page] if options[:page] + + result = if options[:all] + client.get_all(client.account_path("/cards"), params) + else + client.get(client.account_path("/cards"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "show NUMBER", "Show a specific card by number" + def show(number) + result = client.get(client.account_path("/cards/#{number}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Create a new card" + option :board, required: true, type: :string, desc: "Board ID" + option :title, required: true, type: :string, desc: "Card title" + option :description, type: :string, desc: "Card description (rich text/HTML)" + option :description_file, type: :string, desc: "Read description from file" + option :status, type: :string, desc: "Card status" + option :tag_ids, type: :string, desc: "Comma-separated tag IDs" + def create + card_params = { + title: options[:title] + } + + if options[:description_file] + card_params[:description] = File.read(options[:description_file]) + elsif options[:description] + card_params[:description] = options[:description] + end + + card_params[:status] = options[:status] if options[:status] + + if options[:tag_ids] + card_params[:tag_ids] = options[:tag_ids].split(",").map(&:strip) + end + + result = client.post(client.account_path("/boards/#{options[:board]}/cards"), { card: card_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update NUMBER", "Update a card" + option :title, type: :string, desc: "Card title" + option :description, type: :string, desc: "Card description (rich text/HTML)" + option :description_file, type: :string, desc: "Read description from file" + option :status, type: :string, desc: "Card status" + option :tag_ids, type: :string, desc: "Comma-separated tag IDs" + def update(number) + card_params = {} + card_params[:title] = options[:title] if options.key?(:title) + card_params[:status] = options[:status] if options.key?(:status) + + if options[:description_file] + card_params[:description] = File.read(options[:description_file]) + elsif options.key?(:description) + card_params[:description] = options[:description] + end + + if options[:tag_ids] + card_params[:tag_ids] = options[:tag_ids].split(",").map(&:strip) + end + + result = client.put(client.account_path("/cards/#{number}"), { card: card_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete NUMBER", "Delete a card" + def delete(number) + result = client.delete(client.account_path("/cards/#{number}")) + output(result || Response.success(data: { deleted: true })) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/lib/fizzy/commands/column.rb b/lib/fizzy/commands/column.rb new file mode 100644 index 0000000..d477bfa --- /dev/null +++ b/lib/fizzy/commands/column.rb @@ -0,0 +1,74 @@ +module Fizzy + module Commands + class Column < Base + desc "list", "List columns for a board" + option :board, required: true, type: :string, desc: "Board ID" + 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("/boards/#{options[:board]}/columns"), params) + else + client.get(client.account_path("/boards/#{options[:board]}/columns"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "show ID", "Show a specific column" + option :board, required: true, type: :string, desc: "Board ID" + def show(id) + result = client.get(client.account_path("/boards/#{options[:board]}/columns/#{id}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "create", "Create a new column" + option :board, required: true, type: :string, desc: "Board ID" + option :name, required: true, type: :string, desc: "Column name" + option :color, type: :string, desc: "CSS color variable name" + def create + body = { + column: { + name: options[:name], + color: options[:color] + }.compact + } + + result = client.post(client.account_path("/boards/#{options[:board]}/columns"), body) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update ID", "Update a column" + option :board, required: true, type: :string, desc: "Board ID" + option :name, type: :string, desc: "Column name" + option :color, type: :string, desc: "CSS color variable name" + def update(id) + column_params = {} + column_params[:name] = options[:name] if options.key?(:name) + column_params[:color] = options[:color] if options.key?(:color) + + result = client.put(client.account_path("/boards/#{options[:board]}/columns/#{id}"), { column: column_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "delete ID", "Delete a column" + option :board, required: true, type: :string, desc: "Board ID" + def delete(id) + result = client.delete(client.account_path("/boards/#{options[:board]}/columns/#{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/tag.rb b/lib/fizzy/commands/tag.rb new file mode 100644 index 0000000..a31e411 --- /dev/null +++ b/lib/fizzy/commands/tag.rb @@ -0,0 +1,22 @@ +module Fizzy + module Commands + class Tag < Base + desc "list", "List all tags" + 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("/tags"), params) + else + client.get(client.account_path("/tags"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/lib/fizzy/commands/user.rb b/lib/fizzy/commands/user.rb new file mode 100644 index 0000000..cd5f0ba --- /dev/null +++ b/lib/fizzy/commands/user.rb @@ -0,0 +1,50 @@ +module Fizzy + module Commands + class User < Base + desc "list", "List all users" + 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("/users"), params) + else + client.get(client.account_path("/users"), params) + end + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "show ID", "Show a specific user" + def show(id) + result = client.get(client.account_path("/users/#{id}")) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "update ID", "Update a user" + option :name, type: :string, desc: "User name" + def update(id) + user_params = {} + user_params[:name] = options[:name] if options.key?(:name) + + result = client.put(client.account_path("/users/#{id}"), { user: user_params }) + output(result) + rescue Fizzy::Error => e + output_error(e) + end + + desc "deactivate ID", "Deactivate a user" + def deactivate(id) + result = client.delete(client.account_path("/users/#{id}")) + output(result || Response.success(data: { deactivated: true })) + rescue Fizzy::Error => e + output_error(e) + end + end + end +end diff --git a/test/fizzy/commands/board_test.rb b/test/fizzy/commands/board_test.rb new file mode 100644 index 0000000..76c3794 --- /dev/null +++ b/test/fizzy/commands/board_test.rb @@ -0,0 +1,219 @@ +require "test_helper" + +class Fizzy::Commands::BoardTest < 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_boards + stub_request(:get, "https://app.fizzy.do/test_account/boards") + .with(headers: { "Authorization" => "Bearer test_token" }) + .to_return( + status: 200, + body: '[{"id": "1", "name": "Board 1"}, {"id": "2", "name": "Board 2"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new.invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 2, result["data"].length + assert_equal "Board 1", result["data"][0]["name"] + end + + def test_list_with_page + stub_request(:get, "https://app.fizzy.do/test_account/boards") + .with(query: { "page" => "2" }) + .to_return( + status: 200, + body: '[{"id": "3", "name": "Board 3"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { page: 2 }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Board 3", result["data"][0]["name"] + end + + def test_list_with_all_pages + stub_request(:get, "https://app.fizzy.do/test_account/boards") + .to_return( + status: 200, + body: '[{"id": "1", "name": "Board 1"}]', + headers: { + "Content-Type" => "application/json", + "Link" => '; rel="next"' + } + ) + + stub_request(:get, "https://app.fizzy.do/test_account/boards") + .with(query: { "page" => "2" }) + .to_return( + status: 200, + body: '[{"id": "2", "name": "Board 2"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { all: true }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 2, result["data"].length + end + + def test_show_returns_board + stub_request(:get, "https://app.fizzy.do/test_account/boards/123") + .to_return( + status: 200, + body: '{"id": "123", "name": "My Board"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new.invoke(:show, ["123"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "123", result["data"]["id"] + assert_equal "My Board", result["data"]["name"] + end + + def test_show_not_found + stub_request(:get, "https://app.fizzy.do/test_account/boards/999") + .to_return(status: 404, body: '{"error": "Not found"}') + + assert_raises(SystemExit) do + capture_output do + Fizzy::Commands::Board.new.invoke(:show, ["999"]) + end + end + end + + def test_create_board + stub_request(:post, "https://app.fizzy.do/test_account/boards") + .with( + body: { board: { name: "New Board", all_access: true } }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 201, + body: '{"id": "456", "name": "New Board", "all_access": true}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { name: "New Board", all_access: true }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "456", result["data"]["id"] + assert_equal "New Board", result["data"]["name"] + end + + def test_create_with_auto_postpone + stub_request(:post, "https://app.fizzy.do/test_account/boards") + .with( + body: { board: { name: "Board", all_access: true, auto_postpone_period: 7 } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "789", "name": "Board", "auto_postpone_period": 7}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { name: "Board", all_access: true, auto_postpone_period: 7 }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 7, result["data"]["auto_postpone_period"] + end + + def test_update_board + stub_request(:put, "https://app.fizzy.do/test_account/boards/123") + .with( + body: { board: { name: "Updated Board" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "123", "name": "Updated Board"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { name: "Updated Board" }).invoke(:update, ["123"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Updated Board", result["data"]["name"] + end + + def test_update_with_user_ids + stub_request(:put, "https://app.fizzy.do/test_account/boards/123") + .with( + body: { board: { user_ids: ["1", "2", "3"] } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "123", "user_ids": ["1", "2", "3"]}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Board.new([], { user_ids: "1, 2, 3" }).invoke(:update, ["123"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal %w[1 2 3], result["data"]["user_ids"] + end + + def test_delete_board + stub_request(:delete, "https://app.fizzy.do/test_account/boards/123") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Board.new.invoke(:delete, ["123"]) + 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/card_test.rb b/test/fizzy/commands/card_test.rb new file mode 100644 index 0000000..1fd1e35 --- /dev/null +++ b/test/fizzy/commands/card_test.rb @@ -0,0 +1,177 @@ +require "test_helper" + +class Fizzy::Commands::CardTest < 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_cards + stub_request(:get, "https://app.fizzy.do/test_account/cards") + .to_return( + status: 200, + body: '[{"id": "1", "number": 1, "title": "Card 1"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 1, result["data"].length + assert_equal "Card 1", result["data"][0]["title"] + end + + def test_list_with_filters + stub_request(:get, "https://app.fizzy.do/test_account/cards") + .with(query: { "board_id" => "10", "status" => "published" }) + .to_return( + status: 200, + body: '[{"id": "1", "title": "Filtered Card"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { board: "10", status: "published" }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Filtered Card", result["data"][0]["title"] + end + + def test_show_returns_card + stub_request(:get, "https://app.fizzy.do/test_account/cards/42") + .to_return( + status: 200, + body: '{"id": "100", "number": 42, "title": "My Card", "description": "Details"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:show, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 42, result["data"]["number"] + assert_equal "My Card", result["data"]["title"] + end + + def test_create_card + stub_request(:post, "https://app.fizzy.do/test_account/boards/5/cards") + .with( + body: { card: { title: "New Card" } }.to_json, + headers: { "Content-Type" => "application/json" } + ) + .to_return( + status: 201, + body: '{"id": "200", "number": 50, "title": "New Card"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { board: "5", title: "New Card" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "New Card", result["data"]["title"] + end + + def test_create_with_description + stub_request(:post, "https://app.fizzy.do/test_account/boards/5/cards") + .with( + body: { card: { title: "Card", description: "

Rich text

" } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "201", "title": "Card", "description": "

Rich text

"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { board: "5", title: "Card", description: "

Rich text

" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "

Rich text

", result["data"]["description"] + end + + def test_create_with_tags + stub_request(:post, "https://app.fizzy.do/test_account/boards/5/cards") + .with( + body: { card: { title: "Card", tag_ids: ["1", "2"] } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "202", "title": "Card"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { board: "5", title: "Card", tag_ids: "1, 2" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + end + + def test_update_card + stub_request(:put, "https://app.fizzy.do/test_account/cards/42") + .with( + body: { card: { title: "Updated Title" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "100", "number": 42, "title": "Updated Title"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Card.new([], { title: "Updated Title" }).invoke(:update, ["42"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Updated Title", result["data"]["title"] + end + + def test_delete_card + stub_request(:delete, "https://app.fizzy.do/test_account/cards/42") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Card.new.invoke(:delete, ["42"]) + 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/column_test.rb b/test/fizzy/commands/column_test.rb new file mode 100644 index 0000000..2692c0d --- /dev/null +++ b/test/fizzy/commands/column_test.rb @@ -0,0 +1,140 @@ +require "test_helper" + +class Fizzy::Commands::ColumnTest < 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_columns + stub_request(:get, "https://app.fizzy.do/test_account/boards/10/columns") + .to_return( + status: 200, + body: '[{"id": "1", "name": "To Do"}, {"id": "2", "name": "Done"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10" }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 2, result["data"].length + assert_equal "To Do", result["data"][0]["name"] + end + + def test_show_returns_column + stub_request(:get, "https://app.fizzy.do/test_account/boards/10/columns/1") + .to_return( + status: 200, + body: '{"id": "1", "name": "To Do", "color": "blue"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10" }).invoke(:show, ["1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "To Do", result["data"]["name"] + assert_equal "blue", result["data"]["color"] + end + + def test_create_column + stub_request(:post, "https://app.fizzy.do/test_account/boards/10/columns") + .with( + body: { column: { name: "In Progress" } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "3", "name": "In Progress"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10", name: "In Progress" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "In Progress", result["data"]["name"] + end + + def test_create_with_color + stub_request(:post, "https://app.fizzy.do/test_account/boards/10/columns") + .with( + body: { column: { name: "Review", color: "var(--purple)" } }.to_json + ) + .to_return( + status: 201, + body: '{"id": "4", "name": "Review", "color": "var(--purple)"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10", name: "Review", color: "var(--purple)" }).invoke(:create, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "var(--purple)", result["data"]["color"] + end + + def test_update_column + stub_request(:put, "https://app.fizzy.do/test_account/boards/10/columns/1") + .with( + body: { column: { name: "Done", color: "green" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "1", "name": "Done", "color": "green"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10", name: "Done", color: "green" }).invoke(:update, ["1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Done", result["data"]["name"] + assert_equal "green", result["data"]["color"] + end + + def test_delete_column + stub_request(:delete, "https://app.fizzy.do/test_account/boards/10/columns/1") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::Column.new([], { board: "10" }).invoke(:delete, ["1"]) + 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/tag_test.rb b/test/fizzy/commands/tag_test.rb new file mode 100644 index 0000000..bea703e --- /dev/null +++ b/test/fizzy/commands/tag_test.rb @@ -0,0 +1,67 @@ +require "test_helper" + +class Fizzy::Commands::TagTest < 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_tags + stub_request(:get, "https://app.fizzy.do/test_account/tags") + .to_return( + status: 200, + body: '[{"id": "1", "title": "bug"}, {"id": "2", "title": "feature"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Tag.new.invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 2, result["data"].length + assert_equal "bug", result["data"][0]["title"] + assert_equal "feature", result["data"][1]["title"] + end + + def test_list_with_pagination + stub_request(:get, "https://app.fizzy.do/test_account/tags") + .with(query: { "page" => "2" }) + .to_return( + status: 200, + body: '[{"id": "3", "title": "enhancement"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::Tag.new([], { page: 2 }).invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "enhancement", result["data"][0]["title"] + 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/user_test.rb b/test/fizzy/commands/user_test.rb new file mode 100644 index 0000000..4a02733 --- /dev/null +++ b/test/fizzy/commands/user_test.rb @@ -0,0 +1,99 @@ +require "test_helper" + +class Fizzy::Commands::UserTest < 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_users + stub_request(:get, "https://app.fizzy.do/test_account/users") + .to_return( + status: 200, + body: '[{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::User.new.invoke(:list, []) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal 2, result["data"].length + assert_equal "Alice", result["data"][0]["name"] + end + + def test_show_returns_user + stub_request(:get, "https://app.fizzy.do/test_account/users/1") + .to_return( + status: 200, + body: '{"id": "1", "name": "Alice", "email": "alice@example.com"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::User.new.invoke(:show, ["1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Alice", result["data"]["name"] + assert_equal "alice@example.com", result["data"]["email"] + end + + def test_update_user + stub_request(:put, "https://app.fizzy.do/test_account/users/1") + .with( + body: { user: { name: "Alice Smith" } }.to_json + ) + .to_return( + status: 200, + body: '{"id": "1", "name": "Alice Smith"}', + headers: { "Content-Type" => "application/json" } + ) + + output = capture_output do + Fizzy::Commands::User.new([], { name: "Alice Smith" }).invoke(:update, ["1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert_equal "Alice Smith", result["data"]["name"] + end + + def test_deactivate_user + stub_request(:delete, "https://app.fizzy.do/test_account/users/1") + .to_return(status: 204, body: "") + + output = capture_output do + Fizzy::Commands::User.new.invoke(:deactivate, ["1"]) + end + + result = JSON.parse(output) + assert result["success"] + assert result["data"]["deactivated"] + end + + private + + def capture_output + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end