Add JSON response format for full SDK/CLI coverage (#2675)
* Add JSON response format to void-response controller actions
Add respond_to blocks with JSON format to 14 controllers that
previously only rendered HTML or Turbo Stream responses. JSON
callers get head :no_content (or :created/:ok for push
subscriptions, distinguishing new vs existing).
Covers: board involvements, board/account entropies, column
reordering, card readings, card publishing, user roles, user
avatars, account settings, notification settings, join codes
(with 422 error branch), and push subscriptions.
* Add steps index endpoint with JSON view
Add index action to Cards::StepsController so SDK/CLI callers
can list all steps for a card. Reuses the existing _step.json
partial.
* Add JSON views for paginated card list endpoints
Add show.json.jbuilder for stream, not-now, and closed column
endpoints. No controller changes needed — set_page_and_extract_
portion_from and fresh_when already work format-agnostically.
* Add JSON search endpoint with distinct-card pagination
JSON search paginates distinct Card records (via the existing
Card.mentioning scope with .distinct) rather than Search::Record
objects. This ensures page boundaries, Link headers, and counts
reflect unique cards — no short pages from post-pagination dedup.
ID-match searches return a uniform single-element Card[] array
through the same pagination path.
* Add JSON views for settings and configuration endpoints
Add show.json.jbuilder for account settings (name), join codes
(code, usage_count, usage_limit, url, active), and notification
settings (bundle_email_frequency). Tests for show and update
were added in the void-response commit.
* Add JSON response format to data exports
Create returns 201 with export id, status, and created_at so
callers can poll. Show returns any-status exports for JSON (not
just completed) with a download_url when ready, or 404 for
missing/other-user exports.
* Extend access token JSON API
Add index.json and _access_token.json partial for listing tokens.
Add JSON format to destroy. Include id and created_at in the
create response alongside the existing token/description/permission
fields.
* Fix ambiguous precedence warning in export JSON view
* Assert X-Total-Count pagination header in streams JSON test
* Address review feedback
- Guard steps index against HTML requests (redirect to card)
- Normalize created_at to UTC in access token and export JSON
- Add deterministic ordering (.latest) to search JSON pagination
* Return 406 for HTML requests to steps index instead of redirect
* Add wrap_parameters for flat SDK JSON payload compatibility
Five controllers infer the wrong wrapper key from their class
name, so flat JSON bodies like {"role":"admin"} fail with 400.
Add explicit wrap_parameters declarations:
- Users::RolesController → :user
- Notifications::SettingsController → :user_settings
- Account::JoinCodesController → :account_join_code
- Account::SettingsController → :account
- Boards::EntropiesController → :board
Add integration tests that send flat (unwrapped) JSON payloads
for all seven param-bearing endpoints to prove SDK compatibility.
* Address PR review feedback
- Use 201 Created (not 204) for all create actions: publishes, readings,
left/right positions, push subscriptions
- Simplify push subscription to always return :created (no 200/201 variance)
- Remove superfluous respond_to block from steps#index
- Merge exports#show into single respond_to block
- Move export download_url conditional out of inline args
- Ensure join code active is explicitly boolean
- Remove extra parens from search controller assignment
- Add blank lines between format blocks for readability
* Return JSON bodies on create actions
Create actions for boards, cards, columns, comments, and steps now
render the resource as JSON (via their show views) instead of returning
empty 201 responses with only a Location header. SDKs expect a JSON
body they can deserialize into the created resource.
* Return JSON bodies on reaction create actions
Card and comment reaction creates now render the reaction as JSON
instead of returning empty 201 responses, consistent with the other
resource-creating endpoints.
* Allow access tokens API without account scope
Access tokens are identity-level (not account-scoped), so the
controller needs to work with bearer token auth and no account slug
in the URL. Skip require_account so the bearer token auth path
can proceed.
Also fix involvement test to send params in JSON body (not query
string) to match real SDK usage.
This commit is contained in:
@@ -3,7 +3,11 @@ class Account::EntropiesController < ApplicationController
|
||||
|
||||
def update
|
||||
Current.account.entropy.update!(entropy_params)
|
||||
redirect_to account_settings_path, notice: "Account updated"
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_settings_path, notice: "Account updated" }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -6,11 +6,20 @@ class Account::ExportsController < ApplicationController
|
||||
CURRENT_EXPORT_LIMIT = 10
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { @export ? render(:show) : head(:not_found) }
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
Current.account.exports.create!(user: Current.user).build_later
|
||||
redirect_to account_settings_path, notice: "Export started. You'll receive an email when it's ready."
|
||||
@export = Current.account.exports.create!(user: Current.user)
|
||||
@export.build_later
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_settings_path, notice: "Export started. You'll receive an email when it's ready." }
|
||||
format.json { render :show, status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -23,6 +32,7 @@ class Account::ExportsController < ApplicationController
|
||||
end
|
||||
|
||||
def set_export
|
||||
@export = Current.account.exports.completed.find_by(id: params[:id], user: Current.user)
|
||||
scope = request.format.json? ? Current.account.exports : Current.account.exports.completed
|
||||
@export = scope.find_by(id: params[:id], user: Current.user)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Account::JoinCodesController < ApplicationController
|
||||
wrap_parameters :account_join_code
|
||||
|
||||
before_action :set_join_code
|
||||
before_action :ensure_admin, only: %i[ update destroy ]
|
||||
|
||||
@@ -10,15 +12,25 @@ class Account::JoinCodesController < ApplicationController
|
||||
|
||||
def update
|
||||
if @join_code.update(join_code_params)
|
||||
redirect_to account_join_code_path
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_join_code_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @join_code.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@join_code.reset
|
||||
redirect_to account_join_code_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_join_code_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Account::SettingsController < ApplicationController
|
||||
wrap_parameters :account
|
||||
|
||||
before_action :ensure_admin, only: :update
|
||||
before_action :set_account
|
||||
|
||||
@@ -8,7 +10,11 @@ class Account::SettingsController < ApplicationController
|
||||
|
||||
def update
|
||||
@account.update!(account_params)
|
||||
redirect_to account_settings_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_settings_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -18,7 +18,7 @@ class Boards::ColumnsController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: board_column_path(@board, @column, format: :json) }
|
||||
format.json { render :show, status: :created, location: board_column_path(@board, @column, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
class Boards::EntropiesController < ApplicationController
|
||||
wrap_parameters :board
|
||||
|
||||
include BoardScoped
|
||||
|
||||
before_action :ensure_permission_to_admin_board
|
||||
|
||||
def update
|
||||
@board.update!(entropy_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,5 +3,11 @@ class Boards::InvolvementsController < ApplicationController
|
||||
|
||||
def update
|
||||
@board.access_for(Current.user).update!(involvement: params[:involvement])
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,7 +26,7 @@ class BoardsController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to board_path(@board) }
|
||||
format.json { head :created, location: board_path(@board, format: :json) }
|
||||
format.json { render :show, status: :created, location: board_path(@board, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class Cards::Comments::ReactionsController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render "reactions/create" }
|
||||
format.json { head :created }
|
||||
format.json { render "reactions/show", status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class Cards::CommentsController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: card_comment_path(@card, @comment, format: :json) }
|
||||
format.json { render :show, status: :created, location: card_comment_path(@card, @comment, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ class Cards::PublishesController < ApplicationController
|
||||
def create
|
||||
@card.publish
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if add_another_param?
|
||||
card = @board.cards.create!(status: :drafted)
|
||||
redirect_to card_draft_path(card), notice: "Card added"
|
||||
@@ -12,6 +14,10 @@ class Cards::PublishesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def add_another_param?
|
||||
params[:creation_type] == "add_another"
|
||||
|
||||
@@ -21,7 +21,7 @@ class Cards::ReactionsController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render "reactions/create" }
|
||||
format.json { head :created }
|
||||
format.json { render "reactions/show", status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,11 +4,21 @@ class Cards::ReadingsController < ApplicationController
|
||||
def create
|
||||
@notification = @card.read_by(Current.user)
|
||||
record_board_access
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification = @card.unread_by(Current.user)
|
||||
record_board_access
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,12 +3,16 @@ class Cards::StepsController < ApplicationController
|
||||
|
||||
before_action :set_step, only: %i[ show edit update destroy ]
|
||||
|
||||
def index
|
||||
fresh_when etag: @card.steps
|
||||
end
|
||||
|
||||
def create
|
||||
@step = @card.steps.create!(step_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: card_step_path(@card, @step, format: :json) }
|
||||
format.json { render :show, status: :created, location: card_step_path(@card, @step, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ class CardsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
card = @board.cards.create! card_params.merge(creator: Current.user, status: "published")
|
||||
head :created, location: card_path(card, format: :json)
|
||||
@card = @board.cards.create! card_params.merge(creator: Current.user, status: "published")
|
||||
render :show, status: :created, location: card_path(@card, format: :json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,5 +4,10 @@ class Columns::LeftPositionsController < ApplicationController
|
||||
def create
|
||||
@left_column = @column.left_column
|
||||
@column.move_left
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,5 +4,10 @@ class Columns::RightPositionsController < ApplicationController
|
||||
def create
|
||||
@right_column = @column.right_column
|
||||
@column.move_right
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class My::AccessTokensController < ApplicationController
|
||||
skip_before_action :require_account
|
||||
|
||||
def index
|
||||
@access_tokens = my_access_tokens.order(created_at: :desc)
|
||||
end
|
||||
@@ -24,14 +26,19 @@ class My::AccessTokensController < ApplicationController
|
||||
|
||||
format.json do
|
||||
render status: :created, json: \
|
||||
{ token: access_token.token, description: access_token.description, permission: access_token.permission }
|
||||
{ id: access_token.id, token: access_token.token, description: access_token.description,
|
||||
permission: access_token.permission, created_at: access_token.created_at.utc }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
my_access_tokens.find(params[:id]).destroy!
|
||||
redirect_to my_access_tokens_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to my_access_tokens_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Notifications::SettingsController < ApplicationController
|
||||
wrap_parameters :user_settings
|
||||
|
||||
before_action :set_settings
|
||||
|
||||
def show
|
||||
@@ -7,7 +9,11 @@ class Notifications::SettingsController < ApplicationController
|
||||
|
||||
def update
|
||||
@settings.update!(settings_params)
|
||||
redirect_to notifications_settings_path, notice: "Settings updated"
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to notifications_settings_path, notice: "Settings updated" }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,10 +5,22 @@ class SearchesController < ApplicationController
|
||||
@query = params[:q].blank? ? nil : params[:q]
|
||||
|
||||
if card = Current.user.accessible_cards.find_by_id(@query)
|
||||
@card = card
|
||||
respond_to do |format|
|
||||
format.html { @card = card }
|
||||
format.json { set_page_and_extract_portion_from Current.user.accessible_cards.where(id: card.id) }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
set_page_and_extract_portion_from Current.user.search(@query)
|
||||
@recent_search_queries = Current.user.search_queries.order(updated_at: :desc).limit(10)
|
||||
end
|
||||
|
||||
format.json do
|
||||
set_page_and_extract_portion_from \
|
||||
Current.user.accessible_cards.mentioning(@query, user: Current.user).distinct.latest.preloaded
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,11 @@ class Users::AvatarsController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@user.avatar.destroy
|
||||
redirect_to @user
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @user }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,12 +5,21 @@ class Users::PushSubscriptionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@push_subscriptions.create_with(user_agent: request.user_agent).create_or_find_by!(push_subscription_params)
|
||||
subscription = @push_subscriptions.create_with(user_agent: request.user_agent).create_or_find_by!(push_subscription_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { head :no_content }
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@push_subscriptions.destroy_by(id: params[:id])
|
||||
redirect_to user_push_subscriptions_url
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to user_push_subscriptions_url }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
class Users::RolesController < ApplicationController
|
||||
wrap_parameters :user
|
||||
|
||||
before_action :set_user
|
||||
before_action :ensure_permission_to_administer_user
|
||||
|
||||
def update
|
||||
@user.update!(role_params)
|
||||
redirect_to account_settings_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_settings_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
json.(@export, :id, :status)
|
||||
json.created_at @export.created_at.utc
|
||||
|
||||
if @export.completed? && @export.file.attached?
|
||||
json.download_url rails_blob_url(@export.file, disposition: "attachment")
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
json.(@join_code, :code, :usage_count, :usage_limit)
|
||||
json.url join_url(code: @join_code.code, script_name: Current.account.slug)
|
||||
json.active !!@join_code.active?
|
||||
@@ -0,0 +1 @@
|
||||
json.(Current.account, :name)
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @card.steps, partial: "cards/steps/step", as: :step
|
||||
@@ -0,0 +1,2 @@
|
||||
json.(access_token, :id, :description, :permission)
|
||||
json.created_at access_token.created_at.utc
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @access_tokens, partial: "my/access_tokens/access_token", as: :access_token
|
||||
@@ -0,0 +1 @@
|
||||
json.bundle_email_frequency Current.user.settings.bundle_email_frequency
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "reactions/reaction", reaction: @reaction
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -13,6 +13,13 @@ class Account::EntropiesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to account_settings_path
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
put account_entropy_path, params: { entropy: { auto_postpone_period: 2.days } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 2.days, entropies("37s_account").reload.auto_postpone_period
|
||||
end
|
||||
|
||||
test "update requires admin" do
|
||||
logout_and_sign_in_as :david
|
||||
|
||||
|
||||
@@ -76,6 +76,49 @@ class Account::ExportsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_select "h2", "Download Expired"
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_difference -> { Account::Export.count }, 1 do
|
||||
assert_enqueued_with(job: DataExportJob) do
|
||||
post account_exports_path, as: :json
|
||||
end
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
body = @response.parsed_body
|
||||
assert body["id"].present?
|
||||
assert_equal "pending", body["status"]
|
||||
assert_nil body["download_url"]
|
||||
end
|
||||
|
||||
test "show as JSON with completed export" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:jason))
|
||||
export.build
|
||||
|
||||
get account_export_path(export), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_equal export.id, body["id"]
|
||||
assert_equal "completed", body["status"]
|
||||
assert body["download_url"].present?
|
||||
end
|
||||
|
||||
test "show as JSON with pending export" do
|
||||
export = Account::Export.create!(account: Current.account, user: users(:jason))
|
||||
|
||||
get account_export_path(export), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_equal "pending", body["status"]
|
||||
assert_nil body["download_url"]
|
||||
end
|
||||
|
||||
test "show as JSON with missing export" do
|
||||
get account_export_path("nonexistent"), as: :json
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
test "create is forbidden for non-admin members" do
|
||||
logout_and_sign_in_as :david
|
||||
|
||||
|
||||
@@ -24,6 +24,41 @@ class Account::JoinCodesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to account_join_code_path
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get account_join_code_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert body["code"].present?
|
||||
assert body.key?("usage_count")
|
||||
assert body.key?("usage_limit")
|
||||
assert body.key?("url")
|
||||
assert body.key?("active")
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
put account_join_code_path, params: { account_join_code: { usage_limit: 5 } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 5, Current.account.join_code.reload.usage_limit
|
||||
end
|
||||
|
||||
test "update as JSON with invalid data" do
|
||||
huge_number = "99999999999999999999999999999999999"
|
||||
|
||||
put account_join_code_path, params: { account_join_code: { usage_limit: huge_number } }, as: :json
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
assert_changes -> { Current.account.join_code.reload.code } do
|
||||
delete account_join_code_path, as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "update requires admin" do
|
||||
logout_and_sign_in_as :david
|
||||
|
||||
|
||||
@@ -16,6 +16,20 @@ class Account::SettingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to account_settings_path
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get account_settings_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_equal Current.account.name, @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
put account_settings_path, params: { account: { name: "New Account Name" } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal "New Account Name", Current.account.reload.name
|
||||
end
|
||||
|
||||
test "update requires admin" do
|
||||
logout_and_sign_in_as :david
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
require "test_helper"
|
||||
|
||||
class FlatJsonParamsTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "update user role with flat JSON" do
|
||||
put user_role_path(users(:david)), params: { role: "admin" }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert users(:david).reload.admin?
|
||||
end
|
||||
|
||||
test "update notification settings with flat JSON" do
|
||||
logout_and_sign_in_as :david
|
||||
|
||||
assert_changes -> { users(:david).reload.settings.bundle_email_frequency }, from: "never", to: "every_few_hours" do
|
||||
put notifications_settings_path, params: { bundle_email_frequency: "every_few_hours" }, as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "update join code with flat JSON" do
|
||||
put account_join_code_path, params: { usage_limit: 5 }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 5, Current.account.join_code.reload.usage_limit
|
||||
end
|
||||
|
||||
test "update account settings with flat JSON" do
|
||||
put account_settings_path, params: { name: "New Name" }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal "New Name", Current.account.reload.name
|
||||
end
|
||||
|
||||
test "update board entropy with flat JSON" do
|
||||
board = boards(:writebook)
|
||||
|
||||
put board_entropy_path(board), params: { auto_postpone_period: 99.days }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 99.days, board.entropy.reload.auto_postpone_period
|
||||
end
|
||||
|
||||
test "update account entropy with flat JSON" do
|
||||
put account_entropy_path, params: { auto_postpone_period: 2.days }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 2.days, Current.account.entropy.reload.auto_postpone_period
|
||||
end
|
||||
|
||||
test "create push subscription with flat JSON" do
|
||||
stub_dns_resolution("142.250.185.206")
|
||||
|
||||
post user_push_subscriptions_path(users(:kevin)),
|
||||
params: { endpoint: "https://fcm.googleapis.com/fcm/send/abc123", p256dh_key: "key1", auth_key: "key2" },
|
||||
as: :json
|
||||
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
private
|
||||
def stub_dns_resolution(*ips)
|
||||
dns_mock = mock("dns")
|
||||
dns_mock.stubs(:each_address).multiple_yields(*ips)
|
||||
Resolv::DNS.stubs(:open).yields(dns_mock)
|
||||
end
|
||||
end
|
||||
@@ -9,4 +9,11 @@ class Boards::Columns::ClosedsControllerTest < ActionDispatch::IntegrationTest
|
||||
get board_columns_closed_path(boards(:writebook))
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get board_columns_closed_path(boards(:writebook)), as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_kind_of Array, @response.parsed_body
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,4 +9,11 @@ class Boards::Columns::NotNowsControllerTest < ActionDispatch::IntegrationTest
|
||||
get board_columns_not_now_path(boards(:writebook))
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get board_columns_not_now_path(boards(:writebook)), as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_kind_of Array, @response.parsed_body
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,4 +9,12 @@ class Boards::Columns::StreamsControllerTest < ActionDispatch::IntegrationTest
|
||||
get board_columns_stream_path(boards(:writebook))
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get board_columns_stream_path(boards(:writebook)), as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_kind_of Array, @response.parsed_body
|
||||
assert response.headers["X-Total-Count"].present?, "Expected X-Total-Count header"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,6 +75,7 @@ class Boards::ColumnsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :created
|
||||
assert_equal board_column_path(board, Column.last, format: :json), @response.headers["Location"]
|
||||
assert_equal "New Column", @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
|
||||
@@ -16,6 +16,13 @@ class Boards::EntropiesControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
put board_entropy_path(@board), params: { board: { auto_postpone_period: 99.days } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal 99.days, @board.entropy.reload.auto_postpone_period
|
||||
end
|
||||
|
||||
test "update requires board admin permission" do
|
||||
logout_and_sign_in_as :jz
|
||||
|
||||
|
||||
@@ -15,4 +15,15 @@ class Boards::InvolvementsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
board = boards(:writebook)
|
||||
board.access_for(users(:kevin)).access_only!
|
||||
|
||||
assert_changes -> { board.access_for(users(:kevin)).involvement }, from: "access_only", to: "watching" do
|
||||
put board_involvement_path(board), params: { involvement: "watching" }, as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -275,6 +275,7 @@ class BoardsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :created
|
||||
assert_equal board_path(Board.last, format: :json), @response.headers["Location"]
|
||||
assert_equal "My new board", @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
|
||||
@@ -49,6 +49,7 @@ class Cards::Comments::ReactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal "👍", @response.parsed_body["content"]
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
|
||||
@@ -56,6 +56,7 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :created
|
||||
assert_equal card_comment_path(card, Comment.last, format: :json), @response.headers["Location"]
|
||||
assert_equal Comment.last.id, @response.parsed_body["id"]
|
||||
end
|
||||
|
||||
test "create as JSON with custom created_at" do
|
||||
|
||||
@@ -16,6 +16,17 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to card.board
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
card.drafted!
|
||||
|
||||
assert_changes -> { card.reload.published? }, from: false, to: true do
|
||||
post card_publish_path(card), as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
test "create and add another" do
|
||||
card = cards(:logo)
|
||||
card.drafted!
|
||||
|
||||
@@ -53,6 +53,7 @@ class Cards::ReactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal "👍", @response.parsed_body["content"]
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
|
||||
@@ -39,6 +39,18 @@ class Cards::ReadingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
post card_reading_url(cards(:logo)), as: :json
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
notifications(:logo_assignment_kevin).read
|
||||
|
||||
delete card_reading_url(cards(:logo)), as: :json
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "unread notification on destroy" do
|
||||
notifications(:logo_assignment_kevin).read
|
||||
|
||||
|
||||
@@ -53,6 +53,19 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
card = cards(:logo)
|
||||
card.steps.create!(content: "Step one")
|
||||
card.steps.create!(content: "Step two", completed: true)
|
||||
|
||||
get card_steps_path(card), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_equal 2, body.size
|
||||
assert_equal "Step one", body.first["content"]
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
@@ -62,6 +75,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :created
|
||||
assert_equal card_step_path(card, Step.last, format: :json), @response.headers["Location"]
|
||||
assert_equal "New step", @response.parsed_body["content"]
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
|
||||
@@ -192,6 +192,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
card = Card.last
|
||||
assert_equal card_path(card, format: :json), @response.headers["Location"]
|
||||
assert_equal "My new card", @response.parsed_body["title"]
|
||||
|
||||
assert_equal "My new card", card.title
|
||||
assert_equal "Big if true", card.description.to_plain_text
|
||||
|
||||
@@ -21,6 +21,22 @@ class Columns::LeftPositionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal original_position_a, column_b.reload.position
|
||||
end
|
||||
|
||||
test "move column left as JSON" do
|
||||
board = boards(:writebook)
|
||||
columns = board.columns.sorted.to_a
|
||||
|
||||
column_a = columns[0]
|
||||
column_b = columns[1]
|
||||
original_position_a = column_a.position
|
||||
original_position_b = column_b.position
|
||||
|
||||
post column_left_position_path(column_b), as: :json
|
||||
assert_response :created
|
||||
|
||||
assert_equal original_position_b, column_a.reload.position
|
||||
assert_equal original_position_a, column_b.reload.position
|
||||
end
|
||||
|
||||
test "move left refreshes adjacent columns" do
|
||||
column = columns(:writebook_in_progress)
|
||||
|
||||
|
||||
@@ -21,6 +21,22 @@ class Columns::RightPositionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal original_position_a, column_b.reload.position
|
||||
end
|
||||
|
||||
test "move column right as JSON" do
|
||||
board = boards(:writebook)
|
||||
columns = board.columns.sorted.to_a
|
||||
|
||||
column_a = columns[0]
|
||||
column_b = columns[1]
|
||||
original_position_a = column_a.position
|
||||
original_position_b = column_b.position
|
||||
|
||||
post column_right_position_path(column_a), as: :json
|
||||
assert_response :created
|
||||
|
||||
assert_equal original_position_b, column_a.reload.position
|
||||
assert_equal original_position_a, column_b.reload.position
|
||||
end
|
||||
|
||||
test "move right refreshes adjacent columns" do
|
||||
column = columns(:writebook_in_progress)
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ class My::AccessTokensControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
assert_response :created
|
||||
body = @response.parsed_body
|
||||
assert body["id"].present?
|
||||
assert body["token"].present?
|
||||
assert_equal "Fizzy CLI", body["description"]
|
||||
assert_equal "write", body["permission"]
|
||||
assert body["created_at"].present?
|
||||
end
|
||||
|
||||
test "create new token via JSON with bearer token" do
|
||||
@@ -54,6 +56,36 @@ class My::AccessTokensControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
get my_access_tokens_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_kind_of Array, body
|
||||
end
|
||||
|
||||
test "index as JSON with bearer token and no account scope" do
|
||||
sign_out
|
||||
bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" }
|
||||
|
||||
untenanted do
|
||||
get my_access_tokens_path, as: :json, env: bearer_token
|
||||
end
|
||||
|
||||
assert_response :success
|
||||
assert_kind_of Array, @response.parsed_body
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
token = identities(:kevin).access_tokens.create!(description: "To delete", permission: "read")
|
||||
|
||||
assert_difference -> { identities(:kevin).access_tokens.count }, -1 do
|
||||
delete my_access_token_path(token), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "accessing new token after reveal window redirects to index" do
|
||||
assert_changes -> { identities(:kevin).access_tokens.count }, +1 do
|
||||
post my_access_tokens_path, params: { access_token: { description: "GitHub", permission: "read" } }
|
||||
|
||||
@@ -13,6 +13,21 @@ class Notifications::SettingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get notifications_settings_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_equal @user.settings.bundle_email_frequency, @response.parsed_body["bundle_email_frequency"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
assert_changes -> { @user.reload.settings.bundle_email_frequency }, from: "never", to: "every_few_hours" do
|
||||
put notifications_settings_path, params: { user_settings: { bundle_email_frequency: "every_few_hours" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "update email frequency" do
|
||||
assert_changes -> { @user.reload.settings.bundle_email_frequency }, from: "never", to: "every_few_hours" do
|
||||
put notifications_settings_path, params: { user_settings: { bundle_email_frequency: "every_few_hours" } }
|
||||
|
||||
@@ -46,6 +46,36 @@ class SearchesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_select ".search__blank-slate", text: "No matches"
|
||||
end
|
||||
|
||||
test "search as JSON" do
|
||||
get search_path(q: "broken", script_name: "/#{@account.external_account_id}"), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_kind_of Array, body
|
||||
assert_equal 1, body.size
|
||||
assert_equal "Layout is broken", body.first["title"]
|
||||
end
|
||||
|
||||
test "search by card ID as JSON returns array" do
|
||||
get search_path(q: @card.id, script_name: "/#{@account.external_account_id}"), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_kind_of Array, body
|
||||
assert_equal 1, body.size
|
||||
assert_equal @card.id, body.first["id"]
|
||||
end
|
||||
|
||||
test "search as JSON deduplicates cards with multiple search hits" do
|
||||
get search_path(q: "haggis", script_name: "/#{@account.external_account_id}"), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_kind_of Array, body
|
||||
assert_equal 1, body.size
|
||||
assert_equal @comment2_card.id, body.first["id"]
|
||||
end
|
||||
|
||||
test "search highlights matched terms with proper HTML marks" do
|
||||
@board.cards.create!(title: "Testing search highlighting", status: "published", creator: @user)
|
||||
|
||||
|
||||
@@ -49,6 +49,11 @@ class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to users(:david)
|
||||
end
|
||||
|
||||
test "delete self as JSON" do
|
||||
delete user_avatar_path(users(:david)), as: :json
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "unable to delete other" do
|
||||
delete user_avatar_path(users(:kevin))
|
||||
assert_response :forbidden
|
||||
|
||||
@@ -20,6 +20,42 @@ class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
|
||||
|
||||
post user_push_subscriptions_path(users(:david)),
|
||||
params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }, as: :json
|
||||
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
test "create as JSON for duplicate subscription" do
|
||||
subscription_params = { "endpoint" => "https://fcm.googleapis.com/fcm/send/abc123", "p256dh_key" => "123", "auth_key" => "456" }
|
||||
|
||||
users(:david).push_subscriptions.create!(subscription_params)
|
||||
|
||||
assert_no_difference -> { Push::Subscription.count } do
|
||||
post user_push_subscriptions_path(users(:david)),
|
||||
params: { push_subscription: subscription_params }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
subscription = users(:david).push_subscriptions.create!(
|
||||
endpoint: "https://fcm.googleapis.com/fcm/send/abc123",
|
||||
p256dh_key: "123",
|
||||
auth_key: "456"
|
||||
)
|
||||
|
||||
assert_difference -> { Push::Subscription.count }, -1 do
|
||||
delete user_push_subscription_path(users(:david), subscription), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "destroy a push subscription" do
|
||||
subscription = users(:david).push_subscriptions.create!(
|
||||
endpoint: "https://fcm.googleapis.com/fcm/send/abc123",
|
||||
|
||||
@@ -14,6 +14,13 @@ class Users::RolesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert users(:david).reload.admin?
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
put user_role_path(users(:david)), params: { user: { role: "admin" } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert users(:david).reload.admin?
|
||||
end
|
||||
|
||||
test "can't promote to special roles" do
|
||||
assert_no_changes -> { users(:david).reload.role } do
|
||||
put user_role_path(users(:david)), params: { user: { role: "system" } }
|
||||
|
||||
Reference in New Issue
Block a user