Browse Source

feat(docs): Add logic for generating API docs.

jackyalcine 10 months ago
parent
commit
e176cc9e70
Signed by: Jacky Alciné <yo@jacky.wtf> GPG Key ID: 36CD7728BDFD66FF
42 changed files with 533 additions and 306 deletions
  1. 9
    2
      .drone.yml
  2. 1
    1
      .elixir-version
  3. 3
    0
      .gitmodules
  4. 1
    1
      Dockerfile
  5. 1
    1
      Dockerfile.dev
  6. 4
    2
      INSTALL.markdown
  7. 8
    0
      config/config.exs
  8. 5
    3
      config/dev.exs
  9. 1
    1
      config/test.exs
  10. 1
    0
      doc/api
  11. 2
    0
      docker/scripts/build.sh
  12. 1
    1
      lib/indieweb/micropub.ex
  13. 10
    6
      lib/indieweb/micropub/entry.ex
  14. 1
    1
      lib/indieweb/post.ex
  15. 1
    1
      lib/indieweb/relme.ex
  16. 11
    7
      lib/page.ex
  17. 56
    0
      lib/repo/base.ex
  18. 25
    30
      lib/repo/entry.ex
  19. 1
    1
      lib/repo/relme.ex
  20. 3
    33
      lib/web.ex
  21. 10
    7
      mix.exs
  22. 4
    1
      mix.lock
  23. 21
    7
      package-lock.json
  24. 26
    0
      priv/static/swagger.json
  25. 7
    0
      scripts/build-documentation.sh
  26. 0
    12
      test/acceptance/authentication_test.exs
  27. 11
    3
      test/integration/controllers/auth_controller_test.exs
  28. 1
    1
      test/integration/controllers/indie/auth_controller_test.exs
  29. 35
    31
      test/integration/controllers/indie/micropub_controller_test.exs
  30. 12
    14
      test/integration/controllers/indie/rel_me_controller_test.exs
  31. 2
    2
      test/integration/controllers/media_controller_test.exs
  32. 1
    1
      test/integration/controllers/page_controller_test.exs
  33. 42
    42
      test/integration/controllers/setup_controller_test.exs
  34. 3
    1
      test/support/conn_case.ex
  35. 1
    1
      test/support/data_case.ex
  36. 8
    2
      test/test_helper.exs
  37. 27
    24
      test/unit/indieweb/micropub/entry_test.exs
  38. 69
    61
      test/unit/repo/entry_test.exs
  39. 79
    0
      web/controllers/README.md
  40. 7
    4
      web/controllers/indie/micropub_controller.ex
  41. 2
    1
      web/endpoint.ex
  42. 20
    0
      web/router.ex

+ 9
- 2
.drone.yml View File

@@ -28,7 +28,7 @@ steps:
28 28
   - name: set up buckets
29 29
     image: minio/mc
30 30
     commands:
31
-      - sleep 15
31
+      - sleep 5
32 32
       - mc config host add test http://objectstorage:9000 51bcaa680ac2fa4bb9f38bde4bf6f5620c542222393f95e2e6f6d38f838e3c17 51bcaa680ac2fa4bb9f38bde4bf6f5620c542222393f95e2e6f6d38f838e3c17
33 33
       - mc ls test
34 34
       - mc mb test/koype-test
@@ -36,10 +36,12 @@ steps:
36 36
       - mc policy download test/koype-test/videos
37 37
       - mc policy download test/koype-test/audio
38 38
   - name: run tests
39
-    image: elixir:1.6.6-alpine
39
+    image: elixir:1.7.4-alpine
40 40
     volumes:
41 41
       - name: mix
42 42
         path: /root/.mix
43
+      - name: docker-cache
44
+        path: /var/run/host.sock
43 45
     environment:
44 46
       MIX_ENV: test
45 47
       SESSION_SIGNING_SALT: "51bcaa680ac2fa4bb9f38bde4bf6f5620c542222393f95e2e6f6d38f838e3c17"
@@ -85,3 +87,8 @@ volumes:
85 87
     temp: {}
86 88
   - name: mix
87 89
     temp: {}
90
+
91
+volumes:
92
+- name: cache
93
+  host:
94
+    path: /var/run/docker.sock

+ 1
- 1
.elixir-version View File

@@ -1 +1 @@
1
-1.6.6
1
+1.7.4

+ 3
- 0
.gitmodules View File

@@ -0,0 +1,3 @@
1
+[submodule "doc/api"]
2
+	path = doc/api
3
+	url = https://github.com/lord/slate

+ 1
- 1
Dockerfile View File

@@ -1,4 +1,4 @@
1
-FROM elixir:1.6.6-alpine
1
+FROM elixir:1.7.4-alpine
2 2
 LABEL maintainer Jacky Alcine <yo@jacky.wtf>
3 3
 
4 4
 ARG MIX_ENV

+ 1
- 1
Dockerfile.dev View File

@@ -1,4 +1,4 @@
1
-FROM elixir:1.6.6-alpine
1
+FROM elixir:1.7.4-alpine
2 2
 LABEL maintainer Jacky Alcine <yo@jacky.wtf>
3 3
 
4 4
 ARG MIX_ENV

+ 4
- 2
INSTALL.markdown View File

@@ -6,7 +6,7 @@ own their identity online. Or something like that, I'm still working on it.
6 6
 ## Requirements
7 7
 
8 8
   * [Redis][] 5.0.0
9
-  * [Elixir][] 1.6.6
9
+  * [Elixir][] 1.7.0
10 10
   * [SQLite][] 3.24.0
11 11
 
12 12
 ## Development
@@ -19,12 +19,14 @@ variable file to prime your system for use. You can do this by running `cp .env.
19 19
 Being that Koype's tailored to run currently under a Docker environment, the 
20 20
 ideal tool for local development is [Docker Compose][1]. Check out its
21 21
 documentation for instructions on how to install that. Once you've got installed,
22
-run the [development startup script]`[3] in the directory.
22
+run the [development startup script][3] in the directory.
23 23
 
24 24
 Looking for a one-liner / copy-paste friendly?
25 25
 ```sh
26 26
 $ cp .env.example .env
27 27
 $ scripts/dev-setup.sh
28
+# Run the following if you want to see the Slate API documentation.
29
+$ scripts/build-documentation.sh
28 30
 ```
29 31
 
30 32
 [1]: https://docs.docker.com/compose/install/

+ 8
- 0
config/config.exs View File

@@ -77,4 +77,12 @@ config :ex_aws, :s3,
77 77
 
78 78
 config :logster, :filter_parameters, ~w(code secret access_token _csrf_token csrf_token password)
79 79
 
80
+config :koype, :phoenix_swagger,
81
+  swagger_files: %{
82
+    "priv/static/swagger.json" => [
83
+      router: Koype.Web.Router,
84
+      endpoint: Koype.Web.Endpoint
85
+    ]
86
+  }
87
+
80 88
 import_config "#{Mix.env()}.exs"

+ 5
- 3
config/dev.exs View File

@@ -7,12 +7,14 @@ config :koype, Koype.Web.Endpoint,
7 7
   watchers: [npm: ["run", "parcel:watch"]],
8 8
   reloadable_compilers: [
9 9
     :gettext,
10
-    :elixir
10
+    :elixir,
11
+    :phoenix,
12
+    :phoenix_swagger
11 13
   ],
12 14
   live_reload: [
13 15
     interval: 50,
14 16
     patterns: [
15
-      ~r{priv/static/assets/.*(js|css|png|jpeg|jpg|gif|svg)$},
17
+      ~r{priv/static/assets/.*(js|css|png|jpeg|jpg|gif|svg|json)$},
16 18
       ~r{priv/gettext/.*(po)$},
17 19
       ~r{web/views/.*(ex)$},
18 20
       ~r{web/templates/.*(eex)$}
@@ -30,7 +32,7 @@ config :git_hooks,
30 32
     pre_commit: [
31 33
       mix_tasks: [
32 34
         "format",
33
-        "inch"
35
+        "inch --no-protected --only-undocumented --pedantic"
34 36
       ]
35 37
     ]
36 38
   ]

+ 1
- 1
config/test.exs View File

@@ -11,7 +11,7 @@ config :koype, Koype.Web.Endpoint,
11 11
   check_origin: false,
12 12
   server: false
13 13
 
14
-config :logger, level: :debug
14
+config :logger, level: :error
15 15
 
16 16
 config :exvcr,
17 17
   vcr_cassette_library_dir: "test/fixtures/vcr_cassettes",

+ 1
- 0
doc/api

@@ -0,0 +1 @@
1
+Subproject commit de496848c1e7b7116371ae25944aced1c22c47a9

+ 2
- 0
docker/scripts/build.sh View File

@@ -6,6 +6,8 @@ NODE_ENV=${ENV}
6 6
 NPM_CONFIG_LINK=false
7 7
 NPM_CONFIG_PROGRESS=false
8 8
 
9
+apk add git
10
+
9 11
 echo " ---> [npm] Pulling dependencies..."
10 12
 npm install -g npm@latest
11 13
 npm install -g npx@latest

+ 1
- 1
lib/indieweb/micropub.ex View File

@@ -78,7 +78,7 @@ defmodule IndieWeb.Micropub do
78 78
   end
79 79
 
80 80
   def resolve_hcard(mf2_list, base_uri) when is_list(mf2_list) do
81
-    Enum.reduce_while(mf2_list, {:error, :no_author_info}, fn mf2, acc ->
81
+    Enum.reduce_while(mf2_list, {:error, :no_author_info}, fn mf2, _ ->
82 82
       case resolve_hcard(mf2, base_uri) do
83 83
         {:error, _} = error -> {:cont, error}
84 84
         {:ok, _} = result -> {:halt, result}

+ 10
- 6
lib/indieweb/micropub/entry.ex View File

@@ -85,7 +85,8 @@ defmodule IndieWeb.Micropub.Entry do
85 85
             {:ok, refreshed_model} <- do_update_entry(model, scope: scope, params: params),
86 86
             :ok <- send_webmentions(refreshed_model)
87 87
           ) do
88
-            {:ok, state: :updated, model: refreshed_model}
88
+            model_uri = Model.get_uri(refreshed_model)
89
+            {:ok, state: :updated, model: refreshed_model, uri: model_uri}
89 90
           else
90 91
             {:error, _} = error -> error
91 92
           end
@@ -111,7 +112,8 @@ defmodule IndieWeb.Micropub.Entry do
111 112
         {:ok, extended_model} <- Content.parse_extensions(reloaded_model, full_params),
112 113
         :ok <- send_webmentions(model)
113 114
       ) do
114
-        {:ok, state: :created, model: extended_model}
115
+        model_uri = Model.get_uri(extended_model)
116
+        {:ok, state: :created, model: extended_model, uri: model_uri}
115 117
       else
116 118
         {:error, _} = error -> error
117 119
       end
@@ -131,10 +133,11 @@ defmodule IndieWeb.Micropub.Entry do
131 133
 
132 134
         {:ok, entry} ->
133 135
           with(
134
-            {:ok, record} <- Model.delete(entry),
136
+            {:ok, record} <- Koype.Repo.Base.delete(entry),
135 137
             :ok <- send_webmentions(entry)
136 138
           ) do
137
-            {:ok, state: :deleted, model: record}
139
+            model_uri = Model.get_uri(record)
140
+            {:ok, state: :deleted, model: record, uri: model_uri}
138 141
           else
139 142
             {:error, _} = error -> error
140 143
           end
@@ -155,10 +158,11 @@ defmodule IndieWeb.Micropub.Entry do
155 158
 
156 159
         {:ok, model} ->
157 160
           with(
158
-            {:ok, record} <- Model.undelete(model),
161
+            {:ok, record} <- Koype.Repo.Base.undelete(model),
159 162
             :ok <- send_webmentions(record)
160 163
           ) do
161
-            {:ok, state: :undeleted, model: record}
164
+            model_uri = Model.get_uri(record)
165
+            {:ok, state: :undeleted, model: record, uri: model_uri}
162 166
           else
163 167
             {:error, _} = error -> error
164 168
           end

+ 1
- 1
lib/indieweb/post.ex View File

@@ -302,7 +302,7 @@ defmodule IndieWeb.Post do
302 302
           !String.starts_with?(data["content"]["plain"], data["name"]) ->
303 303
         true
304 304
 
305
-      is_binary(data["name"]) and !String.starts_with?(data["content"]["value"], data["name"]) ->
305
+      is_binary(data["name"]) and !String.starts_with?(Enum.join(data["content"]["value"], " "), data["name"]) ->
306 306
         true
307 307
 
308 308
       is_binary(data["summary"]) ->

+ 1
- 1
lib/indieweb/relme.ex View File

@@ -48,7 +48,7 @@ defmodule IndieWeb.RelMe do
48 48
   def add(%URI{scheme: scheme} = uri) do
49 49
     if scheme == "http" || scheme == "https" do
50 50
       case Koype.Http.get(uri) do
51
-        {:ok, %Koype.Http.Response{code: 200} = resp} ->
51
+        {:ok, %Koype.Http.Response{code: code}} when code >= 200 and code < 299 ->
52 52
           %RelMe{}
53 53
           |> RelMe.changeset(%{uri: URI.to_string(uri)})
54 54
           |> Repo.insert()

+ 11
- 7
lib/page.ex View File

@@ -23,21 +23,25 @@ defmodule Koype.Page do
23 23
 
24 24
   # TODO: Add caching to this.
25 25
   def title(uri) do
26
+    host = URI.parse(uri).host
27
+
26 28
     with {:ok, %Http.Response{code: code, body: body}} when code >= 200 and code < 300 <- Http.get(uri) do
27
-      title =
29
+      title_elem =
28 30
         body
29 31
         |> Floki.find("title")
30 32
         |> List.first()
31
-        |> Floki.text()
32 33
 
33
-      if title == "" do
34
-        URI.parse(uri).host
34
+      if is_nil(title_elem) do
35
+        host
35 36
       else
36
-        title
37
+        case Floki.text(title_elem) do
38
+          "" -> host
39
+          title -> title
40
+        end
37 41
       end
38 42
     else
39
-      {:ok, _} -> URI.parse(uri).host
40
-      {:error, _} -> URI.parse(uri).host
43
+      {:ok, _} -> host
44
+      {:error, _} -> host
41 45
     end
42 46
   end
43 47
 

+ 56
- 0
lib/repo/base.ex View File

@@ -0,0 +1,56 @@
1
+# Koype: a IndieWeb-focused, single-tenant website engine for people.
2
+# 
3
+# Copyright © 2019 Jacky Alciné <jacky.is@black.af>
4
+# 
5
+# This file belongs to the Koype project.
6
+# 
7
+# This program is free software: you can redistribute it and/or modify
8
+# it under the terms of the GNU Affero General Public License as published by
9
+# the Free Software Foundation, either version 3 of the License, or
10
+# (at your option) any later version.
11
+# 
12
+# This program is distributed in the hope that it will be useful,
13
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
+# GNU Affero General Public License for more details.
16
+# 
17
+# You should have received a copy of the GNU Affero General Public License
18
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
+defmodule Koype.Repo.Base do
20
+  import Ecto.Changeset, only: [get_field: 2, put_change: 3, change: 2]
21
+
22
+  @doc false
23
+  defmacro __using__(_) do
24
+    quote do
25
+      import Koype.Repo.Base
26
+      import Ecto.Changeset
27
+      import Ecto.Query
28
+      use Ecto.Schema
29
+
30
+      @foreign_key_type :binary_id
31
+      @primary_key {:id, :binary_id, autogenerate: true}
32
+      @timestamps_opts [type: :utc_datetime, usec: true]
33
+    end
34
+  end
35
+
36
+  @doc "Ensure there's a UUID in the given field"
37
+  @spec ensure_uuid(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
38
+  def ensure_uuid(changeset, field) do
39
+    case get_field(changeset, field) do
40
+      nil -> changeset |> put_change(field, Ecto.UUID.generate())
41
+      _ -> changeset
42
+    end
43
+  end
44
+
45
+  def delete(model) do
46
+    model
47
+    |> change(deleted_at: NaiveDateTime.utc_now())
48
+    |> Koype.Repo.update()
49
+  end
50
+
51
+  def undelete(model) do
52
+    model
53
+    |> change(deleted_at: nil)
54
+    |> Koype.Repo.update()
55
+  end
56
+end

+ 25
- 30
lib/repo/entry.ex View File

@@ -17,17 +17,18 @@
17 17
 # You should have received a copy of the GNU Affero General Public License
18 18
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 19
 defmodule Koype.Repo.Entry do
20
-  use Koype.Web, :model
21 20
   use Arc.Ecto.Schema
21
+  use Koype.Repo.Base
22 22
 
23 23
   alias Koype.Storage.Json
24
-  alias Koype.Repo.{Entry, Category}
25
-  alias IndieWeb.Post
24
+  alias Koype.Repo.Category
26 25
 
27 26
   require Logger
28 27
 
29 28
   @required_attrs ~w(type name)a
30 29
   @optional_attrs ~w(object_data slug deleted_at)a
30
+  @route &Koype.Web.Router.Helpers.entry_path/3
31
+  @route_token "post"
31 32
 
32 33
   schema "entries" do
33 34
     field(:name, :string, null: false, default: "Entry")
@@ -45,12 +46,12 @@ defmodule Koype.Repo.Entry do
45 46
   defmodule Json do
46 47
     @moduledoc "Represents structured data for an entry."
47 48
     @enforce_keys [:data]
48
-    @type t :: %Json{version: Version.t(), properties: map(), publisher: map()}
49
+    @type t :: %Json{version: Version, properties: map(), publisher: map()}
49 50
 
50 51
     defstruct ~w(version properties publisher)a
51 52
 
52 53
     @doc "Obtains the JSON data associated for this entry."
53
-    @spec find(model :: Entry) :: {:ok, map()} | {:error, any()}
54
+    @spec find(model :: __MODULE__) :: {:ok, map()} | {:error, any()}
54 55
     def find(model) do
55 56
       case Koype.Storage.Json.find(model) do
56 57
         {:ok, json} -> {:ok, IndieWeb.Micropub.Content.expand_properties(json, model)}
@@ -65,13 +66,13 @@ defmodule Koype.Repo.Entry do
65 66
     {
66 67
       "version": "2018.12.21",
67 68
       "properties": {},
68
-      "publisher": { // Proxy to IndieWeb.App.t
69
+      "publisher": { // Proxy to IndieWeb.App
69 70
         "client": "client_app_uri",
70 71
         "occurred_at": "2018-12-21 14:15:37 PST"
71 72
       }
72 73
     }
73 74
     """
74
-    @spec persist(record :: Entry, data :: map()) :: {:ok, binary()} | {:error, any()}
75
+    @spec persist(record :: __MODULE__, data :: map()) :: {:ok, binary()} | {:error, any()}
75 76
     def persist(record, data) do
76 77
       case Koype.Storage.upload_properties_inline(data, record) do
77 78
         {:error, _} = error -> error
@@ -81,16 +82,16 @@ defmodule Koype.Repo.Entry do
81 82
   end
82 83
 
83 84
   def fetch(value), do: Koype.Repo.find_by_columns(__MODULE__, value, ~w(id slug)a)
84
-  def get_uri(entry), do: Repo.get_uri_for_record(entry, &entry_path/3)
85
-  def resolve_from_uri(url), do: fetch(Repo.resolve_token_from_uri(url, "post"))
86
-  def count, do: Repo.count(__MODULE__)
85
+  def get_uri(entry), do: Koype.Repo.get_uri_for_record(entry, @route)
86
+  def resolve_from_uri(url), do: fetch(Koype.Repo.resolve_token_from_uri(url, @route_token))
87
+  def count, do: Koype.Repo.count(__MODULE__)
87 88
 
88 89
   def changeset(entry, attrs \\ :invalid) do
89 90
     entry
90 91
     |> cast(attrs, @required_attrs ++ @optional_attrs)
91 92
     |> cast_assoc(:categories, with: &Category.changeset/2, required: false)
92 93
     |> validate_required(@required_attrs)
93
-    |> ensure_uuid(:id)
94
+    |> Koype.Repo.Base.ensure_uuid(:id)
94 95
     |> unique_constraint(:slug)
95 96
   end
96 97
 
@@ -98,15 +99,15 @@ defmodule Koype.Repo.Entry do
98 99
   @doc "Creates a new entry."
99 100
   @spec create(data :: map()) :: {:ok, struct()} | {:error, any()}
100 101
   def create(data) do
101
-    post_types = Post.determine_type(data)
102
-    post_type = Post.determine_dominant_type(post_types, data)
102
+    post_types = IndieWeb.Post.determine_type(data)
103
+    post_type = IndieWeb.Post.determine_dominant_type(post_types, data)
103 104
 
104 105
     params = %{
105
-      name: Post.determine_title(post_type, data) || "Entry",
106
+      name: IndieWeb.Post.determine_title(post_type, data) || "Entry",
106 107
       type: Atom.to_string(post_type)
107 108
     }
108 109
 
109
-    cs = changeset(%Entry{}, params)
110
+    cs = changeset(%__MODULE__{}, params)
110 111
 
111 112
     Logger.info(fn ->
112 113
       "Attempting to create a #{post_type} (#{inspect(post_types)}) entry with data #{inspect(data)} and params #{
@@ -115,7 +116,7 @@ defmodule Koype.Repo.Entry do
115 116
     end)
116 117
 
117 118
     with(
118
-      {:ok, record} <- Repo.insert(cs),
119
+      {:ok, record} <- Koype.Repo.insert(cs),
119 120
       {:ok, record} <- do_update_field(:name, record, data),
120 121
       {:ok, record} <- do_update_field(:slug, record, data),
121 122
       {:ok, _} <- Json.persist(record, data)
@@ -127,7 +128,7 @@ defmodule Koype.Repo.Entry do
127 128
   end
128 129
 
129 130
   @doc "Updates an existing entry."
130
-  @spec update(model :: Entry, data :: map()) :: {:ok, Entry} | {:error, any()}
131
+  @spec update(model :: __MODULE__, data :: map()) :: {:ok, __MODULE__} | {:error, any()}
131 132
   def update(model, properties) do
132 133
     with(
133 134
       {:ok, model} <- do_update_field(:name, model, properties),
@@ -141,13 +142,13 @@ defmodule Koype.Repo.Entry do
141 142
     end
142 143
   end
143 144
 
144
-  def with_post_status(status, entries \\ __MODULE__.undeleted()),
145
+  def with_post_status(status, entries \\ Koype.Repo.undeleted(__MODULE__)),
145 146
     do: from(e in entries, where: e.post_status == ^status)
146 147
 
147
-  def published(entries \\ __MODULE__.undeleted()),
148
+  def published(entries \\ Koype.Repo.undeleted(__MODULE__)),
148 149
     do: from(e in with_post_status("published", entries), where: not is_nil(e.published_at))
149 150
 
150
-  def drafts(entries \\ __MODULE__.undeleted()),
151
+  def drafts(entries \\ Koype.Repo.undeleted(__MODULE__)),
151 152
     do: from(e in with_post_status("draft", entries), where: is_nil(e.published_at))
152 153
 
153 154
   defp do_update_field(field, model, properties)
@@ -157,10 +158,10 @@ defmodule Koype.Repo.Entry do
157 158
 
158 159
     model
159 160
     |> Ecto.Changeset.change(slug: slug)
160
-    |> Repo.update()
161
+    |> Koype.Repo.update()
161 162
   end
162 163
 
163
-  defp do_update_field(:slug, %Entry{slug: model_slug} = model, properties) when is_binary(model_slug) do
164
+  defp do_update_field(:slug, %__MODULE__{slug: model_slug} = model, properties) when is_binary(model_slug) do
164 165
     if Map.has_key?(properties, "slug") do
165 166
       do_update_field(:slug, model, Map.put(properties, "slug", properties["slug"]))
166 167
     else
@@ -175,7 +176,7 @@ defmodule Koype.Repo.Entry do
175 176
 
176 177
     model
177 178
     |> Ecto.Changeset.change(name: name)
178
-    |> Repo.update()
179
+    |> Koype.Repo.update()
179 180
   end
180 181
 
181 182
   defp do_update_field(:name, model, properties) do
@@ -199,13 +200,7 @@ end
199 200
 defimpl Jason.Encoder, for: Koype.Repo.Entry do
200 201
   def encode(model, opts) do
201 202
     Jason.Encode.map(
202
-      %{
203
-        id: model.id,
204
-        slug: model.slug,
205
-        type: model.type,
206
-        inserted_at: model.inserted_at,
207
-        updated_at: model.updated_at
208
-      },
203
+      Map.take(model, ~w(id slug type inserted_at updated_at)a),
209 204
       opts
210 205
     )
211 206
   end

+ 1
- 1
lib/repo/relme.ex View File

@@ -17,7 +17,7 @@
17 17
 # You should have received a copy of the GNU Affero General Public License
18 18
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 19
 defmodule Koype.Repo.RelMe do
20
-  use Koype.Web, :model
20
+  use Koype.Repo.Base
21 21
 
22 22
   @required_attrs ~w(uri)a
23 23
 

+ 3
- 33
lib/web.ex View File

@@ -64,40 +64,10 @@ defmodule Koype.Web do
64 64
 
65 65
   def model do
66 66
     quote do
67
-      import Ecto.Changeset
68
-      import Ecto.Query
67
+      use Koype.Repo.Base
69 68
       import Koype.Web.Router.Helpers
70
-
71
-      use Ecto.Schema
72
-      alias Koype.Repo
73
-
74
-      @primary_key {:id, :binary_id, autogenerate: true}
75
-      @foreign_key_type :binary_id
76
-      @timestamps_opts [type: :utc_datetime, usec: true]
77
-
78
-      @doc "Ensure there's a UUID in the given field"
79
-      @spec ensure_uuid(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
80
-      def ensure_uuid(changeset, field) do
81
-        case get_field(changeset, field) do
82
-          nil -> changeset |> put_change(field, Ecto.UUID.generate())
83
-          _ -> changeset
84
-        end
85
-      end
86
-
87
-      def delete(model) do
88
-        model
89
-        |> Ecto.Changeset.change(deleted_at: NaiveDateTime.utc_now())
90
-        |> Koype.Repo.update()
91
-      end
92
-
93
-      def undelete(model) do
94
-        model
95
-        |> Ecto.Changeset.change(deleted_at: nil)
96
-        |> Koype.Repo.update()
97
-      end
98
-
99
-      def deleted(), do: Koype.Repo.deleted(__MODULE__)
100
-      def undeleted(), do: Koype.Repo.undeleted(__MODULE__)
69
+      import Koype.Repo.Base
70
+      require Koype.Repo.Base
101 71
     end
102 72
   end
103 73
 

+ 10
- 7
mix.exs View File

@@ -28,7 +28,7 @@ defmodule Koype.Mixfile do
28 28
       version: @version,
29 29
       elixir: "~> 1.6",
30 30
       elixirc_paths: elixirc_paths(Mix.env()),
31
-      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
31
+      compilers: [:phoenix, :gettext, :phoenix_swagger] ++ Mix.compilers(),
32 32
       start_permanent: Mix.env() == :prod,
33 33
       aliases: aliases(),
34 34
       deps: deps(),
@@ -82,6 +82,7 @@ defmodule Koype.Mixfile do
82 82
       {:apex, "~> 1.2.1", only: [:dev, :test]},
83 83
       {:arc, "~> 0.11.0"},
84 84
       {:arc_ecto, "~> 0.11.1"},
85
+      {:bureaucrat, "~> 0.2.0", only: [:dev, :test]},
85 86
       {:calendar, "~> 0.17.4"},
86 87
       {:confex, "~> 3.3", override: true},
87 88
       {:cowboy, "~> 1.0"},
@@ -92,9 +93,14 @@ defmodule Koype.Mixfile do
92 93
       {:ecto, "~> 2.2.0"},
93 94
       {:ex_aws, "~> 2.0"},
94 95
       {:ex_aws_s3, "~> 2.0"},
96
+      {:ex_cldr, "~> 2.0"},
97
+      {:ex_cldr_numbers, "~> 2.1"},
98
+      {:ex_cldr_territories, "~> 2.0"},
95 99
       {:ex_doc, ">= 0.19.0", only: :dev},
96 100
       {:ex_image_info, "~> 0.2.3"},
101
+      {:ex_json_schema, "~> 0.5"},
97 102
       {:ex_machina, "~> 2.2", only: :test},
103
+      {:ex_url, "~> 1.0.0"},
98 104
       {:excoveralls, "~> 0.8", only: :test},
99 105
       {:explode, "~> 1.0.0"},
100 106
       {:exvcr, "~> 0.10", only: :test},
@@ -108,7 +114,7 @@ defmodule Koype.Mixfile do
108 114
       {:hound, "~> 1.0", only: [:dev, :test]},
109 115
       {:html_sanitize_ex, "~> 1.3.0-rc3"},
110 116
       {:httpoison, "~> 1.3.0"},
111
-      {:inch_ex, "~> 1.0"},
117
+      {:inch_ex, github: "rrrene/inch_ex"},
112 118
       {:inflex, "~> 1.10.0"},
113 119
       {:jason, "~> 1.1"},
114 120
       {:logster, "~> 0.4"},
@@ -121,6 +127,7 @@ defmodule Koype.Mixfile do
121 127
       {:phoenix_html_simplified_helpers, "~> 2.0"},
122 128
       {:phoenix_live_reload, "~> 1.0", only: :dev},
123 129
       {:phoenix_pubsub, "~> 1.0"},
130
+      {:phoenix_swagger, "~> 0.8.1"},
124 131
       {:plug_cowboy, "~> 1.0.0"},
125 132
       {:pretty_print_formatter, "~> 0.1.5", only: :dev},
126 133
       {:redix, "~> 0.8.0"},
@@ -131,11 +138,7 @@ defmodule Koype.Mixfile do
131 138
       {:sqlite_ecto2, "~> 2.2.5"},
132 139
       {:sweet_xml, "~> 0.6"},
133 140
       {:totpex, "~> 0.1.2"},
134
-      {:uuid, "~> 1.1"},
135
-      {:ex_url, "~> 1.0.0"},
136
-      {:ex_cldr, "~> 2.0"},
137
-      {:ex_cldr_numbers, "~> 2.1"},
138
-      {:ex_cldr_territories, "~> 2.0"}
141
+      {:uuid, "~> 1.1"}
139 142
     ]
140 143
   end
141 144
 

+ 4
- 1
mix.lock View File

@@ -5,6 +5,7 @@
5 5
   "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
6 6
   "blankable": {:hex, :blankable, "0.0.1", "2e0b4667fee684f0614620d31a34bb2731341cccb27ed903e330195819ba3ba0", [:mix], [], "hexpm"},
7 7
   "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
8
+  "bureaucrat": {:hex, :bureaucrat, "0.2.4", "a2f268820640aae213a5224cd2ebe3ac89b027b4562894011df4557c2ae9c9e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
8 9
   "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
9 10
   "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
10 11
   "cldr_utils": {:hex, :cldr_utils, "2.0.5", "eba0f4cc86861b74f2c1180fe7f6fa25f9e9a3b06365fa7468213c9ec3fd392c", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
@@ -29,6 +30,7 @@
29 30
   "ex_cldr_territories": {:hex, :ex_cldr_territories, "2.0.0", "e1a56ab2904ab6cff2a270dbe8222927ee2ccfe746a9965142bd7a589a5c50f0", [:mix], [{:ex_cldr, "~> 2.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
30 31
   "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
31 32
   "ex_image_info": {:hex, :ex_image_info, "0.2.4", "610002acba43520a9b1cf1421d55812bde5b8a8aeaf1fe7b1f8823e84e762adb", [:mix], [], "hexpm"},
33
+  "ex_json_schema": {:hex, :ex_json_schema, "0.5.7", "14a1bcd716e432badb19e5f54fd0f3f0be47b34d1b5957944702be90d66a6cf6", [:mix], [], "hexpm"},
32 34
   "ex_machina": {:hex, :ex_machina, "2.2.2", "d84217a6fb7840ff771d2561b8aa6d74a0d8968e4b10ecc0d7e9890dc8fb1c6a", [: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"},
33 35
   "ex_url": {:hex, :ex_url, "1.0.0", "4604c73cc6953ffc1732e5c5bfbc284f41ba0797ea652e5bd37a21edfe4de1cb", [:mix], [{:ex_cldr, "~> 2.0", [hex: :ex_cldr, repo: "hexpm", optional: true]}, {:ex_phone_number, "~> 0.1", [hex: :ex_phone_number, repo: "hexpm", optional: true]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
34 36
   "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},
@@ -51,7 +53,7 @@
51 53
   "httpotion": {:hex, :httpotion, "3.1.0", "14d20d9b0ce4e86e253eb91e4af79e469ad949f57a5d23c0a51b2f86559f6589", [:mix], [{:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: false]}], "hexpm"},
52 54
   "ibrowse": {:hex, :ibrowse, "4.4.1", "2b7d0637b0f8b9b4182de4bd0f2e826a4da2c9b04898b6e15659ba921a8d6ec2", [:rebar3], [], "hexpm"},
53 55
   "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
54
-  "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
56
+  "inch_ex": {:git, "https://github.com/rrrene/inch_ex.git", "e44c722afdc20221a35650c31e048d195530f0b0", []},
55 57
   "inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm"},
56 58
   "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
57 59
   "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
@@ -75,6 +77,7 @@
75 77
   "phoenix_html_simplified_helpers": {:hex, :phoenix_html_simplified_helpers, "2.1.0", "252c80df9a5bf8c8312b8fff57bc9735fcc07c599a2e825967d8b919b89a1eae", [:mix], [{:ecto, "~> 3.0 or ~> 2.2 or ~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, ">= 0.11.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:timex, "~> 3.4 or ~> 3.3 or ~> 3.2", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm"},
76 78
   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.7", "425fff579085f7eacaf009e71940be07338c8d8b78d16e307c50c7d82a381497", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3 or ~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
77 79
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"},
80
+  "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.1", "af7fc985804145e17df316bb988db86d43401af3cff2f5f7ef6c21d22af5086c", [:mix], [{:ex_json_schema, "~> 0.5", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
78 81
   "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
79 82
   "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
80 83
   "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},

+ 21
- 7
package-lock.json View File

@@ -2600,12 +2600,14 @@
2600 2600
         "balanced-match": {
2601 2601
           "version": "1.0.0",
2602 2602
           "bundled": true,
2603
-          "dev": true
2603
+          "dev": true,
2604
+          "optional": true
2604 2605
         },
2605 2606
         "brace-expansion": {
2606 2607
           "version": "1.1.11",
2607 2608
           "bundled": true,
2608 2609
           "dev": true,
2610
+          "optional": true,
2609 2611
           "requires": {
2610 2612
             "balanced-match": "^1.0.0",
2611 2613
             "concat-map": "0.0.1"
@@ -2620,17 +2622,20 @@
2620 2622
         "code-point-at": {
2621 2623
           "version": "1.1.0",
2622 2624
           "bundled": true,
2623
-          "dev": true
2625
+          "dev": true,
2626
+          "optional": true
2624 2627
         },
2625 2628
         "concat-map": {
2626 2629
           "version": "0.0.1",
2627 2630
           "bundled": true,
2628
-          "dev": true
2631
+          "dev": true,
2632
+          "optional": true
2629 2633
         },
2630 2634
         "console-control-strings": {
2631 2635
           "version": "1.1.0",
2632 2636
           "bundled": true,
2633
-          "dev": true
2637
+          "dev": true,
2638
+          "optional": true
2634 2639
         },
2635 2640
         "core-util-is": {
2636 2641
           "version": "1.0.2",
@@ -2747,7 +2752,8 @@
2747 2752
         "inherits": {
2748 2753
           "version": "2.0.3",
2749 2754
           "bundled": true,
2750
-          "dev": true
2755
+          "dev": true,
2756
+          "optional": true
2751 2757
         },
2752 2758
         "ini": {
2753 2759
           "version": "1.3.5",
@@ -2759,6 +2765,7 @@
2759 2765
           "version": "1.0.0",
2760 2766
           "bundled": true,
2761 2767
           "dev": true,
2768
+          "optional": true,
2762 2769
           "requires": {
2763 2770
             "number-is-nan": "^1.0.0"
2764 2771
           }
@@ -2773,6 +2780,7 @@
2773 2780
           "version": "3.0.4",
2774 2781
           "bundled": true,
2775 2782
           "dev": true,
2783
+          "optional": true,
2776 2784
           "requires": {
2777 2785
             "brace-expansion": "^1.1.7"
2778 2786
           }
@@ -2780,12 +2788,14 @@
2780 2788
         "minimist": {
2781 2789
           "version": "0.0.8",
2782 2790
           "bundled": true,
2783
-          "dev": true
2791
+          "dev": true,
2792
+          "optional": true
2784 2793
         },
2785 2794
         "minipass": {
2786 2795
           "version": "2.2.4",
2787 2796
           "bundled": true,
2788 2797
           "dev": true,
2798
+          "optional": true,
2789 2799
           "requires": {
2790 2800
             "safe-buffer": "^5.1.1",
2791 2801
             "yallist": "^3.0.0"
@@ -2804,6 +2814,7 @@
2804 2814
           "version": "0.5.1",
2805 2815
           "bundled": true,
2806 2816
           "dev": true,
2817
+          "optional": true,
2807 2818
           "requires": {
2808 2819
             "minimist": "0.0.8"
2809 2820
           }
@@ -2884,7 +2895,8 @@
2884 2895
         "number-is-nan": {
2885 2896
           "version": "1.0.1",
2886 2897
           "bundled": true,
2887
-          "dev": true
2898
+          "dev": true,
2899
+          "optional": true
2888 2900
         },
2889 2901
         "object-assign": {
2890 2902
           "version": "4.1.1",
@@ -2896,6 +2908,7 @@
2896 2908
           "version": "1.4.0",
2897 2909
           "bundled": true,
2898 2910
           "dev": true,
2911
+          "optional": true,
2899 2912
           "requires": {
2900 2913
             "wrappy": "1"
2901 2914
           }
@@ -3017,6 +3030,7 @@
3017 3030
           "version": "1.0.2",
3018 3031
           "bundled": true,
3019 3032
           "dev": true,
3033
+          "optional": true,
3020 3034
           "requires": {
3021 3035
             "code-point-at": "^1.0.0",
3022 3036
             "is-fullwidth-code-point": "^1.0.0",

+ 26
- 0
priv/static/swagger.json View File

@@ -0,0 +1,26 @@
1
+{
2
+  "tags": [
3
+    {
4
+      "name": "Micropub",
5
+      "description": "Operations for working with Micropub."
6
+    },
7
+    {
8
+      "name": "IndieAuth",
9
+      "description": "Operations for working with IndieAuth."
10
+    }
11
+  ],
12
+  "info": {
13
+    "version": "0.0.1",
14
+    "title": "Koype",
15
+    "license": {
16
+      "name": "AGPLv3"
17
+    }
18
+  },
19
+  "swagger": "2.0",
20
+  "paths": {},
21
+  "info": {
22
+    "version": "0.0.1",
23
+    "title": "<enter your title>"
24
+  },
25
+  "definitions": {}
26
+}

+ 7
- 0
scripts/build-documentation.sh View File

@@ -0,0 +1,7 @@
1
+#!/usr/bin/env sh
2
+
3
+cd doc/api || exit 20
4
+bundle install
5
+bundle exec middleman build
6
+mkdir -p ../../priv/static/doc/api
7
+cp -R build/* ../../priv/static/doc/api

+ 0
- 12
test/acceptance/authentication_test.exs View File

@@ -1,12 +0,0 @@
1
-defmodule Koype.Acceptance.AuthenticationTest do
2
-  use Koype.Web.BrowserCase
3
-  use ExUnit.Case, async: false
4
-  @moduletag acceptance: true
5
-
6
-  hound_session()
7
-
8
-  describe "successfully signs in when using a TOTP code" do
9
-    navigate_to("/")
10
-    setup_site()
11
-  end
12
-end

+ 11
- 3
test/integration/controllers/auth_controller_test.exs View File

@@ -75,7 +75,10 @@ defmodule Koype.Web.AuthControllerTest do
75 75
     test "logs user in successfully" do
76 76
       otp_secret = insert(:otp_secret)
77 77
       code = Totpex.generate_totp(otp_secret.secret)
78
-      conn = build_conn() |> post("/auth", %{"code" => code})
78
+
79
+      conn =
80
+        build_conn()
81
+        |> post("/auth", %{"code" => code})
79 82
 
80 83
       assert redirected_to(conn) == "/"
81 84
       assert owner_authenticated?(conn)
@@ -83,7 +86,9 @@ defmodule Koype.Web.AuthControllerTest do
83 86
     end
84 87
 
85 88
     test "fails to verify OTP code" do
86
-      conn = build_conn() |> post("/auth", %{"code" => 000_000})
89
+      conn =
90
+        build_conn()
91
+        |> post("/auth", %{"code" => 000_000})
87 92
 
88 93
       assert conn.status == 401
89 94
       assert %{"warn" => _} = get_flash(conn)
@@ -93,7 +98,10 @@ defmodule Koype.Web.AuthControllerTest do
93 98
 
94 99
   describe "GET  .logout/2" do
95 100
     test "clears out authenticated session" do
96
-      conn = build_conn() |> owner_sign_in |> get("/auth/logout")
101
+      conn =
102
+        build_conn()
103
+        |> owner_sign_in
104
+        |> get("/auth/logout")
97 105
 
98 106
       assert redirected_to(conn) == "/"
99 107
       refute owner_authenticated?(conn)

+ 1
- 1
test/integration/controllers/indie/auth_controller_test.exs View File

@@ -13,7 +13,7 @@ defmodule Koype.Web.Indie.AuthControllerTest do
13 13
   }
14 14
 
15 15
   describe "GET .authorize/2" do
16
-    test "forces redirect to login page" do
16
+    test "302 forces redirect to login page" do
17 17
       with_mocks([
18 18
         {
19 19
           IndieWeb.App,

+ 35
- 31
test/integration/controllers/indie/micropub_controller_test.exs View File

@@ -13,7 +13,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
13 13
     test "401 when unauthorized" do
14 14
       conn =
15 15
         build_conn()
16
-        |> post(@route)
16
+        |> post(@route, %{})
17 17
 
18 18
       assert json_response(conn, :unauthorized)
19 19
     end
@@ -26,6 +26,8 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
26 26
           "h" => "entry"
27 27
         })
28 28
 
29
+      # |> doc(description: "Missing needed scope for request", operation_id: "micropub_scope_missing")
30
+
29 31
       assert json_response(conn, :bad_request)
30 32
     end
31 33
 
@@ -41,6 +43,8 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
41 43
         |> indie_sign_in_conn(@client_id, ~w(create))
42 44
         |> post(@route, body)
43 45
 
46
+      # |> doc(description: "Creates a new form-encoded post", operation_id: "micropub_form_encoded_post")
47
+
44 48
       assert text_response(conn, :created)
45 49
       assert get_resp_header(conn, "location") |> List.first() =~ "/post/"
46 50
     end
@@ -58,6 +62,8 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
58 62
           }
59 63
         })
60 64
 
65
+      # |> doc(description: "Creates a new entry with JSON", operation_id: "micropub_json_post")
66
+
61 67
       assert text_response(conn, :created)
62 68
       assert List.first(get_resp_header(conn, "location")) =~ "/post/"
63 69
     end
@@ -77,6 +83,8 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
77 83
           }
78 84
         })
79 85
 
86
+      # |> doc(description: "Creates a new entry and categories with JSON", operation_id: "micropub_json_post")
87
+
80 88
       assert text_response(conn, :created)
81 89
       post_uri = List.first(get_resp_header(conn, "location"))
82 90
       assert post_uri =~ "/post/"
@@ -110,6 +118,11 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
110 118
             }
111 119
           })
112 120
 
121
+        # |> doc(
122
+        #   description: "Creates a new photo entry from URI with JSON",
123
+        #   operation_id: "micropub_json_entry_photo_uri"
124
+        # )
125
+
113 126
         assert text_response(conn, :created)
114 127
         assert List.first(get_resp_header(conn, "location")) =~ "/post/"
115 128
       end
@@ -137,6 +150,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
137 150
           })
138 151
 
139 152
         assert json_response(conn, :bad_request)
153
+        refute called(Koype.Storage.Image.store(:_))
140 154
       end
141 155
     end
142 156
 
@@ -154,7 +168,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
154 168
           3
155 169
         )
156 170
 
157
-      with_mock(Koype.Storage.Image, store: fn _ -> {:ok, uri} end) do
171
+      with_mock(Koype.Storage.Image, [:passthrough], store: fn _ -> {:ok, uri} end) do
158 172
         conn =
159 173
           build_conn()
160 174
           |> indie_sign_in_conn(@client_id, ~w(create media))
@@ -211,7 +225,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
211 225
 
212 226
       with_mock(
213 227
         IndieWeb.Micropub.Content,
214
-        handle: fn "update", _ -> {:ok, state: :updated, model: entry} end
228
+        handle: fn "update", _ -> {:ok, state: :updated, model: entry, uri: @entry_uri} end
215 229
       ) do
216 230
         conn =
217 231
           build_conn()
@@ -223,7 +237,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
223 237
             "replace" => params
224 238
           })
225 239
 
226
-        assert text_response(conn, :no_content)
240
+        assert text_response(conn, :no_content) == ""
227 241
       end
228 242
     end
229 243
 
@@ -232,7 +246,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
232 246
 
233 247
       with_mock(
234 248
         IndieWeb.Micropub.Content,
235
-        handle: fn "update", _ -> {:ok, state: :updated, model: entry} end
249
+        handle: fn "update", _ -> {:ok, state: :updated, model: entry, uri: @entry_uri} end
236 250
       ) do
237 251
         conn =
238 252
           build_conn()
@@ -246,7 +260,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
246 260
             }
247 261
           })
248 262
 
249
-        assert text_response(conn, :no_content)
263
+        assert text_response(conn, :no_content) == ""
250 264
       end
251 265
     end
252 266
 
@@ -256,7 +270,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
256 270
 
257 271
       with_mock(
258 272
         IndieWeb.Micropub.Content,
259
-        handle: fn "update", _ -> {:ok, state: :updated, model: entry} end
273
+        handle: fn "update", _ -> {:ok, state: :updated, model: entry, uri: @entry_uri} end
260 274
       ) do
261 275
         conn =
262 276
           build_conn()
@@ -268,7 +282,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
268 282
             "add" => params
269 283
           })
270 284
 
271
-        assert text_response(conn, :no_content)
285
+        assert text_response(conn, :no_content) == ""
272 286
       end
273 287
     end
274 288
 
@@ -277,7 +291,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
277 291
 
278 292
       with_mock(
279 293
         IndieWeb.Micropub.Content,
280
-        handle: fn "update", _ -> {:ok, state: :updated, model: entry} end
294
+        handle: fn "update", _ -> {:ok, state: :updated, model: entry, uri: @entry_uri} end
281 295
       ) do
282 296
         conn =
283 297
           build_conn()
@@ -289,7 +303,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
289 303
             "delete" => ["category"]
290 304
           })
291 305
 
292
-        assert text_response(conn, :no_content)
306
+        assert text_response(conn, :no_content) == ""
293 307
       end
294 308
     end
295 309
 
@@ -302,7 +316,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
302 316
         {
303 317
           IndieWeb.Micropub.Content,
304 318
           [],
305
-          handle: fn "update", _ -> {:ok, state: :updated, model: entry} end
319
+          handle: fn "update", _ -> {:ok, state: :updated, model: entry, uri: @entry_uri} end
306 320
         },
307 321
         {
308 322
           Koype.Repo.Entry.Json,
@@ -322,14 +336,16 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
322 336
             }
323 337
           })
324 338
 
325
-        assert text_response(conn, :no_content)
339
+        assert text_response(conn, :no_content) == ""
326 340
       end
327 341
     end
328 342
 
329
-    test "200 deletes an entire existing entry" do
343
+    test "204 deletes an entire existing entry" do
330 344
       entry = insert(:entry)
331 345
 
332
-      with_mock(IndieWeb.Micropub.Content, handle: fn "delete", _ -> {:ok, state: :deleted, model: entry} end) do
346
+      with_mock(IndieWeb.Micropub.Content,
347
+        handle: fn "delete", _ -> {:ok, state: :deleted, model: entry, uri: @entry_uri} end
348
+      ) do
333 349
         conn =
334 350
           build_conn()
335 351
           |> indie_sign_in_conn(@client_id, ~w(delete))
@@ -339,14 +355,16 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
339 355
             "url" => @entry_uri
340 356
           })
341 357
 
342
-        assert text_response(conn, :gone)
358
+        assert text_response(conn, :no_content)
343 359
       end
344 360
     end
345 361
 
346 362
     test "201 un-deletes an entire existing entry" do
347 363
       entry = insert(:entry)
348 364
 
349
-      with_mock(IndieWeb.Micropub.Content, handle: fn "undelete", _ -> {:ok, state: :undeleted, model: entry} end) do
365
+      with_mock(IndieWeb.Micropub.Content,
366
+        handle: fn "undelete", _ -> {:ok, state: :undeleted, model: entry, uri: @entry_uri} end
367
+      ) do
350 368
         conn =
351 369
           build_conn()
352 370
           |> indie_sign_in_conn(@client_id, ~w(undelete))
@@ -360,21 +378,7 @@ defmodule Koype.Web.Indie.MicropubControllerTest do
360 378
       end
361 379
     end
362 380
 
363
-    test "200 creates a new event" do
364
-      body = %{
365
-        "h" => "entry",
366
-        "content" => Faker.Lorem.paragraph(),
367
-        "category" => Faker.Lorem.words()
368
-      }
369
-
370
-      conn =
371
-        build_conn()
372
-        |> indie_sign_in_conn(@client_id, ~w(create))
373
-        |> post(@route, body)
374
-
375
-      assert text_response(conn, :created)
376
-      assert get_resp_header(conn, "location") |> List.first() =~ "/post/"
377
-    end
381
+    test "201 creates a new event"
378 382
   end
379 383
 
380 384
   describe "POST .upload/2" do

+ 12
- 14
test/integration/controllers/indie/rel_me_controller_test.exs View File

@@ -1,35 +1,33 @@
1 1
 defmodule Koype.Web.RelMeControllerTest do
2 2
   use Koype.Web.ConnCase
3 3
   import Koype.Factory
4
+  import Mock
4 5
 
5 6
   describe "GET .view/2" do
6
-    test "200 renders page with rel-me links" do
7
+    test "200 GET renders page with rel-me links" do
7 8
       links = insert_list(3, :relme)
8
-      conn = build_conn() |> owner_sign_in |> get(rel_me_path(Koype.Web.Endpoint, :view))
9
-
10
-      assert Enum.all?(links, fn link -> html_response(conn, :ok) =~ link.uri end)
9
+      title = Faker.Lorem.sentence()
10
+      image = Faker.Avatar.image_url()
11
+
12
+      with_mocks([
13
+        {IndieWeb.RelMe, [], is_active?: fn _, _ -> true end, all: fn -> Enum.map(links, fn l -> l.uri end) end},
14
+        {Koype.Page, [], title: fn _ -> title end, icon: fn _ -> image end}
15
+      ]) do
16
+        conn = build_conn() |> owner_sign_in |> get(rel_me_path(Koype.Web.Endpoint, :view))
17
+        assert Enum.all?(links, fn link -> html_response(conn, :ok) =~ link.uri end)
18
+      end
11 19
     end
12 20
   end
13 21
 
14 22
   describe "POST .add/2" do
15
-    @tag skip: true
16 23
     test "201 adds a new rel-me link to the page"
17
-
18
-    @tag skip: true
19 24
     test "422 fails to add a rel-me for an existing URI"
20
-
21
-    @tag skip: true
22 25
     test "400 fails if provided a non-URI value"
23 26
   end
24 27
 
25 28
   describe "DELETE .delete/2" do
26
-    @tag skip: true
27 29
     test "200 removes the URI as a rel-me cleanly"
28
-
29
-    @tag skip: true
30 30
     test "422 fails to find a rel-me for the provided URI"
31
-
32
-    @tag skip: true
33 31
     test "400 fails if provided a non-URI value"
34 32
   end
35 33
 end

+ 2
- 2
test/integration/controllers/media_controller_test.exs View File

@@ -13,7 +13,7 @@ defmodule Koype.Web.MediaControllerTest do
13 13
       mock_module =
14 14
         Koype.Storage.Image
15 15
         |> double
16
-        |> allow(:url, fn _, _ -> url end)
16
+        |> allow(:url, fn _, _, _ -> url end)
17 17
 
18 18
       with_mocks([
19 19
         {Koype.Storage, [], module_for_type: fn "image" -> mock_module end}
@@ -22,7 +22,7 @@ defmodule Koype.Web.MediaControllerTest do
22 22
           build_conn()
23 23
           |> get("/media/image/entry:#{entry.id}/#{image_path}")
24 24
 
25
-        assert conn
25
+        assert conn.status_code == 200
26 26
       end
27 27
     end
28 28
   end

+ 1
- 1
test/integration/controllers/page_controller_test.exs View File

@@ -21,7 +21,7 @@ defmodule Koype.Web.PageControllerTest do
21 21
           build_conn()
22 22
           |> get("/")
23 23
 
24
-        assert html_response(conn, :ok)
24
+        assert redirected_to(conn, 302) =~ "/stream"
25 25
       end
26 26
     end
27 27
   end

+ 42
- 42
test/integration/controllers/setup_controller_test.exs View File

@@ -7,7 +7,7 @@ defmodule Koype.Web.SetupControllerTest do
7 7
   describe ".view/2" do
8 8
     @route setup_path(Koype.Web.Endpoint, :view)
9 9
 
10
-    test "redirects user to their homepage when setup is complete" do
10
+    test "302 GET redirects user to their homepage when setup is complete" do
11 11
       with_mock(
12 12
         Koype.Setup,
13 13
         complete?: fn -> true end
@@ -19,8 +19,8 @@ defmodule Koype.Web.SetupControllerTest do
19 19
       end
20 20
     end
21 21
 
22
-    test "renders setup when setup is incomplete" do
23
-      with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :profile end) do
22
+    test "200 GET renders setup when setup is incomplete" do
23
+      with_mock(Koype.Setup, [:passthrough], complete?: fn -> false end, state: fn -> :profile end) do
24 24
         conn = get(build_conn(), @route)
25 25
 
26 26
         assert html_response(conn, :ok)
@@ -28,8 +28,8 @@ defmodule Koype.Web.SetupControllerTest do
28 28
       end
29 29
     end
30 30
 
31
-    test "renders setup for auth when profile setup is complete" do
32
-      with_mock(Koype.Setup, complete?: fn -> false end, state: fn -> :auth end) do
31
+    test "200 GET renders setup for auth when profile setup is complete" do
32
+      with_mock(Koype.Setup, [:passthrough], complete?: fn -> false end, state: fn -> :auth end) do
33 33
         conn = get(build_conn(), @route)
34 34
 
35 35
         assert html_response(conn, :ok) =~ "Authentication"
@@ -39,14 +39,14 @@ defmodule Koype.Web.SetupControllerTest do
39 39
   end
40 40
 
41 41
   describe ".handle/2" do
42
-    test "fails to upload photo for profile" do
42
+    test "422 POST fails to upload photo for profile" do
43 43
       route = setup_path(Koype.Web.Endpoint, :handle)
44 44
 
45 45
       with_mock(Koype.Storage.Image, store: fn _ -> {:error, :upload_error} end) do
46 46
         conn =
47 47
           build_conn()
48 48
           |> add_csrf_token
49
-          |> put(route, %{
49
+          |> post(route, %{
50 50
             name: Faker.Name.name(),
51 51
             nickname: Faker.Internet.user_name(),
52 52
             note: Faker.Lorem.sentence(),
@@ -58,38 +58,7 @@ defmodule Koype.Web.SetupControllerTest do
58 58
       end
59 59
     end
60 60
 
61
-    test "successfully sets up profile" do
62
-      route = setup_path(Koype.Web.Endpoint, :handle)
63
-      photo = make_mock_upload(:image)
64
-      mock_file_name = Faker.File.file_name(:image)
65
-
66
-      with_mock(
67
-        Koype.Storage.Image,
68
-        [:passthrough],
69
-        store: fn _val -> {:ok, mock_file_name} end
70
-      ) do
71
-        conn =
72
-          build_conn()
73
-          |> put_req_header("content-type", "multipart/form-data")
74
-          |> Plug.Test.init_test_session(%{})
75
-          |> add_csrf_token
76
-          |> post(route, %{
77
-            "name" => Faker.Name.name(),
78
-            "nickname" => Faker.Internet.user_name(),
79
-            "note" => Faker.Lorem.sentence(),
80
-            "photo" => photo,
81
-            "prefer_nickname" => true,
82
-            "state" => "profile"
83
-          })
84
-
85
-        assert html_response(conn, 200) =~ ""
86
-        assert get_flash(conn, :success) =~ "Let's continue with the setup"
87
-        assert Koype.Setup.state() == :auth
88
-        assert called(Koype.Storage.Image.store(:_))
89
-      end
90
-    end
91
-
92
-    test "fails if no secret is in the session" do
61
+    test "422 POST fails if no secret is in the session" do
93 62
       route = setup_path(Koype.Web.Endpoint, :handle)
94 63
       code = Enum.random(100_000..999_999)
95 64
 
@@ -100,7 +69,7 @@ defmodule Koype.Web.SetupControllerTest do
100 69
         build_conn()
101 70
         |> Plug.Test.init_test_session(%{})
102 71
         |> add_csrf_token
103
-        |> put(route, %{
72
+        |> post(route, %{
104 73
           code: code,
105 74
           state: "auth"
106 75
         })
@@ -109,7 +78,7 @@ defmodule Koype.Web.SetupControllerTest do
109 78
       assert Koype.Setup.state() == :auth
110 79
     end
111 80
 
112
-    test "fails if code is invalid" do
81
+    test "422 POST fails if code is invalid" do
113 82
       route = setup_path(Koype.Web.Endpoint, :handle)
114 83
       code = Enum.random(100_000..999_999)
115 84
       otp_secret = Koype.Repo.OtpSecret.generate()
@@ -121,7 +90,7 @@ defmodule Koype.Web.SetupControllerTest do
121 90
         build_conn()
122 91
         |> Plug.Test.init_test_session(%{otp_secret: otp_secret})
123 92
         |> add_csrf_token
124
-        |> put(route, %{
93
+        |> post(route, %{
125 94
           code: code,
126 95
           state: "auth"
127 96
         })
@@ -129,5 +98,36 @@ defmodule Koype.Web.SetupControllerTest do
129 98
       assert html_response(conn, 422)
130 99
       assert Koype.Setup.state() == :auth
131 100
     end
101
+
102
+    test "200 POST successfully sets up profile" do
103
+      route = setup_path(Koype.Web.Endpoint, :handle)
104
+      photo = make_mock_upload(:image)
105
+      mock_file_name = Faker.File.file_name(:image)
106
+
107
+      with_mock(
108
+        Koype.Storage.Image,
109
+        [:passthrough],
110
+        store: fn _val -> {:ok, mock_file_name} end
111
+      ) do
112
+        conn =
113
+          build_conn()
114
+          |> put_req_header("content-type", "multipart/form-data")
115
+          |> Plug.Test.init_test_session(%{})
116
+          |> add_csrf_token
117
+          |> post(route, %{
118
+            "name" => Faker.Name.name(),
119
+            "nickname" => Faker.Internet.user_name(),
120
+            "note" => Faker.Lorem.sentence(),
121
+            "photo" => photo,
122
+            "prefer_nickname" => true,
123
+            "state" => "profile"
124
+          })
125
+
126
+        assert html_response(conn, 200) =~ ""
127
+        assert get_flash(conn, :success) =~ "Let's continue with the setup"
128
+        assert Koype.Setup.state() == :auth
129
+        assert called(Koype.Storage.Image.store(:_))
130
+      end
131
+    end
132 132
   end
133 133
 end

+ 3
- 1
test/support/conn_case.ex View File

@@ -22,12 +22,14 @@ defmodule Koype.Web.ConnCase do
22 22
       use Plug.Test
23 23
       import Koype.Web.Router.Helpers
24 24
       import Koype.Guardian
25
+      import Bureaucrat.Helpers
25 26
 
26 27
       # The default endpoint for testing
27 28
       @endpoint Koype.Web.Endpoint
29
+      @owner_uri "https://koype.example"
28 30
 
29 31
       def owner_sign_in(conn),
30
-        do: owner_sign_in(conn, "https://example.com/")
32
+        do: owner_sign_in(conn, @owner_uri)
31 33
 
32 34
       def owner_sign_in(conn, host),
33 35
         do: owner_sign_in(conn, host, %{"id" => host})

+ 1
- 1
test/support/data_case.ex View File

@@ -55,7 +55,7 @@ defmodule Koype.DataCase do
55 55
 
56 56
   def setup_site() do
57 57
     secret = Koype.Repo.OtpSecret.generate()
58
-    {:ok, otp} = Koype.Repo.OtpSecret.create(secret)
58
+    Koype.Repo.OtpSecret.create(secret)
59 59
     Koype.Profile.set("name", Faker.Name.name())
60 60
     Koype.Profile.set("nickname", Faker.Internet.user_name())
61 61
     Koype.Profile.set("note", Faker.Lorem.sentence())

+ 8
- 2
test/test_helper.exs View File

@@ -3,8 +3,14 @@
3 3
   {:ok, _pid} = Application.ensure_all_started(app)
4 4
 end)
5 5
 
6
-Application.put_env(:wallaby, :base_url, Koype.Web.Endpoint.url())
6
+Bureaucrat.start(
7
+  env_var: "DOC",
8
+  writer: Bureaucrat.SwaggerSlateMarkdownWriter,
9
+  default_path: "doc/api/source/index.html.md",
10
+  swagger: "priv/static/swagger.json" |> File.read!() |> Jason.decode!(),
11
+  json_library: Jason
12
+)
7 13
 
8
-ExUnit.configure(exclude: [acceptance: true, skip: true])
14
+ExUnit.configure(exclude: [acceptance: true, skip: true], formatters: [ExUnit.CLIFormatter, Bureaucrat.Formatter])
9 15
 Ecto.Adapters.SQL.Sandbox.mode(Koype.Repo, :manual)
10 16
 ExUnit.start()

+ 27
- 24
test/unit/indieweb/micropub/entry_test.exs View File

@@ -225,7 +225,7 @@ defmodule IndieWeb.Micropub.EntryTest do
225 225
     end
226 226
 
227 227
     test "successfully creates a new text entry" do
228
-      assert {:ok, state: :created, model: _} =
228
+      assert {:ok, [state: :created, model: _, uri: _]} =
229 229
                Subject.invoke(
230 230
                  "create",
231 231
                  scope: ~w(create),
@@ -253,7 +253,7 @@ defmodule IndieWeb.Micropub.EntryTest do
253 253
             persist: fn _, _ -> {:ok, Faker.File.file_name(:text)} end,
254 254
             find: fn _ -> {:ok, entry_json} end
255 255
           ) do
256
-            assert {:ok, state: :created, model: model} =
256
+            assert {:ok, state: :created, model: model, uri: _} =
257 257
                      Subject.invoke(
258 258
                        "create",
259 259
                        scope: ~w(create),
@@ -272,7 +272,7 @@ defmodule IndieWeb.Micropub.EntryTest do
272 272
     test "successfully handles the act of creating a new entry with categories" do
273 273
       categories = Faker.Lorem.words()
274 274
 
275
-      assert {:ok, state: :created, model: result} =
275
+      assert {:ok, state: :created, model: result, uri: _} =
276 276
                Subject.invoke(
277 277
                  "create",
278 278
                  scope: ~w(create),
@@ -289,7 +289,7 @@ defmodule IndieWeb.Micropub.EntryTest do
289 289
       entry = insert(:entry)
290 290
       url = Model.get_uri(entry)
291 291
 
292
-      assert {:ok, state: :deleted, model: deleted_model} =
292
+      assert {:ok, state: :deleted, model: deleted_model, uri: _} =
293 293
                Subject.invoke(
294 294
                  "delete",
295 295
                  scope: ~w(delete),
@@ -306,17 +306,20 @@ defmodule IndieWeb.Micropub.EntryTest do
306 306
       entry = insert(:entry) |> as_deleted_entry
307 307
       url = Model.get_uri(entry)
308 308
 
309
-      assert {:ok, state: :undeleted, model: deleted_model} =
310
-               Subject.invoke(
311
-                 "undelete",
312
-                 scope: ~w(undelete),
313
-                 params: [
314
-                   reserved: %{"url" => url},
315
-                   content: %{}
316
-                 ]
317
-               )
309
+      with_mock(Koype.Repo.Base, undelete: fn ^entry -> {:ok, entry} end) do
310
+        assert {:ok, state: :undeleted, model: deleted_model, uri: _} =
311
+                 Subject.invoke(
312
+                   "undelete",
313
+                   scope: ~w(undelete),
314
+                   params: [
315
+                     reserved: %{"url" => url},
316
+                     content: %{}
317
+                   ]
318
+                 )
318 319
 
319
-      assert deleted_model.deleted_at == nil
320
+        assert deleted_model.deleted_at == nil
321
+        assert called(Koype.Repo.Base.undelete(entry))
322
+      end
320 323
     end
321 324
 
322 325
     test "successfully handles the act of updating an entry - add" do
@@ -329,7 +332,7 @@ defmodule IndieWeb.Micropub.EntryTest do
329 332
         persist: fn _, _ -> {:ok, Faker.File.file_name(:text)} end,
330 333
         find: fn _ -> {:ok, entry_json} end
331 334
       ) do
332
-        assert {:ok, state: :updated, model: model} =
335
+        assert {:ok, state: :updated, model: model, uri: _} =
333 336
                  Subject.invoke(
334 337
                    "update",
335 338
                    scope: ~w(update),
@@ -360,7 +363,7 @@ defmodule IndieWeb.Micropub.EntryTest do
360 363
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
361 364
         find: fn ^entry -> {:ok, entry_json} end
362 365
       ) do
363
-        assert {:ok, state: :updated, model: _} =
366
+        assert {:ok, state: :updated, model: _, uri: _} =
364 367
                  Subject.invoke(
365 368
                    "update",
366 369
                    scope: ~w(update),
@@ -392,7 +395,7 @@ defmodule IndieWeb.Micropub.EntryTest do
392 395
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
393 396
         find: fn ^entry -> {:ok, entry_json} end
394 397
       ) do
395
-        assert {:ok, state: :updated, model: _} =
398
+        assert {:ok, state: :updated, model: _, uri: _} =
396 399
                  Subject.invoke(
397 400
                    "update",
398 401
                    scope: ~w(update),
@@ -420,7 +423,7 @@ defmodule IndieWeb.Micropub.EntryTest do
420 423
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
421 424
         find: fn ^entry -> {:ok, entry_json} end
422 425
       ) do
423
-        assert {:ok, state: :updated, model: _} =
426
+        assert {:ok, state: :updated, model: _, uri: _} =
424 427
                  Subject.invoke(
425 428
                    "update",
426 429
                    scope: ~w(update),
@@ -459,7 +462,7 @@ defmodule IndieWeb.Micropub.EntryTest do
459 462
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
460 463
         find: fn ^entry -> {:ok, entry_json} end
461 464
       ) do
462
-        assert {:ok, state: :updated, model: _} =
465
+        assert {:ok, state: :updated, model: _, uri: _} =
463 466
                  Subject.invoke(
464 467
                    "update",
465 468
                    scope: ~w(update),
@@ -497,7 +500,7 @@ defmodule IndieWeb.Micropub.EntryTest do
497 500
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
498 501
         find: fn ^entry -> {:ok, entry_json} end
499 502
       ) do
500
-        assert {:ok, state: :updated, model: _} =
503
+        assert {:ok, state: :updated, model: _, uri: _} =
501 504
                  Subject.invoke(
502 505
                    "update",
503 506
                    scope: ~w(update),
@@ -532,7 +535,7 @@ defmodule IndieWeb.Micropub.EntryTest do
532 535
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
533 536
         find: fn ^entry -> {:ok, entry_json} end
534 537
       ) do
535
-        assert {:ok, state: :updated, model: _} =
538
+        assert {:ok, state: :updated, model: _, uri: _} =
536 539
                  Subject.invoke(
537 540
                    "update",
538 541
                    scope: ~w(update),
@@ -569,7 +572,7 @@ defmodule IndieWeb.Micropub.EntryTest do
569 572
         end,
570 573
         find: fn ^entry -> {:ok, entry_json} end
571 574
       ) do
572
-        assert {:ok, state: :updated, model: _} =
575
+        assert {:ok, state: :updated, model: _, uri: _} =
573 576
                  Subject.invoke(
574 577
                    "update",
575 578
                    scope: ~w(update),
@@ -599,7 +602,7 @@ defmodule IndieWeb.Micropub.EntryTest do
599 602
         persist: fn ^entry, _ -> {:ok, Faker.File.file_name(:text)} end,
600 603
         find: fn ^entry -> {:ok, entry_json} end
601 604
       ) do
602
-        assert {:ok, state: :updated, model: _} =
605
+        assert {:ok, state: :updated, model: _, uri: _} =
603 606
                  Subject.invoke(
604 607
                    "update",
605 608
                    scope: ~w(update),
@@ -671,7 +674,7 @@ defmodule IndieWeb.Micropub.EntryTest do
671 674
       test "converts " <> property <> " URI to fleshed out info" do
672 675
         results = %{
673 676
           "url" => @url,
674
-          "title" => "Where Will the Current State of Blogging and Social Media Take Us?",
677
+          "title" => "Where Will the Current State of Blogging and Social Media Take Us?",
675 678
           "author" => %{
676 679
             "name" => "Jacky Alciné",
677 680
             "photo" =>

+ 69
- 61
test/unit/repo/entry_test.exs View File

@@ -1,18 +1,19 @@
1
-defmodule Koype.Repo.EntryTest do
1
+defmodule Koype.Repo.SubjectTest do
2 2
   use Koype.Web.ConnCase
3 3
   use ExUnit.Case, async: false
4 4
   use ExVCR.Mock
5 5
   import Mock
6 6
   import Koype.Factory
7
-  alias Koype.{Repo, Repo.Entry}
7
+  alias Koype.Repo
8
+  alias Koype.Repo.Entry, as: Subject
8 9
   alias Phoenix.HTML.SimplifiedHelpers.Truncate, as: T
9
-  doctest Entry
10
+  doctest Subject
10 11
 
11 12
   describe ".changeset/2" do
12 13
     test "validates successfully provided payload" do
13 14
       params = params_for(:entry)
14 15
 
15
-      cs = Entry.changeset(%Entry{}, params)
16
+      cs = Subject.changeset(%Subject{}, params)
16 17
       assert [] = cs.errors
17 18
     end
18 19
 
@@ -21,7 +22,7 @@ defmodule Koype.Repo.EntryTest do
21 22
         params_for(:entry)
22 23
         |> Map.delete(:slug)
23 24
 
24
-      cs = Entry.changeset(%Entry{}, params)
25
+      cs = Subject.changeset(%Subject{}, params)
25 26
       assert [] = cs.errors
26 27
     end
27 28
 
@@ -31,57 +32,37 @@ defmodule Koype.Repo.EntryTest do
31 32
         |> Map.delete(:type)
32 33
         |> Map.delete("type")
33 34
 
34
-      cs = Entry.changeset(%Entry{}, params)
35
+      cs = Subject.changeset(%Subject{}, params)
35 36
       assert [] == cs.errors
36 37
     end
37 38
 
38
-    @tag :skip
39 39
     test "fails if the slug is already in use" do
40 40
       existing_entry = insert(:entry)
41 41
       params = params_for(:entry, %{slug: existing_entry.slug})
42
-      cs = Repo.insert(Entry.changeset(%Entry{}, params))
43
-      refute [] = cs.errors
42
+
43
+      assert_raise(Sqlite.DbConnection.Error, ~r/UNIQUE constraint failed/, fn ->
44
+        cs = Repo.insert(Subject.changeset(%Subject{}, params))
45
+        refute [] = cs.errors
46
+      end)
44 47
     end
45 48
   end
46 49
 
47 50
   describe ".create/2" do
48
-    test "successfully creates a new text-based hEntry (note)" do
49
-      properties = %{
50
-        "name" => Faker.Lorem.sentence(),
51
-        "content" => %{"value" => [Faker.Lorem.paragraph()]}
52
-      }
53
-
54
-      with_mock(Entry.Json, persist: fn _entry, _props -> {:ok, "foo"} end) do
55
-        assert {:ok, _entry} = Entry.create(properties)
56
-        assert called(Entry.Json.persist(:_, %{"name" => :_, "content" => :_}))
57
-      end
58
-    end
59
-
60
-    test "successfully creates a new html-based hEntry (note)" do
61
-      properties = %{
62
-        "name" => Faker.Lorem.sentence(),
63
-        "content" => %{
64
-          "value" => [Faker.Lorem.paragraph()],
65
-          "html" => Faker.Lorem.paragraph()
66
-        }
67
-      }
68
-
69
-      with_mock(Entry.Json, persist: fn _entry, _props -> {:ok, "foo"} end) do
70
-        assert {:ok, _entry} = Entry.create(properties)
71
-        assert called(Entry.Json.persist(:_, %{"name" => :_, "content" => :_}))
72
-      end
51
+    test "successfully creates a new entry" do
52
+      entry_json = build(:entry_json)
53
+      assert {:ok, _entry} = Subject.create(entry_json)
73 54
     end
74 55
 
75
-    test "scrubs out invalid properties for a hEntry" do
56
+    test "scrubs out invalid properties for a hSubject" do
76 57
       properties = %{
77 58
         "name" => Faker.Lorem.sentence(),
78 59
         "content" => %{"value" => [Faker.Lorem.sentence()]},
79 60
         "foo" => Faker.Lorem.paragraph()
80 61
       }
81 62
 
82
-      with_mock(Entry.Json, persist: fn _entry, _props -> {:ok, Faker.File.file_name(:text)} end) do
83
-        assert {:ok, _} = Entry.create(properties)
84
-        assert called(Entry.Json.persist(:_, :_))
63
+      with_mock(Subject.Json, persist: fn _entry, _props -> {:ok, Faker.File.file_name(:text)} end) do
64
+        assert {:ok, _} = Subject.create(properties)
65
+        assert called(Subject.Json.persist(:_, :_))
85 66
       end
86 67
     end
87 68
 
@@ -92,9 +73,9 @@ defmodule Koype.Repo.EntryTest do
92 73
 
93 74
       expected_name = T.truncate(String.slice(List.first(properties["content"]["value"]), 0..50))
94 75
 
95
-      with_mock(Entry.Json, persist: fn _entry, _props -> {:ok, "foo"} end) do
96
-        assert {:ok, %Entry{name: ^expected_name}} = Entry.create(properties)
97
-        assert called(Entry.Json.persist(:_, :_))
76
+      with_mock(Subject.Json, persist: fn _entry, _props -> {:ok, "foo"} end) do
77
+        assert {:ok, %Subject{name: ^expected_name}} = Subject.create(properties)
78
+        assert called(Subject.Json.persist(:_, :_))
98 79
       end
99 80
     end
100 81
   end
@@ -102,43 +83,46 @@ defmodule Koype.Repo.EntryTest do
102 83
   describe ".update/2" do
103 84
     test "updates the slug for the entry" do
104 85
       entry = insert(:entry)
105
-      entry_json = build(:entry_json) |> Map.put("name", Faker.Lorem.word())
86
+
87
+      {:ok, entry_json} =
88
+        build(:entry_json, %{"name" => Faker.Lorem.word()}) |> IndieWeb.Micropub.Content.process_properties()
89
+
106 90
       new_props = %{"slug" => Faker.Lorem.word()}
107 91
       new_entry_json = Map.merge(entry_json, new_props)
108 92
 
109 93
       with_mocks([
110 94
         {
111
-          Entry.Json,
95
+          Subject.Json,
112 96
           [:passthrough],
113 97
           persist: fn _entry, props -> {:ok, props} end
114 98
         }
115 99
       ]) do
116
-        assert {:ok, entry} = Entry.update(entry, new_entry_json)
117
-        assert called(Entry.Json.persist(entry, new_entry_json))
100
+        assert {:ok, entry} = Subject.update(entry, new_entry_json)
101
+        assert called(Subject.Json.persist(entry, new_entry_json))
118 102
         assert %{"slug" => entry.slug} == new_props
119 103
       end
120 104
     end
121 105
 
122 106
     test "generates a name for the entry" do
123 107
       entry = insert(:entry, %{name: nil})
124
-      entry_json = build(:entry_json)
108
+      {:ok, entry_json} = build(:entry_json) |> IndieWeb.Micropub.Content.process_properties()
125 109
       name = String.slice(List.first(entry_json["content"]["value"]), 0..50)
126 110
 
127 111
       with_mocks([
128 112
         {
129
-          Entry.Json,
113
+          Subject.Json,
130 114
           [:passthrough],
131 115
           persist: fn _entry, props -> {:ok, props} end
132 116
         }
133 117
       ]) do
134
-        assert {:ok, entry} = Entry.update(entry, entry_json)
118
+        assert {:ok, entry} = Subject.update(entry, entry_json)
135 119
         assert entry.name == name
136
-        assert called(Entry.Json.persist(entry, entry_json))
120
+        assert called(Subject.Json.persist(entry, entry_json))
137 121
       end
138 122
     end
139 123
 
140
-    @tag :skip
141 124
     test "updates a mf2 property of the entry"
125
+    test "regenerates name of entry on update"
142 126
   end
143 127
 
144 128
   describe ".get_uri/2" do
@@ -146,7 +130,7 @@ defmodule Koype.Repo.EntryTest do
146 130
       entry = insert(:entry)
147 131
 
148 132
       assert String.ends_with?(
149
-               URI.decode(Entry.get_uri(entry)),
133
+               URI.decode(Subject.get_uri(entry)),
150 134
                entry.slug
151 135
              )
152 136
     end
@@ -155,7 +139,7 @@ defmodule Koype.Repo.EntryTest do
155 139
       entry = insert(:entry, %{slug: nil})
156 140
 
157 141
       assert String.ends_with?(
158
-               URI.decode(Entry.get_uri(entry)),
142
+               URI.decode(Subject.get_uri(entry)),
159 143
                entry.id
160 144
              )
161 145
     end
@@ -165,39 +149,63 @@ defmodule Koype.Repo.EntryTest do
165 149
     test "finds the entry by the provided ID" do
166 150
       entry = insert(:entry)
167 151
 
168
-      assert {:ok, _} = Entry.fetch(entry.id)
152
+      assert {:ok, _} = Subject.fetch(entry.id)
169 153
     end
170 154
 
171 155
     test "finds the entry by the provided slug" do
172 156
       entry = insert(:entry)
173 157
 
174
-      assert {:ok, _} = Entry.fetch(entry.slug)
158
+      assert {:ok, _} = Subject.fetch(entry.slug)
175 159
     end
176 160
 
177 161
     test "fails to find entry" do
178
-      assert {:error, :model_not_found} = Entry.fetch(UUID.uuid1())
162
+      assert {:error, :model_not_found} = Subject.fetch(UUID.uuid1())
179 163
     end
180 164
   end
181 165
 
182 166
   describe ".resolve_from_uri/1" do
183 167
     test "extracts the slug from the path" do
184 168
       entry = insert(:entry)
185
-      uri = Entry.get_uri(entry)
169
+      uri = Subject.get_uri(entry)
186 170
 
187
-      assert {:ok, entry} == Entry.resolve_from_uri(uri)
171
+      assert {:ok, entry} == Subject.resolve_from_uri(uri)
188 172
     end
189 173
 
190 174
     test "extracts the ID from the path" do
191 175
       entry = insert(:entry, %{slug: nil})
192
-      uri = Entry.get_uri(entry)
176
+      uri = Subject.get_uri(entry)
177
+
178
+      assert {:ok, entry} == Subject.resolve_from_uri(uri)
179
+    end
180
+  end
181
+
182
+  describe ".delete/1" do
183
+    test "deletes an entry" do
184
+      entry = insert(:entry)
185
+      assert {:ok, model} = Koype.Repo.Base.delete(entry)
186
+      assert model.deleted_at
187
+    end
188
+  end
193 189
 
194
-      assert {:ok, entry} == Entry.resolve_from_uri(uri)
190
+  describe ".undelete/1" do
191
+    test "undeletes an entry" do
192
+      entry = insert(:entry) |> as_deleted_entry
193
+      assert {:ok, model} = Koype.Repo.Base.undelete(entry)
194
+      refute model.deleted_at
195 195
     end
196 196
   end
197 197
 
198 198
   describe ".with_post_status/2" do
199
-    test "shows all of the published posts"
200
-    test "shows all of the deleted drafts posts"
199
+    test "shows all of the undeleted published posts" do
200
+      insert_list(3, :entry, %{post_status: "published"})
201
+      assert Subject.with_post_status("published") |> Koype.Repo.count() == 3
202
+    end
203
+
204
+    test "shows all of the deleted drafts posts" do
205
+      insert_list(3, :entry, %{post_status: "published"}) |> Enum.each(&as_deleted_entry/1)
206
+      assert Subject.with_post_status("published", Koype.Repo.deleted(Subject)) |> Koype.Repo.count() == 3
207
+    end
208
+
201 209
     test "fails if invalid post status provided"
202 210
   end
203 211
 end

+ 79
- 0
web/controllers/README.md View File

@@ -0,0 +1,79 @@
1
+# API Documentation
2
+
3
+  * [Koype.Web.Indie.MicropubController](#koype-web-indie-micropubcontroller)
4
+    * [new](#koype-web-indie-micropubcontroller-new)
5
+
6
+## Koype.Web.Indie.MicropubController
7
+### <a id=koype-web-indie-micropubcontroller-new></a>new
8
+#### Missing needed scope for request
9
+##### Request
10
+* __Method:__ POST
11
+* __Path:__ /api/indie/micropub
12
+* __Request headers:__
13
+```
14
+authorization: bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrb3lwZSIsImV4cCI6MTU0OTc0MzMwNiwiaWF0IjoxNTQ3MzI0MTA2LCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5taWNyb3B1Yi5hcHB0ZXN0IiwiaXNzIjoia295cGUiLCJqdGkiOiI5NGVjYTFmNi0wZDE5LTQyZDctODBiNy1kNjg4NGU2MDczZWIiLCJuYmYiOjE1NDczMjQxMDUsInNjb3BlIjoicmVhZCIsInN1YiI6Imh0dHBzOi8vZXhhbXBsZS5taWNyb3B1Yi5hcHB0ZXN0IiwidHlwIjoiYWNjZXNzIn0.R7i5RAxUQa1qkbwAXUSIfYqvOrr8K37p8Yruh8DllrudfBZKCVlsQhWa7ULjZSCtp6GNgSSnHl83W5FL948VXQ
15
+content-type: multipart/mixed; boundary=plug_conn_test
16
+```
17
+* __Request body:__
18
+```json
19
+{
20
+  "h": "entry"
21
+}
22
+```
23
+
24
+##### Response
25
+* __Status__: 400
26
+* __Response headers:__
27
+```
28
+cache-control: max-age=0, private, must-revalidate
29
+x-request-id: 2lsj64t0a7l3h1ht18000025
30
+content-type: application/json; charset=utf-8
31
+```
32
+* __Response body:__
33
+```json
34
+{
35
+  "statusCode": 400,
36
+  "message": "Bad Request",
37
+  "error": "Bad Request"
38
+}
39
+```
40
+
41
+#### Creates a new form-encoded post
42
+##### Request
43
+* __Method:__ POST
44
+* __Path:__ /api/indie/micropub
45
+* __Request headers:__
46
+```
47
+authorization: bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJrb3lwZSIsImV4cCI6MTU0OTc0MzMwNywiaWF0IjoxNTQ3MzI0MTA3LCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5taWNyb3B1Yi5hcHB0ZXN0IiwiaXNzIjoia295cGUiLCJqdGkiOiIwZTZhMjU5Yy02Yzg1LTRiYmQtOTZhMC0wN2ZjNTNlMGM1ZDMiLCJuYmYiOjE1NDczMjQxMDYsInNjb3BlIjoiY3JlYXRlIiwic3ViIjoiaHR0cHM6Ly9leGFtcGxlLm1pY3JvcHViLmFwcHRlc3QiLCJ0eXAiOiJhY2Nlc3MifQ.YEDvjJAZtSLfFaXSnJeGMYy4Eyb1NShpDhuTHWWdytXJDRBdOS5mbypOlnU_qpq77XM-ACUmjYfTypb9cafQ-A
48
+content-type: multipart/mixed; boundary=plug_conn_test
49
+```
50
+* __Request body:__
51
+```json
52
+{
53
+  "h": "entry",
54
+  "content": "Perferendis ea quia explicabo eum hic omnis repellendus saepe quibusdam! Deleniti autem neque aliquid. Quis amet officiis minima aperiam nobis quaerat autem. Quia dicta temporibus consequuntur necessitatibus id. Sint accusantium illo eum!",
55
+  "category": [
56
+    "animi",
57
+    "quia",
58
+    "laboriosam",
59
+    "minima",
60
+    "omnis",
61
+    "numquam"
62
+  ]
63
+}
64
+```
65
+
66
+##### Response
67
+* __Status__: 201
68
+* __Response headers:__
69
+```
70
+content-type: text/plain; charset=utf-8
71
+cache-control: max-age=0, private, must-revalidate
72
+x-request-id: 2lsj64ul07s2gr7j140001q3
73
+location: http://a1e2ce9b.ngrok.io/post/3901e4a2-d455-427a-a342-321b77453782
74
+```
75
+* __Response body:__
76
+```json
77
+
78
+```
79
+

+ 7
- 4
web/controllers/indie/micropub_controller.ex View File

@@ -18,7 +18,6 @@
18 18
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 19
 defmodule Koype.Web.Indie.MicropubController do
20 20
   use Koype.Web, :controller
21
-  alias Koype.Repo.Entry
22 21
   alias Koype.Storage
23 22
   alias Koype.Web.AuthenticationHelpers, as: AuthHelper
24 23
   alias IndieWeb.Post
@@ -155,12 +154,16 @@ defmodule Koype.Web.Indie.MicropubController do
155 154
         Explode.bad_request(conn)
156 155
 
157 156
       # TODO: Need to push this up to entry but still allow for this kind of output.
158
-      {:ok, state: _, model: model} ->
159
-        model_uri = Entry.get_uri(model)
157
+      {:ok, state: state, model: model, uri: model_uri} ->
160 158
         Logger.info("Providing #{model_uri} as the permalink for #{model.id}.")
161 159
 
160
+        state_to_status = %{
161
+          deleted: :no_content,
162
+          updated: :no_content
163
+        }
164
+
162 165
         conn
163
-        |> put_status(201)
166
+        |> put_status(Map.get(state_to_status, state, :created))
164 167
         |> put_resp_header("location", model_uri)
165 168
         |> text("")
166 169
     end

+ 2
- 1
web/endpoint.ex View File

@@ -53,7 +53,8 @@ defmodule Koype.Web.Endpoint do
53 53
     Plug.Static,
54 54
     at: "/",
55 55
     from: :koype,
56
-    only: ~w(assets)
56
+    gzip: false,
57
+    only: ~w(assets doc)
57 58
   )
58 59
 
59 60
   plug(Koype.Web.Router)

+ 20
- 0
web/router.ex View File

@@ -127,8 +127,28 @@ defmodule Koype.Web.Router do
127 127
     post("/micropub/media", Indie.MicropubController, :upload)
128 128
   end
129 129
 
130
+  scope "/api/swagger" do
131
+    forward("/", PhoenixSwagger.Plug.SwaggerUI, otp_app: :koype, swagger_file: "swagger.json")
132
+  end
133
+
130 134
   scope "/", Koype.Web do
131 135
     pipe_through([:browser])
132 136
     get("/*path", FallbackController, :catch_all)
133 137
   end
138
+
139
+  def swagger_info do
140
+    %{
141
+      "info" => %{
142
+        "version" => Koype.version(),
143
+        "title" => "Koype",
144
+        "license" => %{
145
+          "name" => "AGPLv3"
146
+        }
147
+      },
148
+      "tags" => [
149
+        %{"name" => "Micropub", "description" => "Operations for working with Micropub."},
150
+        %{"name" => "IndieAuth", "description" => "Operations for working with IndieAuth."}
151
+      ]
152
+    }
153
+  end
134 154
 end

Loading…
Cancel
Save