diff --git a/Gemfile b/Gemfile index b84d708a..627d8666 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,16 @@ source 'https://rubygems.org' # Specify your gem's dependencies in checkoff.gemspec gemspec + +# ruby-asana gem is pending key bugfixes for checkoff as of +# 2021-07-29: +# +# See +# https://github.com/Asana/ruby-asana/issues/109 +# https://github.com/Asana/ruby-asana/issues/110 +# +gem 'asana', + git: 'https://github.com/apiology/ruby-asana', + branch: 'checkoff_fixes' + +# gem 'asana', path: '/Users/broz/src/ruby-asana' diff --git a/Gemfile.lock b/Gemfile.lock index e6087d12..7068e242 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,14 @@ +GIT + remote: https://github.com/apiology/ruby-asana + revision: 83732f040728a4ef1cfc9ed61f65878dbee6aa23 + branch: checkoff_fixes + specs: + asana (0.10.4) + faraday (~> 1.0) + faraday_middleware (~> 1.0) + faraday_middleware-multi_json (~> 0.0) + oauth2 (~> 1.4) + PATH remote: . specs: @@ -17,11 +28,6 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - asana (0.10.3) - faraday (~> 1.0) - faraday_middleware (~> 1.0) - faraday_middleware-multi_json (~> 0.0) - oauth2 (~> 1.4) ast (2.4.2) bump (0.10.0) cache (0.4.1) @@ -125,6 +131,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + asana! bump bundler checkoff! diff --git a/coverage/.last_run.json b/coverage/.last_run.json index a68a4b2f..94c56322 100644 --- a/coverage/.last_run.json +++ b/coverage/.last_run.json @@ -1,6 +1,6 @@ { "result": { - "line": 97.56, - "branch": 85.71 + "line": 98.57, + "branch": 91.66 } } diff --git a/lib/checkoff/cli.rb b/lib/checkoff/cli.rb index e6f06883..dbc296f5 100755 --- a/lib/checkoff/cli.rb +++ b/lib/checkoff/cli.rb @@ -12,6 +12,112 @@ require_relative 'sections' module Checkoff + # Move tasks from one place to another + class MvSubcommand + def validate_and_assign_from_location(from_workspace_arg, from_project_arg, from_section_arg) + if from_workspace_arg == :default_workspace + # Figure out what to do here - we accept a default + # workspace gid and default workspace_gid arguments elsewhere. + # however, there are undefaulted workspace_name arguments as + # well... + raise NotImplementedError, 'Not implemented: Teach me how to look up default workspace name' + end + + @from_workspace_name = from_workspace_arg + @from_project_name = project_arg_to_name(from_project_arg) + @from_section_name = from_section_arg + end + + def create_to_project_name(to_project_arg) + if to_project_arg == :source_project + from_project_name + else + project_arg_to_name(to_project_arg) + end + end + + def create_to_section_name(to_section_arg) + if to_section_arg == :source_section + from_section_name + else + to_section_arg + end + end + + def validate_and_assign_to_location(to_workspace_arg, to_project_arg, to_section_arg) + @to_workspace_name = to_workspace_arg + @to_workspace_name = from_workspace_name if to_workspace_arg == :source_workspace + @to_project_name = create_to_project_name(to_project_arg) + @to_section_name = create_to_section_name(to_section_arg) + + return unless from_workspace_name != to_workspace_name + + raise NotImplementedError, 'Not implemented: Teach me how to move tasks between workspaces' + end + + def initialize(from_workspace_arg:, + from_project_arg:, + from_section_arg:, + to_workspace_arg:, + to_project_arg:, + to_section_arg:, + config: Checkoff::ConfigLoader.load(:asana), + projects: Checkoff::Projects.new(config: config), + sections: Checkoff::Sections.new(config: config), + logger: $stderr) + validate_and_assign_from_location(from_workspace_arg, from_project_arg, from_section_arg) + validate_and_assign_to_location(to_workspace_arg, to_project_arg, to_section_arg) + + @projects = projects + @sections = sections + @logger = logger + end + + def move_tasks(tasks, to_project, to_section) + tasks.each do |task| + # a. check if already in correct project and section (TODO) + # b. if not, put it there + @logger.puts "Moving #{task.name} to #{to_section.name}..." + task.add_project(project: to_project.gid, section: to_section.gid) + end + end + + def fetch_tasks(from_workspace_name, from_project_name, from_section_name) + if from_section_name == :all_sections + raise NotImplementedError, 'Not implemented: Teach me how to move all sections of a project' + end + + sections.tasks(from_workspace_name, from_project_name, from_section_name) + end + + def run + # 0. Look up project and section gids + to_project = projects.project_or_raise(to_workspace_name, to_project_name) + to_section = sections.section_or_raise(to_workspace_name, to_project_name, to_section_name) + + # 1. Get list of tasks which match + tasks = fetch_tasks(from_workspace_name, from_project_name, from_section_name) + # 2. for each task, + move_tasks(tasks, to_project, to_section) + # 3. tell the user we're done' + @logger.puts 'Done moving tasks' + end + + private + + attr_reader :from_workspace_name, :from_project_name, :from_section_name, + :to_workspace_name, :to_project_name, :to_section_name, + :projects, :sections + + def project_arg_to_name(project_arg) + if project_arg.start_with? ':' + project_arg[1..].to_sym + else + project_arg + end + end + end + # CLI subcommand that shows tasks in JSON form class ViewSubcommand def initialize(workspace_name, project_name, section_name, @@ -151,5 +257,50 @@ class CheckoffGLIApp puts ViewSubcommand.new(workspace_name, project_name, section_name, task_name).run end end + + desc 'Move tasks from one section to another within a project' + + # rubocop:disable Metrics/BlockLength + command :mv do |c| + c.flag :from_workspace, + type: String, + default_value: :default_workspace, + desc: 'Workspace to move tasks from' + c.flag :from_project, + type: String, + required: true, + desc: 'Project to move tasks from' + c.flag :from_section, + type: String, + default_value: :all_sections, + desc: 'Section to move tasks from' + c.flag :to_workspace, + type: String, + default_value: :source_workspace, + desc: 'Workspace to move tasks to' + c.flag :to_project, + type: String, + default_value: :source_project, + desc: 'Section to move tasks to' + c.flag :to_section, + type: String, + default_value: :source_section, + desc: 'Section to move tasks to' + c.action do |_global_options, options, _args| + from_workspace = options.fetch('from_workspace') + from_project = options.fetch('from_project') + from_section = options.fetch('from_section') + to_workspace = options.fetch('to_workspace') + to_project = options.fetch('to_project') + to_section = options.fetch('to_section') + MvSubcommand.new(from_workspace_arg: from_workspace, + from_project_arg: from_project, + from_section_arg: from_section, + to_workspace_arg: to_workspace, + to_project_arg: to_project, + to_section_arg: to_section).run + end + end + # rubocop:enable Metrics/BlockLength end end diff --git a/lib/checkoff/projects.rb b/lib/checkoff/projects.rb index bf366569..47800e90 100644 --- a/lib/checkoff/projects.rb +++ b/lib/checkoff/projects.rb @@ -59,6 +59,14 @@ def project(workspace_name, project_name) end cache_method :project, LONG_CACHE_TIME + def project_or_raise(workspace_name, project_name) + project = project(workspace_name, project_name) + raise "Could not find project #{project_name} under workspace #{workspace_name}." if project.nil? + + project + end + cache_method :project_or_raise, LONG_CACHE_TIME + # find uncompleted tasks in a list def active_tasks(tasks) tasks.select { |task| task.completed_at.nil? } @@ -95,5 +103,6 @@ def my_tasks(workspace_name) gid = result.gid projects.find_by_id(gid) end + cache_method :my_tasks, LONG_CACHE_TIME end end diff --git a/lib/checkoff/sections.rb b/lib/checkoff/sections.rb index 49820378..da48ffd5 100644 --- a/lib/checkoff/sections.rb +++ b/lib/checkoff/sections.rb @@ -44,13 +44,10 @@ def tasks(workspace_name, project_name, section_name, extra_fields: []) section = section_or_raise(workspace_name, project_name, section_name) options = projects.task_options - # asana-0.10.3 gem doesn't support per_page - not sure if API - # itself does - options.delete(:per_page) options[:options][:fields] += extra_fields options[:completed_since] = '9999-12-01' if only_uncompleted - client.tasks.get_tasks_for_section(section_gid: section.gid, - **options).to_a + client.tasks.get_tasks(section: section.gid, + **options) end cache_method :tasks, SHORT_CACHE_TIME @@ -61,6 +58,18 @@ def section_task_names(workspace_name, project_name, section_name) end cache_method :section_task_names, SHORT_CACHE_TIME + def section_or_raise(workspace_name, project_name, section_name) + section = section(workspace_name, project_name, section_name) + if section.nil? + valid_sections = sections_or_raise(workspace_name, project_name).map(&:name) + + raise "Could not find section #{section_name} under project #{project_name} " \ + "under workspace #{workspace_name}. Valid sections: #{valid_sections}" + end + section + end + cache_method :section_or_raise, LONG_CACHE_TIME + private # Given a project object, pull all tasks, then provide a Hash of @@ -106,16 +115,5 @@ def section(workspace_name, project_name, section_name) sections = sections_or_raise(workspace_name, project_name) sections.find { |section| section.name.chomp(':') == section_name.chomp(':') } end - - def section_or_raise(workspace_name, project_name, section_name) - section = section(workspace_name, project_name, section_name) - if section.nil? - valid_sections = sections_or_raise(workspace_name, project_name).map(&:name) - - raise "Could not find section #{section_name} under project #{project_name} " \ - "under workspace #{workspace_name}. Valid sections: #{valid_sections}" - end - section - end end end diff --git a/test/unit/test_cli_help.rb b/test/unit/test_cli_help.rb new file mode 100644 index 00000000..52508345 --- /dev/null +++ b/test/unit/test_cli_help.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require_relative 'class_test' +require 'checkoff/cli' + +# Test the Checkoff::CLI class with the help option +class TestCLIHelp < Minitest::Test + let_mock :config, :workspaces, :sections, :tasks, + :workspace, :workspace_gid, :task_a, :task_b, :task_c + + def expected_json_no_section_specified + '{"":[{"name":"task_a","due":"fake_date"}],' \ + '"section_name:":[{"name":"task_b","due":"fake_date"},' \ + '{"name":"task_c","due":"fake_date"}]}' + end + + def section_name_str + 'section_name:' + end + + def project_name + 'my_project' + end + + def task_name + 'my_task' + end + + def expect_tasks_by_section_pulled + @mocks[:sections] + .expects(:tasks_by_section) + .with(workspace_name, project_name) + .returns(nil => [task_a], section_name_str => [task_b, task_c]) + end + + def mock_run_with_no_section_specified_normal_project(due_on:, due_at:) + expect_tasks_by_section_pulled + expect_three_tasks_queried(due_on: due_on, due_at: due_at) + end + + def expect_task_named(task, task_name) + task.expects(:name).returns(task_name).at_least(0) + end + + def expect_task_due_on(task, due_on) + task.expects(:due_on).returns(due_on).at_least(0) + end + + def expect_task_due_at(task, due_at) + task.expects(:due_at).returns(due_at).at_least(0) + end + + def three_tasks + { task_a => 'task_a', task_b => 'task_b', task_c => 'task_c' } + end + + def expect_task_queried(task, task_name, due_on, due_at) + expect_task_named(task, task_name) + expect_task_due_on(task, due_on) + expect_task_due_at(task, due_at) + end + + def expect_three_tasks_queried(due_on:, due_at:) + three_tasks.each do |task, task_name| + expect_task_queried(task, task_name, due_on, due_at) + end + end + + def workspace_name + 'my workspace' + end + + def expect_workspaces_created + Checkoff::Workspaces.expects(:new).returns(workspaces).at_least(0) + end + + def expect_config_loaded + Checkoff::ConfigLoader.expects(:load).returns(config).at_least(0) + end + + def expect_sections_created + Checkoff::Sections.expects(:new).returns(sections).at_least(0) + end + + def expect_tasks_created + Checkoff::Tasks.expects(:new).returns(tasks).at_least(0) + end + + def set_mocks + @mocks = { + config: config, + workspaces: workspaces, + sections: sections, + tasks: tasks, + stderr: $stderr, + stdout: $stdout, + } + end + + def get_test_object(&twiddle_mocks) + set_mocks + expect_workspaces_created + expect_config_loaded + expect_sections_created + expect_tasks_created + + yield @mocks if twiddle_mocks + Checkoff::CheckoffGLIApp + end + + def test_run_with_help_arg + cli = get_test_object do + @mocks[:stdout].expects(:puts).at_least(1) + end + assert_equal(0, cli.run(['--help'])) + end +end diff --git a/test/unit/test_cli_mv.rb b/test/unit/test_cli_mv.rb new file mode 100644 index 00000000..cf478f9e --- /dev/null +++ b/test/unit/test_cli_mv.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'checkoff/cli' +require_relative 'test_helper' + +# Test the Checkoff::CLI class with mv subcommand +class TestCLIMv < Minitest::Test + let_mock :config, :workspaces, :sections, :tasks + + def expect_workspaces_created + Checkoff::Workspaces.expects(:new).returns(workspaces).at_least(0) + end + + def expect_config_loaded + Checkoff::ConfigLoader.expects(:load).returns(config).at_least(0) + end + + def expect_sections_created + Checkoff::Sections.expects(:new).returns(sections).at_least(0) + end + + def expect_tasks_created + Checkoff::Tasks.expects(:new).returns(tasks).at_least(0) + end + + def set_mocks + @mocks = { + config: config, + workspaces: workspaces, + sections: sections, + tasks: tasks, + stderr: $stderr, + stdout: $stdout, + } + end + + def get_test_object(&_twiddle_mocks) + set_mocks + expect_workspaces_created + expect_config_loaded + expect_sections_created + expect_tasks_created + + yield @mocks + Checkoff::CheckoffGLIApp + end + + let_mock :mv_subcommand + + def mock_mv_original_use_case + Checkoff::MvSubcommand.expects(:new).with(from_workspace_arg: 'from_workspace_name', + from_project_arg: ':my_tasks', + from_section_arg: 'Recently assigned', + to_workspace_arg: :source_workspace, + to_project_arg: :source_project, + to_section_arg: 'Later').returns(mv_subcommand) + mv_subcommand.expects(:run) + end + + def test_mv_original_use_case + cli = get_test_object do + mock_mv_original_use_case + end + cli.run(['mv', + '--from-workspace=from_workspace_name', + '--from-project=:my_tasks', + '--from-section=Recently assigned', + '--to_section=Later']) + end +end diff --git a/test/unit/test_cli_quickadd.rb b/test/unit/test_cli_quickadd.rb new file mode 100644 index 00000000..93a28f15 --- /dev/null +++ b/test/unit/test_cli_quickadd.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'checkoff/cli' +require_relative 'test_helper' + +# Test the Checkoff::CLI class with quickadd subcommand +class TestCLIQuickadd < Minitest::Test + let_mock :config, :workspaces, :sections, :tasks, + :workspace, :workspace_gid, :task_a, :task_b, :task_c + + def workspace_name + 'my workspace' + end + + def expect_workspaces_created + Checkoff::Workspaces.expects(:new).returns(workspaces).at_least(0) + end + + def expect_config_loaded + Checkoff::ConfigLoader.expects(:load).returns(config).at_least(0) + end + + def expect_sections_created + Checkoff::Sections.expects(:new).returns(sections).at_least(0) + end + + def expect_tasks_created + Checkoff::Tasks.expects(:new).returns(tasks).at_least(0) + end + + def set_mocks + @mocks = { + config: config, + workspaces: workspaces, + sections: sections, + tasks: tasks, + stderr: $stderr, + stdout: $stdout, + } + end + + def get_test_object(&_twiddle_mocks) + set_mocks + expect_workspaces_created + expect_config_loaded + expect_sections_created + expect_tasks_created + + yield @mocks + Checkoff::CheckoffGLIApp + end + + def mock_quickadd + @mocks[:workspaces].expects(:workspace_by_name).with(workspace_name).returns(workspace) + + workspace.expects(:gid).returns(workspace_gid) + @mocks[:tasks].expects(:add_task).with('my task name', + workspace_gid: workspace_gid) + end + + def test_quickadd + cli = get_test_object do + mock_quickadd + end + cli.run(['quickadd', workspace_name, 'my task name']) + end +end diff --git a/test/unit/test_cli.rb b/test/unit/test_cli_view.rb similarity index 82% rename from test/unit/test_cli.rb rename to test/unit/test_cli_view.rb index b5ea388f..8254ba3c 100644 --- a/test/unit/test_cli.rb +++ b/test/unit/test_cli_view.rb @@ -1,15 +1,41 @@ # frozen_string_literal: true -require_relative 'class_test' require 'checkoff/cli' +require_relative 'test_helper' -# Test the Checkoff::CLI class -class TestCLI < Minitest::Test - let_mock :workspace, :workspace_gid, :task_a, :task_b, :task_c, - :config, :workspaces, :sections, :tasks +# Test the Checkoff::CLI class with view subcommand +class TestCLIView < Minitest::Test + let_mock :config, :workspaces, :sections, :tasks, + :workspace, :workspace_gid, :task_a, :task_b, :task_c - def workspace_name - 'my workspace' + def expected_json_no_section_specified + '{"":[{"name":"task_a","due":"fake_date"}],' \ + '"section_name:":[{"name":"task_b","due":"fake_date"},' \ + '{"name":"task_c","due":"fake_date"}]}' + end + + def section_name_str + 'section_name:' + end + + def project_name + 'my_project' + end + + def task_name + 'my_task' + end + + def expect_tasks_by_section_pulled + @mocks[:sections] + .expects(:tasks_by_section) + .with(workspace_name, project_name) + .returns(nil => [task_a], section_name_str => [task_b, task_c]) + end + + def mock_run_with_no_section_specified_normal_project(due_on:, due_at:) + expect_tasks_by_section_pulled + expect_three_tasks_queried(due_on: due_on, due_at: due_at) end def expect_task_named(task, task_name) @@ -40,16 +66,57 @@ def expect_three_tasks_queried(due_on:, due_at:) end end - def section_name_str - 'section_name:' + def workspace_name + 'my workspace' end - def project_name - 'my_project' + def expect_workspaces_created + Checkoff::Workspaces.expects(:new).returns(workspaces).at_least(0) end - def task_name - 'my_task' + def expect_config_loaded + Checkoff::ConfigLoader.expects(:load).returns(config).at_least(0) + end + + def expect_sections_created + Checkoff::Sections.expects(:new).returns(sections).at_least(0) + end + + def expect_tasks_created + Checkoff::Tasks.expects(:new).returns(tasks).at_least(0) + end + + def set_mocks + @mocks = { + config: config, + workspaces: workspaces, + sections: sections, + tasks: tasks, + stderr: $stderr, + stdout: $stdout, + } + end + + def get_test_object(&_twiddle_mocks) + set_mocks + expect_workspaces_created + expect_config_loaded + expect_sections_created + expect_tasks_created + + yield @mocks + Checkoff::CheckoffGLIApp + end + + def test_run_with_no_section_specified_normal_project + cli = get_test_object do + mock_run_with_no_section_specified_normal_project(due_on: 'fake_date', due_at: nil) + @mocks[:stdout].expects(:puts).with(expected_json_no_section_specified) + end + assert_equal(0, + cli.run(['view', + workspace_name, + project_name])) end def expect_three_tasks_pulled_and_queried(project_name:, @@ -164,34 +231,16 @@ def test_view_run_with_section_specified_normal_project section_name_str])) end - def expect_tasks_by_section_pulled - @mocks[:sections] - .expects(:tasks_by_section) - .with(workspace_name, project_name) - .returns(nil => [task_a], section_name_str => [task_b, task_c]) - end - - def mock_run_with_no_section_specified_normal_project(due_on:, due_at:) - expect_tasks_by_section_pulled - expect_three_tasks_queried(due_on: due_on, due_at: due_at) - end - - def expected_json_no_section_specified - '{"":[{"name":"task_a","due":"fake_date"}],' \ - '"section_name:":[{"name":"task_b","due":"fake_date"},' \ - '{"name":"task_c","due":"fake_date"}]}' - end - def mock_run_with_no_project_specified @mocks[:stderr].expects(:puts).at_least(1) end def test_run_with_no_project_specified - asana_my_tasks = get_test_object do + cli = get_test_object do mock_run_with_no_project_specified @mocks[:stdout].expects(:puts) end - assert_equal(64, asana_my_tasks.run(['view', workspace_name])) + assert_equal(64, cli.run(['view', workspace_name])) end def expected_json_view_not_due @@ -199,14 +248,14 @@ def expected_json_view_not_due end def test_view_not_due - asana_my_tasks = get_test_object do + cli = get_test_object do mock_run_with_no_section_specified_normal_project(due_on: nil, due_at: nil) @mocks[:stdout].expects(:puts).with(expected_json_view_not_due) end assert_equal(0, - asana_my_tasks.run(['view', - workspace_name, - project_name])) + cli.run(['view', + workspace_name, + project_name])) end def expected_json_view_due_at @@ -216,84 +265,13 @@ def expected_json_view_due_at end def test_view_due_at - asana_my_tasks = get_test_object do + cli = get_test_object do mock_run_with_no_section_specified_normal_project(due_on: nil, due_at: 'fake time') @mocks[:stdout].expects(:puts).with(expected_json_view_due_at) end assert_equal(0, - asana_my_tasks.run(['view', - workspace_name, - project_name])) - end - - def test_run_with_no_section_specified_normal_project - asana_my_tasks = get_test_object do - mock_run_with_no_section_specified_normal_project(due_on: 'fake_date', due_at: nil) - @mocks[:stdout].expects(:puts).with(expected_json_no_section_specified) - end - assert_equal(0, - asana_my_tasks.run(['view', - workspace_name, - project_name])) - end - - def test_run_with_help_arg - asana_my_tasks = get_test_object do - @mocks[:stdout].expects(:puts).at_least(1) - end - assert_equal(0, asana_my_tasks.run(['--help'])) - end - - def mock_quickadd - @mocks[:workspaces].expects(:workspace_by_name).with(workspace_name).returns(workspace) - - workspace.expects(:gid).returns(workspace_gid) - @mocks[:tasks].expects(:add_task).with('my task name', - workspace_gid: workspace_gid) - end - - def test_quickadd - asana_my_tasks = get_test_object do - mock_quickadd - end - asana_my_tasks.run(['quickadd', workspace_name, 'my task name']) - end - - def set_mocks - @mocks = { - config: config, - workspaces: workspaces, - sections: sections, - tasks: tasks, - stderr: $stderr, - stdout: $stdout, - } - end - - def expect_workspaces_created - Checkoff::Workspaces.expects(:new).returns(workspaces).at_least(0) - end - - def expect_config_loaded - Checkoff::ConfigLoader.expects(:load).returns(config).at_least(0) - end - - def expect_sections_created - Checkoff::Sections.expects(:new).returns(sections).at_least(0) - end - - def expect_tasks_created - Checkoff::Tasks.expects(:new).returns(tasks).at_least(0) - end - - def get_test_object(&twiddle_mocks) - set_mocks - expect_workspaces_created - expect_config_loaded - expect_sections_created - expect_tasks_created - - yield @mocks if twiddle_mocks - Checkoff::CheckoffGLIApp + cli.run(['view', + workspace_name, + project_name])) end end diff --git a/test/unit/test_helper.rb b/test/unit/test_helper.rb index 2139be42..2d65c94e 100755 --- a/test/unit/test_helper.rb +++ b/test/unit/test_helper.rb @@ -53,7 +53,7 @@ def define_singleton_method_by_proc(obj, name, block) def get_initializer_mocks(clazz, skip_these_keys: []) parameters = clazz.instance_method(:initialize).parameters named_parameters = parameters.select do |name, _value| - name == :key + %i[key keyreq].include? name end mock_syms = named_parameters.map { |_name, value| value } - skip_these_keys diff --git a/test/unit/test_mv_subcommand.rb b/test/unit/test_mv_subcommand.rb new file mode 100644 index 00000000..f1db8899 --- /dev/null +++ b/test/unit/test_mv_subcommand.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +require 'checkoff/cli' +require_relative 'class_test' + +# Test the Checkoff::MvSubcommand class used in CLI processing +class TestMvSubcommand < ClassTest + extend Forwardable + + def_delegators(:@mocks, :projects, :sections, + :logger) + + let_mock :to_project, :to_project_gid, + :to_section, :to_section_gid, + :task_a, :task_a_name + + attr_reader :from_workspace_arg, :from_project_arg, :from_section_arg, + :to_workspace_arg, :to_project_arg, :to_section_arg, + :from_workspace_name, :from_project_name, :from_section_name, + :to_workspace_name, :to_project_name, :to_section_name + + def argument_to_name(arg) + if arg.start_with? ':' + arg[1..].to_sym + else + arg + end + end + + def expect_project_pulled(workspace_name, project_name, project) + projects.expects(:project_or_raise) + .with(workspace_name, project_name) + .returns(project) + end + + def expect_section_pulled(workspace_name, project_name, section_name, section) + sections.expects(:section_or_raise).with(workspace_name, project_name, section_name) + .returns(section) + end + + def expect_tasks_pulled(workspace_name, project_name, section_name, tasks) + return if section_name == :all_sections # not implemented yet + + sections.expects(:tasks).with(workspace_name, project_name, section_name) + .returns(tasks) + end + + def expect_task_named(task, task_name) + task.expects(:name).returns(task_name) + end + + def expect_section_named(section, section_name) + section.expects(:name).returns(section_name) + end + + def expect_project_gid_pulled(project, project_gid) + project.expects(:gid).returns(project_gid) + end + + def expect_section_gid_pulled(section, section_gid) + section.expects(:gid).returns(section_gid) + end + + def expect_task_added_to_project(task, project_gid, section_gid) + task.expects(:add_project).with(project: project_gid, section: section_gid) + end + + def allow_logger_used + logger.expects(:puts).at_least(0) + end + + def set_initializer_arguments + @mocks[:from_workspace_arg] = from_workspace_arg + @mocks[:from_project_arg] = from_project_arg + @mocks[:from_section_arg] = from_section_arg + @mocks[:to_workspace_arg] = to_workspace_arg + @mocks[:to_project_arg] = to_project_arg + @mocks[:to_section_arg] = to_section_arg + end + + def determine_to_workspace_name(from_workspace_arg, to_workspace_arg) + if to_workspace_arg == :source_workspace + from_workspace_arg + else + to_workspace_arg + end + end + + def determine_to_project_name(from_project_name, to_project_arg) + if to_project_arg == :source_project + from_project_name + else + to_project_arg + end + end + + def determine_to_section_name(from_section_name, to_section_arg) + if to_section_arg == :source_section + from_section_name + else + to_section_arg + end + end + + def expect_task_added_to_section(task, task_name) + return if from_section_name == :all_sections # not implemented yet + + expect_task_named(task, task_name) + expect_section_named(to_section, to_section_name) + expect_project_gid_pulled(to_project, to_project_gid) + expect_section_gid_pulled(to_section, to_section_gid) + expect_task_added_to_project(task, to_project_gid, to_section_gid) + end + + def set_names + @from_workspace_name = from_workspace_arg + @from_project_name = argument_to_name(from_project_arg) + @from_section_name = from_section_arg + @to_workspace_name = determine_to_workspace_name(from_workspace_arg, to_workspace_arg) + @to_project_name = determine_to_project_name(from_project_name, to_project_arg) + @to_section_name = determine_to_section_name(from_section_name, to_section_arg) + end + + def expect_to_objects_pulled + expect_project_pulled(to_workspace_name, to_project_name, to_project) + expect_section_pulled(to_workspace_name, to_project_name, to_section_name, to_section) + end + + def expect_run + set_names + set_initializer_arguments + + return if from_workspace_name != to_workspace_name # not implemented yet + + expect_to_objects_pulled + expect_tasks_pulled(from_workspace_name, from_project_name, from_section_name, [task_a]) + expect_task_added_to_section(task_a, task_a_name) + allow_logger_used + end + + def mock_run_to_different_workspace + @from_workspace_arg = 'My workspace' + @from_project_arg = 'My project' + @from_section_arg = 'My section' + @to_workspace_arg = 'Some other workspace' + @to_project_arg = 'Some other project' + @to_section_arg = :source_section + + expect_run + end + + def test_run_to_different_workspace + assert_raises(NotImplementedError) do + get_test_object do + mock_run_to_different_workspace + end + end + end + + def mock_run_from_all_sections + @from_workspace_arg = 'My workspace' + @from_project_arg = ':my_tasks' + @from_section_arg = :all_sections + @to_workspace_arg = :source_workspace + @to_project_arg = 'Some other project' + @to_section_arg = :source_section + + expect_run + end + + def test_run_from_all_sections + mv_subcommand = get_test_object do + mock_run_from_all_sections + end + assert_raises(NotImplementedError) do + mv_subcommand.run + end + end + + def mock_run_from_regular_project + @from_workspace_arg = 'My workspace' + @from_project_arg = 'My project' + @from_section_arg = 'My section' + @to_workspace_arg = :source_workspace + @to_project_arg = 'Some other project' + @to_section_arg = :source_section + + expect_run + end + + def test_run_from_regular_project + mv_subcommand = get_test_object do + mock_run_from_regular_project + end + mv_subcommand.run + end + + def mock_run_to_same_section_different_project + @from_workspace_arg = 'My workspace' + @from_project_arg = ':my_tasks' + @from_section_arg = 'Recently assigned' + @to_workspace_arg = :source_workspace + @to_project_arg = 'Some other project' + @to_section_arg = :source_section + + expect_run + end + + def test_run_to_same_section_different_project + mv_subcommand = get_test_object do + mock_run_to_same_section_different_project + end + mv_subcommand.run + end + + def mock_run_with_explicit_to_project + @from_workspace_arg = 'My workspace' + @from_project_arg = ':my_tasks' + @from_section_arg = 'Recently assigned' + @to_workspace_arg = :source_workspace + @to_project_arg = 'Some other project' + @to_section_arg = 'Later' + + expect_run + end + + def test_run_with_explicit_to_project + mv_subcommand = get_test_object do + mock_run_with_explicit_to_project + end + mv_subcommand.run + end + + def mock_run_from_my_tasks + @from_workspace_arg = 'My workspace' + @from_project_arg = ':my_tasks' + @from_section_arg = 'Recently assigned' + @to_workspace_arg = :source_workspace + @to_project_arg = :source_project + @to_section_arg = 'Later' + + expect_run + end + + def test_run_from_my_tasks + mv_subcommand = get_test_object do + mock_run_from_my_tasks + end + mv_subcommand.run + end + + def mock_init_default_workspace_not_implemented + @from_workspace_arg = :default_workspace + @from_project_arg = ':my_tasks' + @from_section_arg = 'Recently assigned' + @to_workspace_arg = :source_workspace + @to_project_arg = :source_project + @to_section_arg = 'Later' + + set_initializer_arguments + end + + def test_init_default_workspace_not_implemented + assert_raises(NotImplementedError) do + get_test_object do + mock_init_default_workspace_not_implemented + end + end + end + + def test_init + mv_subcommand = get_test_object do + @from_workspace_arg = 'My workspace' + @from_project_arg = ':my_tasks' + @from_section_arg = 'Recently assigned' + @to_workspace_arg = :source_workspace + @to_project_arg = :source_project + @to_section_arg = 'Later' + + set_initializer_arguments + end + refute mv_subcommand.nil? + end + + def class_under_test + Checkoff::MvSubcommand + end +end diff --git a/test/unit/test_projects.rb b/test/unit/test_projects.rb index 548e390c..cb370e81 100644 --- a/test/unit/test_projects.rb +++ b/test/unit/test_projects.rb @@ -90,6 +90,30 @@ def setup_user_task_list_pulled user_task_list.expects(:gid).returns(my_tasks_in_workspace_gid) end + def mock_project_or_raise_unknown + setup_config + setup_client_pulled + setup_workspace_pulled + setup_projects_pulled + setup_projects_queried(workspace_gid: workspace_one_gid) + end + + def test_project_or_raise_unknown + asana = get_test_object do + mock_project_or_raise_unknown + end + assert_raises(RuntimeError) do + asana.project_or_raise('Workspace 1', 'Does not exist') + end + end + + def test_project_or_raise_my_tasks + asana = get_test_object do + mock_project_my_tasks + end + assert_equal(my_tasks_project, asana.project_or_raise('Workspace 1', :my_tasks)) + end + def mock_project_my_tasks setup_config setup_client_pulled diff --git a/test/unit/test_sections.rb b/test/unit/test_sections.rb index ebd9059c..d986ec21 100644 --- a/test/unit/test_sections.rb +++ b/test/unit/test_sections.rb @@ -156,10 +156,7 @@ def original_task_options end def fixed_task_options(only_uncompleted:) - out = { - foo: 'bar', - options: { fields: [] }, - } + out = original_task_options out[:completed_since] = '9999-12-01' if only_uncompleted out end @@ -170,8 +167,8 @@ def expect_original_task_options_pulled def expect_tasks_api_called_for_section(only_uncompleted:) options = fixed_task_options(only_uncompleted: only_uncompleted) - tasks.expects(:get_tasks_for_section).with(section_gid: section1_gid, - **options) + tasks.expects(:get_tasks).with(section: section1_gid, + **options) .returns([task_c]) end