Skip to content

Commit

Permalink
Add behaviors for the various library modules (#1)
Browse files Browse the repository at this point in the history
This should allow consumers of the shortcut api library to safely
mock the behavior with Hammox. On this end, we have confidence that
the behaviors are implemented
  • Loading branch information
zgagnon authored Dec 3, 2024
1 parent 0d73a04 commit 62e14d7
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 39 deletions.
27 changes: 27 additions & 0 deletions lib/epics.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ShortcutApi.Epics do
import ShortcutApi.ApiHelpers

@behaviour ShortcutApi.EpicsBehavior
@moduledoc """
API wrapper for Shortcut Epics endpoints.
See: https://developer.shortcut.com/api/rest/v3#Epics
Expand Down Expand Up @@ -168,3 +169,29 @@ defmodule ShortcutApi.Epics do
make_request(:get, "/epic-workflow/states/#{state_id}", token)
end
end

defmodule ShortcutApi.EpicsBehavior do
@moduledoc """
A behaviour for interacting with Shortcut Epics API endpoints.
This module defines the required callbacks for operations related to epics,
including retrieving, creating, updating, deleting, and managing epic states
and statistics. Implementations of this behaviour must provide the necessary
logic to handle interactions with the Shortcut API.
All callbacks require a valid Shortcut API token.
"""
@callback get_epic(token :: String.t(), epic_id :: pos_integer()) ::
{:ok, map()} | {:error, any()}
@callback create_epic(token :: String.t(), params :: map()) :: {:ok, map()} | {:error, any()}
@callback update_epic(token :: String.t(), epic_id :: pos_integer(), params :: map()) ::
{:ok, map()} | {:error, any()}
@callback delete_epic(token :: String.t(), epic_id :: pos_integer()) ::
{:ok, map()} | {:error, any()}
@callback list_epics(token :: String.t()) :: {:ok, list(map())} | {:error, any()}
@callback get_epic_workflow(token :: String.t()) :: {:ok, map()} | {:error, any()}
@callback get_epic_stats(token :: String.t(), epic_id :: pos_integer()) ::
{:ok, map()} | {:error, any()}
@callback get_epic_state(token :: String.t(), state_id :: pos_integer()) ::
{:ok, map()} | {:error, any()}
end
13 changes: 13 additions & 0 deletions lib/projects.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ShortcutApi.Projects do
import ShortcutApi.ApiHelpers

@behaviour ShortcutApi.ProjectsBehavior
@moduledoc """
API wrapper for Shortcut Projects endpoints.
See: https://developer.shortcut.com/api/rest/v3#Projects
Expand Down Expand Up @@ -112,3 +113,15 @@ defmodule ShortcutApi.Projects do
make_request(:delete, "/projects/#{project_id}", token)
end
end

defmodule ShortcutApi.ProjectsBehavior do
@moduledoc """
The Projects behaviour.
"""

@callback list_projects(String.t()) :: {:ok, map() | list(map())} | {:error, any()}
@callback get_project(String.t(), pos_integer()) :: {:ok, map()} | {:error, any()}
@callback create_project(String.t(), map()) :: {:ok, map()} | {:error, any()}
@callback update_project(String.t(), pos_integer(), map()) :: {:ok, map()} | {:error, any()}
@callback delete_project(String.t(), pos_integer()) :: {:ok, map()} | {:error, any()}
end
34 changes: 34 additions & 0 deletions lib/stories.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule ShortcutApi.Stories do
@behaviour ShortcutApi.StoriesBehavior
import ShortcutApi.ApiHelpers

@moduledoc """
Expand Down Expand Up @@ -96,3 +97,36 @@ defmodule ShortcutApi.Stories do
make_request(:delete, "/stories/#{story_id}", token)
end
end

defmodule ShortcutApi.StoriesBehavior do
@moduledoc """
Behavior for Shortcut API Stories operations.
This module defines the callbacks required for interacting with
Shortcut Stories API endpoints. Implementations of this behavior
should provide the logic for each function to handle API requests
according to the Shortcut API documentation.
Each function requires a valid Shortcut API token and operates on
story data, facilitating creating, retrieving, updating, and
deleting stories.
## Callbacks
* `get_story/2`: Retrieves a single story by its ID.
* `create_story/2`: Creates a new story with the provided parameters.
* `update_story/3`: Updates an existing story identified by its ID.
* `delete_story/2`: Deletes a story by its ID.
"""
@callback get_story(token :: String.t(), story_id :: pos_integer()) ::
{:ok, map() | list(map())} | {:error, any()}

@callback create_story(token :: String.t(), params :: map()) ::
{:ok, map() | list(map())} | {:error, any()}

@callback update_story(token :: String.t(), story_id :: pos_integer(), params :: map()) ::
{:ok, map() | list(map())} | {:error, any()}

@callback delete_story(token :: String.t(), story_id :: pos_integer()) ::
{:ok, map() | list(map())} | {:error, any()}
end
9 changes: 5 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ defmodule ShortcutApiEx.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:req, "~> 0.5.0"},
{:jason, "~> 1.4"},
{:bypass, "~> 2.1", only: :test},
{:plug_cowboy, "~> 2.5", only: :test},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:hammox, "~> 0.7", only: :test},
{:jason, "~> 1.4"},
{:plug_cowboy, "~> 2.5", only: :test},
{:req, "~> 0.5.0"}
]
end

Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hammox": {:hex, :hammox, "0.7.0", "a49dc95e0a78e1c38db11c2b6eadff38f25418ef92ecf408bd90d95d459f35a2", [:mix], [{:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: false]}, {:ordinal, "~> 0.1", [hex: :ordinal, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5e228c4587f23543f90c11394957878178c489fad46da421c37ca696e37dd91b"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"ordinal": {:hex, :ordinal, "0.2.0", "d3eda0cb04ee1f0ca0aae37bf2cf56c28adce345fe56a75659031b6068275191", [:mix], [], "hexpm", "defca8f10dee9f03a090ed929a595303252700a9a73096b6f2f8d88341690d65"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
Expand Down
36 changes: 20 additions & 16 deletions test/epics_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ defmodule ShortcutApi.EpicsTest do
{:ok, bypass: bypass}
end

test "get_epic/2", %{bypass: bypass} do
setup_all do
Hammox.protect(ShortcutApi.Epics, ShortcutApi.EpicsBehavior)
end

test "get_epic/2", %{bypass: bypass, get_epic_2: get_epic} do
epic_id = 123

Bypass.expect(bypass, "GET", "/api/v3/epics/#{epic_id}", fn conn ->
Expand All @@ -21,12 +25,12 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, epic} = ShortcutApi.Epics.get_epic("fake-token", epic_id)
{:ok, epic} = get_epic.("fake-token", epic_id)
assert epic["id"] == epic_id
assert epic["name"] == "Test Epic"
end

test "create_epic/2", %{bypass: bypass} do
test "create_epic/2", %{bypass: bypass, create_epic_2: create_epic} do
epic_params = %{"name" => "New Epic", "workflow_state_id" => 789}

Bypass.expect(bypass, "POST", "/api/v3/epics", fn conn ->
Expand All @@ -40,12 +44,12 @@ defmodule ShortcutApi.EpicsTest do
|> Plug.Conn.resp(201, Jason.encode!(response))
end)

{:ok, epic} = ShortcutApi.Epics.create_epic("fake-token", epic_params)
{:ok, epic} = create_epic.("fake-token", epic_params)
assert epic["name"] == "New Epic"
assert epic["workflow_state_id"] == 789
end

test "update_epic/3", %{bypass: bypass} do
test "update_epic/3", %{bypass: bypass, update_epic_3: update_epic} do
epic_id = 123
update_params = %{"name" => "Updated Epic"}

Expand All @@ -61,12 +65,12 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, epic} = ShortcutApi.Epics.update_epic("fake-token", epic_id, update_params)
{:ok, epic} = update_epic.("fake-token", epic_id, update_params)
assert epic["id"] == epic_id
assert epic["name"] == "Updated Epic"
end

test "delete_epic/2", %{bypass: bypass} do
test "delete_epic/2", %{bypass: bypass, delete_epic_2: delete_epic} do
epic_id = 123

Bypass.expect(bypass, "DELETE", "/api/v3/epics/#{epic_id}", fn conn ->
Expand All @@ -75,10 +79,10 @@ defmodule ShortcutApi.EpicsTest do
|> Plug.Conn.resp(200, Jason.encode!(%{}))
end)

assert {:ok, %{}} = ShortcutApi.Epics.delete_epic("fake-token", epic_id)
assert {:ok, %{}} = delete_epic.("fake-token", epic_id)
end

test "list_epics/1", %{bypass: bypass} do
test "list_epics/1", %{bypass: bypass, list_epics_1: list_epics} do
Bypass.expect(bypass, "GET", "/api/v3/epics", fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
Expand All @@ -91,13 +95,13 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, epics} = ShortcutApi.Epics.list_epics("fake-token")
{:ok, epics} = list_epics.("fake-token")
assert length(epics) == 2
assert Enum.at(epics, 0)["name"] == "Epic 1"
assert Enum.at(epics, 1)["name"] == "Epic 2"
end

test "get_epic_workflow/1", %{bypass: bypass} do
test "get_epic_workflow/1", %{bypass: bypass, get_epic_workflow_1: get_epic_workflow} do
Bypass.expect(bypass, "GET", "/api/v3/epic-workflow", fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
Expand All @@ -113,12 +117,12 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, workflow} = ShortcutApi.Epics.get_epic_workflow("fake-token")
{:ok, workflow} = get_epic_workflow.("fake-token")
assert length(workflow["states"]) == 3
assert Enum.at(workflow["states"], 1)["name"] == "In Progress"
end

test "get_epic_stats/2", %{bypass: bypass} do
test "get_epic_stats/2", %{bypass: bypass, get_epic_stats_2: get_epic_stats} do
epic_id = 123

Bypass.expect(bypass, "GET", "/api/v3/epics/#{epic_id}/stats", fn conn ->
Expand All @@ -136,12 +140,12 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, stats} = ShortcutApi.Epics.get_epic_stats("fake-token", epic_id)
{:ok, stats} = get_epic_stats.("fake-token", epic_id)
assert stats["num_points"] == 10
assert stats["num_stories"] == 5
end

test "get_epic_state/2", %{bypass: bypass} do
test "get_epic_state/2", %{bypass: bypass, get_epic_state_2: get_epic_state} do
state_id = 123

Bypass.expect(bypass, "GET", "/api/v3/epic-workflow/states/#{state_id}", fn conn ->
Expand All @@ -158,7 +162,7 @@ defmodule ShortcutApi.EpicsTest do
)
end)

{:ok, state} = ShortcutApi.Epics.get_epic_state("fake-token", state_id)
{:ok, state} = get_epic_state.("fake-token", state_id)
assert state["id"] == state_id
assert state["name"] == "In Progress"
assert state["type"] == "started"
Expand Down
24 changes: 14 additions & 10 deletions test/projects_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ defmodule ShortcutApi.ProjectsTest do
{:ok, bypass: bypass}
end

test "list_projects/1", %{bypass: bypass} do
setup_all do
Hammox.protect(ShortcutApi.Projects, ShortcutApi.ProjectsBehavior)
end

test "list_projects/1", %{bypass: bypass, list_projects_1: list_projects} do
Bypass.expect(bypass, "GET", "/api/v3/projects", fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
Expand All @@ -22,13 +26,13 @@ defmodule ShortcutApi.ProjectsTest do
)
end)

{:ok, projects} = ShortcutApi.Projects.list_projects("fake-token")
{:ok, projects} = list_projects.("fake-token")
assert length(projects) == 2
assert Enum.at(projects, 0)["name"] == "Project 1"
assert Enum.at(projects, 1)["name"] == "Project 2"
end

test "get_project/2", %{bypass: bypass} do
test "get_project/2", %{bypass: bypass, get_project_2: get_project} do
project_id = 123

Bypass.expect(bypass, "GET", "/api/v3/projects/#{project_id}", fn conn ->
Expand All @@ -40,12 +44,12 @@ defmodule ShortcutApi.ProjectsTest do
)
end)

{:ok, project} = ShortcutApi.Projects.get_project("fake-token", project_id)
{:ok, project} = get_project.("fake-token", project_id)
assert project["id"] == project_id
assert project["name"] == "Test Project"
end

test "create_project/2", %{bypass: bypass} do
test "create_project/2", %{bypass: bypass, create_project_2: create_project} do
project_params = %{"name" => "New Project"}

Bypass.expect(bypass, "POST", "/api/v3/projects", fn conn ->
Expand All @@ -59,12 +63,12 @@ defmodule ShortcutApi.ProjectsTest do
|> Plug.Conn.resp(201, Jason.encode!(response))
end)

{:ok, project} = ShortcutApi.Projects.create_project("fake-token", project_params)
{:ok, project} = create_project.("fake-token", project_params)
assert project["name"] == "New Project"
assert project["id"] == 456
end

test "update_project/3", %{bypass: bypass} do
test "update_project/3", %{bypass: bypass, update_project_3: update_project} do
project_id = 123
update_params = %{"name" => "Updated Project"}

Expand All @@ -80,12 +84,12 @@ defmodule ShortcutApi.ProjectsTest do
)
end)

{:ok, project} = ShortcutApi.Projects.update_project("fake-token", project_id, update_params)
{:ok, project} = update_project.("fake-token", project_id, update_params)
assert project["id"] == project_id
assert project["name"] == "Updated Project"
end

test "delete_project/2", %{bypass: bypass} do
test "delete_project/2", %{bypass: bypass, delete_project_2: delete_project} do
project_id = 123

Bypass.expect(bypass, "DELETE", "/api/v3/projects/#{project_id}", fn conn ->
Expand All @@ -94,6 +98,6 @@ defmodule ShortcutApi.ProjectsTest do
|> Plug.Conn.resp(200, Jason.encode!(%{}))
end)

assert {:ok, %{}} = ShortcutApi.Projects.delete_project("fake-token", project_id)
assert {:ok, %{}} = delete_project.("fake-token", project_id)
end
end
Loading

0 comments on commit 62e14d7

Please sign in to comment.