Skip to content

Commit 8e251cc

Browse files
authored
Merge pull request #26 from nativeapptemplate/refactor/extract-shared-concerns
Extract shared error helpers and add comprehensive test coverage
2 parents f21b10c + 42090da commit 8e251cc

16 files changed

Lines changed: 347 additions & 74 deletions

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,8 @@ bin/rails test
172172
### Adding Background Jobs
173173
1. Create job class in `app/jobs/`
174174
2. Specify queue with `queue_as :default` (or :critical, :low, etc.)
175-
3. Call with `MyJob.perform_later(args)`
175+
3. Call with `MyJob.perform_later(args)`
176+
177+
## Testing Policy
178+
179+
Create test passing all of path including unhappy path. Creating and updating that test is must.

app/controllers/api/v1/shopkeeper/account/passwords_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ def update
55
if current_shopkeeper.update_with_password(password_params)
66
render json: {status: 200}, status: :ok
77
else
8-
render json: {code: 422, error_message: current_shopkeeper.errors.full_messages.to_sentence}, status: :unprocessable_entity
8+
render_validation_error(current_shopkeeper)
99
end
1010
end
1111

app/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create
2525
if accounts_invitation.save_and_send_invite
2626
render json: AccountsInvitationSerializer.new(accounts_invitation).serializable_hash, status: :created
2727
else
28-
render json: {code: 422, error_message: accounts_invitation.errors.full_messages.to_sentence}, status: :unprocessable_entity
28+
render_validation_error(accounts_invitation)
2929
end
3030
end
3131

@@ -35,7 +35,7 @@ def update
3535
if @accounts_invitation.update(invitation_params_update)
3636
render json: AccountsInvitationSerializer.new(@accounts_invitation).serializable_hash
3737
else
38-
render json: {code: 422, error_message: @accounts_invitation.errors.full_messages.to_sentence}, status: :unprocessable_entity
38+
render_validation_error(@accounts_invitation)
3939
end
4040
end
4141

app/controllers/api/v1/shopkeeper/accounts_controller.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def create
4646

4747
render json: AccountSerializer.new(account, options).serializable_hash, status: :created
4848
else
49-
render json: {code: 422, error_message: account.errors.full_messages.to_sentence}, status: :unprocessable_entity
49+
render_validation_error(account)
5050
end
5151
end
5252

@@ -60,7 +60,7 @@ def update
6060
}
6161
render json: AccountSerializer.new(@account, options).serializable_hash
6262
else
63-
render json: {code: 422, error_message: @account.errors.full_messages.to_sentence}, status: :unprocessable_entity
63+
render_validation_error(@account)
6464
end
6565
end
6666

@@ -98,6 +98,6 @@ def pundit_user
9898
def prevent_personal_account_deletion
9999
return unless @account.personal?
100100

101-
render json: {code: 422, error_message: I18n.t("api.shopkeeper.accounts.personal.cannot_delete")}, status: :unprocessable_entity
101+
render_error(code: 422, message: I18n.t("api.shopkeeper.accounts.personal.cannot_delete"), status: :unprocessable_entity)
102102
end
103103
end

app/controllers/api/v1/shopkeeper/accounts_invitations_controller.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ def show
55
authorize @accounts_invitation, :show_by_token?
66

77
if @accounts_invitation.expired?
8-
render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone
9-
return
8+
return render_error(code: 410, message: I18n.t("api.shopkeeper.accounts_invitations.expired"), status: :gone)
109
end
1110

1211
options = {}
@@ -18,15 +17,14 @@ def update
1817
authorize @accounts_invitation, :accept?
1918

2019
if @accounts_invitation.expired?
21-
render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone
22-
return
20+
return render_error(code: 410, message: I18n.t("api.shopkeeper.accounts_invitations.expired"), status: :gone)
2321
end
2422

2523
if @accounts_invitation.accept!(current_shopkeeper)
2624
render json: {status: 200}, status: :ok
2725
else
2826
error_message = @accounts_invitation.errors.full_messages.first || I18n.t("something_went_wrong")
29-
render json: {code: 422, error_message: error_message}, status: :unprocessable_entity
27+
render_error(code: 422, message: error_message, status: :unprocessable_entity)
3028
end
3129
end
3230

@@ -42,6 +40,6 @@ def destroy
4240
def set_accounts_invitation
4341
@accounts_invitation = AccountsInvitation.find_by!(token: params[:id])
4442
rescue ActiveRecord::RecordNotFound
45-
render json: {code: 404, error_message: I18n.t("api.shopkeeper.accounts_invitations.not_found")}, status: :not_found
43+
render_error(code: 404, message: I18n.t("api.shopkeeper.accounts_invitations.not_found"), status: :not_found)
4644
end
4745
end

app/controllers/api/v1/shopkeeper/accounts_shopkeepers_controller.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def update
3636

3737
render json: AccountsShopkeeperSerializer.new(@accounts_shopkeeper, options).serializable_hash
3838
else
39-
render json: {code: 422, error_message: @accounts_shopkeeper.errors.full_messages.to_sentence}, status: :unprocessable_entity
39+
render_validation_error(@accounts_shopkeeper)
4040
end
4141
end
4242

@@ -70,12 +70,12 @@ def accounts_shopkeeper_params
7070
def require_non_personal_account!
7171
return unless @account.personal?
7272

73-
render json: {code: 422, error_message: I18n.t("api.shopkeeper.accounts_shopkeepers.require_non_personal_account")}, status: :unprocessable_entity
73+
render_error(code: 422, message: I18n.t("api.shopkeeper.accounts_shopkeepers.require_non_personal_account"), status: :unprocessable_entity)
7474
end
7575

7676
def safeguard_account_owner_deletion!
7777
return unless @accounts_shopkeeper.account_owner?
7878

79-
render json: {code: 401, error_message: I18n.t("unauthorized")}, status: :unauthorized
79+
render_error(code: 401, message: I18n.t("unauthorized"), status: :unauthorized)
8080
end
8181
end

app/controllers/api/v1/shopkeeper/base_controller.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ def authorize(record, query = nil)
2323

2424
private
2525

26+
def render_validation_error(record)
27+
render json: {code: 422, error_message: record.errors.full_messages.to_sentence}, status: :unprocessable_entity
28+
end
29+
30+
def render_error(code:, message:, status:)
31+
render json: {code: code, error_message: message}, status: status
32+
end
33+
2634
def user_not_authorized
27-
render json: {code: 401, error_message: I18n.t("unauthorized")}, status: :unauthorized
35+
render_error(code: 401, message: I18n.t("unauthorized"), status: :unauthorized)
2836
end
2937
end

app/controllers/api/v1/shopkeeper/item_tags_controller.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create
2525
if item_tag.save
2626
render json: ItemTagSerializer.new(item_tag).serializable_hash, status: :created
2727
else
28-
render json: {code: 422, error_message: item_tag.errors.full_messages.to_sentence}, status: :unprocessable_entity
28+
render_validation_error(item_tag)
2929
end
3030
end
3131

@@ -35,7 +35,7 @@ def update
3535
if @item_tag.update(item_tag_params)
3636
render json: ItemTagSerializer.new(@item_tag).serializable_hash
3737
else
38-
render json: {code: 422, error_message: @item_tag.errors.full_messages.to_sentence}, status: :unprocessable_entity
38+
render_validation_error(@item_tag)
3939
end
4040
end
4141

@@ -83,7 +83,7 @@ def set_shop
8383
def set_item_tag
8484
@item_tag = current_shopkeeper.item_tags.find(params[:id])
8585
rescue ActiveRecord::RecordNotFound
86-
render json: {code: 404, error_message: I18n.t("api.shopkeeper.item_tags.not_found")}, status: :not_found
86+
render_error(code: 404, message: I18n.t("api.shopkeeper.item_tags.not_found"), status: :not_found)
8787
end
8888

8989
def item_tag_params

app/controllers/api/v1/shopkeeper/shops_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def create
3232
if shop.save
3333
render json: ShopSerializer.new(shop).serializable_hash, status: :created
3434
else
35-
render json: {code: 422, error_message: shop.errors.full_messages.to_sentence}, status: :unprocessable_entity
35+
render_validation_error(shop)
3636
end
3737
end
3838

@@ -42,7 +42,7 @@ def update
4242
if @shop.update(shop_params_update)
4343
render json: ShopSerializer.new(@shop).serializable_hash
4444
else
45-
render json: {code: 422, error_message: @shop.errors.full_messages.to_sentence}, status: :unprocessable_entity
45+
render_validation_error(@shop)
4646
end
4747
end
4848

test/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller_test.rb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class Api::V1::Shopkeeper::Accounts::AccountsInvitationsControllerTest < ActionD
6969
end
7070

7171
assert_response :unprocessable_entity
72+
assert_equal 422, response.parsed_body["code"]
73+
assert response.parsed_body["error_message"].present?
7274
end
7375

7476
test "create requires admin role" do
@@ -118,6 +120,8 @@ class Api::V1::Shopkeeper::Accounts::AccountsInvitationsControllerTest < ActionD
118120
headers: @shopkeeper.create_new_auth_token
119121

120122
assert_response :unprocessable_entity
123+
assert_equal 422, response.parsed_body["code"]
124+
assert response.parsed_body["error_message"].present?
121125
end
122126

123127
test "update requires admin role" do
@@ -158,16 +162,23 @@ class Api::V1::Shopkeeper::Accounts::AccountsInvitationsControllerTest < ActionD
158162
assert_response :unauthorized
159163
end
160164

161-
test "resend sends invitation email again and touches created_at" do
162-
original_created_at = @invitation.created_at
165+
test "resend sends invitation email again" do
166+
post resend_api_v1_shopkeeper_account_accounts_invitation_path(@account, @invitation.token),
167+
headers: @shopkeeper.create_new_auth_token
163168

164-
travel_to(1.hour.from_now) do
165-
post resend_api_v1_shopkeeper_account_accounts_invitation_path(@account, @invitation.token),
166-
headers: @shopkeeper.create_new_auth_token
169+
assert_response :success
170+
assert_enqueued_emails 1
171+
end
167172

168-
assert_response :success
169-
assert @invitation.reload.created_at > original_created_at
170-
end
173+
test "resend resets expiration for expired invitation" do
174+
@invitation.update_column(:created_at, (AccountsInvitation::EXPIRES_IN + 1.minute).ago)
175+
assert @invitation.expired?
176+
177+
post resend_api_v1_shopkeeper_account_accounts_invitation_path(@account, @invitation.token),
178+
headers: @shopkeeper.create_new_auth_token
179+
180+
assert_response :success
181+
assert_not @invitation.reload.expired?
171182
end
172183

173184
test "resend requires admin role" do

0 commit comments

Comments
 (0)