From e86971df2f63ad2d790d6cc4b893f64a70cbdc82 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 24 Oct 2017 16:05:36 +0200 Subject: [PATCH] Add GitHub API pagination, repository issue syncing, acceptance tests --- .env.example | 5 + circle.yml | 2 +- config/dev.exs | 2 +- config/test.exs | 24 +- lib/code_corps/github/api/api.ex | 2 + lib/code_corps/github/api/eager_api.ex | 83 ++++ lib/code_corps/github/api/headers.ex | 9 +- lib/code_corps/github/api/repository.ex | 31 ++ lib/code_corps/github/github.ex | 8 + lib/code_corps/github/sync/sync.ex | 23 +- lib/code_corps/validators/time_validator.ex | 5 +- mix.exs | 6 +- mix.lock | 11 +- test/fixtures/github/endpoints/issues.json | 400 ++++++++++++++++++ .../code_corps/github/api/repository_test.exs | 51 +++ test/lib/code_corps/github/sync/sync_test.exs | 35 ++ test/support/github/success_api.ex | 9 + test/support/github/test_helpers.ex | 69 +++ test/test_helper.exs | 1 + 19 files changed, 747 insertions(+), 29 deletions(-) create mode 100644 lib/code_corps/github/api/eager_api.ex create mode 100644 lib/code_corps/github/api/repository.ex create mode 100644 test/fixtures/github/endpoints/issues.json create mode 100644 test/lib/code_corps/github/api/repository_test.exs diff --git a/.env.example b/.env.example index 1fcd2f1c8..6d9e64099 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,12 @@ export CLOUDEX_SECRET= export CLOUDFRONT_DOMAIN= export GITHUB_APP_CLIENT_ID= export GITHUB_APP_CLIENT_SECRET= +export GITHUB_APP_ID= export GITHUB_APP_PEM= +export GITHUB_TEST_APP_CLIENT_ID= +export GITHUB_TEST_APP_CLIENT_SECRET= +export GITHUB_TEST_APP_ID= +export GITHUB_TEST_APP_PEM= export INTERCOM_IDENTITY_SECRET_KEY= export POSTMARK_API_KEY= export S3_BUCKET= diff --git a/circle.yml b/circle.yml index c9a57227c..9a005620c 100644 --- a/circle.yml +++ b/circle.yml @@ -17,7 +17,7 @@ test: if [ ${CIRCLE_PR_USERNAME} ]; then MIX_ENV=test mix test; else - MIX_ENV=test mix coveralls.circle; + MIX_ENV=test mix coveralls.circle --include acceptance:true; fi post: - mix inch.report diff --git a/config/dev.exs b/config/dev.exs index 977c3e0a9..0c72407e7 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -7,7 +7,7 @@ use Mix.Config # watchers to your application. For example, we use it # with brunch.io to recompile .js and .css sources. config :code_corps, CodeCorpsWeb.Endpoint, - http: [port: 4000, ip: {0, 0, 0, 0, 0, 0, 0, 0}], + http: [port: 4000], debug_errors: true, code_reloader: true, check_origin: false diff --git a/config/test.exs b/config/test.exs index 9ad78b4f7..f53ffca53 100644 --- a/config/test.exs +++ b/config/test.exs @@ -41,8 +41,18 @@ config :code_corps, :icon_color_generator, CodeCorps.RandomIconColor.TestGenerat # Set Corsica logging to output no console warning when rejecting a request config :code_corps, :corsica_log_level, [rejected: :debug] -# Set the GitHub module -config :code_corps, github: CodeCorps.GitHubTesting +# fall back to sample pem if none is available as an ENV variable +pem = case System.get_env("GITHUB_TEST_APP_PEM") do + nil -> "./test/fixtures/github/app.pem" |> File.read! + encoded_pem -> encoded_pem |> Base.decode64! +end + +config :code_corps, + github: CodeCorps.GitHub.SuccessAPI, + github_app_id: System.get_env("GITHUB_TEST_APP_ID"), + github_app_client_id: System.get_env("GITHUB_TEST_APP_CLIENT_ID"), + github_app_client_secret: System.get_env("GITHUB_TEST_APP_CLIENT_SECRET"), + github_app_pem: pem config :sentry, environment_name: Mix.env || :test @@ -57,13 +67,3 @@ config :code_corps, config :code_corps, :cloudex, CloudexTest config :cloudex, api_key: "test_key", secret: "test_secret", cloud_name: "test_cloud_name" - -# fall back to sample pem if none is available as an ENV variable -pem = case System.get_env("GITHUB_APP_PEM") do - nil -> "./test/fixtures/github/app.pem" |> File.read! - encoded_pem -> encoded_pem |> Base.decode64! -end - -config :code_corps, - github: CodeCorps.GitHub.SuccessAPI, - github_app_pem: pem diff --git a/lib/code_corps/github/api/api.ex b/lib/code_corps/github/api/api.ex index c27bc54cb..ef732c6e5 100644 --- a/lib/code_corps/github/api/api.ex +++ b/lib/code_corps/github/api/api.ex @@ -14,6 +14,8 @@ defmodule CodeCorps.GitHub.API do |> marshall_response() end + defdelegate get_all(url, headers, opts), to: CodeCorps.GitHub.EagerAPI + @doc """ Get access token headers for a given `CodeCorps.User` and `CodeCorps.GithubAppInstallation`. diff --git a/lib/code_corps/github/api/eager_api.ex b/lib/code_corps/github/api/eager_api.ex new file mode 100644 index 000000000..38b9f8e17 --- /dev/null +++ b/lib/code_corps/github/api/eager_api.ex @@ -0,0 +1,83 @@ +defmodule CodeCorps.GitHub.EagerAPI do + @moduledoc """ + Eager loads a resource from the GitHub API by fetching all of its pages in + parallel. + """ + + def get_all(url, headers, options) do + HTTPoison.start + {:ok, response} = HTTPoison.get(url, headers, options) + + first_page = Poison.decode!(response.body) + case response.headers |> retrieve_total_pages do + 1 -> first_page + total -> + first_page ++ get_remaining_pages(total, url, headers, options) |> List.flatten + end + end + + defp get_remaining_pages(total, url, headers, options) do + 2..total + |> Enum.to_list + |> Enum.map(&Task.async(fn -> + params = options[:params] ++ [page: &1] + HTTPoison.get(url, headers, options ++ [params: params]) + end)) + |> Enum.map(&Task.await(&1, 10000)) + |> Enum.map(&handle_page_response/1) + end + + defp handle_page_response({:ok, %{body: body}}), do: Poison.decode!(body) + + def retrieve_total_pages(headers) do + case headers |> List.keyfind("Link", 0, nil) do + nil -> 1 + {"Link", value} -> value |> extract_total_pages + end + end + + defp extract_total_pages(links_string) do + # We use regex to parse the pagination info from the GitHub API response + # headers. + # + # The headers render pages in the following format: + # + # ``` + # {"Link", '; rel="next", + # ; rel="last", + # ; rel="first", + # ; rel="prev"' + # ``` + # + # If the response has no list header, then we have received all the records + # from the only possible page. + # + # If the response has a list header, the value will contain at least the + # "last" relation. + links_string + |> String.split(", ") + |> Enum.map(fn link -> + rel = get_rel(link) + page = get_page(link) + {rel, page} + end) + |> Enum.into(%{}) + |> Map.get("last") + end + + defp get_rel(link) do + # Searches for `rel=` + Regex.run(~r{rel="([a-z]+)"}, link) |> List.last() + end + + defp get_page(link) do + # Searches for the following variations: + # ``` + # ?page={match}> + # ?page={match}&... + # &page={match}> + # &page={match}&... + # ``` + Regex.run(~r{[&/?]page=([^>&]+)}, link) |> List.last |> String.to_integer + end +end diff --git a/lib/code_corps/github/api/headers.ex b/lib/code_corps/github/api/headers.ex index cc3b3297b..1a46764c9 100644 --- a/lib/code_corps/github/api/headers.ex +++ b/lib/code_corps/github/api/headers.ex @@ -33,11 +33,12 @@ defmodule CodeCorps.GitHub.API.Headers do end @spec add_access_token_header(%{String.t => String.t}, list) :: %{String.t => String.t} - defp add_access_token_header(%{} = headers, [access_token: nil]), do: headers - defp add_access_token_header(%{} = headers, [access_token: access_token]) do - Map.put(headers, "Authorization", "token #{access_token}") + defp add_access_token_header(%{} = headers, opts) do + case opts[:access_token] do + nil -> headers + token -> headers |> Map.put("Authorization", "token #{token}") + end end - defp add_access_token_header(headers, []), do: headers @spec add_jwt_header(%{String.t => String.t}) :: %{String.t => String.t} defp add_jwt_header(%{} = headers) do diff --git a/lib/code_corps/github/api/repository.ex b/lib/code_corps/github/api/repository.ex new file mode 100644 index 000000000..2ea6c7cee --- /dev/null +++ b/lib/code_corps/github/api/repository.ex @@ -0,0 +1,31 @@ +defmodule CodeCorps.GitHub.API.Repository do + @moduledoc ~S""" + Functions for working with issues on GitHub. + """ + + alias CodeCorps.{ + GitHub, + GitHub.API, + GithubAppInstallation, + GithubRepo, + } + + @spec issues(GithubRepo.t) :: {:ok, list(map)} | {:error, GitHub.api_error_struct} + def issues(%GithubRepo{github_app_installation: %GithubAppInstallation{} = installation} = github_repo) do + with {:ok, access_token} <- API.Installation.get_access_token(installation), + issues <- fetch_issues(github_repo, access_token) + do + {:ok, issues} + else + {:error, error} -> {:error, error} + end + end + + defp fetch_issues(%GithubRepo{github_app_installation: %GithubAppInstallation{github_account_login: owner}, name: repo}, access_token) do + per_page = 100 + path = "repos/#{owner}/#{repo}/issues" + params = [per_page: per_page, state: "all"] + opts = [access_token: access_token, params: params] + GitHub.get_all(path, %{}, opts) + end +end diff --git a/lib/code_corps/github/github.ex b/lib/code_corps/github/github.ex index b0d538caf..8994ff705 100644 --- a/lib/code_corps/github/github.ex +++ b/lib/code_corps/github/github.ex @@ -124,6 +124,14 @@ defmodule CodeCorps.GitHub do end end + def get_all(endpoint, headers, options) do + api().get_all( + api_url_for(endpoint), + headers |> Headers.user_request(options), + options |> add_default_options() + ) + end + @token_url "https://github.com/login/oauth/access_token" @doc """ diff --git a/lib/code_corps/github/sync/sync.ex b/lib/code_corps/github/sync/sync.ex index 5bb3d310c..12c00a5c2 100644 --- a/lib/code_corps/github/sync/sync.ex +++ b/lib/code_corps/github/sync/sync.ex @@ -6,14 +6,13 @@ defmodule CodeCorps.GitHub.Sync do GithubPullRequest, GithubRepo, GitHub.Sync.Utils.RepoFinder, - Repo, - Task + Repo } alias Ecto.Multi @type outcome :: {:ok, list(Comment.t)} | {:ok, GithubPullRequest.t} - | {:ok, list(Task.t)} + | {:ok, list(CodeCorps.Task.t)} | {:error, :repo_not_found} | {:error, :fetching_issue} | {:error, :fetching_pull_request} @@ -100,6 +99,24 @@ defmodule CodeCorps.GitHub.Sync do |> transact() end + def sync_issues(repo) do + {:ok, issues} = GitHub.API.Repository.issues(repo) + Enum.map(issues, &sync_issue(&1, repo)) + end + + def sync_issue(issue, repo) do + Multi.new + |> Multi.merge(__MODULE__, :return_repo, [repo]) + |> Multi.merge(GitHub.Sync.Issue, :sync, [issue]) + |> transact() + end + + @doc false + def return_repo(_, repo) do + Multi.new + |> Multi.run(:repo, fn _ -> {:ok, repo} end) + end + @doc false def find_repo(_, payload) do Multi.new diff --git a/lib/code_corps/validators/time_validator.ex b/lib/code_corps/validators/time_validator.ex index 7940c56e0..7e146ab09 100644 --- a/lib/code_corps/validators/time_validator.ex +++ b/lib/code_corps/validators/time_validator.ex @@ -11,7 +11,10 @@ defmodule CodeCorps.Validators.TimeValidator do def validate_time_after(%{data: data} = changeset, field) do previous_time = Map.get(data, field) current_time = Changeset.get_change(changeset, field) - case current_time |> Timex.after?(previous_time) do + is_after = current_time |> Timex.after?(previous_time) + is_equal = current_time |> Timex.equal?(previous_time) + after_or_equal = is_after || is_equal + case after_or_equal do true -> changeset false -> Changeset.add_error(changeset, field, "cannot be before the last recorded time") end diff --git a/mix.exs b/mix.exs index 8d47268e3..c3cc6edc0 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,7 @@ defmodule CodeCorps.Mixfile do {:cowboy, "~> 1.0"}, {:benchfella, "~> 0.3.0", only: :dev}, {:bypass, "~> 0.8.1", only: :test}, - {:cloudex, "~> 0.1.17"}, + {:cloudex, "~> 0.2.2"}, {:comeonin, "~> 3.1"}, {:corsica, "~> 1.0"}, # CORS {:credo, "~> 0.8", only: [:dev, :test]}, # Code style suggestions @@ -66,6 +66,7 @@ defmodule CodeCorps.Mixfile do {:ex_machina, "~> 2.0", only: :test}, # test factories {:guardian, "~> 0.14.5"}, # Authentication (JWT) {:hackney, ">= 1.4.4"}, + {:httpoison, "~> 0.13"}, {:inch_ex, "~> 0.5", only: [:dev, :test]}, # Inch CI {:inflex, "~> 1.8.1"}, {:ja_serializer, "~> 0.12"}, # JSON API @@ -406,6 +407,7 @@ defmodule CodeCorps.Mixfile do "ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.migrate": ["ecto.migrate", "ecto.dump"], "ecto.rollback": ["ecto.rollback", "ecto.dump"], - "test": ["ecto.create --quiet", "ecto.migrate", "test"]] + "test": ["ecto.create --quiet", "ecto.migrate", "test"], + "test.acceptance": ["ecto.create --quiet", "ecto.migrate", "test --include acceptance:true"]] end end diff --git a/mix.lock b/mix.lock index bfa200b24..69aadfb2b 100644 --- a/mix.lock +++ b/mix.lock @@ -5,8 +5,8 @@ "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], []}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, - "cloudex": {:hex, :cloudex, "0.1.20", "c8b0f36da0d33c4756cfbfdd7bf2bcd4722d58a51cb48da75a6c54d6186bad30", [:mix], [{:httpoison, "~> 0.11.0", [hex: :httpoison, optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, optional: false]}, {:timex, "~> 3.1.7", [hex: :timex, optional: false]}, {:tzdata, "~> 0.5.11", [hex: :tzdata, optional: false]}]}, + "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, + "cloudex": {:hex, :cloudex, "0.2.2", "da554dab88672163346a1614917df2a27ae82d07e956ee31de758f0b04c163bf", [:mix], [{:httpoison, "~> 0.13.0", [repo: "hexpm", hex: :httpoison, optional: false]}, {:poison, "~> 3.1.0", [repo: "hexpm", hex: :poison, optional: false]}, {:timex, "~> 3.1.7", [repo: "hexpm", hex: :timex, optional: false]}, {:tzdata, "~> 0.5.11", [repo: "hexpm", hex: :tzdata, optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []}, "comeonin": {:hex, :comeonin, "3.2.0", "cb10995a22aed6812667efb3856f548818c85d85394d8132bc116fbd6995c1ef", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, optional: false]}]}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], []}, @@ -29,9 +29,9 @@ "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, "guardian": {:hex, :guardian, "0.14.5", "6d4e89b673accdacbc092ad000dc7494019426bd898eebf699caf1d19000cdcd", [:mix], [{:jose, "~> 1.8", [hex: :jose, optional: false]}, {:phoenix, "~> 1.2 and < 1.4.0", [hex: :phoenix, optional: true]}, {:plug, "~> 1.3", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0 and < 4.0.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]}, - "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, - "httpoison": {:hex, :httpoison, "0.11.0", "b9240a9c44fc46fcd8618d17898859ba09a3c1b47210b74316c0ffef10735e76", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, - "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, + "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [repo: "hexpm", hex: :certifi, optional: false]}, {:idna, "5.1.0", [repo: "hexpm", hex: :idna, optional: false]}, {:metrics, "1.0.1", [repo: "hexpm", hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [repo: "hexpm", hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [repo: "hexpm", hex: :ssl_verify_fun, optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [repo: "hexpm", hex: :hackney, optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [repo: "hexpm", hex: :unicode_util_compat, optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, "inflex": {:hex, :inflex, "1.8.1", "9fa9684ff1a872eab7415c0be500cc1b7782f28da6ed75423081e75f92831b1c", [:mix], []}, "ja_serializer": {:hex, :ja_serializer, "0.12.0", "ba4ec5fc7afa6daba815b5cb2b9bd0de410554ac4f0ed54e954d39decb353ca4", [:mix], [{:inflex, "~> 1.4", [hex: :inflex, optional: false]}, {:plug, "> 1.0.0", [hex: :plug, optional: false]}, {:poison, ">= 1.4.0", [hex: :poison, optional: false]}, {:scrivener, "~> 1.2 or ~> 2.0", [hex: :scrivener, optional: true]}]}, @@ -65,5 +65,6 @@ "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, "timex_ecto": {:hex, :timex_ecto, "3.0.5", "3ec6c25e10d2c0020958e5df64d2b5e690e441faa2c2259da8bc6bd3d7f39256", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:timex, "~> 3.0", [hex: :timex, optional: false]}]}, "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], []}, "uri_query": {:hex, :uri_query, "0.1.2", "ae35b83b472f3568c2c159eee3f3ccf585375d8a94fb5382db1ea3589e75c3b4", [:mix], []}, "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [:mix], []}} diff --git a/test/fixtures/github/endpoints/issues.json b/test/fixtures/github/endpoints/issues.json new file mode 100644 index 000000000..ce8d2f94c --- /dev/null +++ b/test/fixtures/github/endpoints/issues.json @@ -0,0 +1,400 @@ +[ + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/8", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/8/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/8/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/8/events", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/8", + "id": 180773863, + "number": 8, + "title": "Add a README description", + "user": { + "login": "skalnik", + "id": 2546, + "avatar_url": "https://avatars1.githubusercontent.com/u/2546?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/skalnik", + "html_url": "https://github.com/skalnik", + "followers_url": "https://api.github.com/users/skalnik/followers", + "following_url": "https://api.github.com/users/skalnik/following{/other_user}", + "gists_url": "https://api.github.com/users/skalnik/gists{/gist_id}", + "starred_url": "https://api.github.com/users/skalnik/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/skalnik/subscriptions", + "organizations_url": "https://api.github.com/users/skalnik/orgs", + "repos_url": "https://api.github.com/users/skalnik/repos", + "events_url": "https://api.github.com/users/skalnik/events{/privacy}", + "received_events_url": "https://api.github.com/users/skalnik/received_events", + "type": "User", + "site_admin": true + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2016-10-03T23:37:43Z", + "updated_at": "2016-10-03T23:37:43Z", + "closed_at": null, + "author_association": "NONE", + "pull_request": { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/8", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/8", + "diff_url": "https://github.com/baxterthehacker/public-repo/pull/8.diff", + "patch_url": "https://github.com/baxterthehacker/public-repo/pull/8.patch" + }, + "body": "Just a few more details\n" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/7", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/7/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/7/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/7/events", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/7", + "id": 180773082, + "number": 7, + "title": "Update README.md", + "user": { + "login": "baxterthehacker", + "id": 6752317, + "avatar_url": "https://avatars3.githubusercontent.com/u/6752317?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/baxterthehacker", + "html_url": "https://github.com/baxterthehacker", + "followers_url": "https://api.github.com/users/baxterthehacker/followers", + "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", + "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", + "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", + "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", + "repos_url": "https://api.github.com/users/baxterthehacker/repos", + "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", + "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2016-10-03T23:32:25Z", + "updated_at": "2016-10-03T23:32:25Z", + "closed_at": null, + "author_association": "OWNER", + "pull_request": { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/7", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/7", + "diff_url": "https://github.com/baxterthehacker/public-repo/pull/7.diff", + "patch_url": "https://github.com/baxterthehacker/public-repo/pull/7.patch" + }, + "body": "" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/6", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/6/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/6/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/6/events", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/6", + "id": 180771923, + "number": 6, + "title": "Update README with more details", + "user": { + "login": "skalnik", + "id": 2546, + "avatar_url": "https://avatars1.githubusercontent.com/u/2546?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/skalnik", + "html_url": "https://github.com/skalnik", + "followers_url": "https://api.github.com/users/skalnik/followers", + "following_url": "https://api.github.com/users/skalnik/following{/other_user}", + "gists_url": "https://api.github.com/users/skalnik/gists{/gist_id}", + "starred_url": "https://api.github.com/users/skalnik/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/skalnik/subscriptions", + "organizations_url": "https://api.github.com/users/skalnik/orgs", + "repos_url": "https://api.github.com/users/skalnik/repos", + "events_url": "https://api.github.com/users/skalnik/events{/privacy}", + "received_events_url": "https://api.github.com/users/skalnik/received_events", + "type": "User", + "site_admin": true + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2016-10-03T23:25:02Z", + "updated_at": "2016-10-03T23:25:02Z", + "closed_at": null, + "author_association": "NONE", + "pull_request": { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/6", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/6", + "diff_url": "https://github.com/baxterthehacker/public-repo/pull/6.diff", + "patch_url": "https://github.com/baxterthehacker/public-repo/pull/6.patch" + }, + "body": "Added a few more details\n" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/5", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/5/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/5/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/5/events", + "html_url": "https://github.com/baxterthehacker/public-repo/issues/5", + "id": 146597194, + "number": 5, + "title": "Test task via Github API", + "user": { + "login": "sytzeloor", + "id": 878221, + "avatar_url": "https://avatars1.githubusercontent.com/u/878221?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sytzeloor", + "html_url": "https://github.com/sytzeloor", + "followers_url": "https://api.github.com/users/sytzeloor/followers", + "following_url": "https://api.github.com/users/sytzeloor/following{/other_user}", + "gists_url": "https://api.github.com/users/sytzeloor/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sytzeloor/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sytzeloor/subscriptions", + "organizations_url": "https://api.github.com/users/sytzeloor/orgs", + "repos_url": "https://api.github.com/users/sytzeloor/repos", + "events_url": "https://api.github.com/users/sytzeloor/events{/privacy}", + "received_events_url": "https://api.github.com/users/sytzeloor/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2016-04-07T12:08:05Z", + "updated_at": "2016-04-07T12:18:29Z", + "closed_at": "2016-04-07T12:08:44Z", + "author_association": "NONE", + "body": "This is a test task posted via the Github API\n" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/4", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/4/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/4/events", + "html_url": "https://github.com/baxterthehacker/public-repo/issues/4", + "id": 89658654, + "number": 4, + "title": "Documentation doesn't match repo?", + "user": { + "login": "Efreak", + "id": 749751, + "avatar_url": "https://avatars0.githubusercontent.com/u/749751?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Efreak", + "html_url": "https://github.com/Efreak", + "followers_url": "https://api.github.com/users/Efreak/followers", + "following_url": "https://api.github.com/users/Efreak/following{/other_user}", + "gists_url": "https://api.github.com/users/Efreak/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Efreak/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Efreak/subscriptions", + "organizations_url": "https://api.github.com/users/Efreak/orgs", + "repos_url": "https://api.github.com/users/Efreak/repos", + "events_url": "https://api.github.com/users/Efreak/events{/privacy}", + "received_events_url": "https://api.github.com/users/Efreak/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2015-06-19T20:53:19Z", + "updated_at": "2015-06-22T15:26:33Z", + "closed_at": "2015-06-22T15:26:33Z", + "author_association": "NONE", + "body": "1. According to [docs](https://developer.github.com/v3/activity/events/types/#releaseevent), there is a release. The repo does not have one. Going to the html_url gets me an error.\n2. Several fields mentioned in the documentation are missing or incorrectly names:\n 1. [issue event](https://developer.github.com/v3/activity/events/types/#issuesevent)\n 1. shows a payload.label item, but there is no such item in the example. \n 2. payload.assignee is actually payload.issue.assignee in the example.\n 2. [Push](https://developer.github.com/v3/activity/events/types/#pushevent) is missing the size, \n3. It would be nice if there was a simpler object available. For example, _sender_, _author_, _repository_, etc are always the same type of object; you can remove these entirely and just replace [ReleaseEvent](https://developer.github.com/v3/activity/events/types/#releaseevent)'s example with the following:\n \n ``` javascript\n {\n \"action\": \"published\",\n \"release\": {\n \"url\": \"https://api.github.com/repos/baxterthehacker/public-repo/releases/1261438\",\n \"assets_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/releases/1261438/assets\",\n \"upload_url\": \"https://uploads.github.com/repos/baxterthehacker/public-repo/releases/1261438/assets{?name}\",\n \"html_url\": \"https://github.com/baxterthehacker/public-repo/releases/tag/0.0.1\",\n \"id\": 1261438,\n \"tag_name\": \"0.0.1\",\n \"target_commitish\": \"master\",\n \"name\": null,\n \"draft\": false,\n \"author\": Object, //replaced with a generic\n \"prerelease\": false,\n \"created_at\": \"2015-05-05T23:40:12Z\",\n \"published_at\": \"2015-05-05T23:40:38Z\",\n \"assets\": [\n \n ],\n \"tarball_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/tarball/0.0.1\",\n \"zipball_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/zipball/0.0.1\",\n \"body\": null\n },\n \"repository\": Object, //replaced with generic\n \"sender\": Object //replaced with generic\n }\n ```\n \n I realize that the documentation is just markdown, but it would be even better if there was a json code format that allowed collapsing and expanding objects. Something like this [open source](http://quickjsonformatter.codeplex.com/license) [JsonFormatter](http://www.bodurov.com/JsonFormatter/) or like [CodeMirror](https://github.com/codemirror/codemirror)\n4. I would expect to see unavailable endpoints either not listed or in their own section at the bottom of the page, rather than in between endpoints that are still in use.\n5. It would be nice if there was an endpoint _tester_ that we could use to see exactly what the response would be for specific actions. I don't currently know whether the example code or the documentation is correct when they conflict, and I don't yet have anything to test with.\n" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/3", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/3/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/3/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/3/events", + "html_url": "https://github.com/baxterthehacker/public-repo/issues/3", + "id": 77519650, + "number": 3, + "title": "1", + "user": { + "login": "rou1song", + "id": 10277444, + "avatar_url": "https://avatars3.githubusercontent.com/u/10277444?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rou1song", + "html_url": "https://github.com/rou1song", + "followers_url": "https://api.github.com/users/rou1song/followers", + "following_url": "https://api.github.com/users/rou1song/following{/other_user}", + "gists_url": "https://api.github.com/users/rou1song/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rou1song/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rou1song/subscriptions", + "organizations_url": "https://api.github.com/users/rou1song/orgs", + "repos_url": "https://api.github.com/users/rou1song/repos", + "events_url": "https://api.github.com/users/rou1song/events{/privacy}", + "received_events_url": "https://api.github.com/users/rou1song/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2015-05-18T06:18:32Z", + "updated_at": "2015-05-18T06:35:55Z", + "closed_at": null, + "author_association": "NONE", + "body": "" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/events", + "html_url": "https://github.com/baxterthehacker/public-repo/issues/2", + "id": 73464126, + "number": 2, + "title": "Spelling error in the README file", + "user": { + "login": "baxterthehacker", + "id": 6752317, + "avatar_url": "https://avatars3.githubusercontent.com/u/6752317?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/baxterthehacker", + "html_url": "https://github.com/baxterthehacker", + "followers_url": "https://api.github.com/users/baxterthehacker/followers", + "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", + "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", + "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", + "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", + "repos_url": "https://api.github.com/users/baxterthehacker/repos", + "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", + "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + { + "id": 208045946, + "url": "https://api.github.com/repos/baxterthehacker/public-repo/labels/bug", + "name": "bug", + "color": "fc2929", + "default": true + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 4, + "created_at": "2015-05-05T23:40:28Z", + "updated_at": "2017-09-26T01:26:26Z", + "closed_at": null, + "author_association": "OWNER", + "body": "It looks like you accidently spelled 'commit' with two 't's.\n" + }, + { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1", + "repository_url": "https://api.github.com/repos/baxterthehacker/public-repo", + "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1/comments", + "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1/events", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/1", + "id": 73464123, + "number": 1, + "title": "Update the README with new information", + "user": { + "login": "baxterthehacker", + "id": 6752317, + "avatar_url": "https://avatars3.githubusercontent.com/u/6752317?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/baxterthehacker", + "html_url": "https://github.com/baxterthehacker", + "followers_url": "https://api.github.com/users/baxterthehacker/followers", + "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", + "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", + "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", + "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", + "repos_url": "https://api.github.com/users/baxterthehacker/repos", + "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", + "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 6, + "created_at": "2015-05-05T23:40:27Z", + "updated_at": "2017-03-08T18:21:05Z", + "closed_at": "2015-05-05T23:41:12Z", + "author_association": "OWNER", + "pull_request": { + "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1", + "html_url": "https://github.com/baxterthehacker/public-repo/pull/1", + "diff_url": "https://github.com/baxterthehacker/public-repo/pull/1.diff", + "patch_url": "https://github.com/baxterthehacker/public-repo/pull/1.patch" + }, + "body": "This is a pretty simple change that we need to pull into master.\n" + } +] diff --git a/test/lib/code_corps/github/api/repository_test.exs b/test/lib/code_corps/github/api/repository_test.exs new file mode 100644 index 000000000..11abd5c62 --- /dev/null +++ b/test/lib/code_corps/github/api/repository_test.exs @@ -0,0 +1,51 @@ +defmodule CodeCorps.GitHub.API.RepositoryTest do + @moduledoc false + + use CodeCorps.DbAccessCase + + import CodeCorps.GitHub.TestHelpers + + alias CodeCorps.{ + GitHub.API.Repository + } + + describe "issues/1" do + test "calls github API for issues returns response" do + owner = "baxterthehacker" + repo = "public-repo" + url = "https://api.github.com/repos/#{owner}/#{repo}/issues" + github_app_installation = insert(:github_app_installation, github_account_login: owner) + github_repo = insert(:github_repo, github_app_installation: github_app_installation, name: repo) + + {:ok, issues} = Repository.issues(github_repo) + + assert_received({ + endpoint_url, + [ + {"Accept", "application/vnd.github.machine-man-preview+json"}, + {"Authorization", "token" <> _tok} + ], + [ + :with_body, + _headers, + {:params, [per_page: 100, state: "all"]} + ] + }) + assert url == endpoint_url + assert Enum.count(issues) == 8 + end + + @tag acceptance: true + test "calls github API with the real API" do + owner = "coderly" + repo = "github-app-testing" + github_app_installation = insert(:github_app_installation, github_account_login: owner, github_id: 63365) + github_repo = insert(:github_repo, github_app_installation: github_app_installation, name: repo) + + with_real_api do + {:ok, issues} = Repository.issues(github_repo) + assert Enum.count(issues) == 2 + end + end + end +end diff --git a/test/lib/code_corps/github/sync/sync_test.exs b/test/lib/code_corps/github/sync/sync_test.exs index 6afcfa89a..fe42d5236 100644 --- a/test/lib/code_corps/github/sync/sync_test.exs +++ b/test/lib/code_corps/github/sync/sync_test.exs @@ -86,4 +86,39 @@ defmodule CodeCorps.GitHub.SyncTest do assert comment.user.github_id == comment_user_github_id end end + + describe "sync_issues/1" do + test "syncs issues with the repo" do + owner = "baxterthehacker" + repo = "public-repo" + github_app_installation = insert(:github_app_installation, github_account_login: owner) + github_repo = insert(:github_repo, github_app_installation: github_app_installation, name: repo) + %{project: project} = insert(:project_github_repo, github_repo: github_repo) + insert(:task_list, project: project, inbox: true) + + Sync.sync_issues(github_repo) + + assert Repo.aggregate(GithubComment, :count, :id) == 0 + assert Repo.aggregate(GithubIssue, :count, :id) == 8 + assert Repo.aggregate(GithubPullRequest, :count, :id) == 0 + assert Repo.aggregate(Comment, :count, :id) == 0 + assert Repo.aggregate(Task, :count, :id) == 8 + end + + @tag acceptance: true + test "syncs issues with the repo with the real API" do + github_repo = setup_real_repo() + + with_real_api do + Sync.sync_issues(github_repo) + end + + assert Repo.aggregate(GithubComment, :count, :id) == 0 + assert Repo.aggregate(GithubIssue, :count, :id) == 2 + assert Repo.aggregate(GithubPullRequest, :count, :id) == 0 + assert Repo.aggregate(Comment, :count, :id) == 0 + assert Repo.aggregate(Task, :count, :id) == 2 + assert Repo.aggregate(User, :count, :id) == 1 + end + end end diff --git a/test/support/github/success_api.ex b/test/support/github/success_api.ex index ddaaa033e..7c726d576 100644 --- a/test/support/github/success_api.ex +++ b/test/support/github/success_api.ex @@ -33,6 +33,12 @@ defmodule CodeCorps.GitHub.SuccessAPI do {:ok, mock_response(method, url, headers, body, options)} end + def get_all(url, headers, options) do + send(self(), {url, headers, options}) + + mock_response(:get, url, headers, %{}, options) + end + defp mock_response(:post, "https://github.com/login/oauth/access_token", _, _, _) do %{"access_token" => "foo_auth_token"} end @@ -64,6 +70,9 @@ defmodule CodeCorps.GitHub.SuccessAPI do defp mock_response(:patch, ["repos", _owner, _repo, "issues", "comments", _id], _, _, _) do load_endpoint_fixture("issue_comment") end + defp mock_response(:get, ["repos", _owner, _repo, "issues"], _, _, _) do + load_endpoint_fixture("issues") + end defp mock_response(:get, ["repos", _owner, _repo, "pulls", _number], _, _, _) do load_endpoint_fixture("pull_request") end diff --git a/test/support/github/test_helpers.ex b/test/support/github/test_helpers.ex index ca0d9dba9..add673383 100644 --- a/test/support/github/test_helpers.ex +++ b/test/support/github/test_helpers.ex @@ -1,4 +1,6 @@ defmodule CodeCorps.GitHub.TestHelpers do + import CodeCorps.Factories + @spec load_endpoint_fixture(String.t) :: map def load_endpoint_fixture(id) do "./test/fixtures/github/endpoints/#{id}.json" |> File.read! |> Poison.decode! @@ -9,6 +11,61 @@ defmodule CodeCorps.GitHub.TestHelpers do "./test/fixtures/github/events/#{id}.json" |> File.read! |> Poison.decode! end + @spec setup_real_repo :: %CodeCorps.GithubRepo{} + def setup_real_repo do + # Data is from the real repository + # + # Uses: + # + # - the real repository owner + # - the real repository name + # - the real GitHub user id of the repository owner + # - the real GitHub App id + repo_owner = "coderly" + repo_name = "github-app-testing" + repo_owner_id = 321667 + app_github_id = 63365 + + # Create the user + # + # Simulates: + # + # - user (the repo owner) connecting their account with GitHub + user = insert(:user, github_id: repo_owner_id) + + # Create the organization and project for that organization + # + # Simulates: + # + # - user creating an organization + # - organization creating a project + # - project being bootstrapped with an inbox task list to receive new tasks + organization = insert(:organization, owner: user) + project = insert(:project, organization: organization) + insert(:task_list, project: project, inbox: true) + + # Create the GitHub App installation on the organization + # + # Simulates: + # + # - installation webhook + # - user installing the organization + github_app_installation = insert(:github_app_installation, github_account_login: repo_owner, github_id: app_github_id, project: project, user: user) + insert(:organization_github_app_installation, github_app_installation: github_app_installation, organization: organization) + + # Create the repo on the installation + # + # Simulates: + # + # - installation or installation_repositories webhook + # - user connecting the repository to the project + github_repo = insert(:github_repo, github_app_installation: github_app_installation, name: repo_name) + insert(:project_github_repo, github_repo: github_repo, project: project) + + # Return the %CodeCorps.GithubRepo{} record + github_repo + end + @doc ~S""" Allows setting a mock Github API module for usage in specific tests To use it, define a module containing the methods expected to be called, then @@ -33,4 +90,16 @@ defmodule CodeCorps.GitHub.TestHelpers do Application.put_env(:code_corps, :github, old_mock) end end + + @spec with_real_api(do: function) :: any + defmacro with_real_api(do: block) do + quote do + old_mock = Application.get_env(:code_corps, :github) + Application.put_env(:code_corps, :github, CodeCorps.GitHub.API) + + unquote(block) + + Application.put_env(:code_corps, :github, old_mock) + end + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 07876e70f..1222bc40b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,6 +3,7 @@ {:ok, _} = Application.ensure_all_started(:ex_machina) {:ok, _} = Application.ensure_all_started(:bypass) +ExUnit.configure exclude: [acceptance: true] ExUnit.start Ecto.Adapters.SQL.Sandbox.mode(CodeCorps.Repo, :manual)