create posts table
This commit is contained in:
parent
d7ac169607
commit
b051e06454
21 changed files with 577 additions and 41 deletions
.credo.exs.formatter.exs
config
lib
mix.exsmix.lockpriv/repo/migrations
test
56
.credo.exs
56
.credo.exs
|
@ -100,34 +100,34 @@
|
|||
# Some of the rules have `priority: :high`, meaning Credo runs them unless we explicitly disable them
|
||||
# (removing them from this file wouldn't be enough, the `false` is required)
|
||||
#
|
||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, false},
|
||||
{Credo.Check.Design.AliasUsage, false},
|
||||
{Credo.Check.Readability.AliasOrder, false},
|
||||
{Credo.Check.Readability.BlockPipe, false},
|
||||
{Credo.Check.Readability.LargeNumbers, false},
|
||||
{Credo.Check.Readability.ModuleDoc, false},
|
||||
{Credo.Check.Readability.MultiAlias, false},
|
||||
{Credo.Check.Readability.OneArityFunctionInPipe, false},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, false},
|
||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, false},
|
||||
{Credo.Check.Readability.PreferImplicitTry, false},
|
||||
{Credo.Check.Readability.SinglePipe, false},
|
||||
{Credo.Check.Readability.StrictModuleLayout, false},
|
||||
{Credo.Check.Readability.StringSigils, false},
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, false},
|
||||
{Credo.Check.Readability.WithSingleClause, false},
|
||||
{Credo.Check.Refactor.CaseTrivialMatches, false},
|
||||
{Credo.Check.Refactor.CondStatements, false},
|
||||
{Credo.Check.Refactor.FilterCount, false},
|
||||
{Credo.Check.Refactor.MapInto, false},
|
||||
{Credo.Check.Refactor.MapJoin, false},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, false},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, false},
|
||||
{Credo.Check.Refactor.PipeChainStart, false},
|
||||
{Credo.Check.Refactor.RedundantWithClauseResult, false},
|
||||
{Credo.Check.Refactor.UnlessWithElse, false},
|
||||
{Credo.Check.Refactor.WithClauses, false},
|
||||
# {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
|
||||
# {Credo.Check.Consistency.ParameterPatternMatching, false},
|
||||
# {Credo.Check.Design.AliasUsage, false},
|
||||
# {Credo.Check.Readability.AliasOrder, false},
|
||||
# {Credo.Check.Readability.BlockPipe, false},
|
||||
# {Credo.Check.Readability.LargeNumbers, false},
|
||||
# {Credo.Check.Readability.ModuleDoc, false},
|
||||
# {Credo.Check.Readability.MultiAlias, false},
|
||||
# {Credo.Check.Readability.OneArityFunctionInPipe, false},
|
||||
# {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false},
|
||||
# {Credo.Check.Readability.PipeIntoAnonymousFunctions, false},
|
||||
# {Credo.Check.Readability.PreferImplicitTry, false},
|
||||
# {Credo.Check.Readability.SinglePipe, false},
|
||||
# {Credo.Check.Readability.StrictModuleLayout, false},
|
||||
# {Credo.Check.Readability.StringSigils, false},
|
||||
# {Credo.Check.Readability.UnnecessaryAliasExpansion, false},
|
||||
# {Credo.Check.Readability.WithSingleClause, false},
|
||||
# {Credo.Check.Refactor.CaseTrivialMatches, false},
|
||||
# {Credo.Check.Refactor.CondStatements, false},
|
||||
# {Credo.Check.Refactor.FilterCount, false},
|
||||
# {Credo.Check.Refactor.MapInto, false},
|
||||
# {Credo.Check.Refactor.MapJoin, false},
|
||||
# {Credo.Check.Refactor.NegatedConditionsInUnless, false},
|
||||
# {Credo.Check.Refactor.NegatedConditionsWithElse, false},
|
||||
# {Credo.Check.Refactor.PipeChainStart, false},
|
||||
# {Credo.Check.Refactor.RedundantWithClauseResult, false},
|
||||
# {Credo.Check.Refactor.UnlessWithElse, false},
|
||||
# {Credo.Check.Refactor.WithClauses, false},
|
||||
|
||||
#
|
||||
# Checks scheduled for next check update (opt-in for now)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[
|
||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||
subdirectories: ["priv/*/migrations"],
|
||||
plugins: [Styler, Phoenix.LiveView.HTMLFormatter],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
|
||||
]
|
||||
|
|
|
@ -3,7 +3,8 @@ import Config
|
|||
config :esbuild,
|
||||
version: "0.17.11",
|
||||
sloanely_but_surely: [
|
||||
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||
args:
|
||||
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
]
|
||||
|
@ -27,7 +28,9 @@ config :sloanely_but_surely, Web.Endpoint,
|
|||
config :sloanely_but_surely,
|
||||
namespace: Core,
|
||||
ecto_repos: [Core.Repo],
|
||||
generators: [timestamp_type: :utc_datetime, binary_id: true]
|
||||
generators: [timestamp_type: :utc_datetime_usec, binary_id: true]
|
||||
|
||||
config :sloanely_but_surely, Core.Repo, migration_timestamps: [type: :utc_datetime_usec]
|
||||
|
||||
config :tailwind,
|
||||
version: "3.4.3",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Core do
|
||||
@moduledoc false
|
||||
use Boundary, deps: [Schema], exports: [Accounts]
|
||||
use Boundary, deps: [Schema], exports: [Accounts, Posts]
|
||||
end
|
||||
|
|
55
lib/core/posts.ex
Normal file
55
lib/core/posts.ex
Normal file
|
@ -0,0 +1,55 @@
|
|||
defmodule Core.Posts do
|
||||
@moduledoc false
|
||||
|
||||
alias Core.Posts.Post
|
||||
|
||||
def get!(id) do
|
||||
Core.Repo.get!(Schema.Post, id)
|
||||
end
|
||||
|
||||
def get_by_publish_date_and_slug!(%Date{} = publish_date, slug) when is_binary(slug) do
|
||||
publish_date
|
||||
|> Post.Query.where_publish_date_and_slug(slug)
|
||||
|> Core.Repo.one!()
|
||||
end
|
||||
|
||||
def change_post(%Schema.Post{} = post \\ %Schema.Post{}, attrs) do
|
||||
Post.content_changeset(post, attrs)
|
||||
end
|
||||
|
||||
def create_post(attrs) do
|
||||
attrs
|
||||
|> change_post()
|
||||
|> Core.Repo.insert()
|
||||
end
|
||||
|
||||
def update_post(%Schema.Post{} = post, attrs) do
|
||||
post
|
||||
|> change_post(attrs)
|
||||
|> Core.Repo.update()
|
||||
end
|
||||
|
||||
def publish_post(%Schema.Post{} = post, published_at \\ DateTime.utc_now()) do
|
||||
post
|
||||
|> Post.publish_changeset(published_at)
|
||||
|> Core.Repo.update()
|
||||
end
|
||||
|
||||
def delete_post(%Schema.Post{} = post, deleted_at \\ DateTime.utc_now()) do
|
||||
post
|
||||
|> Post.delete_changeset(deleted_at)
|
||||
|> Core.Repo.update()
|
||||
end
|
||||
|
||||
def unpublish_post(%Schema.Post{} = post) do
|
||||
post
|
||||
|> Post.unpublish_changeset()
|
||||
|> Core.Repo.update()
|
||||
end
|
||||
|
||||
def undelete_post(%Schema.Post{} = post) do
|
||||
post
|
||||
|> Post.undelete_changeset()
|
||||
|> Core.Repo.update()
|
||||
end
|
||||
end
|
100
lib/core/posts/post.ex
Normal file
100
lib/core/posts/post.ex
Normal file
|
@ -0,0 +1,100 @@
|
|||
defmodule Core.Posts.Post do
|
||||
@moduledoc false
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
def content_changeset(%Schema.Post{} = post, attrs) do
|
||||
changeset =
|
||||
post
|
||||
|> cast(attrs, [:tid, :kind, :slug, :title, :body])
|
||||
|> validate_required([:kind], message: "must have a kind")
|
||||
|> validate_required([:body], message: "must have a body")
|
||||
|
||||
changeset =
|
||||
case get_field(changeset, :kind) do
|
||||
:blog ->
|
||||
validate_required(changeset, [:title, :slug])
|
||||
|
||||
:status ->
|
||||
changeset
|
||||
|> put_change(:slug, nil)
|
||||
|> put_change(:title, nil)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
|
||||
changeset
|
||||
|> validate_format(:slug, ~r/^[a-z](?:[a-z-]*[a-z])?$/)
|
||||
|> unique_constraint([:slug])
|
||||
end
|
||||
|
||||
def publish_changeset(%Schema.Post{} = post, published_at) do
|
||||
changeset = change(post)
|
||||
|
||||
if is_nil(get_field(changeset, :published_at)) do
|
||||
put_change(changeset, :published_at, published_at)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def delete_changeset(%Schema.Post{} = post, deleted_at) do
|
||||
changeset = change(post)
|
||||
|
||||
if is_nil(get_field(changeset, :deleted_at)) do
|
||||
put_change(changeset, :deleted_at, deleted_at)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def unpublish_changeset(%Schema.Post{} = post) do
|
||||
post
|
||||
|> change()
|
||||
|> put_change(:published_at, nil)
|
||||
end
|
||||
|
||||
def undelete_changeset(%Schema.Post{} = post) do
|
||||
post
|
||||
|> change()
|
||||
|> put_change(:deleted_at, nil)
|
||||
end
|
||||
|
||||
defmodule Query do
|
||||
@moduledoc false
|
||||
import Ecto.Query
|
||||
|
||||
def base do
|
||||
from _ in Schema.Post, as: :posts
|
||||
end
|
||||
|
||||
def current(query \\ base()) do
|
||||
where(query, [posts: p], is_nil(p.deleted_at))
|
||||
end
|
||||
|
||||
def default_order(query \\ base()) do
|
||||
order_by(query, [posts: p], desc: :inserted_at, desc: :updated_at)
|
||||
end
|
||||
|
||||
def where_publish_date_and_slug(query \\ current(), publish_date, slug) do
|
||||
where(
|
||||
query,
|
||||
[posts: p],
|
||||
p.slug == ^slug and
|
||||
fragment("(? at time zone 'UTC' at time zone 'America/New_York')::date", p.published_at) ==
|
||||
^publish_date
|
||||
)
|
||||
end
|
||||
|
||||
def published(query \\ base()) do
|
||||
query
|
||||
|> current()
|
||||
|> where([posts: p], not is_nil(p.published_at))
|
||||
end
|
||||
|
||||
def deleted(query \\ base()) do
|
||||
where(query, [posts: p], not is_nil(p.deleted_at))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,15 @@
|
|||
defmodule Schema do
|
||||
@moduledoc false
|
||||
use Boundary, deps: [], exports: [User, UserToken]
|
||||
use Boundary, deps: [], exports: [Post, User, UserToken]
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
@type t() :: %__MODULE__{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
21
lib/schema/post.ex
Normal file
21
lib/schema/post.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Schema.Post do
|
||||
@moduledoc false
|
||||
use Schema
|
||||
|
||||
@post_kinds ~w[status blog]a
|
||||
|
||||
schema "posts" do
|
||||
field :tid, :string
|
||||
field :kind, Ecto.Enum, values: @post_kinds
|
||||
field :slug, :string
|
||||
field :title, :string
|
||||
field :body, :string
|
||||
|
||||
field :published_at, :utc_datetime_usec
|
||||
field :deleted_at, :utc_datetime_usec
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def kinds, do: @post_kinds
|
||||
end
|
|
@ -1,9 +1,7 @@
|
|||
defmodule Schema.User do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
use Schema
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users" do
|
||||
field :username, :string
|
||||
field :password, :string, virtual: true, redact: true
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
defmodule Schema.UserToken do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
use Schema
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users_tokens" do
|
||||
field :token, :binary
|
||||
field :context, :string
|
||||
|
|
21
lib/web/controllers/post_controller.ex
Normal file
21
lib/web/controllers/post_controller.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Web.PostController do
|
||||
use Web, :controller
|
||||
|
||||
def show(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do
|
||||
with {year, ""} <- Integer.parse(year),
|
||||
{month, ""} <- Integer.parse(month),
|
||||
{day, ""} <- Integer.parse(day),
|
||||
{:ok, publish_date} <- Date.new(year, month, day) do
|
||||
post = Core.Posts.get_by_publish_date_and_slug!(publish_date, slug)
|
||||
|
||||
conn
|
||||
|> render_post(post)
|
||||
else
|
||||
_ -> raise Ecto.NoResultsError
|
||||
end
|
||||
end
|
||||
|
||||
defp render_post(conn, %Schema.Post{kind: :blog} = blog) do
|
||||
render(conn, :show_blog, blog: blog)
|
||||
end
|
||||
end
|
5
lib/web/controllers/post_html.ex
Normal file
5
lib/web/controllers/post_html.ex
Normal file
|
@ -0,0 +1,5 @@
|
|||
defmodule Web.PostHTML do
|
||||
use Web, :html
|
||||
|
||||
embed_templates "post_html/*"
|
||||
end
|
9
lib/web/controllers/post_html/show_blog.html.heex
Normal file
9
lib/web/controllers/post_html/show_blog.html.heex
Normal file
|
@ -0,0 +1,9 @@
|
|||
<article>
|
||||
<header class="mb-2">
|
||||
<h1 class="text-2xl font-bold">{@blog.title}</h1>
|
||||
</header>
|
||||
|
||||
<p>{@blog.body}</p>
|
||||
|
||||
<%!-- <footer class="mt-4 border-t border-gray-200"></footer> --%>
|
||||
</article>
|
|
@ -40,6 +40,8 @@ defmodule Web.Router do
|
|||
|
||||
delete "/admin/users/log_out", UserSessionController, :delete
|
||||
|
||||
get "/:year/:month/:day/:slug", PostController, :show
|
||||
|
||||
# live_session :current_user, on_mount: [{Web.UserAuth, :mount_current_user}] do
|
||||
# end
|
||||
end
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -55,8 +55,8 @@ defmodule SlaonelyButSurely.MixProject do
|
|||
{:boundary, "~> 0.10.4"},
|
||||
|
||||
# Added dev and/or test dependencies
|
||||
{:styler, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:faker, "~> 0.18", only: :test}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -16,6 +16,7 @@
|
|||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
|
||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||
|
|
35
priv/repo/migrations/20250325174616_create_posts_table.exs
Normal file
35
priv/repo/migrations/20250325174616_create_posts_table.exs
Normal file
|
@ -0,0 +1,35 @@
|
|||
defmodule Core.Repo.Migrations.CreatePostsTable do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:posts, primary_key: false) do
|
||||
add :id, :uuid, primary_key: true
|
||||
add :tid, :text
|
||||
|
||||
add :kind, :text, null: false
|
||||
add :body, :text, null: false
|
||||
|
||||
# blog only fields
|
||||
add :slug, :text
|
||||
add :title, :text
|
||||
|
||||
add :published_at, :utc_datetime_usec
|
||||
add :deleted_at, :utc_datetime_usec
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:posts, [:kind])
|
||||
create unique_index(:posts, [:slug])
|
||||
create index(:posts, [:published_at])
|
||||
|
||||
create index(
|
||||
:posts,
|
||||
["((published_at at time zone 'UTC' at time zone 'America/New_York')::date)"],
|
||||
name: :posts_published_at_date_index
|
||||
)
|
||||
|
||||
create index(:posts, [:deleted_at])
|
||||
create index(:posts, [:inserted_at])
|
||||
create index(:posts, [:updated_at])
|
||||
end
|
||||
end
|
194
test/core/posts_test.exs
Normal file
194
test/core/posts_test.exs
Normal file
|
@ -0,0 +1,194 @@
|
|||
defmodule Test.Core.PostsTest do
|
||||
use Test.DataCase, async: true
|
||||
|
||||
describe "get!/1" do
|
||||
@tag post: :post
|
||||
test "returns the post with the given id", %{posts: %{post: post}} do
|
||||
assert result = Core.Posts.get!(post.id)
|
||||
assert %Schema.Post{} = result
|
||||
assert result.id == post.id
|
||||
end
|
||||
|
||||
test "raises an Ecto.NoResultsError if there is no matching post" do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Core.Posts.get!(Faker.UUID.v4())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_by_publish_date_and_slug!/2" do
|
||||
@tag blog: :blog
|
||||
test "returns the post with the given publish date and slug", %{blogs: %{blog: blog}} do
|
||||
assert {:ok, blog} = Core.Posts.publish_post(blog)
|
||||
publish_date = DateTime.to_date(blog.published_at)
|
||||
assert result = Core.Posts.get_by_publish_date_and_slug!(publish_date, blog.slug)
|
||||
assert %Schema.Post{} = result
|
||||
assert result.id == blog.id
|
||||
end
|
||||
|
||||
@tag blog: :blog
|
||||
test "raises an Ecto.NoResultsError if there is no matching post", %{blogs: %{blog: blog}} do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Core.Posts.get_by_publish_date_and_slug!(~D[1993-11-28], "first-day-on-earth")
|
||||
end
|
||||
|
||||
assert {:ok, blog} = Core.Posts.publish_post(blog)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Core.Posts.get_by_publish_date_and_slug!(~D[1993-11-28], blog.slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_post/1/2" do
|
||||
test "returns a changeset" do
|
||||
assert %Ecto.Changeset{} = Core.Posts.change_post(%{})
|
||||
assert %Ecto.Changeset{} = Core.Posts.change_post(%Schema.Post{}, %{})
|
||||
|
||||
assert %Ecto.Changeset{} =
|
||||
Test.Fixtures.Posts.blog_post(:blog)
|
||||
|> Core.Posts.change_post()
|
||||
|
||||
assert %Ecto.Changeset{} =
|
||||
%Schema.Post{}
|
||||
|> Core.Posts.change_post(Test.Fixtures.Posts.blog_post(:blog))
|
||||
|
||||
assert %Ecto.Changeset{} =
|
||||
Test.Fixtures.Posts.status_post(:status)
|
||||
|> Core.Posts.change_post()
|
||||
|
||||
assert %Ecto.Changeset{} =
|
||||
%Schema.Post{}
|
||||
|> Core.Posts.change_post(Test.Fixtures.Posts.status_post(:status))
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_post/1" do
|
||||
test "creates a status post" do
|
||||
assert {:ok, %Schema.Post{}} =
|
||||
Test.Fixtures.Posts.status_post(:status)
|
||||
|> Core.Posts.create_post()
|
||||
end
|
||||
|
||||
test "creates a blug post" do
|
||||
assert {:ok, %Schema.Post{}} =
|
||||
Test.Fixtures.Posts.blog_post(:blog)
|
||||
|> Core.Posts.create_post()
|
||||
end
|
||||
|
||||
test "returns an error changeset for invalid attrs" do
|
||||
assert {:error, %Ecto.Changeset{} = changeset} = Core.Posts.create_post(%{})
|
||||
refute changeset.valid?
|
||||
assert "must have a kind" in errors_on(changeset).kind
|
||||
assert "must have a body" in errors_on(changeset).body
|
||||
|
||||
assert {:error, %Ecto.Changeset{} = changeset} = Core.Posts.create_post(%{kind: :blog})
|
||||
refute changeset.valid?
|
||||
assert "must have a body" in errors_on(changeset).body
|
||||
assert "can't be blank" in errors_on(changeset).title
|
||||
assert "can't be blank" in errors_on(changeset).slug
|
||||
|
||||
assert {:error, %Ecto.Changeset{} = changeset} =
|
||||
Core.Posts.create_post(%{
|
||||
kind: :blog,
|
||||
title: "Hello, World!",
|
||||
slug: "hello-world!",
|
||||
body: "This is a sample blog posts."
|
||||
})
|
||||
|
||||
refute changeset.valid?
|
||||
assert "has invalid format" in errors_on(changeset).slug
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_post/2" do
|
||||
@tag blog: :to_update
|
||||
test "updates a blog post", %{blogs: %{to_update: post}} do
|
||||
assert {:ok, %Schema.Post{} = post} = Core.Posts.update_post(post, %{title: "A new title"})
|
||||
assert post.tid == "to_update"
|
||||
assert post.title == "A new title"
|
||||
end
|
||||
|
||||
@tag status: :to_update
|
||||
test "updates a status post", %{statuses: %{to_update: post}} do
|
||||
assert {:ok, %Schema.Post{} = post} = Core.Posts.update_post(post, %{body: "Hello, World!"})
|
||||
assert post.tid == "to_update"
|
||||
assert post.body == "Hello, World!"
|
||||
end
|
||||
end
|
||||
|
||||
describe "publish_post/1/2" do
|
||||
@describetag post: :to_publish
|
||||
|
||||
test "publishes a post", %{posts: %{to_publish: to_publish}} do
|
||||
refute to_publish.published_at
|
||||
assert {:ok, published} = Core.Posts.publish_post(to_publish)
|
||||
assert published.tid == "to_publish"
|
||||
assert published.published_at
|
||||
end
|
||||
|
||||
test "does not update an already published post", %{posts: %{to_publish: to_publish}} do
|
||||
assert {:ok, published} = Core.Posts.publish_post(to_publish)
|
||||
assert {:ok, published_again} = Core.Posts.publish_post(published)
|
||||
assert published.updated_at == published_again.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_post/1/2" do
|
||||
@describetag post: :to_delete
|
||||
|
||||
test "soft deletes a post", %{posts: %{to_delete: post}} do
|
||||
refute post.deleted_at
|
||||
assert {:ok, post} = Core.Posts.delete_post(post)
|
||||
assert post.deleted_at
|
||||
end
|
||||
|
||||
test "does not update an already deleted post", %{posts: %{to_delete: to_delete}} do
|
||||
assert {:ok, deleted} = Core.Posts.delete_post(to_delete)
|
||||
assert {:ok, deleted_again} = Core.Posts.delete_post(deleted)
|
||||
assert deleted.updated_at == deleted_again.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "unpublish_post/1" do
|
||||
@describetag posts: [:published, :not_published]
|
||||
setup %{posts: %{published: published, not_published: not_published}} do
|
||||
{:ok, published} = Core.Posts.publish_post(published)
|
||||
|
||||
[posts: %{published: published, not_published: not_published}]
|
||||
end
|
||||
|
||||
test "unpublishes a post", %{posts: %{published: published}} do
|
||||
assert published.published_at
|
||||
assert {:ok, unpublished} = Core.Posts.unpublish_post(published)
|
||||
refute unpublished.published_at
|
||||
end
|
||||
|
||||
test "does not update a post that isn't published", %{posts: %{not_published: not_published}} do
|
||||
refute not_published.published_at
|
||||
assert {:ok, not_updated} = Core.Posts.unpublish_post(not_published)
|
||||
assert not_published.updated_at == not_updated.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "undelete_post/1" do
|
||||
@describetag posts: [:deleted, :not_deleted]
|
||||
setup %{posts: %{deleted: deleted, not_deleted: not_deleted}} do
|
||||
{:ok, deleted} = Core.Posts.delete_post(deleted)
|
||||
|
||||
[posts: %{deleted: deleted, not_deleted: not_deleted}]
|
||||
end
|
||||
|
||||
test "undeletees a post", %{posts: %{deleted: deleted}} do
|
||||
assert deleted.deleted_at
|
||||
assert {:ok, undeleted} = Core.Posts.undelete_post(deleted)
|
||||
refute undeleted.deleted_at
|
||||
end
|
||||
|
||||
test "does not update a post that isn't deleted", %{posts: %{not_deleted: not_deleted}} do
|
||||
refute not_deleted.deleted_at
|
||||
assert {:ok, not_updated} = Core.Posts.undelete_post(not_deleted)
|
||||
assert not_deleted.updated_at == not_updated.updated_at
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,8 @@ defmodule Test.DataCase do
|
|||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
import Test.SharedSetup
|
||||
|
||||
alias Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
using do
|
||||
|
@ -34,6 +36,8 @@ defmodule Test.DataCase do
|
|||
:ok
|
||||
end
|
||||
|
||||
setup [:handle_post_tags, :handle_blog_tags, :handle_status_tags]
|
||||
|
||||
@doc """
|
||||
Sets up the sandbox based on the test tags.
|
||||
"""
|
||||
|
|
31
test/support/test/fixtures/posts.ex
vendored
Normal file
31
test/support/test/fixtures/posts.ex
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Test.Fixtures.Posts do
|
||||
@moduledoc false
|
||||
|
||||
def status_post(tid, attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
tid: "#{tid}",
|
||||
kind: :status,
|
||||
body: Faker.Lorem.paragraph(),
|
||||
title: nil,
|
||||
status: nil
|
||||
})
|
||||
end
|
||||
|
||||
def blog_post(tid, attrs \\ %{}) do
|
||||
title = Faker.Lorem.sentence()
|
||||
|
||||
slug =
|
||||
title
|
||||
|> String.downcase()
|
||||
|> String.replace(~r/[^\w]/, "-")
|
||||
|> String.trim("-")
|
||||
|
||||
Enum.into(attrs, %{
|
||||
tid: "#{tid}",
|
||||
kind: :blog,
|
||||
title: title,
|
||||
slug: slug,
|
||||
body: Enum.join(Faker.Lorem.paragraphs(), "\n\n")
|
||||
})
|
||||
end
|
||||
end
|
48
test/support/test/shared_setup.ex
Normal file
48
test/support/test/shared_setup.ex
Normal file
|
@ -0,0 +1,48 @@
|
|||
defmodule Test.SharedSetup do
|
||||
def handle_post_tags(tags) do
|
||||
posts =
|
||||
for tid <- get_test_ids(tags, [:post, :posts]), into: %{} do
|
||||
fun = Enum.random([&Test.Fixtures.Posts.blog_post/1, &Test.Fixtures.Posts.status_post/1])
|
||||
{:ok, post} = Core.Posts.create_post(fun.(tid))
|
||||
{tid, post}
|
||||
end
|
||||
|
||||
[posts: posts]
|
||||
end
|
||||
|
||||
def handle_blog_tags(tags) do
|
||||
blogs =
|
||||
for tid <- get_test_ids(tags, [:blog, :blogs]), into: %{} do
|
||||
{:ok, blog} =
|
||||
Test.Fixtures.Posts.blog_post(tid)
|
||||
|> Core.Posts.create_post()
|
||||
|
||||
{tid, blog}
|
||||
end
|
||||
|
||||
[blogs: blogs]
|
||||
end
|
||||
|
||||
def handle_status_tags(tags) do
|
||||
statuses =
|
||||
for tid <- get_test_ids(tags, [:status, :statuses]), into: %{} do
|
||||
{:ok, status} =
|
||||
Test.Fixtures.Posts.status_post(tid)
|
||||
|> Core.Posts.create_post()
|
||||
|
||||
{tid, status}
|
||||
end
|
||||
|
||||
[statuses: statuses]
|
||||
end
|
||||
|
||||
defp get_test_ids(tags, keys) do
|
||||
keys
|
||||
|> Enum.flat_map(fn key ->
|
||||
tags
|
||||
|> Map.get(key)
|
||||
|> List.wrap()
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue