Browse Source

feat(hcard): Format + add representative h-card parsisng.

jackyalcine 4 months ago
parent
commit
bfd95135c8
Signed by: Jacky Alciné <yo@jacky.wtf> GPG Key ID: 537A4F904B15268D

+ 1
- 1
.formatter.exs View File

@@ -1,4 +1,4 @@
1 1
 [
2 2
   inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
3
-  line_length: 120
3
+  line_length: 80
4 4
 ]

+ 1
- 0
config/dev.exs View File

@@ -0,0 +1 @@
1
+

+ 1
- 0
config/prod.exs View File

@@ -0,0 +1 @@
1
+

+ 1
- 1
lib/indieweb.ex View File

@@ -1,7 +1,7 @@
1 1
 defmodule IndieWeb do
2 2
   @moduledoc """
3 3
   The IndieWeb is a people-focused alternative to the "corporate web". 
4
-  
4
+
5 5
   This library provides common facilities for handling interactions and 
6 6
   logic in the IndieWeb space. For more information; check out the open
7 7
   Wiki over at <https://indieweb.org>.

+ 31
- 7
lib/indieweb/auth.ex View File

@@ -4,8 +4,14 @@ defmodule IndieWeb.Auth do
4 4
   """
5 5
 
6 6
   @doc "Provides the adapter to be used by `IndieWeb.Auth` for stateful actions."
7
-  @spec adapter() :: IndieWeb.Auth.Adapter.t
8
-  def adapter(), do: Application.get_env(:indieweb, :auth_adapter, IndieWeb.Auth.Adapters.Default)
7
+  @spec adapter() :: IndieWeb.Auth.Adapter.t()
8
+  def adapter(),
9
+    do:
10
+      Application.get_env(
11
+        :indieweb,
12
+        :auth_adapter,
13
+        IndieWeb.Auth.Adapters.Default
14
+      )
9 15
 
10 16
   @doc "Provides endpoint information for well known endpoints in IndieAuth."
11 17
   @spec endpoint_for(atom(), binary()) :: binary() | nil
@@ -15,7 +21,9 @@ defmodule IndieWeb.Auth do
15 21
     IndieWeb.LinkRel.find(url, "#{component}_endpoint") |> List.first()
16 22
   end
17 23
 
18
-  def endpoint_for(:redirect_uri, url), do: IndieWeb.LinkRel.find(url, "redirect_uri")
24
+  def endpoint_for(:redirect_uri, url),
25
+    do: IndieWeb.LinkRel.find(url, "redirect_uri")
26
+
19 27
   def endpoint_for(_, _), do: nil
20 28
 
21 29
   @spec authenticate(map()) :: {:ok, any()} | {:error, any()}
@@ -26,7 +34,12 @@ defmodule IndieWeb.Auth do
26 34
       {:error, _} = error ->
27 35
         error
28 36
 
29
-      {:ok, %{"client_id" => client_id, "redirect_uri" => redirect_uri, "state" => state}} ->
37
+      {:ok,
38
+       %{
39
+         "client_id" => client_id,
40
+         "redirect_uri" => redirect_uri,
41
+         "state" => state
42
+       }} ->
30 43
         code = IndieWeb.Auth.Code.generate(client_id, redirect_uri)
31 44
         do_generate_redirect_uri(redirect_uri, code, state)
32 45
     end
@@ -37,9 +50,19 @@ defmodule IndieWeb.Auth do
37 50
       {:error, _} = error ->
38 51
         error
39 52
 
40
-      {:ok, %{"client_id" => client_id, "redirect_uri" => redirect_uri, "state" => state} = args} ->
53
+      {:ok,
54
+       %{
55
+         "client_id" => client_id,
56
+         "redirect_uri" => redirect_uri,
57
+         "state" => state
58
+       } = args} ->
41 59
         scope = Map.get(args, "scope", "read")
42
-        code = IndieWeb.Auth.Code.generate(client_id, redirect_uri, %{"scope" => scope})
60
+
61
+        code =
62
+          IndieWeb.Auth.Code.generate(client_id, redirect_uri, %{
63
+            "scope" => scope
64
+          })
65
+
43 66
         do_generate_redirect_uri(redirect_uri, code, state)
44 67
     end
45 68
   end
@@ -51,7 +74,8 @@ defmodule IndieWeb.Auth do
51 74
       redirect_uri
52 75
       |> URI.parse()
53 76
       |> Map.get(:query)
54
-      |> (&(URI.decode_query(&1 || "", %{"code" => code, "state" => state}) |> URI.encode_query())).()
77
+      |> (&(URI.decode_query(&1 || "", %{"code" => code, "state" => state})
78
+            |> URI.encode_query())).()
55 79
 
56 80
     redirect_uri
57 81
     |> URI.parse()

+ 20
- 5
lib/indieweb/auth/adapter.ex View File

@@ -1,12 +1,27 @@
1 1
 defmodule IndieWeb.Auth.Adapter do
2 2
   @moduledoc "Provides an abstraction regarding stateful actions in IndieAuth."
3 3
 
4
-  @callback code_generate(client_id :: binary(), redirect_uri :: binary(), data :: map()) :: binary()
5
-  @callback code_persist(code :: binary(), client_id :: binary(), redirect_uri :: binary(), args :: map()) :: :ok | {:error, any()}
6
-  @callback code_destroy(client_id :: binary(), redirect_uri :: binary(), args :: map()) :: :ok
7
-  @callback code_verify(binary(), binary(), binary(), map()) :: :ok | {:error, any()}
4
+  @callback code_generate(
5
+              client_id :: binary(),
6
+              redirect_uri :: binary(),
7
+              data :: map()
8
+            ) :: binary()
9
+  @callback code_persist(
10
+              code :: binary(),
11
+              client_id :: binary(),
12
+              redirect_uri :: binary(),
13
+              args :: map()
14
+            ) :: :ok | {:error, any()}
15
+  @callback code_destroy(
16
+              client_id :: binary(),
17
+              redirect_uri :: binary(),
18
+              args :: map()
19
+            ) :: :ok
20
+  @callback code_verify(binary(), binary(), binary(), map()) ::
21
+              :ok | {:error, any()}
8 22
   @callback scope_get(code :: binary()) :: binary() | nil
9
-  @callback scope_persist(code :: binary(), scope :: binary()) :: :ok | {:error, any()}
23
+  @callback scope_persist(code :: binary(), scope :: binary()) ::
24
+              :ok | {:error, any()}
10 25
   @callback valid_user?(uri :: binary()) :: boolean()
11 26
   @callback token_generate(binary(), binary()) :: binary()
12 27
   @callback token_info(binary()) :: nil | {:error, any()} | map()

+ 17
- 6
lib/indieweb/auth/adapters/default.ex View File

@@ -18,12 +18,18 @@ defmodule IndieWeb.Auth.Adapters.Default do
18 18
 
19 19
   @impl true
20 20
   def code_persist(code, client_id, redirect_uri, args) do
21
-    IndieWeb.Cache.set(do_make_key_for_client(client_id, redirect_uri, args), code, expire: @code_age)
21
+    IndieWeb.Cache.set(
22
+      do_make_key_for_client(client_id, redirect_uri, args),
23
+      code,
24
+      expire: @code_age
25
+    )
22 26
   end
23 27
 
24 28
   @impl true
25 29
   def code_verify(code, client_id, redirect_uri, args) do
26
-    case IndieWeb.Cache.get(do_make_key_for_client(client_id, redirect_uri, args)) do
30
+    case IndieWeb.Cache.get(
31
+           do_make_key_for_client(client_id, redirect_uri, args)
32
+         ) do
27 33
       {:ok, nil} ->
28 34
         {:error, :code_not_found}
29 35
 
@@ -66,7 +72,9 @@ defmodule IndieWeb.Auth.Adapters.Default do
66 72
   end
67 73
 
68 74
   def valid_user?(uri) do
69
-    resolve_user_fn = Application.get_env(:indieweb, :resolve_user_fn, fn _ -> true end)
75
+    resolve_user_fn =
76
+      Application.get_env(:indieweb, :resolve_user_fn, fn _ -> true end)
77
+
70 78
     resolve_user_fn.(uri)
71 79
   end
72 80
 
@@ -82,17 +90,20 @@ defmodule IndieWeb.Auth.Adapters.Default do
82 90
   end
83 91
 
84 92
   def token_generate(client_id, scope) do
85
-    IndieWeb.Cache.set(do_make_token(client_id, scope), URI.encode_query(%{"scope" => scope, "client_id" => client_id}))
93
+    IndieWeb.Cache.set(
94
+      do_make_token(client_id, scope),
95
+      URI.encode_query(%{"scope" => scope, "client_id" => client_id})
96
+    )
86 97
   end
87 98
 
88 99
   defp do_make_key_for_client(client_id, redirect_uri, args) do
89 100
     [
90 101
       client_id,
91 102
       redirect_uri,
92
-      args |> URI.encode_query
103
+      args |> URI.encode_query()
93 104
     ]
94 105
     |> Enum.join("_")
95
-    |> URI.encode_query
106
+    |> URI.encode_query()
96 107
     |> (fn data -> :erlang.phash(:indieweb_auth_code_key, :sha256) end).()
97 108
   end
98 109
 

+ 1
- 1
lib/indieweb/auth/scope.ex View File

@@ -10,6 +10,6 @@ defmodule IndieWeb.Auth.Scope do
10 10
 
11 11
   @spec get(binary()) :: binary() | nil
12 12
   def get(code) do
13
-    IndieWeb.Auth.adapter.scope_get(code)
13
+    IndieWeb.Auth.adapter().scope_get(code)
14 14
   end
15 15
 end

+ 4
- 1
lib/indieweb/auth/token.ex View File

@@ -4,7 +4,10 @@ defmodule IndieWeb.Auth.Token do
4 4
   def generate(code, client_id, redirect_uri) do
5 5
     with(
6 6
       scope when is_binary(scope) <- IndieWeb.Auth.Scope.get(code),
7
-      :ok <- IndieWeb.Auth.Code.verify(code, client_id, redirect_uri, %{"scope" => scope})
7
+      :ok <-
8
+        IndieWeb.Auth.Code.verify(code, client_id, redirect_uri, %{
9
+          "scope" => scope
10
+        })
8 11
     ) do
9 12
       IndieWeb.Auth.Code.destroy(client_id, redirect_uri, %{"scope" => scope})
10 13
       IndieWeb.Auth.adapter().token_generate(client_id, scope)

+ 8
- 2
lib/indieweb/cache.ex View File

@@ -9,8 +9,14 @@ defmodule IndieWeb.Cache do
9 9
   """
10 10
 
11 11
   @doc "Obtains an implementation of a `IndieWeb.Cache.Adapter` module."
12
-  @spec adapter() :: IndieWeb.Cache.Adapter.t
13
-  def adapter, do: Application.get_env(:indieweb, :cache_adapter, IndieWeb.Cache.Adapters.Cachex)
12
+  @spec adapter() :: IndieWeb.Cache.Adapter.t()
13
+  def adapter,
14
+    do:
15
+      Application.get_env(
16
+        :indieweb,
17
+        :cache_adapter,
18
+        IndieWeb.Cache.Adapters.Cachex
19
+      )
14 20
 
15 21
   @doc "Fetches the value defined by `key` from the adapter; returning `value` if it doesn't exist."
16 22
   @spec get(binary(), any()) :: any() | nil

+ 2
- 1
lib/indieweb/cache/adapter.ex View File

@@ -12,5 +12,6 @@ defmodule IndieWeb.Cache.Adapter do
12 12
   @callback delete(key :: binary()) :: :ok | :error
13 13
 
14 14
   @doc "Defines the method of setting of a cached value."
15
-  @callback set(key :: binary(), value :: any(), options :: keyword()) :: :ok | :error
15
+  @callback set(key :: binary(), value :: any(), options :: keyword()) ::
16
+              :ok | :error
16 17
 end

+ 3
- 1
lib/indieweb/cache/adapters/cachex.ex View File

@@ -31,7 +31,9 @@ defmodule IndieWeb.Cache.Adapters.Cachex do
31 31
         end
32 32
 
33 33
         :ok
34
-      _ -> :error
34
+
35
+      _ ->
36
+        :error
35 37
     end
36 38
   end
37 39
 end

+ 259
- 0
lib/indieweb/hcard.ex View File

@@ -0,0 +1,259 @@
1
+defmodule IndieWeb.HCard do
2
+  @moduledoc """
3
+  Extracts the representative [h-card](http://indieweb.org/h-card) of the provided URI.
4
+  """
5
+
6
+  @doc """
7
+  Obtains a remote page and determines the h-card of the current page.
8
+
9
+  This takes a few approaches to find a h-card for the page in question.
10
+  It'll first search for a top-level h-card on the page.
11
+  If there isn't an obvious one, it'll search for a representative h-card.
12
+  """
13
+  def resolve(uri) when is_binary(uri) do
14
+    %{scheme: scheme, authority: authority} =
15
+      URI.parse(uri |> String.trim_trailing("/"))
16
+
17
+    host = "#{scheme}://#{authority}"
18
+
19
+    approaches = [
20
+      fn ->
21
+        case fetch_representative(uri) do
22
+          {:ok, hcard} -> hcard
23
+          _ -> nil
24
+        end
25
+      end,
26
+      fn ->
27
+        with(
28
+          {:ok, %IndieWeb.Http.Response{body: body}} <- IndieWeb.Http.get(uri),
29
+          mf2 when is_map(mf2) <- Microformats2.parse(body, uri),
30
+          {:ok, hcard} <- do_check_author_of_first_entry(mf2, {host, uri})
31
+        ) do
32
+          hcard
33
+        else
34
+          _ ->
35
+            nil
36
+        end
37
+      end,
38
+      fn ->
39
+        do_format_hcard(do_stub_out_hcard(uri), host)
40
+      end
41
+    ]
42
+
43
+    result = Enum.find_value(approaches, & &1.())
44
+
45
+    case result do
46
+      nil -> {:error, :no_hcard_found}
47
+      formatted_hcard -> {:ok, formatted_hcard}
48
+    end
49
+  end
50
+
51
+  def resolve(mf2, uri) when is_map(mf2) do
52
+    %{scheme: scheme, authority: authority} =
53
+      URI.parse(uri |> String.trim_trailing("/"))
54
+
55
+    host = "#{scheme}://#{authority}"
56
+
57
+    case fetch_representative(mf2, {host, uri}) do
58
+      {:ok, hcard} -> hcard
59
+      _ -> nil
60
+    end
61
+  end
62
+
63
+  @doc """
64
+  Obtains the representative h-card of the provided URI.
65
+
66
+  The [steps for parsing a representative h-card][1] are as follows:
67
+
68
+  * If the page contains an `h-card` with `uid` and `url` properties
69
+  both matching the page URL, the first such `h-card` is the
70
+  **representative h-card**.
71
+  * If no representative h-card was found, if the page contains an
72
+  `h-card` with a url property value which also has a rel=me
73
+  relation, the first such `h-card` is the **representative h-card**
74
+  * If no representative `h-card` was found, if the page contains one
75
+  single `h-card`, and the `h-card` has a url property matching the
76
+  page URL, that `h-card` is the representative `h-card`
77
+  * <thunk>
78
+
79
+  [1]: http://microformats.org/wiki/representative-h-card-parsing
80
+  """
81
+  def fetch_representative(uri) when is_binary(uri) do
82
+    with(
83
+      {:ok, %IndieWeb.Http.Response{body: body}} <- IndieWeb.Http.get(uri),
84
+      mf2 when is_map(mf2) <- Microformats2.parse(body, uri),
85
+      {:ok, hcard} <- fetch_representative(mf2, uri)
86
+    ) do
87
+      {:ok, hcard}
88
+    else
89
+      _ -> nil
90
+    end
91
+  end
92
+
93
+  def fetch_representative(mf2, uri) do
94
+    %{scheme: scheme, authority: authority} =
95
+      URI.parse(uri |> String.trim_trailing("/"))
96
+
97
+    root_uri = "#{scheme}://#{authority}"
98
+
99
+    approaches = [
100
+      &do_find_uid_url/2,
101
+      &do_find_with_matching_relme/2,
102
+      &do_find_solo/2
103
+    ]
104
+
105
+    Enum.reduce_while(approaches, {:error, :no_hcard_found}, fn approach, acc ->
106
+      case approach.(mf2, {root_uri, uri}) do
107
+        nil ->
108
+          {:cont, acc}
109
+
110
+        hcard when is_map(hcard) ->
111
+          {:halt, {:ok, do_format_hcard(hcard, root_uri)}}
112
+      end
113
+    end)
114
+  end
115
+
116
+  # TODO: We search rel=author and rel=home as well in hopes of expanding those
117
+  # for whose have might MicroFormats2 support but not fully.
118
+  defp do_find_with_matching_relme(mf2, {host, _}) do
119
+    rel_mes =
120
+      ~w(home me author)
121
+      |> Enum.map(fn key -> Map.get(mf2, :rels, %{}) |> Map.get(key, []) end)
122
+      |> List.flatten()
123
+      |> Enum.map(&String.trim_trailing(&1, "/"))
124
+      |> Enum.map(&URI.parse/1)
125
+      |> Enum.map(&URI.to_string/1)
126
+
127
+    cards = Microformats2.Utility.extract_all(mf2, "card")
128
+
129
+    Enum.find_value(cards, fn hcard ->
130
+      urls = Microformats2.Utility.get_value(hcard, :url, [])
131
+
132
+      Enum.find_value(urls, fn current_url ->
133
+        hcard_uri =
134
+          IndieWeb.Http.make_absolute_uri(current_url, host)
135
+          |> String.trim_trailing("/")
136
+          |> URI.parse()
137
+          |> URI.to_string()
138
+
139
+        if Enum.member?(rel_mes, hcard_uri) do
140
+          hcard
141
+        else
142
+          nil
143
+        end
144
+      end)
145
+    end)
146
+  end
147
+
148
+  # TODO: We can have multiple UIDs as well - maybe tuple setup.
149
+  defp do_find_uid_url(mf2, {_, uri}) do
150
+    cards = Microformats2.Utility.extract_all(mf2, "card")
151
+
152
+    Enum.find_value(cards, fn hcard ->
153
+      hcard_uri =
154
+        Microformats2.Utility.get_value(hcard, :url, ["/"])
155
+        |> List.first()
156
+        |> String.trim_trailing("/")
157
+
158
+      hcard_uid =
159
+        Microformats2.Utility.get_value(hcard, :uid, []) |> List.first()
160
+
161
+      valid_uid =
162
+        if is_nil(hcard_uid) do
163
+          true
164
+        else
165
+          String.trim_trailing(hcard_uid, "/") == uri
166
+        end
167
+
168
+      if hcard_uri == uri && valid_uid do
169
+        hcard
170
+      else
171
+        nil
172
+      end
173
+    end)
174
+  end
175
+
176
+  defp do_check_author_of_first_entry(mf2, {host, _}) do
177
+    top_items =
178
+      mf2[:items]
179
+      |> Enum.reject(fn item -> Enum.member?(item["type"], "h-card") end)
180
+
181
+    Enum.find_value(top_items, fn item ->
182
+      authors = Microformats2.Utility.get_value(item, "author")
183
+
184
+      Enum.find_value(authors, fn
185
+        author_map when is_map(author_map) ->
186
+          {:ok, do_format_hcard(author_map, host)}
187
+
188
+        author_uri when is_binary(author_uri) ->
189
+          resolved_author_uri =
190
+            IndieWeb.Http.make_absolute_uri(author_uri, host)
191
+
192
+          case IndieWeb.HCard.resolve(resolved_author_uri) do
193
+            {:ok, _} = result -> result
194
+          end
195
+      end)
196
+    end)
197
+  end
198
+
199
+  defp do_find_solo(mf2, _) do
200
+    mf2
201
+    |> Map.get(:items, [])
202
+    |> Enum.filter(fn item ->
203
+      Enum.member?(Map.get(item, :type, []), "h-card")
204
+    end)
205
+    |> List.first()
206
+  end
207
+
208
+  # TODO: Use JF2 instead of this simplified content.
209
+  defp do_format_hcard(hcard, host) do
210
+    url =
211
+      IndieWeb.Http.make_absolute_uri(
212
+        Microformats2.Utility.get_value(hcard, :url, ["/"]) |> List.first(),
213
+        host
214
+      )
215
+
216
+    ~w(name nickname note email label)
217
+    |> Enum.map(fn prop ->
218
+      {prop,
219
+       Microformats2.Utility.get_value(hcard, String.to_atom(prop), [])
220
+       |> List.first()}
221
+    end)
222
+    |> Enum.reject(fn {_, v} -> is_nil(v) end)
223
+    |> Map.new()
224
+    |> Map.put(
225
+      "url",
226
+      url
227
+    )
228
+    |> Map.put_new_lazy("uid", fn ->
229
+      uid = Microformats2.Utility.get_value(hcard, :uid, []) |> List.first()
230
+
231
+      if is_nil(uid) do
232
+        url
233
+      else
234
+        IndieWeb.Http.make_absolute_uri(uid, host)
235
+      end
236
+    end)
237
+    |> Map.put_new_lazy("photo", fn ->
238
+      photo_uri =
239
+        Microformats2.Utility.get_value(hcard, :photo, []) |> List.first()
240
+
241
+      if !is_nil(photo_uri) do
242
+        IndieWeb.Http.make_absolute_uri(photo_uri, host)
243
+      else
244
+        nil
245
+      end
246
+    end)
247
+  end
248
+
249
+  defp do_stub_out_hcard(uri) do
250
+    %{
251
+      properties: %{
252
+        name: [URI.parse(uri) |> Map.get(:host)],
253
+        url: [uri],
254
+        uid: [uri]
255
+      },
256
+      type: ["h-card"]
257
+    }
258
+  end
259
+end

+ 37
- 13
lib/indieweb/http.ex View File

@@ -7,11 +7,17 @@ defmodule IndieWeb.Http do
7 7
 
8 8
   @doc "Obtains an implementation of a `IndieWeb.Http.Adapter` module."
9 9
   @spec adapter() :: IndieWeb.HTTP.Adapter.t()
10
-  def adapter, do: Application.get_env(:indieweb, :http_adapter, IndieWeb.Http.Adapters.HTTPotion)
10
+  def adapter,
11
+    do:
12
+      Application.get_env(
13
+        :indieweb,
14
+        :http_adapter,
15
+        IndieWeb.Http.Adapters.HTTPotion
16
+      )
11 17
 
12 18
   @doc "Sends a HTTP request to the URI `uri` with the provided options."
13 19
   @spec request(binary(), atom(), keyword()) ::
14
-  {:ok, IndieWeb.Http.Response.t()} | {:error, IndieWeb.Http.Error.t()}
20
+          {:ok, IndieWeb.Http.Response.t()} | {:error, IndieWeb.Http.Error.t()}
15 21
   def request(uri, method \\ :get, opts \\ []) do
16 22
     adapter().request(uri, method, opts)
17 23
   end
@@ -22,30 +28,48 @@ defmodule IndieWeb.Http do
22 28
 
23 29
     See `request/3` for more information about making requests.
24 30
     """
25
-    def unquote(method)(uri, opts \\ []), do: IndieWeb.Http.request(uri, unquote(method), opts)
31
+    def unquote(method)(uri, opts \\ []),
32
+      do: IndieWeb.Http.request(uri, unquote(method), opts)
26 33
   end
27 34
 
28 35
   def extract_link_header_values(headers) do
29 36
     Map.take(headers, ["link", "Link"])
30 37
     |> Map.values()
31 38
     |> List.flatten()
32
-    |> Enum.map(fn header -> String.split(header, ",", trim: true)  end)
39
+    |> Enum.map(fn header -> String.split(header, ",", trim: true) end)
33 40
     |> List.flatten()
34 41
     |> Enum.map(fn header ->
35
-      [rel, value] = header
36
-                     |> String.trim
37
-                     |> String.split(";")
38
-                     |> Enum.reverse
39
-                     |> Enum.map(fn part -> String.trim(part) end)
42
+      [rel, value] =
43
+        header
44
+        |> String.trim()
45
+        |> String.split(";")
46
+        |> Enum.reverse()
47
+        |> Enum.map(fn part -> String.trim(part) end)
40 48
 
41
-      rel_values = rel |> String.split("=") |> List.last |> String.trim("\"") |> String.split
42
-      link_value = value |> String.trim_leading("<") |> String.trim_trailing(">")
49
+      rel_values =
50
+        rel
51
+        |> String.split("=")
52
+        |> List.last()
53
+        |> String.trim("\"")
54
+        |> String.split()
55
+
56
+      link_value =
57
+        value |> String.trim_leading("<") |> String.trim_trailing(">")
43 58
 
44 59
       Enum.map(rel_values, fn rel_value -> {rel_value, link_value} end)
45 60
     end)
46
-    |> List.flatten
47
-    |> Enum.reduce(%{}, fn {key, val}, acc -> 
61
+    |> List.flatten()
62
+    |> Enum.reduce(%{}, fn {key, val}, acc ->
48 63
       Map.put(acc, key, Enum.sort(Map.get(acc, key, []) ++ [val]))
49 64
     end)
50 65
   end
66
+
67
+  def make_absolute_uri(path, _) when path in ["", nil], do: path
68
+
69
+  def make_absolute_uri(path, base_uri)
70
+      when path == base_uri and is_binary(path),
71
+      do: path
72
+
73
+  def make_absolute_uri(path, base_uri) when is_binary(path),
74
+    do: URI.merge(base_uri, path) |> URI.to_string()
51 75
 end

+ 3
- 1
lib/indieweb/http/adapter.ex View File

@@ -1,5 +1,7 @@
1 1
 defmodule IndieWeb.Http.Adapter do
2 2
   @moduledoc "Provides an abstraction on handling HTTP actions."
3 3
   @doc "Defines the method for making a general HTTP request."
4
-  @callback request(uri :: binary(), method :: atom(), opts :: keyword()) :: {:ok, IndieWeb.Http.Response.t} | {:error, IndieWeb.Http.Error.t}
4
+  @callback request(uri :: binary(), method :: atom(), opts :: keyword()) ::
5
+              {:ok, IndieWeb.Http.Response.t()}
6
+              | {:error, IndieWeb.Http.Error.t()}
5 7
 end

+ 19
- 10
lib/indieweb/http/adapters/httpotion.ex View File

@@ -3,22 +3,31 @@ defmodule IndieWeb.Http.Adapters.HTTPotion do
3 3
 
4 4
   @impl true
5 5
   def request(uri, method, opts) do
6
-    options = [
7
-      timeout: Keyword.get(opts, :timeout, IndieWeb.Http.timeout()),
8
-      follow_redirects: true,
9
-      auto_sni: true,
10
-      headers: Keyword.get(opts, :headers, %{}) |> Map.to_list(),
11
-      body: Keyword.get(opts, :body, %{}) |> URI.encode_query,
12
-      query: Keyword.get(opts, :query, nil)
13
-    ] |> Enum.reject(fn {_, v} -> is_nil(v) end) |> Keyword.new
6
+    options =
7
+      [
8
+        timeout: Keyword.get(opts, :timeout, IndieWeb.Http.timeout()),
9
+        follow_redirects: true,
10
+        auto_sni: true,
11
+        headers: Keyword.get(opts, :headers, %{}) |> Map.to_list(),
12
+        body: Keyword.get(opts, :body, %{}) |> URI.encode_query(),
13
+        query: Keyword.get(opts, :query, nil)
14
+      ]
15
+      |> Enum.reject(fn {_, v} -> is_nil(v) end)
16
+      |> Keyword.new()
14 17
 
15 18
     case HTTPotion.request(method, uri, options) do
16 19
       %HTTPotion.ErrorResponse{} = err_resp ->
17 20
         {:error, %IndieWeb.Http.Error{message: err_resp.message, raw: err_resp}}
18 21
 
19
-      %HTTPotion.Response{status_code: code, body: body, headers: headers} = resp ->
22
+      %HTTPotion.Response{status_code: code, body: body, headers: headers} =
23
+          resp ->
20 24
         {:ok,
21
-         %IndieWeb.Http.Response{code: code, body: body, headers: headers.hdrs, raw: resp}}
25
+         %IndieWeb.Http.Response{
26
+           code: code,
27
+           body: body,
28
+           headers: headers.hdrs,
29
+           raw: resp
30
+         }}
22 31
     end
23 32
   end
24 33
 end

+ 7
- 1
lib/indieweb/http/response.ex View File

@@ -2,5 +2,11 @@ defmodule IndieWeb.Http.Response do
2 2
   @moduledoc "Defines a response obtained when making a network request."
3 3
   @enforce_keys ~w(code body headers raw)a
4 4
   defstruct ~w(body code headers raw)a
5
-  @type t :: %__MODULE__{body: binary(), code: non_neg_integer, headers: Access.t(), raw: any()}
5
+
6
+  @type t :: %__MODULE__{
7
+          body: binary(),
8
+          code: non_neg_integer,
9
+          headers: Access.t(),
10
+          raw: any()
11
+        }
6 12
 end

+ 11
- 6
lib/indieweb/link_rel.ex View File

@@ -5,22 +5,26 @@ defmodule IndieWeb.LinkRel do
5 5
 
6 6
   def find(url, value) do
7 7
     with(
8
-      {:ok, %IndieWeb.Http.Response{code: code, body: body, headers: headers}} when code < 299 and code >= 200 <-
9
-        IndieWeb.Http.get(url)
8
+      {:ok, %IndieWeb.Http.Response{code: code, body: body, headers: headers}}
9
+      when code < 299 and code >= 200 <- IndieWeb.Http.get(url)
10 10
     ) do
11
-      header_endpoints = IndieWeb.Http.extract_link_header_values(headers) |> Map.get(value, [])
11
+      header_endpoints =
12
+        IndieWeb.Http.extract_link_header_values(headers) |> Map.get(value, [])
12 13
 
13 14
       rel_endpoints =
14 15
         case Microformats2.parse(body, url) do
15 16
           %{rel_urls: rel_url_map} ->
16
-            Enum.filter(rel_url_map, fn {_url, %{rels: rels}} -> value in rels end)
17
+            Enum.filter(rel_url_map, fn {_url, %{rels: rels}} ->
18
+              value in rels
19
+            end)
17 20
             |> Enum.map(fn {key, _} -> key end)
18 21
 
19 22
           _ ->
20 23
             []
21 24
         end
22 25
 
23
-      (header_endpoints ++ rel_endpoints) |> Enum.map(&do_normalize_url(&1, url))
26
+      (header_endpoints ++ rel_endpoints)
27
+      |> Enum.map(&do_normalize_url(&1, url))
24 28
     else
25 29
       _ -> []
26 30
     end
@@ -34,7 +38,8 @@ defmodule IndieWeb.LinkRel do
34 38
         URI.parse(scheme <> "://" <> host <> url) |> URI.to_string()
35 39
 
36 40
       # Relative to the current page's path.
37
-      %{host: nil, scheme: nil} == URI.parse(url) and !String.starts_with?(url, "/") ->
41
+      %{host: nil, scheme: nil} == URI.parse(url) and
42
+          !String.starts_with?(url, "/") ->
38 43
         page_url <> "/" <> url
39 44
 
40 45
       url == "" ->

+ 19
- 12
lib/indieweb/post.ex View File

@@ -90,10 +90,10 @@ defmodule IndieWeb.Post do
90 90
   Determines if the provided type is a response type.
91 91
 
92 92
   ## Examples
93
-  
93
+
94 94
       iex> IndieWeb.Post.is_response_type?(:note)
95 95
       false
96
-  
96
+
97 97
       iex> IndieWeb.Post.is_response_type?(:rsvp)
98 98
       true
99 99
   """
@@ -127,7 +127,7 @@ defmodule IndieWeb.Post do
127 127
   what kind of post these properties result in.
128 128
 
129 129
   ## Examples
130
-  
130
+
131 131
 
132 132
       iex> IndieWeb.Post.determine_type(%{"content" => %{"value" => ["Foo."]}, "name" => ["Foo."]}, ~w(note article)a)
133 133
       :note
@@ -140,7 +140,8 @@ defmodule IndieWeb.Post do
140 140
 
141 141
 
142 142
   """
143
-  @doc since: "http://ptd.spec.indieweb.org/#changes-from-28-october-2016-wd-to-1-march-2017-wd"
143
+  @doc since:
144
+         "http://ptd.spec.indieweb.org/#changes-from-28-october-2016-wd-to-1-march-2017-wd"
144 145
   @spec determine_type(map(), list()) :: atom()
145 146
   def determine_type(properties, types) do
146 147
     cond do
@@ -210,9 +211,9 @@ defmodule IndieWeb.Post do
210 211
         key -> to_string(key)
211 212
       end)
212 213
 
213
-    types = 
214
+    types =
214 215
       property_names
215
-      |> Enum.map(&(Map.get(@properties_to_kind, &1, nil)))
216
+      |> Enum.map(&Map.get(@properties_to_kind, &1, nil))
216 217
       |> Enum.reject(&is_nil/1)
217 218
 
218 219
     if types == [] do
@@ -231,13 +232,19 @@ defmodule IndieWeb.Post do
231 232
   end
232 233
 
233 234
   defp do_detect_article(properties) do
234
-    content = cond do
235
-      properties["content"]["value"] != [] -> properties["content"]["value"]
236
-      properties["summary"]["value"] != [] -> properties["summary"]["value"]
237
-      true -> ""
238
-    end
235
+    content =
236
+      cond do
237
+        properties["content"]["value"] != [] -> properties["content"]["value"]
238
+        properties["summary"]["value"] != [] -> properties["summary"]["value"]
239
+        true -> ""
240
+      end
241
+
242
+    name =
243
+      properties
244
+      |> Map.get("name", [])
245
+      |> Enum.map(&String.trim/1)
246
+      |> Enum.join(" ")
239 247
 
240
-    name = properties |> Map.get("name", []) |> Enum.map(&String.trim/1) |> Enum.join(" ")
241 248
     !List.starts_with?(content, [name]) and name != ""
242 249
   end
243 250
 end

+ 5
- 2
lib/indieweb/webmention.ex View File

@@ -61,7 +61,9 @@ defmodule IndieWeb.Webmention do
61 61
     case IndieWeb.LinkRel.find(page_url, "webmention") do
62 62
       list when length(list) != 0 ->
63 63
         {:ok, List.first(list)}
64
-      _ -> {:error, :no_endpoint_found}
64
+
65
+      _ ->
66
+        {:error, :no_endpoint_found}
65 67
     end
66 68
   end
67 69
 
@@ -73,7 +75,8 @@ defmodule IndieWeb.Webmention do
73 75
 
74 76
   [1]: https://www.w3.org/TR/webmention
75 77
   """
76
-  @spec send(binary(), any()) :: {:ok, IndieWeb.Webmention.SendResponse.t()} | {:error, any()}
78
+  @spec send(binary(), any()) ::
79
+          {:ok, IndieWeb.Webmention.SendResponse.t()} | {:error, any()}
77 80
   def send(target_url, source) do
78 81
     with(
79 82
       {:ok, source_url} <- resolve_source_url(source),

+ 9
- 6
lib/microformats2/utility.ex View File

@@ -1,4 +1,4 @@
1
-defmodule Microformats2.Utiltiy do
1
+defmodule Microformats2.Utility do
2 2
   @moduledoc false
3 3
 
4 4
   def get_format(mf2, format) do
@@ -10,28 +10,31 @@ defmodule Microformats2.Utiltiy do
10 10
 
11 11
   def get_value(mf2, property, default_value \\ [])
12 12
 
13
-  def get_value(%{"properties" => properties}, property, default_value) do
13
+  def get_value(%{properties: properties}, property, default_value) do
14 14
     Map.get(properties, property, default_value)
15 15
   end
16 16
 
17 17
   def get_value(_, _, _), do: {:error, :no_properties}
18 18
 
19 19
   def extract_all(mf2, format) do
20
-    items = Map.take(mf2, ~w(items children)) |> Map.values() |> List.flatten()
20
+    items = Map.take(mf2, ~w(items children)a) |> Map.values() |> List.flatten()
21 21
     Stream.flat_map(items, fn item -> do_parse_item(item, format) end)
22 22
   end
23 23
 
24 24
   defp do_parse_item(item, format) do
25
-    types = Map.get(item, "type", [])
25
+    types = Map.get(item, :type, [])
26 26
 
27 27
     if Enum.member?(types, "h-#{format}") do
28 28
       [item]
29 29
     else
30
-      Stream.concat(do_extract_from_properties(item, format), extract_all(item, format))
30
+      Stream.concat(
31
+        do_extract_from_properties(item, format),
32
+        extract_all(item, format)
33
+      )
31 34
     end
32 35
   end
33 36
 
34
-  defp do_extract_from_properties(%{"properties" => properties}, format) do
37
+  defp do_extract_from_properties(%{properties: properties}, format) do
35 38
     Stream.flat_map(properties, fn
36 39
       {_, items} ->
37 40
         Stream.flat_map(items, fn

+ 1
- 1
mix.exs View File

@@ -17,7 +17,7 @@ defmodule IndieWeb.MixProject do
17 17
       test_coverage: [tool: ExCoveralls],
18 18
       preferred_cli_env: [
19 19
         ci: :test,
20
-        "coveralls.detail": :test,
20
+        "coveralls.detail": :test
21 21
       ],
22 22
       deps: deps()
23 23
     ]

+ 53
- 0
test/fixtures/vcr_cassettes/hcard_finds_from_authorship.json
File diff suppressed because it is too large
View File


+ 53
- 0
test/fixtures/vcr_cassettes/hcard_finds_from_homepage.json
File diff suppressed because it is too large
View File


+ 150
- 0
test/fixtures/vcr_cassettes/hcard_generate_from_uri.json
File diff suppressed because it is too large
View File


+ 8
- 2
test/support/auth.ex View File

@@ -15,7 +15,10 @@ defmodule IndieWeb.Test.AuthAdapter do
15 15
   def code_generate(_, _, _), do: @code
16 16
 
17 17
   def code_verify(_, @client_id <> "_wrong", _, _), do: {:error, :code_mismatch}
18
-  def code_verify(_, _, @redirect_uri <> "_wrong", _), do: {:error, :code_mismatch}
18
+
19
+  def code_verify(_, _, @redirect_uri <> "_wrong", _),
20
+    do: {:error, :code_mismatch}
21
+
19 22
   def code_verify(_, _, _, _), do: :ok
20 23
 
21 24
   def code_persist(_, _, "fails", _), do: {:error, :test}
@@ -33,7 +36,10 @@ defmodule IndieWeb.Test.AuthAdapter do
33 36
 
34 37
   def token_info(@token <> "_wrong_user"), do: {:error, :incorrect_me_for_token}
35 38
   def token_info(@token <> "_invalid"), do: {:error, :invalid_token}
36
-  def token_info(@token), do: %{"client_id" => @client_id, "me" => @me, "scope" => "create read"}
39
+
40
+  def token_info(@token),
41
+    do: %{"client_id" => @client_id, "me" => @me, "scope" => "create read"}
42
+
37 43
   def token_info(_), do: nil
38 44
 
39 45
   def token_revoke(@token <> "_invalid"), do: :error

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

@@ -4,4 +4,6 @@ defmodule IndieWeb.Test.CacheAdapter do
4 4
   defdelegate set(key, value), to: IndieWeb.Cache.Adapters.Cachex
5 5
 end
6 6
 
7
-Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter, persistent: true)
7
+Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter,
8
+  persistent: true
9
+)

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

@@ -2,4 +2,6 @@ defmodule IndieWeb.Test.HttpAdapter do
2 2
   defdelegate request(uri, methods, opts), to: IndieWeb.Http.Adapters.HTTPotion
3 3
 end
4 4
 
5
-Application.put_env(:indieweb, :http_adapter, IndieWeb.Test.HttpAdapter, persistent: true)
5
+Application.put_env(:indieweb, :http_adapter, IndieWeb.Test.HttpAdapter,
6
+  persistent: true
7
+)

+ 7
- 2
test/support/webmention.ex View File

@@ -7,7 +7,12 @@ defmodule IndieWeb.Test.WebmentionUrlAdapter do
7 7
 
8 8
   def to_source_url(target_url)
9 9
   def to_source_url(:fake_source), do: URI.parse("https://source.indieweb/fake")
10
-  def to_source_url(_), do: :nil
10
+  def to_source_url(_), do: nil
11 11
 end
12 12
 
13
-Application.put_env(:indieweb, :webmention_url_adapter, IndieWeb.Test.WebmentionUrlAdapter, persistent: true)
13
+Application.put_env(
14
+  :indieweb,
15
+  :webmention_url_adapter,
16
+  IndieWeb.Test.WebmentionUrlAdapter,
17
+  persistent: true
18
+)

+ 31
- 6
test/unit/indieweb/auth/code_test.exs View File

@@ -3,29 +3,54 @@ defmodule IndieWeb.Auth.CodeTest do
3 3
   alias IndieWeb.Auth.Code, as: Subject
4 4
 
5 5
   setup do
6
-    Application.put_env(:indieweb, :auth_adapter, IndieWeb.Test.AuthAdapter, persistent: true)
6
+    Application.put_env(:indieweb, :auth_adapter, IndieWeb.Test.AuthAdapter,
7
+      persistent: true
8
+    )
7 9
   end
8 10
 
9 11
   describe ".generate/2" do
10 12
     test "provides a new code" do
11
-      assert :ok = Subject.persist("code", "https://indieauth.code", "https://indieauth.code/redirect")
13
+      assert :ok =
14
+               Subject.persist(
15
+                 "code",
16
+                 "https://indieauth.code",
17
+                 "https://indieauth.code/redirect"
18
+               )
12 19
     end
13 20
   end
14 21
 
15 22
   describe ".persist/3" do
16 23
     test "saves the provided code for the client & redirect_uri" do
17
-      assert :ok = Subject.persist("code", "https://indieauth.persist", "https://indieauth.persists/redirect")
18
-      assert {:error, :test} = Subject.persist("code", "https://indieauth.persist", "fails")
24
+      assert :ok =
25
+               Subject.persist(
26
+                 "code",
27
+                 "https://indieauth.persist",
28
+                 "https://indieauth.persists/redirect"
29
+               )
30
+
31
+      assert {:error, :test} =
32
+               Subject.persist("code", "https://indieauth.persist", "fails")
19 33
     end
20 34
   end
21 35
 
22 36
   describe ".verify/3" do
23 37
     test "confirms if a code was stored for this value with no data" do
24
-      assert :ok = Subject.verify("code", "https://indieauth.code", "https://indieauth.code/foo")
38
+      assert :ok =
39
+               Subject.verify(
40
+                 "code",
41
+                 "https://indieauth.code",
42
+                 "https://indieauth.code/foo"
43
+               )
25 44
     end
26 45
 
27 46
     test "confirms if a code was stored for this value with data" do
28
-      assert :ok = Subject.verify("code", "https://indieauth.code", "https://indieauth.code/foo", %{"prop" => "val"})
47
+      assert :ok =
48
+               Subject.verify(
49
+                 "code",
50
+                 "https://indieauth.code",
51
+                 "https://indieauth.code/foo",
52
+                 %{"prop" => "val"}
53
+               )
29 54
     end
30 55
   end
31 56
 end

+ 5
- 2
test/unit/indieweb/auth/token_test.exs View File

@@ -45,8 +45,11 @@ defmodule IndieWeb.Auth.TokenTest do
45 45
 
46 46
   describe ".info_for/1" do
47 47
     test "successfully fetches info about token" do
48
-      assert %{"me" => TestAdapter.me(), "client_id" => TestAdapter.client_id(), "scope" => "create read"} ==
49
-               Subject.info_for(TestAdapter.token())
48
+      assert %{
49
+               "me" => TestAdapter.me(),
50
+               "client_id" => TestAdapter.client_id(),
51
+               "scope" => "create read"
52
+             } == Subject.info_for(TestAdapter.token())
50 53
     end
51 54
 
52 55
     test "fails if token does not point to user" do

+ 46
- 11
test/unit/indieweb/auth_test.exs View File

@@ -4,28 +4,36 @@ defmodule IndieWeb.AuthTest do
4 4
   alias IndieWeb.Auth, as: Subject
5 5
 
6 6
   setup do
7
-    Application.put_env(:indieweb, :auth_adapter, IndieWeb.Test.AuthAdapter, persistent: true)
7
+    Application.put_env(:indieweb, :auth_adapter, IndieWeb.Test.AuthAdapter,
8
+      persistent: true
9
+    )
8 10
   end
9 11
 
10 12
   describe ".endpoint_for/2" do
11 13
     @endpoint "https://jacky.wtf/endpoint"
12 14
 
13 15
     test "authorization endpoint - successfully finds" do
14
-      html = "<html><head><link rel='authorization_endpoint' href='#{@endpoint}' /></head></html>"
16
+      html =
17
+        "<html><head><link rel='authorization_endpoint' href='#{@endpoint}' /></head></html>"
18
+
15 19
       use_cassette :stub, uri: "~r/*/", body: html do
16
-        assert @endpoint = Subject.endpoint_for(:authorization, "https://foobar.com")
20
+        assert @endpoint =
21
+                 Subject.endpoint_for(:authorization, "https://foobar.com")
17 22
       end
18 23
     end
19 24
 
20 25
     test "authorization endpoint - finds none for site" do
21 26
       html = "<html><head></head></html>"
27
+
22 28
       use_cassette :stub, uri: "~r/*/", body: html do
23 29
         refute Subject.endpoint_for(:authorization, "https://foobar.com")
24 30
       end
25 31
     end
26 32
 
27 33
     test "token endpoint - successfully finds" do
28
-      html = "<html><head><link rel='token_endpoint' href='#{@endpoint}' /></head></html>"
34
+      html =
35
+        "<html><head><link rel='token_endpoint' href='#{@endpoint}' /></head></html>"
36
+
29 37
       use_cassette :stub, uri: "~r/*/", body: html do
30 38
         assert @endpoint = Subject.endpoint_for(:token, "https://foobar.com")
31 39
       end
@@ -33,6 +41,7 @@ defmodule IndieWeb.AuthTest do
33 41
 
34 42
     test "token endpoint - finds none for site" do
35 43
       html = "<html><head></head></html>"
44
+
36 45
       use_cassette :stub, uri: "~r/*/", body: html do
37 46
         refute Subject.endpoint_for(:authorization, "https://foobar.com")
38 47
       end
@@ -50,10 +59,19 @@ defmodule IndieWeb.AuthTest do
50 59
         "me" => @user_profile_url,
51 60
         "redirect_uri" => @redirect_uri,
52 61
         "response_type" => "id",
53
-        "state" => "state",
62
+        "state" => "state"
54 63
       }
55 64
 
56
-      signed_url = Enum.join([@redirect_uri, "?", URI.encode_query(%{"state" => params["state"], "code" => IndieWeb.Test.AuthAdapter.code()})])
65
+      signed_url =
66
+        Enum.join([
67
+          @redirect_uri,
68
+          "?",
69
+          URI.encode_query(%{
70
+            "state" => params["state"],
71
+            "code" => IndieWeb.Test.AuthAdapter.code()
72
+          })
73
+        ])
74
+
57 75
       assert signed_url == Subject.authenticate(params)
58 76
     end
59 77
 
@@ -64,12 +82,20 @@ defmodule IndieWeb.AuthTest do
64 82
         "redirect_uri" => @redirect_uri,
65 83
         "response_type" => "code",
66 84
         "scope" => "read",
67
-        "state" => "state",
85
+        "state" => "state"
68 86
       }
69 87
 
70
-      signed_url = Enum.join([@redirect_uri, "?", URI.encode_query(%{"state" => params["state"], "code" => IndieWeb.Test.AuthAdapter.code()})])
71
-      assert signed_url == Subject.authenticate(params)
88
+      signed_url =
89
+        Enum.join([
90
+          @redirect_uri,
91
+          "?",
92
+          URI.encode_query(%{
93
+            "state" => params["state"],
94
+            "code" => IndieWeb.Test.AuthAdapter.code()
95
+          })
96
+        ])
72 97
 
98
+      assert signed_url == Subject.authenticate(params)
73 99
     end
74 100
 
75 101
     test "generate an authorization code when defaulting to edfault 'read' scope" do
@@ -78,10 +104,19 @@ defmodule IndieWeb.AuthTest do
78 104
         "me" => @user_profile_url,
79 105
         "redirect_uri" => @redirect_uri,
80 106
         "response_type" => "code",
81
-        "state" => "state",
107
+        "state" => "state"
82 108
       }
83 109
 
84
-      signed_url = Enum.join([@redirect_uri, "?", URI.encode_query(%{"state" => params["state"], "code" => IndieWeb.Test.AuthAdapter.code()})])
110
+      signed_url =
111
+        Enum.join([
112
+          @redirect_uri,
113
+          "?",
114
+          URI.encode_query(%{
115
+            "state" => params["state"],
116
+            "code" => IndieWeb.Test.AuthAdapter.code()
117
+          })
118
+        ])
119
+
85 120
       assert signed_url == Subject.authenticate(params)
86 121
     end
87 122
   end

+ 8
- 2
test/unit/indieweb/cache_test.exs View File

@@ -4,13 +4,19 @@ defmodule IndieWeb.CacheTest do
4 4
   doctest Subject
5 5
 
6 6
   setup do
7
-    Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter, persistent: true)
7
+    Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter,
8
+      persistent: true
9
+    )
10
+
8 11
     :ok
9 12
   end
10 13
 
11 14
   describe ".adapter/0" do
12 15
     test "pulls the one defined in configuration" do
13
-      Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter, persistent: true)
16
+      Application.put_env(:indieweb, :cache_adapter, IndieWeb.Test.CacheAdapter,
17
+        persistent: true
18
+      )
19
+
14 20
       assert Subject.adapter() == IndieWeb.Test.CacheAdapter
15 21
     end
16 22
 

+ 113
- 0
test/unit/indieweb/hcard_test.exs View File

@@ -0,0 +1,113 @@
1
+defmodule IndieWeb.HCardTest do
2
+  use IndieWeb.TestCase, async: false
3
+  use ExVCR.Mock
4
+  alias IndieWeb.HCard, as: Subject
5
+
6
+  @url "https://indieweb.card"
7
+  @hcard %{
8
+    "url" => @url,
9
+    "uid" => @url,
10
+    "photo" => "https://indieweb.card/photo",
11
+    "note" => "This is a note.",
12
+    "name" => "Fake name"
13
+  }
14
+
15
+  describe ".fetch_representative/1" do
16
+    test "finds top-level h-card contains a u-uid and u-url" do
17
+      html = """
18
+      <html>
19
+      <body>
20
+      <div class="h-card">
21
+      <img class="u-photo" src="#{@hcard["photo"]}" />
22
+      <a href="#{@url}" class="u-url u-uid">
23
+      <span class="p-name">#{@hcard["name"]}</span>
24
+      </a>
25
+      <p class="p-note">#{@hcard["note"]}</p>
26
+      </div>
27
+      </body>
28
+      </html>
29
+      """
30
+
31
+      mf2 = Microformats2.parse(html, @url)
32
+      assert {:ok, @hcard} = Subject.fetch_representative(mf2, @url)
33
+    end
34
+
35
+    test "finds h-card where url matches a rel=me path" do
36
+      html = """
37
+      <html>
38
+      <link rel="me" href="#{@hcard["url"]}/relme"
39
+      <body>
40
+      <div class="h-card">
41
+      <img class="u-photo" src="#{@hcard["photo"]}" />
42
+      <a href="#{@url}/relme" class="u-url u-uid">
43
+      <span class="p-name">#{@hcard["name"]}</span>
44
+      </a>
45
+      <p class="p-note">#{@hcard["note"]}</p>
46
+      </div>
47
+      </body>
48
+      </html>
49
+      """
50
+
51
+      mf2 = Microformats2.parse(html, @url)
52
+
53
+      expected_hcard =
54
+        Map.merge(@hcard, %{
55
+          "url" => @hcard["url"] <> "/relme",
56
+          "uid" => @hcard["url"] <> "/relme"
57
+        })
58
+
59
+      assert {:ok, ^expected_hcard} = Subject.fetch_representative(mf2, @url)
60
+    end
61
+
62
+    test "finds lone h-card on page" do
63
+      html = """
64
+      <html>
65
+      <body>
66
+      <div class="h-card">
67
+      <img class="u-photo" src="#{@hcard["photo"]}" />
68
+      <a href="#{@url}/lone" class="u-url u-uid">
69
+      <span class="p-name">#{@hcard["name"]}</span>
70
+      </a>
71
+      <p class="p-note">#{@hcard["note"]}</p>
72
+      </div>
73
+      </body>
74
+      </html>
75
+      """
76
+
77
+      mf2 = Microformats2.parse(html, @url)
78
+
79
+      expected_hcard =
80
+        Map.merge(@hcard, %{
81
+          "url" => @hcard["url"] <> "/lone",
82
+          "uid" => @hcard["url"] <> "/lone"
83
+        })
84
+
85
+      assert {:ok, ^expected_hcard} = Subject.fetch_representative(mf2, @url)
86
+    end
87
+  end
88
+
89
+  describe ".resolve/1" do
90
+    test "successfully resolves a h-card from the provided URI" do
91
+      use_cassette "hcard_finds_from_homepage" do
92
+        assert {:ok, %{"name" => "Aaron Parecki"}} =
93
+                 Subject.resolve("https://aaronparecki.com")
94
+      end
95
+    end
96
+
97
+    test "successfully resolves a h-card from authorship" do
98
+      use_cassette "hcard_finds_from_authorship" do
99
+        assert {:ok, %{"name" => "Aaron Parecki"}} =
100
+                 Subject.resolve(
101
+                   "https://aaronparecki.com/2018/12/17/7/blocking-domains"
102
+                 )
103
+      end
104
+    end
105
+
106
+    test "resolves generic h-card from non-MF2 site" do
107
+      use_cassette "hcard_generate_from_uri" do
108
+        assert {:ok, %{"name" => "firefox.com"}} =
109
+                 Subject.resolve("http://firefox.com")
110
+      end
111
+    end
112
+  end
113
+end

+ 16
- 4
test/unit/indieweb/http_test.exs View File

@@ -5,7 +5,10 @@ defmodule IndieWeb.HttpTest do
5 5
   doctest Subject
6 6
 
7 7
   setup do
8
-    Application.put_env(:indieweb, :http_adapter, IndieWeb.Test.HttpAdapter, persistent: true)
8
+    Application.put_env(:indieweb, :http_adapter, IndieWeb.Test.HttpAdapter,
9
+      persistent: true
10
+    )
11
+
9 12
     :ok
10 13
   end
11 14
 
@@ -23,7 +26,8 @@ defmodule IndieWeb.HttpTest do
23 26
   describe ".request/2" do
24 27
     test "successfully sends a HTTP GET request by default" do
25 28
       use_cassette :stub, uri: "~r/*", method: :get do
26
-        assert {:ok, %IndieWeb.Http.Response{code: 200}} = Subject.request("https://indieweb.org")
29
+        assert {:ok, %IndieWeb.Http.Response{code: 200}} =
30
+                 Subject.request("https://indieweb.org")
27 31
       end
28 32
     end
29 33
 
@@ -33,7 +37,8 @@ defmodule IndieWeb.HttpTest do
33 37
           assert {:ok, %IndieWeb.Http.Response{}} =
34 38
                    Subject.request("https://indieweb.org", unquote(method))
35 39
 
36
-          assert {:ok, %IndieWeb.Http.Response{}} = Subject.unquote(method)("https://indieweb.org")
40
+          assert {:ok, %IndieWeb.Http.Response{}} =
41
+                   Subject.unquote(method)("https://indieweb.org")
37 42
         end
38 43
       end
39 44
     end
@@ -59,7 +64,14 @@ defmodule IndieWeb.HttpTest do
59 64
         assert {:ok, resp} = IndieWeb.Http.head("https://v2.jacky.wtf")
60 65
         assert values = IndieWeb.Http.extract_link_header_values(resp.headers)
61 66
         assert %{"self" => ["https://v2.jacky.wtf"]} = values
62
-        assert %{"me" => ["https://playvicious.social/@jalcine", "https://twitter.com/jackyalcine", "https://www.instagram.com/jackyalcine/"]} = values
67
+
68
+        assert %{
69
+                 "me" => [
70
+                   "https://playvicious.social/@jalcine",
71
+                   "https://twitter.com/jackyalcine",
72
+                   "https://www.instagram.com/jackyalcine/"
73
+                 ]
74
+               } = values
63 75
       end
64 76
     end
65 77
   end

+ 5
- 1
test/unit/indieweb/post_test.exs View File

@@ -9,7 +9,11 @@ defmodule IndieWeb.PostTest do
9 9
     end
10 10
 
11 11
     test "detects a rsvp" do
12
-      assert [:rsvp] = Subject.extract_types(%{"rsvp" => ["yes"], "content" => %{"value" => ""}})
12
+      assert [:rsvp] =
13
+               Subject.extract_types(%{
14
+                 "rsvp" => ["yes"],
15
+                 "content" => %{"value" => ""}
16
+               })
13 17
     end
14 18
 
15 19
     test "detects a reply" do

+ 48
- 19
test/unit/indieweb/webmention_test.exs View File

@@ -4,7 +4,10 @@ defmodule IndieWeb.WebmentionTest do
4 4
   alias IndieWeb.Webmention, as: Subject
5 5
 
6 6
   setup do
7
-    Application.put_env(:indieweb, :webmention_url_adapter, IndieWeb.Test.WebmentionUrlAdapter,
7
+    Application.put_env(
8
+      :indieweb,
9
+      :webmention_url_adapter,
10
+      IndieWeb.Test.WebmentionUrlAdapter,
8 11
       persistent: true
9 12
     )
10 13
   end
@@ -13,56 +16,72 @@ defmodule IndieWeb.WebmentionTest do
13 16
     test "HTTP Link header, unquoted rel, relative URL" do
14 17
       use_cassette "webmention_test_header_unquoted_relative" do
15 18
         assert {:ok, "https://webmention.rocks/test/1/webmention"} =
16
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/1")
19
+                 IndieWeb.Webmention.discover_endpoint(
20
+                   "https://webmention.rocks/test/1"
21
+                 )
17 22
       end
18 23
     end
19 24
 
20 25
     test "HTTP Link header, unquoted rel, absolute URL" do
21 26
       use_cassette "webmention_test_header_unquoted_absolute" do
22 27
         assert {:ok, "https://webmention.rocks/test/2/webmention"} =
23
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/2")
28
+                 IndieWeb.Webmention.discover_endpoint(
29
+                   "https://webmention.rocks/test/2"
30
+                 )
24 31
       end
25 32
     end
26 33
 
27 34
     test "HTML <link> tag, relative URL" do
28 35
       use_cassette "webmention_test_tag_link_relative" do
29 36
         assert {:ok, "https://webmention.rocks/test/3/webmention"} =
30
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/3")
37
+                 IndieWeb.Webmention.discover_endpoint(
38
+                   "https://webmention.rocks/test/3"
39
+                 )
31 40
       end
32 41
     end
33 42
 
34 43
     test "HTML <link> tag, absolute URL" do
35 44
       use_cassette "webmention_test_tag_link_absolute" do
36 45
         assert {:ok, "https://webmention.rocks/test/4/webmention"} =
37
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/4")
46
+                 IndieWeb.Webmention.discover_endpoint(
47
+                   "https://webmention.rocks/test/4"
48
+                 )
38 49
       end
39 50
     end
40 51
 
41 52
     test "HTML <a> tag, relative URL" do
42 53
       use_cassette "webmention_test_tag_a_relative" do
43 54
         assert {:ok, "https://webmention.rocks/test/5/webmention"} =
44
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/5")
55
+                 IndieWeb.Webmention.discover_endpoint(
56
+                   "https://webmention.rocks/test/5"
57
+                 )
45 58
       end
46 59
     end
47 60
 
48 61
     test "HTML <a> tag, absolute URL" do
49 62
       use_cassette "webmention_test_tag_a_absolute" do
50 63
         assert {:ok, "https://webmention.rocks/test/6/webmention"} =
51
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/6")
64
+                 IndieWeb.Webmention.discover_endpoint(
65
+                   "https://webmention.rocks/test/6"
66
+                 )
52 67
       end
53 68
     end
54 69
 
55 70
     test "HTTP Link header with strange casing" do
56 71
       use_cassette "webmention_test_header_strange_casing" do
57 72
         assert {:ok, "https://webmention.rocks/test/7/webmention"} =
58
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/7")
73
+                 IndieWeb.Webmention.discover_endpoint(
74
+                   "https://webmention.rocks/test/7"
75
+                 )
59 76
       end
60 77
     end
61 78
 
62 79
     test "HTTP Link header, quoted rel" do
63 80
       use_cassette "webmention_test_header_quoted_rel" do
64 81
         assert {:ok, "https://webmention.rocks/test/8/webmention"} =
65
-                 IndieWeb.Webmention.discover_endpoint("https://webmention.rocks/test/8")
82
+                 IndieWeb.Webmention.discover_endpoint(
83
+                   "https://webmention.rocks/test/8"
84
+                 )
66 85
       end
67 86
     end
68 87
 
@@ -178,7 +197,9 @@ defmodule IndieWeb.WebmentionTest do
178 197
   describe ".send/3" do
179 198
     test "successfully sends a Webmention" do
180 199
       use_cassette "webmention_send_success" do
181
-        assert {:ok, send_resp} = Subject.send("https://webmention.target/page", :fake_source)
200
+        assert {:ok, send_resp} =
201
+                 Subject.send("https://webmention.target/page", :fake_source)
202
+
182 203
         assert %{code: 201} = send_resp
183 204
       end
184 205
     end
@@ -190,7 +211,10 @@ defmodule IndieWeb.WebmentionTest do
190 211
 
191 212
     test "fails if no Webmention endpoint was found for target" do
192 213
       assert {:error, :webmention_send_failure, reason: :no_endpoint_found} =
193
-               Subject.send("https://webmention.target/page?no=endpoint", :fake_source)
214
+               Subject.send(
215
+                 "https://webmention.target/page?no=endpoint",
216
+                 :fake_source
217
+               )
194 218
     end
195 219
   end
196 220
 
@@ -202,14 +226,16 @@ defmodule IndieWeb.WebmentionTest do
202 226
                  target: "https://target.indieweb/fake"
203 227
                )
204 228
 
205
-      assert [from: "https://webmention.target/source", target: :fake_source] = resp
229
+      assert [from: "https://webmention.target/source", target: :fake_source] =
230
+               resp
206 231
     end
207 232
 
208 233
     test "fails if target URI does not resolve to anything" do
209
-      assert {:error, :webmention_receive_failure, reason: :no_target} = Subject.receive(
210
-        source: "https://webmention.target/source",
211
-        target: "https://target.indieweb/goes/nowhere"
212
-      )
234
+      assert {:error, :webmention_receive_failure, reason: :no_target} =
235
+               Subject.receive(
236
+                 source: "https://webmention.target/source",
237
+                 target: "https://target.indieweb/goes/nowhere"
238
+               )
213 239
     end
214 240
 
215 241
     @tag skip: true
@@ -221,17 +247,20 @@ defmodule IndieWeb.WebmentionTest do
221 247
 
222 248
   describe ".resolve_target_from_url/1" do
223 249
     test "generates URI for provided object" do
224
-      assert {:ok, :fake_source} = Subject.resolve_target_from_url("https://target.indieweb/fake")
250
+      assert {:ok, :fake_source} =
251
+               Subject.resolve_target_from_url("https://target.indieweb/fake")
225 252
     end
226 253
 
227 254
     test "fails if no adapter is set" do
228 255
       Application.delete_env(:indieweb, :webmention_url_adapter)
229 256
 
230
-      assert {:error, :no_adapter} = Subject.resolve_target_from_url("https://target.indieweb/fake")
257
+      assert {:error, :no_adapter} =
258
+               Subject.resolve_target_from_url("https://target.indieweb/fake")
231 259
     end
232 260
 
233 261
     test "fails if adapter returns nil" do
234
-      assert {:error, :no_target} = Subject.resolve_target_from_url("https://target.indieweb/nil")
262
+      assert {:error, :no_target} =
263
+               Subject.resolve_target_from_url("https://target.indieweb/nil")
235 264
     end
236 265
   end
237 266
 

Loading…
Cancel
Save