webhukhs 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99b94333e5ec2959d8096f909a5370fccc5b68b9388e0a7028584a5cb334d660
4
- data.tar.gz: 85c0dfd6138967c9c9f0fca36666ed07f7a9b714a608c34b13ed725d73dff67f
3
+ metadata.gz: 4ffeb74619482ef4f9e42e69a58768c40ce5db996de005b0edd305bf44aacdb0
4
+ data.tar.gz: a812b122d5d561c47626fea17f0e06869545b6a06302f9b9de4ece966409b5ce
5
5
  SHA512:
6
- metadata.gz: 1ee87b2fb85e85a69493977d30cc21bbba0831d5b69973d560d4229260fdd7357a1a343c7da3634b503817827632e7f6ab8c1f5b92cd081030f11c68a8086053
7
- data.tar.gz: e2820a7337b24bf51cc85e400889969f4744102c3ddc3948df9ae051d2baa96af6a8ae6ffad329cac90952f9d32362232ee2cd2a812dd7059fb04b9fabcb5ed1
6
+ metadata.gz: 8efe8a18da360410eb87cd5d38ef2ee1788240bc6ced802660be235703f1805036b30f51846157cf9f535ad84888f16f7d8eae67f68d24cdd33fafa2cc0bcccb
7
+ data.tar.gz: 87997be0db27c11cfa17659360cd1a0673811473af0f356c6d3ed77ad1e1a56b2759a3c1d0c721a5355377f3015ca90f3788694063056a01e2684aad9b4eeecf
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ### Changed
9
+ - Admin URLs no longer include `/received`.
10
+ - Restrict admin sorting to ID, with an ascending/descending toggle, to avoid expensive column sorts.
11
+ - Abbreviate UUIDs to their first and last 4 characters in the admin listing, keeping the full ID on hover.
12
+ - Group the admin listing's Received and Updated timestamps in the rightmost columns.
13
+
8
14
  ## 0.8.1
9
15
 
10
16
  ### Fixed
data/README.md CHANGED
@@ -76,7 +76,11 @@ Now you will be able to accept webhooks on `/webhooks/example` path.
76
76
 
77
77
  Webhukhs comes with a read-only admin for looking at the webhooks you have received: a sortable,
78
78
  filterable list, and a page per webhook with its body (pretty-printed when it is JSON) and the
79
- request headers it arrived with. It is not loaded by default - require it in your Gemfile (it has to be
79
+ request headers it arrived with.
80
+
81
+ ![Webhukhs admin listing with compact UUIDs, status filters, and received and updated timestamps](.github/screenshots/admin-id-sorting.png)
82
+
83
+ It is not loaded by default - require it in your Gemfile (it has to be
80
84
  loaded before the app boots, so `config/routes.rb` is too late):
81
85
 
82
86
  ```ruby
@@ -92,6 +96,9 @@ Rails.application.routes.draw do
92
96
  end
93
97
  ```
94
98
 
99
+ The list lives at the mount point itself (`/admin/webhooks` above), with each webhook at
100
+ `/admin/webhooks/:id`.
101
+
95
102
  The admin does no authentication of its own, and webhook bodies tend to carry personal data and
96
103
  secrets - so mount it behind whatever guards the rest of your admin. For example, with Devise:
97
104
 
@@ -121,8 +128,13 @@ no build step.
121
128
  To try it out, run `bin/dev` from a checkout of this repository. It starts the app in `example`
122
129
  with some webhooks seeded, and the admin at http://localhost:3000/admin/webhukhs.
123
130
 
124
- The list defaults to descending ID order using the existing primary-key index; no extra migration is
125
- needed. Click **Received** to sort by timestamp instead.
131
+ The list sorts only by ID, descending by default, using the existing primary-key index; no extra
132
+ migration is needed. Click **ID** to switch between ascending and descending order. Other columns
133
+ are not sortable, avoiding expensive sorts on large webhook tables. Handler and status filters are
134
+ still available.
135
+
136
+ UUIDs are abbreviated in the list, for example `01a1..6e51`; hover over the ID to see the full
137
+ value. Detail pages and copied IDs retain the full UUID.
126
138
 
127
139
  **We strongly recommend against UUIDv4 primary keys.** Use UUIDv7 or auto-incrementing integer IDs
128
140
  instead. UUIDv4 IDs are random, so sorting by ID does not reflect creation time, and inserts have
@@ -339,7 +339,7 @@ p.muted {
339
339
  }
340
340
 
341
341
  .cell-id {
342
- max-width: 14ch;
342
+ max-width: 16ch;
343
343
  }
344
344
 
345
345
  .header-value {
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Webhukhs::Admin::Engine.routes.draw do
4
- resources :received_webhooks, only: [:index, :show], path: "received"
4
+ resources :received_webhooks, only: [:index, :show], path: ""
5
5
  root to: "received_webhooks#index"
6
6
  end
@@ -8,8 +8,6 @@ module Webhukhs
8
8
  class ReceivedWebhooksController < ApplicationController
9
9
  # Webhooks per index page
10
10
  PAGE_SIZE = 50
11
- # Additional sortable columns beyond the default id order
12
- SORTABLE_COLUMNS = %w[created_at updated_at handler_module_name handler_event_id status].freeze
13
11
 
14
12
  helper_method :handler_options, :status_options
15
13
 
@@ -17,17 +15,14 @@ module Webhukhs
17
15
  #
18
16
  # @return [void]
19
17
  def index
20
- @sort = params[:sort].presence_in(SORTABLE_COLUMNS) || "id"
21
18
  @direction = (params[:direction] == "asc") ? "asc" : "desc"
22
19
  @page = [params[:page].to_i, 1].max
23
20
 
24
21
  filters = {handler_module_name: params[:handler], status: params[:status]}.compact_blank
25
22
 
26
- # Most columns have plenty of ties (status especially), and without a tiebreaker the DB is free
27
- # to return them in a different order for every OFFSET - rows then repeat or vanish between pages
28
23
  rows = ReceivedWebhook
29
24
  .where(filters)
30
- .order(@sort => @direction, "id" => @direction)
25
+ .order(id: @direction)
31
26
  .offset((@page - 1) * PAGE_SIZE)
32
27
  .limit(PAGE_SIZE + 1)
33
28
  @next_page = @page + 1 if rows.length > PAGE_SIZE
@@ -4,6 +4,20 @@ module Webhukhs
4
4
  module Admin
5
5
  # Table-building helpers for the admin views.
6
6
  module ViewHelper
7
+ # Canonical UUID format, independent of UUID version.
8
+ UUID_RE = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
9
+
10
+ # Displays the first 4 and last 4 UUID characters, keeping the full ID on hover.
11
+ #
12
+ # @param uuid_str [String] ID to display
13
+ # @return [String, ActiveSupport::SafeBuffer]
14
+ def short_id(uuid_str)
15
+ return uuid_str unless UUID_RE.match?(uuid_str)
16
+
17
+ shortened = [uuid_str.first(4), uuid_str.last(4)].join("..")
18
+ content_tag(:abbr, shortened, title: uuid_str)
19
+ end
20
+
7
21
  # Renders a table inside a scroll container. Cells are `white-space: nowrap`, so one long
8
22
  # column would otherwise take the whole page sideways with it.
9
23
  #
@@ -22,19 +36,14 @@ module Webhukhs
22
36
  tag.div(tag.table(safe_join([thead, tag.tbody(safe_join(rows))]), class: "admin-table"), class: "table-scroll")
23
37
  end
24
38
 
25
- # Column header linking to the index sorted by that column. Clicking the active column
26
- # flips the direction, any other column starts out descending.
39
+ # ID column header linking to the index with the sort direction reversed.
27
40
  #
28
- # @param label [String] header text
29
- # @param column [String] column to sort by
30
- # @param sort [String] currently sorted column
31
41
  # @param direction [String] current direction, "asc" or "desc"
32
42
  # @return [ActiveSupport::SafeBuffer]
33
- def sort_link(label, column, sort:, direction:)
34
- active = column == sort
35
- next_direction = (active && direction == "desc") ? "asc" : "desc"
36
- query = request.query_parameters.except("page").merge("sort" => column, "direction" => next_direction)
37
- label = safe_join([label, tag.span((direction == "asc") ? "▲" : "▼", class: "sort-arrow")], " ") if active
43
+ def sort_link(direction:)
44
+ next_direction = (direction == "desc") ? "asc" : "desc"
45
+ query = request.query_parameters.except("page", "sort").merge("direction" => next_direction)
46
+ label = safe_join(["Id", tag.span((direction == "asc") ? "▲" : "▼", class: "sort-arrow")], " ")
38
47
  link_to(label, query, class: "sort-link")
39
48
  end
40
49
 
@@ -9,33 +9,32 @@
9
9
  </div>
10
10
 
11
11
  <%= form_tag received_webhooks_path, method: :get, class: "filters" do %>
12
- <%= hidden_field_tag :sort, @sort, id: nil %>
13
12
  <%= hidden_field_tag :direction, @direction, id: nil %>
14
13
  <%= select_tag :handler, options_for_select([["Any handler", ""]] + handler_options, params[:handler]), "aria-label": "Handler" %>
15
14
  <%= select_tag :status, options_for_select([["Any status", ""]] + status_options.map { |status, _| [status.humanize, status] }, params[:status]), "aria-label": "Status" %>
16
15
  <%= submit_tag "Filter", name: nil %>
17
- <%= link_to "Clear", received_webhooks_path(sort: @sort, direction: @direction) %>
16
+ <%= link_to "Clear", received_webhooks_path(direction: @direction) %>
18
17
  <% end %>
19
18
 
20
19
  <%
21
20
  columns = [
22
- ["Id", "id"],
23
- ["Received", "created_at"],
24
- ["Handler", "handler_module_name"],
25
- ["Status", "status"],
26
- ["Event id", "handler_event_id"],
27
- ["Updated", "updated_at"]
28
- ].map { |label, column| sort_link(label, column, sort: @sort, direction: @direction) }
21
+ sort_link(direction: @direction),
22
+ "Handler",
23
+ "Status",
24
+ "Event id",
25
+ "Received",
26
+ "Updated"
27
+ ]
29
28
  %>
30
29
  <div id="received-webhooks">
31
30
  <%# The id cell carries the link, and the CSS stretches it over the whole row %>
32
31
  <%= admin_table(columns, @webhooks) do |webhook|
33
32
  [
34
- link_to(tag.code(webhook.id, class: "cell-ellipsis cell-id", title: webhook.id), received_webhook_path(webhook), class: "row-link"),
35
- time_tag(webhook.created_at, webhook.created_at.iso8601),
33
+ link_to(tag.code(short_id(webhook.id.to_s), class: "cell-ellipsis cell-id"), received_webhook_path(webhook), class: "row-link", title: webhook.id, "aria-label": webhook.id),
36
34
  webhook.handler_module_name,
37
35
  tag.span(webhook.status, class: ["badge", webhook.status_class]),
38
36
  tag.code(webhook.handler_event_id, class: "cell-ellipsis", title: webhook.handler_event_id),
37
+ time_tag(webhook.created_at, webhook.created_at.iso8601),
39
38
  time_tag(webhook.updated_at, webhook.updated_at.iso8601)
40
39
  ]
41
40
  end %>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webhukhs
4
- VERSION = "0.8.1"
4
+ VERSION = "0.8.2"
5
5
  end
data/test/admin_test.rb CHANGED
@@ -20,48 +20,97 @@ class AdminTest < ActionDispatch::IntegrationTest
20
20
  end
21
21
 
22
22
  def listed_event_ids
23
- css_select("table.admin-table tbody tr td:nth-child(5) code").map(&:text)
23
+ css_select("table.admin-table tbody tr td:nth-child(4) code").map(&:text)
24
24
  end
25
25
 
26
26
  test "lists webhooks by descending id regardless of received time by default" do
27
27
  create_webhook(handler_event_id: "first", created_at: 1.day.ago)
28
28
  create_webhook(handler_event_id: "last", created_at: 2.days.ago)
29
29
 
30
- get "/admin/received"
30
+ get "/admin"
31
31
 
32
32
  assert_response :ok
33
33
  assert_equal ["last", "first"], listed_event_ids
34
34
  # The whole row is the link, so it has to be there in the first cell
35
- assert_select "tbody tr td:first-child a.row-link[href^='/admin/received/']", count: 2
36
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=asc&sort=id", text: "Id ▼"
37
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=desc&sort=created_at", text: "Received"
35
+ assert_select "tbody tr td:first-child a.row-link[href^='/admin/']", count: 2
36
+ assert_select "th a.sort-link[href=?]", "/admin/?direction=asc", text: "Id ▼"
37
+ assert_select "th a", count: 1
38
38
  assert_select "th a.sort-link span.sort-arrow", text: "▼", count: 1
39
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=desc&sort=status", text: "Status"
39
+ assert_equal ["Id ▼", "Handler", "Status", "Event id", "Received", "Updated"], css_select("th").map(&:text)
40
+ end
41
+
42
+ test "groups receipt and update timestamps in the rightmost columns" do
43
+ webhook = create_webhook(created_at: 2.days.ago, updated_at: 1.day.ago)
44
+
45
+ get "/admin"
46
+
47
+ assert_select "tbody tr td:nth-child(5) time[datetime=?]", webhook.created_at.iso8601
48
+ assert_select "tbody tr td:nth-child(6) time[datetime=?]", webhook.updated_at.iso8601
40
49
  end
41
50
 
42
51
  test "sorts by id in ascending order when requested" do
43
52
  create_webhook(handler_event_id: "first", created_at: 1.day.ago)
44
53
  create_webhook(handler_event_id: "last", created_at: 2.days.ago)
45
54
 
46
- get "/admin/received", params: {sort: "id", direction: "asc"}
55
+ get "/admin", params: {direction: "asc"}
47
56
 
48
57
  assert_equal ["first", "last"], listed_event_ids
49
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=desc&sort=id", text: "Id ▲"
58
+ assert_select "th a.sort-link[href=?]", "/admin/?direction=desc", text: "Id ▲"
59
+ end
60
+
61
+ test "short_id abbreviates UUIDs in an abbr with the full id as its title" do
62
+ {
63
+ "01a1895c-806f-7518-a388-c6c6a4496e51" => "01a1..6e51",
64
+ "01A1895C-806F-7518-A388-C6C6A4496E51" => "01A1..6E51",
65
+ "f47ac10b-58cc-4372-a567-0e02b2c3d479" => "f47a..d479"
66
+ }.each do |id, label|
67
+ assert_equal %(<abbr title="#{id}">#{label}</abbr>), Webhukhs::Admin::ApplicationController.helpers.short_id(id)
68
+ end
69
+ end
70
+
71
+ test "short_id leaves non-UUID strings unchanged" do
72
+ uuid = "01a1895c-806f-7518-a388-c6c6a4496e51"
73
+ ids = ["123", "manual-id", "prefix#{uuid}", "#{uuid}suffix", "#{uuid}\n"]
74
+ groups = uuid.split("-")
75
+ groups.each_index do |index|
76
+ [groups[index][0...-1], "#{groups[index]}0", "g#{groups[index][1..]}"].each do |invalid_group|
77
+ ids << groups.each_with_index.map { |group, i| (i == index) ? invalid_group : group }.join("-")
78
+ end
79
+ end
80
+
81
+ ids.each do |id|
82
+ assert_equal id, Webhukhs::Admin::ApplicationController.helpers.short_id(id)
83
+ end
84
+ end
85
+
86
+ test "keeps the full id in the row link and hover text" do
87
+ webhook = create_webhook
88
+
89
+ get "/admin"
90
+
91
+ assert_select "a.row-link[href=?][title=?][aria-label=?]", "/admin/#{webhook.id}", webhook.id.to_s, webhook.id.to_s do
92
+ assert_select "code.cell-id", text: webhook.id.to_s
93
+ assert_select "abbr", count: 0
94
+ end
50
95
  end
51
96
 
52
- test "still sorts by received time when explicitly requested" do
53
- create_webhook(handler_event_id: "new", created_at: 1.day.ago)
54
- create_webhook(handler_event_id: "old", created_at: 2.days.ago)
97
+ test "ignores legacy sort columns in either direction" do
98
+ create_webhook(handler_event_id: "z", status: "skipped", created_at: 1.day.ago, updated_at: 1.hour.ago)
99
+ create_webhook(handler_event_id: "a", handler_module_name: "PrivateHandler", status: "error", created_at: 2.days.ago, updated_at: 2.hours.ago)
55
100
 
56
- get "/admin/received", params: {sort: "created_at"}
101
+ %w[created_at updated_at handler_module_name handler_event_id status].each do |column|
102
+ get "/admin", params: {sort: column, direction: "desc"}
103
+ assert_equal ["a", "z"], listed_event_ids, column
57
104
 
58
- assert_equal ["new", "old"], listed_event_ids
105
+ get "/admin", params: {sort: column, direction: "asc"}
106
+ assert_equal ["z", "a"], listed_event_ids, column
107
+ end
59
108
  end
60
109
 
61
110
  test "offers an auto-refresh switch, off by default, for the listing it refetches" do
62
111
  create_webhook
63
112
 
64
- get "/admin/received"
113
+ get "/admin"
65
114
 
66
115
  assert_select ".page-heading label.switch input#auto-refresh[type=checkbox][role=switch]:not([checked])"
67
116
  # admin.js swaps this element for the one in the refetched page
@@ -76,33 +125,8 @@ class AdminTest < ActionDispatch::IntegrationTest
76
125
 
77
126
  assert_response :ok
78
127
  assert_equal ["at-root"], listed_event_ids
79
- end
80
-
81
- test "sorts by a whitelisted column in either direction" do
82
- create_webhook(handler_event_id: "b", status: "skipped")
83
- create_webhook(handler_event_id: "a", status: "error")
84
-
85
- get "/admin/received", params: {sort: "status", direction: "asc"}
86
- assert_equal ["a", "b"], listed_event_ids
87
- # Clicking the active column again flips the direction
88
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=desc&sort=status", text: "Status ▲"
89
-
90
- get "/admin/received", params: {sort: "status", direction: "desc"}
91
- assert_equal ["b", "a"], listed_event_ids
92
- end
93
-
94
- test "breaks ties by id in the sort direction, so pages do not overlap" do
95
- # Event ids run opposite to ids: SQLite walks the dedup index on (handler_module_name,
96
- # handler_event_id) to sort by handler, and would order the ties by event id otherwise
97
- first = create_webhook(handler_event_id: "z")
98
- second = create_webhook(handler_event_id: "a")
99
- assert_operator first.id, :<, second.id
100
-
101
- get "/admin/received", params: {sort: "handler_module_name", direction: "desc"}
102
- assert_equal ["a", "z"], listed_event_ids
103
-
104
- get "/admin/received", params: {sort: "handler_module_name", direction: "asc"}
105
- assert_equal ["z", "a"], listed_event_ids
128
+ assert_select ".admin-header a[href=?]", "/admin/", text: "Received webhooks"
129
+ assert_select "form.filters[action=?]", "/admin/"
106
130
  end
107
131
 
108
132
  test "ignores unsupported sort columns and directions" do
@@ -110,7 +134,7 @@ class AdminTest < ActionDispatch::IntegrationTest
110
134
  create_webhook(handler_event_id: "new", created_at: 1.day.ago, status: "skipped", body: "z")
111
135
  create_webhook(handler_event_id: "old", created_at: 2.days.ago, status: "error", body: "a")
112
136
 
113
- get "/admin/received", params: {sort: "body", direction: "sideways"}
137
+ get "/admin", params: {sort: "body", direction: "sideways"}
114
138
 
115
139
  assert_response :ok
116
140
  assert_equal ["old", "new"], listed_event_ids
@@ -121,25 +145,27 @@ class AdminTest < ActionDispatch::IntegrationTest
121
145
  create_webhook(handler_event_id: "test-error", status: "error")
122
146
  create_webhook(handler_event_id: "private-processed", handler_module_name: "PrivateHandler")
123
147
 
124
- get "/admin/received", params: {handler: "WebhookTestHandler"}
148
+ get "/admin", params: {handler: "WebhookTestHandler"}
125
149
  assert_equal ["test-error", "test-processed"], listed_event_ids.sort
126
150
 
127
- get "/admin/received", params: {status: "error"}
151
+ get "/admin", params: {status: "error"}
128
152
  assert_equal ["test-error"], listed_event_ids
129
153
 
130
- get "/admin/received", params: {handler: "", status: ""}
154
+ get "/admin", params: {handler: "", status: ""}
131
155
  assert_equal 3, listed_event_ids.length
132
156
  end
133
157
 
134
158
  test "offers every configured handler and status as a filter" do
135
- get "/admin/received", params: {handler: "PrivateHandler"}
159
+ get "/admin", params: {handler: "PrivateHandler", direction: "asc"}
136
160
 
137
161
  handlers = css_select("select[name=handler] option").map { |option| option["value"] }
138
162
  assert_equal ["", *Webhukhs.configuration.active_handlers.values.uniq.sort], handlers
139
163
  assert_select "select[name=handler] option[value='']", text: "Any handler"
140
164
  assert_select "select[name=status] option[value='']", text: "Any status"
141
- # Clearing the filters keeps the sort
142
- assert_select "form.filters a[href=?]", "/admin/received?direction=desc&sort=id", text: "Clear"
165
+ # Filtering and clearing the filters keep the direction, without a sort column
166
+ assert_select "form.filters input[name=direction][value=asc]"
167
+ assert_select "form.filters input[name=sort]", count: 0
168
+ assert_select "form.filters a[href=?]", "/admin/?direction=asc", text: "Clear"
143
169
  assert_select "select[name=handler] option[selected]", text: "PrivateHandler"
144
170
 
145
171
  statuses = css_select("select[name=status] option").map { |option| option["value"] }
@@ -150,7 +176,7 @@ class AdminTest < ActionDispatch::IntegrationTest
150
176
  original_handlers = Webhukhs.configuration.active_handlers
151
177
  Webhukhs.configuration.active_handlers = {a: WebhookTestHandler, b: "WebhookTestHandler"}
152
178
 
153
- get "/admin/received"
179
+ get "/admin"
154
180
 
155
181
  handlers = css_select("select[name=handler] option").map { |option| option["value"] }
156
182
  assert_equal ["", "WebhookTestHandler"], handlers
@@ -162,16 +188,16 @@ class AdminTest < ActionDispatch::IntegrationTest
162
188
  page_size = Webhukhs::Admin::ReceivedWebhooksController::PAGE_SIZE
163
189
  (page_size + 1).times { |i| create_webhook(handler_event_id: "evt-#{i}", created_at: i.minutes.ago) }
164
190
 
165
- get "/admin/received", params: {sort: "status"}
191
+ get "/admin", params: {direction: "asc"}
166
192
  assert_equal page_size, listed_event_ids.length
167
- assert_equal "evt-#{page_size}", listed_event_ids.first # all ties on status, so by id
193
+ assert_equal (0...page_size).map { |i| "evt-#{i}" }, listed_event_ids
168
194
  assert_select "a[rel=prev]", count: 0
169
- assert_select "a[rel=next][href=?]", "/admin/received?page=2&sort=status"
195
+ assert_select "a[rel=next][href=?]", "/admin/?direction=asc&page=2"
170
196
 
171
- get "/admin/received", params: {sort: "status", page: 2}
172
- assert_equal ["evt-0"], listed_event_ids
197
+ get "/admin", params: {direction: "asc", page: 2}
198
+ assert_equal ["evt-#{page_size}"], listed_event_ids
173
199
  assert_select "a[rel=next]", count: 0
174
- assert_select "a[rel=prev][href=?]", "/admin/received?page=1&sort=status"
200
+ assert_select "a[rel=prev][href=?]", "/admin/?direction=asc&page=1"
175
201
  end
176
202
 
177
203
  test "loads one row past the page and no more" do
@@ -181,7 +207,7 @@ class AdminTest < ActionDispatch::IntegrationTest
181
207
  loaded = 0
182
208
  counter = ->(*, payload) { loaded += payload[:record_count] if payload[:class_name] == "Webhukhs::ReceivedWebhook" }
183
209
  ActiveSupport::Notifications.subscribed(counter, "instantiation.active_record") do
184
- get "/admin/received"
210
+ get "/admin"
185
211
  end
186
212
 
187
213
  assert_equal page_size + 1, loaded
@@ -190,7 +216,7 @@ class AdminTest < ActionDispatch::IntegrationTest
190
216
  test "shows no next page when the last page is exactly full" do
191
217
  Webhukhs::Admin::ReceivedWebhooksController::PAGE_SIZE.times { create_webhook }
192
218
 
193
- get "/admin/received"
219
+ get "/admin"
194
220
 
195
221
  assert_select "a[rel=next]", count: 0
196
222
  end
@@ -198,20 +224,20 @@ class AdminTest < ActionDispatch::IntegrationTest
198
224
  test "treats a page below 1 as the first page" do
199
225
  create_webhook(handler_event_id: "only")
200
226
 
201
- get "/admin/received", params: {page: -3}
227
+ get "/admin", params: {page: -3}
202
228
 
203
229
  assert_equal ["only"], listed_event_ids
204
230
  assert_select "a[rel=prev]", count: 0
205
231
  end
206
232
 
207
- test "drops the page when changing the sort" do
208
- get "/admin/received", params: {page: 3}
233
+ test "keeps filters but drops the page and legacy sort when reversing the order" do
234
+ get "/admin", params: {page: 3, sort: "updated_at", handler: "PrivateHandler", status: "error"}
209
235
 
210
- assert_select "th a.sort-link[href=?]", "/admin/received?direction=desc&sort=status"
236
+ assert_select "th a.sort-link[href=?]", "/admin/?direction=asc&handler=PrivateHandler&status=error"
211
237
  end
212
238
 
213
239
  test "says so when there is nothing to list" do
214
- get "/admin/received"
240
+ get "/admin"
215
241
 
216
242
  assert_select "div.table-scroll > table.admin-table > thead > tr > th", count: 6
217
243
  assert_select "table.admin-table > tbody > tr > td.empty[colspan='6']", text: "No records."
@@ -234,10 +260,11 @@ class AdminTest < ActionDispatch::IntegrationTest
234
260
  create_webhook(handler_event_id: "decoy", body: "not this one")
235
261
  webhook = create_webhook(body: '{"event":"thing.happened","data":{"id":1}}')
236
262
 
237
- get "/admin/received/#{webhook.id}"
263
+ get "/admin/#{webhook.id}"
238
264
 
239
265
  assert_response :ok
240
266
  assert_select "h1", text: /WebhookTestHandler/
267
+ assert_select ".breadcrumb a[href=?]", "/admin/", text: "Received webhooks"
241
268
  assert_select "[role=tab][aria-selected=true]", text: "Pretty"
242
269
  assert_select "[role=tab][aria-selected=false]", text: "As sent"
243
270
  # assert_select squashes whitespace, which is the very thing under test here
@@ -250,7 +277,7 @@ class AdminTest < ActionDispatch::IntegrationTest
250
277
  test "shows a body that is not JSON as sent, replacing invalid UTF-8" do
251
278
  webhook = create_webhook(body: "event=thing&broken=\xFF".b)
252
279
 
253
- get "/admin/received/#{webhook.id}"
280
+ get "/admin/#{webhook.id}"
254
281
 
255
282
  assert_response :ok
256
283
  assert_select "[role=tab]", count: 0
@@ -263,7 +290,7 @@ class AdminTest < ActionDispatch::IntegrationTest
263
290
  json = %({"padding":"#{"x" * limit}"})
264
291
  webhook = create_webhook(body: json)
265
292
 
266
- get "/admin/received/#{webhook.id}"
293
+ get "/admin/#{webhook.id}"
267
294
 
268
295
  assert_response :ok
269
296
  assert_select "[role=tab]", count: 0
@@ -303,7 +330,7 @@ class AdminTest < ActionDispatch::IntegrationTest
303
330
  "action_dispatch.request.path_parameters" => {"service_id" => "test"}
304
331
  })
305
332
 
306
- get "/admin/received/#{webhook.id}"
333
+ get "/admin/#{webhook.id}"
307
334
 
308
335
  rows = css_select("table.admin-table thead:has(th:contains('Header')) + tbody tr").map do |row|
309
336
  row.css("td").map(&:text)
@@ -318,7 +345,7 @@ class AdminTest < ActionDispatch::IntegrationTest
318
345
  test "shows webhooks stored before headers were recorded" do
319
346
  webhook = create_webhook(request_headers: nil)
320
347
 
321
- get "/admin/received/#{webhook.id}"
348
+ get "/admin/#{webhook.id}"
322
349
 
323
350
  assert_response :ok
324
351
  assert_select "td.empty[colspan='2']", text: "No records."
@@ -327,14 +354,14 @@ class AdminTest < ActionDispatch::IntegrationTest
327
354
  test "renders copy buttons for the identifiers" do
328
355
  webhook = create_webhook(handler_event_id: "evt_123")
329
356
 
330
- get "/admin/received/#{webhook.id}"
357
+ get "/admin/#{webhook.id}"
331
358
 
332
359
  assert_select "button.btn-copy[type=button][title='Copy to clipboard'][data-copy=?]", webhook.id.to_s
333
360
  assert_select "button.btn-copy[data-copy=?]", "evt_123"
334
361
  end
335
362
 
336
363
  test "locks the pages down with a content security policy" do
337
- get "/admin/received"
364
+ get "/admin"
338
365
 
339
366
  assert_equal "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; " \
340
367
  "connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'",
@@ -342,7 +369,7 @@ class AdminTest < ActionDispatch::IntegrationTest
342
369
  end
343
370
 
344
371
  test "serves its own assets, fingerprinted, from under the mount point" do
345
- get "/admin/received"
372
+ get "/admin"
346
373
 
347
374
  assert_select "link[rel=stylesheet][href^='/admin/admin.css?s='][integrity^='sha256-']"
348
375
  assert_select "script[type=module][src^='/admin/admin.js?s='][integrity^='sha256-']"
@@ -89,20 +89,21 @@ class AdminClickThrough < ActiveSupport::TestCase
89
89
  PAGE_SIZE.times { |i| create_webhook(handler_event_id: "evt-#{i}", created_at: i.minutes.ago) }
90
90
  create_webhook(handler_event_id: "the-failed-one", status: "error", created_at: 2.days.ago)
91
91
 
92
- visit "/received"
92
+ visit ""
93
93
  assert_equal "Received webhooks", @browser.at_css("h1").text
94
94
  # The stylesheet must have made it through the CSP - unstyled, the header would not be a flexbox
95
95
  assert_equal "flex", @browser.evaluate("getComputedStyle(document.querySelector('.admin-header')).display")
96
96
  assert_equal PAGE_SIZE, @browser.css("tbody tr").length
97
- assert_equal "the-failed-one", texts("tbody tr td:nth-child(5)").first
97
+ assert_equal "the-failed-one", texts("tbody tr td:nth-child(4)").first
98
98
 
99
- click_and_wait_for_page(@browser.at_xpath("//th/a[contains(., 'Received')]"))
100
- assert_includes @browser.current_url, "direction=desc"
101
- assert_equal "evt-0", texts("tbody tr td:nth-child(5)").first
102
-
103
- click_and_wait_for_page(@browser.at_xpath("//th/a[contains(., 'Received')]"))
99
+ assert_equal 1, @browser.css("th a").length
100
+ click_and_wait_for_page(@browser.at_xpath("//th/a[contains(., 'Id')]"))
104
101
  assert_includes @browser.current_url, "direction=asc"
105
- assert_equal "the-failed-one", texts("tbody tr td:nth-child(5)").first
102
+ assert_equal "evt-0", texts("tbody tr td:nth-child(4)").first
103
+
104
+ click_and_wait_for_page(@browser.at_xpath("//th/a[contains(., 'Id')]"))
105
+ assert_includes @browser.current_url, "direction=desc"
106
+ assert_equal "the-failed-one", texts("tbody tr td:nth-child(4)").first
106
107
 
107
108
  # Not Ferrum's Node#select: it blanks the select by assigning it `undefined` first, which
108
109
  # left it blank on the Chromium snapshot CI got
@@ -113,9 +114,9 @@ class AdminClickThrough < ActiveSupport::TestCase
113
114
  JS
114
115
  assert_equal "error", @browser.evaluate("document.querySelector('select[name=status]').value")
115
116
  click_and_wait_for_page(@browser.at_css("form.filters input[type=submit]"))
116
- assert_equal ["the-failed-one"], texts("tbody tr td:nth-child(5)")
117
+ assert_equal ["the-failed-one"], texts("tbody tr td:nth-child(4)")
117
118
  # The sort survives filtering
118
- assert_includes @browser.current_url, "direction=asc"
119
+ assert_includes @browser.current_url, "direction=desc"
119
120
 
120
121
  click_and_wait_for_page(@browser.at_xpath("//form//a[text()='Clear']"))
121
122
  assert_equal "Any status", @browser.evaluate("document.querySelector('select[name=status]').selectedOptions[0].text")
@@ -129,18 +130,18 @@ class AdminClickThrough < ActiveSupport::TestCase
129
130
  test "opens a webhook by clicking anywhere on its row" do
130
131
  webhook = create_webhook(handler_event_id: "evt-clicked")
131
132
 
132
- visit "/received"
133
+ visit ""
133
134
  # The status cell is not a link, but the row link stretched over it takes the click
134
- click_and_wait_for_page(@browser.at_css("tbody tr td:nth-child(4)"))
135
+ click_and_wait_for_page(@browser.at_css("tbody tr td:nth-child(3)"))
135
136
 
136
- assert @browser.current_url.end_with?("/received/#{webhook.id}"), @browser.current_url
137
+ assert_equal "#{@base_url}/#{webhook.id}", @browser.current_url
137
138
  assert_includes @browser.at_css("h1").text, "WebhookTestHandler"
138
139
  end
139
140
 
140
141
  test "switches between the pretty and the as-sent body" do
141
142
  webhook = create_webhook(body: '{"event":"thing.happened","data":{"id":1}}')
142
143
 
143
- visit "/received/#{webhook.id}"
144
+ visit "/#{webhook.id}"
144
145
  pretty = -> { @browser.evaluate("document.getElementById('body-pretty').hidden") == false }
145
146
  raw = -> { @browser.evaluate("document.getElementById('body-raw').hidden") == false }
146
147
  assert pretty.call
@@ -161,7 +162,7 @@ class AdminClickThrough < ActiveSupport::TestCase
161
162
  test "copies to the clipboard" do
162
163
  webhook = create_webhook(handler_event_id: "evt-to-copy")
163
164
 
164
- visit "/received/#{webhook.id}"
165
+ visit "/#{webhook.id}"
165
166
  # Headless Chrome has no clipboard to grant, so we only check that the right text gets handed to it
166
167
  @browser.execute("navigator.clipboard.writeText = (text) => { window.copied = text; return Promise.resolve() }")
167
168
 
@@ -178,19 +179,19 @@ class AdminClickThrough < ActiveSupport::TestCase
178
179
  form = create_webhook(body: "event=paid&amount=12.50")
179
180
  large = create_webhook(body: "x" * (Webhukhs::Admin::ReceivedWebhookPresenter::DISPLAY_LIMIT + 1))
180
181
 
181
- visit "/received/#{form.id}"
182
+ visit "/#{form.id}"
182
183
  assert_equal "This body is not valid JSON, so it is shown as it was sent", @browser.at_css(".flash.alert").text
183
184
  assert_equal "event=paid&amount=12.50", @browser.at_css("pre.body-raw code").text
184
185
  assert_empty @browser.css("[role=tab]")
185
186
 
186
- visit "/received/#{large.id}"
187
+ visit "/#{large.id}"
187
188
  assert_equal "The body was 256 KB, display is truncated", @browser.at_css(".flash.alert").text
188
189
  end
189
190
 
190
191
  test "picks up new webhooks while auto-refresh is on, and remembers the switch" do
191
192
  create_webhook(handler_event_id: "evt-before")
192
193
 
193
- visit "/received"
194
+ visit ""
194
195
  switch = -> { @browser.evaluate("document.getElementById('auto-refresh').checked") }
195
196
  refute switch.call
196
197
  @browser.at_css("label.switch").click
@@ -198,14 +199,14 @@ class AdminClickThrough < ActiveSupport::TestCase
198
199
 
199
200
  create_webhook(handler_event_id: "evt-arrived")
200
201
  # One poll interval, with some slack
201
- wait_for("the new webhook to show up", timeout: 8) { texts("tbody tr td:nth-child(5)").include?("evt-arrived") }
202
- assert_equal ["evt-arrived", "evt-before"], texts("tbody tr td:nth-child(5)")
202
+ wait_for("the new webhook to show up", timeout: 8) { texts("tbody tr td:nth-child(4)").include?("evt-arrived") }
203
+ assert_equal ["evt-arrived", "evt-before"], texts("tbody tr td:nth-child(4)")
203
204
 
204
- visit "/received"
205
+ visit ""
205
206
  assert switch.call, "The switch should stay on after a reload"
206
207
  # After a page load only the timer refetches, so this one has to come in through a poll
207
208
  create_webhook(handler_event_id: "evt-polled")
208
- wait_for("the polled webhook to show up", timeout: 8) { texts("tbody tr td:nth-child(5)").include?("evt-polled") }
209
+ wait_for("the polled webhook to show up", timeout: 8) { texts("tbody tr td:nth-child(4)").include?("evt-polled") }
209
210
  @browser.at_css("label.switch").click
210
211
  wait_for("the switch to turn off") { !switch.call }
211
212
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webhukhs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislav Katkov