~afontaine/home unlisted

b8c67c01dedecb905fdaf9e706a43f88aa503c83 — Andrew Fontaine a month ago e9c8c65
Add some Tests around Post Creation

It didn't work last time, so I added some tests to make sure it works
like I expect.

Also raise any errors to be able to capture them in logs in case it
happens again.
M apps/blog/lib/blog/content.ex => apps/blog/lib/blog/content.ex +14 -9
@@ 7,6 7,7 @@ defmodule Blog.Content do
  alias Blog.Repo

  alias Blog.Content.Post
  alias Blog.Content.PostQueries
  alias Blog.Content.Tag

  @doc """


@@ 19,9 20,7 @@ defmodule Blog.Content do

  """
  def list_posts do
    Post
    |> join(:left, [post], tags in assoc(post, :tags))
    |> preload([_, tags], tags: tags)
    PostQueries.reverse()
    |> Repo.all()
  end



@@ 41,12 40,14 @@ defmodule Blog.Content do
  """
  def get_post!(slug),
    do:
      Post
      |> where([post], post.slug == ^slug)
      |> join(:left, [post], _ in assoc(post, :tags))
      |> join(:left, [post, tag], posts in assoc(tag, :posts), on: post.id != posts.id)
      |> order_by([_, _, posts], desc: posts.inserted_at)
      |> preload([_, tags, posts], tags: {tags, posts: posts})
      PostQueries.with_slug(slug)
      |> Repo.one!()

  def get_post_with_related_posts!(slug),
    do:
      PostQueries.with_slug(slug)
      |> PostQueries.with_related_posts()
      |> preload([tags: tags, posts: posts], tags: {tags, posts: posts})
      |> Repo.one!()

  def last_post(), do: Repo.one(from p in Post, order_by: [desc: p.id], limit: 1)


@@ 117,6 118,10 @@ defmodule Blog.Content do
    Post.changeset(post, attrs)
  end

  def list_tags do
    Repo.all(Tag)
  end

  def fetch_tags(tags) do
    tags
    |> Enum.reject(&(&1 == ""))

A apps/blog/lib/blog/content/post_queries.ex => apps/blog/lib/blog/content/post_queries.ex +43 -0
@@ 0,0 1,43 @@
defmodule Blog.Content.PostQueries do
  import Ecto.Query

  alias Blog.Content.Post

  def all_posts(query \\ base()), do: query

  def with_slug(slug, query \\ base()), do: where(query, [post: post], post.slug == ^slug)

  def with_tags(query \\ base()),
    do: with_join(query, :tags)

  def with_related_posts(query \\ base()),
    do:
      query
      |> with_join(:posts)

  def reverse(query \\ base()), do: order_by(query, [post: post], desc: post.inserted_at)

  defp with_join(query, :posts) do
    if has_named_binding?(query, :posts) do
      query
    else
      query
      |> with_join(:tags)
      |> join(:left, [post: post, tags: tags], posts in assoc(tags, :posts),
        as: :posts,
        on: post.id != posts.id
      )
    end
  end

  defp with_join(query, :tags) do
    if has_named_binding?(query, :tags) do
      query
    else
      query
      |> join(:left, [post: post], _ in assoc(post, :tags), as: :tags)
    end
  end

  defp base, do: from(_ in Post, as: :post)
end

M apps/blog/lib/blog/mail/post.ex => apps/blog/lib/blog/mail/post.ex +1 -1
@@ 42,7 42,7 @@ defmodule Blog.Mail.Post do

  defp put_author(%Ecto.Changeset{valid?: true, changes: %{envelope: envelope}} = changeset) do
    case Mail.get_from(envelope) do
      {name, addr} -> put_change(changeset, :author, "<#{name} #{addr}>")
      {name, addr} -> put_change(changeset, :author, "#{name} <#{addr}>")
      author -> put_change(changeset, :author, author)
    end
  end

M apps/blog/mix.exs => apps/blog/mix.exs +2 -1
@@ 42,7 42,8 @@ defmodule Blog.MixProject do
      {:postgrex, ">= 0.0.0"},
      {:jason, "~> 1.0"},
      {:slugy, "~> 4.1"},
      {:mail, "~> 0.2.2"}
      {:mail, "~> 0.2.2"},
      {:ex_machina, "~>2.7", only: :test}
    ]
  end


M apps/blog/test/blog/content_test.exs => apps/blog/test/blog/content_test.exs +22 -14
@@ 9,22 9,21 @@ defmodule Blog.ContentTest do
    @valid_attrs %{
      message_id: "some message_id",
      subject: "some subject",
      text: "some text"
      text: "some text",
      author: "some author",
      tags: ["some tag"]
    }
    @update_attrs %{
      message_id: "some updated message_id",
      subject: "some updated subject",
      text: "some updated text"
      text: "some updated text",
      author: "some updated author",
      tags: [%{tag: "some updated tag"}]
    }
    @invalid_attrs %{message_id: nil, subject: nil, text: nil}
    @invalid_attrs %{message_id: nil, subject: nil, text: nil, tags: [], author: nil}

    def post_fixture(attrs \\ %{}) do
      {:ok, post} =
        attrs
        |> Enum.into(@valid_attrs)
        |> Content.create_post()

      post
    def post_fixture do
      Factory.insert(:post)
    end

    test "list_posts/0 returns all posts" do


@@ 49,7 48,10 @@ defmodule Blog.ContentTest do
    end

    test "update_post/2 with valid data updates the post" do
      post = post_fixture()
      post =
        post_fixture()
        |> Repo.preload(:tags)

      assert {:ok, %Post{} = post} = Content.update_post(post, @update_attrs)
      assert post.message_id == "some updated message_id"
      assert post.subject == "some updated subject"


@@ 57,9 59,12 @@ defmodule Blog.ContentTest do
    end

    test "update_post/2 with invalid data returns error changeset" do
      post = post_fixture()
      post =
        post_fixture()
        |> Repo.preload(:tags)

      assert {:error, %Ecto.Changeset{}} = Content.update_post(post, @invalid_attrs)
      assert post == Content.get_post!(post.slug)
      assert post == Content.get_post!(post.slug) |> Repo.preload(:tags)
    end

    test "delete_post/1 deletes the post" do


@@ 69,7 74,10 @@ defmodule Blog.ContentTest do
    end

    test "change_post/1 returns a post changeset" do
      post = post_fixture()
      post =
        post_fixture()
        |> Repo.preload(:tags)

      assert %Ecto.Changeset{} = Content.change_post(post)
    end
  end

M apps/blog/test/support/data_case.ex => apps/blog/test/support/data_case.ex +2 -0
@@ 24,6 24,8 @@ defmodule Blog.DataCase do
      import Ecto.Changeset
      import Ecto.Query
      import Blog.DataCase

      alias Blog.Factory
    end
  end


A apps/blog/test/support/factory.ex => apps/blog/test/support/factory.ex +24 -0
@@ 0,0 1,24 @@
defmodule Blog.Factory do
  use ExMachina.Ecto, repo: Blog.Repo

  alias Blog.Content.Tag
  alias Blog.Content.Post

  def tag_factory do
    tag = sequence(:tag, &"tag-#{&1}")

    %Tag{
      tag: tag
    }
  end

  def post_factory do
    %Post{
      author: "test",
      subject: sequence(:subject, &"This is a Post #{&1}"),
      message_id: sequence(:message_id, &"#{&1}"),
      text: "hello world",
      slug: sequence(:slug, &"this-is-a-post-#{&1}")
    }
  end
end

M apps/blog/test/test_helper.exs => apps/blog/test/test_helper.exs +1 -0
@@ 1,2 1,3 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Blog.Repo, :manual)
{:ok, _} = Application.ensure_all_started(:ex_machina)

M apps/home_web/lib/home_web/controllers/post_controller.ex => apps/home_web/lib/home_web/controllers/post_controller.ex +3 -4
@@ 4,15 4,14 @@ defmodule HomeWeb.PostController do
  alias Blog.Content

  def index(conn, _params) do
    posts =
      Content.list_posts()
      |> Enum.reverse()
    posts = Content.list_posts()

    render(conn, "index.html", posts: posts)
  end

  def show(conn, %{"slug" => slug}) do
    post = Content.get_post!(slug)
    post = Content.get_post_with_related_posts!(slug)

    render(conn, "show.html", post: post)
  end
end

M apps/home_web/lib/home_web/controllers/receive_controller.ex => apps/home_web/lib/home_web/controllers/receive_controller.ex +1 -1
@@ 11,7 11,7 @@ defmodule HomeWeb.ReceiveController do
        |> Content.create_post()

      x ->
        x
        raise x
    end

    conn

M apps/home_web/mix.exs => apps/home_web/mix.exs +2 -1
@@ 50,7 50,8 @@ defmodule HomeWeb.MixProject do
      {:atomex, "~> 0.4.1"},
      {:tzdata, "~> 1.1"},
      {:home, in_umbrella: true},
      {:blog, in_umbrella: true}
      {:blog, in_umbrella: true},
      {:ex_machina, "~>2.7", only: :test}
    ]
  end


M apps/home_web/test/home_web/controllers/page_controller_test.exs => apps/home_web/test/home_web/controllers/page_controller_test.exs +1 -1
@@ 3,6 3,6 @@ defmodule HomeWeb.PageControllerTest do

  test "GET /", %{conn: conn} do
    conn = get(conn, "/")
    assert html_response(conn, 200) =~ "Welcome to Phoenix!"
    assert html_response(conn, 200) =~ "Andrew Fontaine"
  end
end

M apps/home_web/test/home_web/controllers/post_controller_test.exs => apps/home_web/test/home_web/controllers/post_controller_test.exs +7 -1
@@ 3,7 3,13 @@ defmodule HomeWeb.PostControllerTest do

  alias Blog.Content

  @create_attrs %{message_id: "some message_id", subject: "some subject", text: "some text"}
  @create_attrs %{
    message_id: "some message_id",
    subject: "some subject",
    text: "some text",
    tags: [],
    author: "some author"
  }

  def fixture(:post) do
    {:ok, post} = Content.create_post(@create_attrs)

A apps/home_web/test/home_web/controllers/receive_controller_test.exs => apps/home_web/test/home_web/controllers/receive_controller_test.exs +111 -0
@@ 0,0 1,111 @@
defmodule HomeWeb.ReceiveControllerTest do
  use HomeWeb.ConnCase

  alias Blog.Content
  alias Blog.Content.Tag

  @valid_attrs %{
    "id" => 153_012,
    "created" => "2021-03-27T17:31:05+00:00",
    "subject" => "[nix, elixir] Test Email Subject",
    "message_id" => "<87k0psv70b.fsf@example.com>",
    "parent_id" => nil,
    "thread_id" => nil,
    "sender" => %{
      "canonical_name" => "~afontaine",
      "name" => "afontaine"
    },
    "patchset" => nil,
    "is_patch" => false,
    "is_request_pull" => false,
    "replies" => 0,
    "participants" => 0,
    "envelope" =>
      Enum.join(
        [
          "From nobody Sat Mar 27 17:31:05 2021",
          "Authentication-Results: mail-b.sr.ht; dkim=pass header.d=afontaine.ca header.i=@afontaine.ca",
          "Received: from mail.example.com (mail.example.com [127.0.0.1])",
          "\tby mail-b.sr.ht (Postfix) with ESMTPS id 9FE5811EF0E",
          "\tfor <~afontaine/example@lists.sr.ht>; Sat, 27 Mar 2021 17:31:03 +0000 (UTC)",
          "Date: Sat, 27 Mar 2021 17:30:53 +0000",
          "To: ~afontaine/blog-testing@lists.sr.ht",
          "From: Andrew Fontaine <andrew@example.com>",
          "Reply-To: Andrew Fontaine <andrew@example.com>",
          "Subject: Test Email Subject",
          "Message-ID: <87k0psv70b.fsf@example.com>",
          "MIME-Version: 1.0",
          "Content-Type: text/plain; charset=utf-8",
          "Content-Transfer-Encoding: quoted-printable",
          "",
          "",
          "test body",
          "",
          "--=20",
          "",
          "Andrew Fontaine",
          "",
          ""
        ],
        "\r\n"
      )
  }
  describe "POST /api/post/receive" do
    setup %{conn: conn} = context do
      post(conn, "/api/post/receive", @valid_attrs)
      [post] = Content.list_posts() |> Blog.Repo.preload(:tags)
      Map.put(context, :post, post)
    end

    test "creates a blog post", %{post: post} do
      assert @valid_attrs["subject"] =~ "#{post.subject}"
      assert post.text == "test body"
    end

    test "creates all the tags for a post", %{post: post} do
      tags =
        post.tags
        |> Enum.map(fn %Tag{tag: t} -> t end)
        |> Enum.join(", ")

      assert @valid_attrs["subject"] =~ "[#{tags}]"
    end

    test "sets the author for the post", %{post: post} do
      assert "Andrew Fontaine <andrew@example.com>" = post.author
    end

    test "sets the message ID", %{post: post} do
      assert @valid_attrs["message_id"] == post.message_id
    end
  end

  describe "with existing tags" do
    setup %{conn: conn} = context do
      tag = Factory.insert(:tag)

      post(conn, "/api/post/receive", %{
        @valid_attrs
        | "subject" => "[#{tag.tag}, test] an existing tag"
      })

      context
      |> Map.put(:tag, tag)
      |> Map.put(:post, List.last(Content.list_posts()))
    end

    test "doesn't create duplicate tags", %{tag: tag} do
      assert 1 ==
               Blog.Content.list_tags()
               |> Enum.filter(fn %{tag: t} -> t == tag.tag end)
               |> length()
    end

    test "still creates new tags" do
      assert 1 ==
               Blog.Content.list_tags()
               |> Enum.filter(fn %{tag: t} -> t == "test" end)
               |> length()
    end
  end
end

M apps/home_web/test/support/conn_case.ex => apps/home_web/test/support/conn_case.ex +1 -0
@@ 25,6 25,7 @@ defmodule HomeWeb.ConnCase do
      import HomeWeb.ConnCase

      alias HomeWeb.Router.Helpers, as: Routes
      alias Blog.Factory

      # The default endpoint for testing
      @endpoint HomeWeb.Endpoint

M apps/home_web/test/test_helper.exs => apps/home_web/test/test_helper.exs +1 -0
@@ 1,2 1,3 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Blog.Repo, :manual)
{:ok, _} = Application.ensure_all_started(:ex_machina)

M mix.exs => mix.exs +1 -3
@@ 44,9 44,7 @@ defmodule Home.Umbrella.MixProject do
  defp aliases do
    [
      # run `mix setup` in all child apps
      setup: ["cmd mix setup"],
      prettier: "cmd prettier --write . --color --ignore-path ./.gitignore --loglevel warn",
      format: ["format", "prettier"]
      setup: ["cmd mix setup"]
    ]
  end
end

M mix.lock => mix.lock +1 -0
@@ 11,6 11,7 @@
  "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
  "ecto": {:hex, :ecto, "3.5.8", "8ebf12be6016cb99313348ba7bb4612f4114b9a506d6da79a2adc7ef449340bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea0be182ea8922eb7742e3ae8e71b67ee00ae177de1bf76210299a5f16ba4c77"},
  "ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
  "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
  "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
  "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},