[sessionctl] Create shorthand for adding a mod to a new story

This defaults the story and mod name to the mod url, which can be overridden
using the existing flags.

Usage: sessionctl add_mod slider_mod
       sessionctl --story_name=story --mod_name=mod add_mod slider_mod

TEST=sessionctl_unittest and manual testing
MF-154 #comment [sessionctl] Create shorthand for adding a mod to a new
story.

Change-Id: I4b3d16a6c08fbf7d096b0884303881ec90301b76
diff --git a/bin/sessionctl/main.cc b/bin/sessionctl/main.cc
index b139cb0..743137c 100644
--- a/bin/sessionctl/main.cc
+++ b/bin/sessionctl/main.cc
@@ -67,33 +67,34 @@
 }
 
 std::string GetUsage() {
-  return R"(sessionctl <flags> <command>
+  return R"(sessionctl <flags> <command> <argument>
     Example:
-    sessionctl --mod_url=slider_mod --mod_name=mod1 --story_name=story1
-               --focus_mod --focus_story add_mod
+    sessionctl add_mod slider_mod
+
+    sessionctl --mod_name=mod1 --story_name=story1 --focus_mod=false 
+               --focus_story=false add_mod slider_mod
 
     sessionctl --mod_name=mod1 --story_name=story1 remove_mod
 
     <flags>
     --story_name=STORY_NAME
     --mod_name=MOD_NAME
-    --mod_url=MOD_URL
-        mods have a unique "mod_url".
-        It is the mod package's name.
-        In BUILD.gn fuchsia_package_name = "mod_url" or mod_url comes from
-        flutter_app("mod_url") when there is no fuchsia_package_name set.
-    --focus_mod
-        If flag is set then the mod is focused.
-    --focus_story
-        If flag is set then the story is focused.
+    --focus_mod=false
+        Don't focus the mod.
+    --focus_story=false
+        Don't focus the story.
     --json_out
         If flag is set output json for consuming instead of text.
 
     <command>
     add_mod
-      Add new mod or update an existing mod if found.
-        required: --story_name, --mod_name, --mod_url
-        optional: --focus_mod, --focus_story, --json_out
+      [--story_name=foo] [--mod_name=bar] [--focus_mod=false] [--focus_story=false] add_mod MOD_URL
+        Add new mod or update an existing mod if a mod with --mod_name already 
+        exists in --story_name.
+        Defaults --story_name and --mod_name to MOD_URL. 
+        Defaults --focus_mod and --focus_story to 'true'.
+        optional: --story_name, --mod_name, --focus_mod, --focus_story, 
+                  --json_out
 
     remove_mod
       Remove the mod.
@@ -103,9 +104,15 @@
       Delete the story.
         required: --story_name
         optional: --json_out
-    
+
     list_stories
-      List all the stories in the current session.)";
+      List all the stories in the current session.
+      
+    <argument>
+    MOD_URL
+      Mods have a unique "mod_url". It's the mod package's name.
+      In BUILD.gn fuchsia_package_name = "mod_url" or mod_url comes from
+      flutter_app("mod_url") when there is no fuchsia_package_name set.)";
 }
 
 PuppetMasterPtr ConnectToPuppetMaster(const ActiveSession& session) {
diff --git a/bin/sessionctl/session_ctl_app.cc b/bin/sessionctl/session_ctl_app.cc
index c4504eb..7c7139e 100644
--- a/bin/sessionctl/session_ctl_app.cc
+++ b/bin/sessionctl/session_ctl_app.cc
@@ -69,43 +69,34 @@
 
 std::string SessionCtlApp::ExecuteAddModCommand(
     const fxl::CommandLine& command_line) {
-  std::string mod_url;
-  std::string story_name;
-  std::string mod_name;
-
-  std::vector<std::string> missing_flags;
-
-  if (command_line.HasOption(kModUrlFlagString)) {
-    command_line.GetOptionValue(kModUrlFlagString, &mod_url);
-  } else {
-    missing_flags.emplace_back(kModUrlFlagString);
-  }
-
-  if (command_line.HasOption(kStoryNameFlagString)) {
-    command_line.GetOptionValue(kStoryNameFlagString, &story_name);
-  } else {
-    missing_flags.emplace_back(kStoryNameFlagString);
-  }
-
-  if (command_line.HasOption(kModNameFlagString)) {
-    command_line.GetOptionValue(kModNameFlagString, &mod_name);
-  } else {
-    missing_flags.emplace_back(kModNameFlagString);
-  }
-
-  std::string parsing_error = GenerateMissingFlagString(missing_flags);
-  if (!parsing_error.empty()) {
+  if (command_line.positional_args().size() == 1) {
+    auto parsing_error = "Missing mod_url. Ex: sessionctl add_mod slider_mod";
     logger_.LogError(kAddModCommandString, parsing_error);
     return parsing_error;
   }
 
+  // Get the mod url and default the mod name and story name to the mod url
+  std::string mod_url = command_line.positional_args().at(1);
+  std::string mod_name = mod_url;
+  std::string story_name = mod_url;
+
+  // If the following options aren't specified, their respective values will
+  // remain unchanged.
+  command_line.GetOptionValue(kStoryNameFlagString, &story_name);
+  command_line.GetOptionValue(kModNameFlagString, &mod_name);
+
   auto commands = MakeAddModCommands(mod_url, mod_name);
 
-  if (command_line.HasOption(kFocusModFlagString)) {
+  // Focus the mod and story by default
+  std::string focus_mod;
+  command_line.GetOptionValue(kFocusModFlagString, &focus_mod);
+  if (focus_mod == "" || focus_mod == "true") {
     commands.push_back(MakeFocusModCommand(mod_name));
   }
 
-  if (command_line.HasOption(kFocusStoryFlagString)) {
+  std::string focus_story;
+  command_line.GetOptionValue(kFocusStoryFlagString, &focus_story);
+  if (focus_story == "" || focus_story == "true") {
     commands.push_back(MakeFocusStoryCommand());
   }
 
@@ -118,7 +109,7 @@
   PostTaskExecuteStoryCommand(kAddModCommandString, std::move(commands),
                               params);
 
-  return parsing_error;
+  return "";
 }
 
 std::string SessionCtlApp::ExecuteDeleteStoryCommand(
diff --git a/bin/sessionctl/session_ctl_app_unittest.cc b/bin/sessionctl/session_ctl_app_unittest.cc
index 4bc98ea..cc05c26 100644
--- a/bin/sessionctl/session_ctl_app_unittest.cc
+++ b/bin/sessionctl/session_ctl_app_unittest.cc
@@ -82,37 +82,56 @@
 
 TEST_F(SessionCtlAppTest, AddMod) {
   // Add a mod
-  auto command_line = fxl::CommandLineFromInitializerList(
-      {"sessionctl", "--mod_name=mod", "--story_name=story", "--mod_url=foo"});
+  auto command_line =
+      fxl::CommandLineFromInitializerList({"sessionctl", "add_mod", "mod_url"});
   SessionCtlApp sessionctl = CreateSessionCtl(command_line);
   RunLoopUntilCommandExecutes([&] {
     return sessionctl.ExecuteCommand(kAddModCommandString, command_line);
   });
 
-  // Assert the story and the mod were added
-  auto story_data = GetStoryData("story");
+  // Assert the story and the mod were added with default story and mod names
+  auto story_data = GetStoryData("mod_url");
   ASSERT_TRUE(story_data);
-  EXPECT_EQ("story", story_data->story_name);
-  EXPECT_EQ("mod",
+  EXPECT_EQ("mod_url", story_data->story_name);
+  EXPECT_EQ("mod_url",
             test_executor_.last_commands().at(0).add_mod().mod_name->at(0));
   EXPECT_EQ(1, test_executor_.execute_count());
 }
 
-TEST_F(SessionCtlAppTest, AddModMissingFlags) {
-  // Attempt to add a mod without the required flags
-  auto command_line = fxl::CommandLineFromInitializerList({"sessionctl"});
+TEST_F(SessionCtlAppTest, AddModOverrideDefaults) {
+  // Add a mod
+  auto command_line = fxl::CommandLineFromInitializerList(
+      {"sessionctl", "--story_name=s", "--mod_name=m", "add_mod", "mod_url"});
+  SessionCtlApp sessionctl = CreateSessionCtl(command_line);
+  RunLoopUntilCommandExecutes([&] {
+    return sessionctl.ExecuteCommand(kAddModCommandString, command_line);
+  });
+
+  // Assert the story and the mod were added with overriden story and mod names
+  auto story_data = GetStoryData("s");
+  ASSERT_TRUE(story_data);
+  EXPECT_EQ("s", story_data->story_name);
+  EXPECT_EQ("m",
+            test_executor_.last_commands().at(0).add_mod().mod_name->at(0));
+  EXPECT_EQ(1, test_executor_.execute_count());
+}
+
+TEST_F(SessionCtlAppTest, AddModMissingModUrl) {
+  // Attempt to add a mod without a mod url
+  auto command_line =
+      fxl::CommandLineFromInitializerList({"sessionctl", "add_mod"});
   SessionCtlApp sessionctl = CreateSessionCtl(command_line);
   std::string error =
       sessionctl.ExecuteCommand(kAddModCommandString, command_line);
 
   RunLoopUntilIdle();
-  EXPECT_EQ("flags missing: --mod_url --story_name --mod_name", error);
+  EXPECT_EQ("Missing mod_url. Ex: sessionctl add_mod slider_mod", error);
 }
 
 TEST_F(SessionCtlAppTest, RemoveMod) {
   // Add a mod
   auto command_line = fxl::CommandLineFromInitializerList(
-      {"sessionctl", "--mod_name=mod", "--story_name=story", "--mod_url=foo"});
+      {"sessionctl", "add_mod", "mod"});
   SessionCtlApp sessionctl = CreateSessionCtl(command_line);
   RunLoopUntilCommandExecutes([&] {
     return sessionctl.ExecuteCommand(kAddModCommandString, command_line);
@@ -120,15 +139,15 @@
 
   // Remove the mod
   command_line = fxl::CommandLineFromInitializerList(
-      {"sessionctl", "--mod_name=mod", "--story_name=story"});
+      {"sessionctl", "--mod_name=mod", "--story_name=mod"});
   RunLoopUntilCommandExecutes([&] {
     return sessionctl.ExecuteCommand(kRemoveModCommandString, command_line);
   });
 
   // Assert session_storage still contains the story
-  auto story_data = GetStoryData("story");
+  auto story_data = GetStoryData("mod");
   ASSERT_TRUE(story_data);
-  EXPECT_EQ("story", story_data->story_name);
+  EXPECT_EQ("mod", story_data->story_name);
   EXPECT_EQ("mod",
             test_executor_.last_commands().at(0).remove_mod().mod_name->at(0));
   EXPECT_EQ(2, test_executor_.execute_count());
@@ -148,7 +167,7 @@
 TEST_F(SessionCtlAppTest, DeleteStory) {
   // Add a mod
   auto command_line = fxl::CommandLineFromInitializerList(
-      {"sessionctl", "--mod_name=mod", "--story_name=story", "--mod_url=foo"});
+      {"sessionctl", "add_mod", "mod"});
   SessionCtlApp sessionctl = CreateSessionCtl(command_line);
   RunLoopUntilCommandExecutes([&] {
     return sessionctl.ExecuteCommand(kAddModCommandString, command_line);
@@ -156,12 +175,12 @@
 
   // Remove the story
   command_line =
-      fxl::CommandLineFromInitializerList({"sessionctl", "--story_name=story"});
+      fxl::CommandLineFromInitializerList({"sessionctl", "--story_name=mod"});
   RunLoopUntilCommandExecutes([&] {
     return sessionctl.ExecuteCommand(kDeleteStoryCommandString, command_line);
   });
 
-  auto story_data = GetStoryData("story");
+  auto story_data = GetStoryData("mod");
   EXPECT_FALSE(story_data);
   EXPECT_EQ(1, test_executor_.execute_count());
 }