From 070f3efa4222cc51a844fc974b50fcea21e4d54b Mon Sep 17 00:00:00 2001 From: dadachi Date: Sat, 14 Mar 2026 09:03:37 +0900 Subject: [PATCH 1/2] Add Pundit policies for controllers that skipped verify_authorized Replace skip_after_action :verify_authorized and manual before_action auth checks with proper Pundit authorize calls in AccountsController, AccountsShopkeepersController, Accounts::AccountsInvitationsController, AccountsInvitationsController, MeController, and PasswordsController. Co-Authored-By: Claude Opus 4.6 --- .../account/passwords_controller.rb | 4 +- .../accounts_invitations_controller.rb | 25 ++-- .../api/v1/shopkeeper/accounts_controller.rb | 34 ++--- .../accounts_invitations_controller.rb | 7 +- .../accounts_shopkeepers_controller.rb | 21 ++-- .../api/v1/shopkeeper/me_controller.rb | 5 +- app/policies/api/shopkeeper/account_policy.rb | 23 ++++ .../shopkeeper/accounts_invitation_policy.rb | 40 ++++++ .../shopkeeper/accounts_shopkeeper_policy.rb | 19 +++ app/policies/api/shopkeeper/me_policy.rb | 9 ++ .../api/shopkeeper/password_policy.rb | 5 + .../v1/shopkeeper/accounts_controller_test.rb | 89 ++++++++++++- .../accounts_invitations_controller_test.rb | 22 ++++ .../api/shopkeeper/account_policy_test.rb | 67 ++++++++++ .../accounts_invitation_policy_test.rb | 119 ++++++++++++++++++ .../accounts_shopkeeper_policy_test.rb | 67 ++++++++++ .../policies/api/shopkeeper/me_policy_test.rb | 20 +++ .../api/shopkeeper/password_policy_test.rb | 15 +++ 18 files changed, 548 insertions(+), 43 deletions(-) create mode 100644 app/policies/api/shopkeeper/account_policy.rb create mode 100644 app/policies/api/shopkeeper/accounts_invitation_policy.rb create mode 100644 app/policies/api/shopkeeper/accounts_shopkeeper_policy.rb create mode 100644 app/policies/api/shopkeeper/me_policy.rb create mode 100644 app/policies/api/shopkeeper/password_policy.rb create mode 100644 test/policies/api/shopkeeper/account_policy_test.rb create mode 100644 test/policies/api/shopkeeper/accounts_invitation_policy_test.rb create mode 100644 test/policies/api/shopkeeper/accounts_shopkeeper_policy_test.rb create mode 100644 test/policies/api/shopkeeper/me_policy_test.rb create mode 100644 test/policies/api/shopkeeper/password_policy_test.rb diff --git a/app/controllers/api/v1/shopkeeper/account/passwords_controller.rb b/app/controllers/api/v1/shopkeeper/account/passwords_controller.rb index 1a0f193..ffea499 100644 --- a/app/controllers/api/v1/shopkeeper/account/passwords_controller.rb +++ b/app/controllers/api/v1/shopkeeper/account/passwords_controller.rb @@ -1,7 +1,7 @@ class Api::V1::Shopkeeper::Account::PasswordsController < Api::V1::Shopkeeper::BaseController - skip_after_action :verify_authorized - def update + authorize :password + if current_shopkeeper.update_with_password(password_params) render json: {status: 200}, status: :ok else diff --git a/app/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller.rb b/app/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller.rb index b465242..5b878da 100644 --- a/app/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller.rb +++ b/app/controllers/api/v1/shopkeeper/accounts/accounts_invitations_controller.rb @@ -1,21 +1,25 @@ class Api::V1::Shopkeeper::Accounts::AccountsInvitationsController < Api::V1::Shopkeeper::BaseController before_action :set_account - before_action :require_account_admin, except: %i[index show] before_action :set_accounts_invitation, only: %i[show update destroy resend] - skip_after_action :verify_authorized def index + authorize AccountsInvitation + @accounts_invitations = @account.accounts_invitations.order(name: :asc) render json: AccountsInvitationSerializer.new(@accounts_invitations).serializable_hash end def show + authorize @accounts_invitation + options = {} options[:include] = [:account, :invited_by] render json: AccountsInvitationSerializer.new(@accounts_invitation, options).serializable_hash end def create + authorize AccountsInvitation + accounts_invitation = @account.accounts_invitations.build(invitation_params_create) if accounts_invitation.save_and_send_invite @@ -26,6 +30,8 @@ def create end def update + authorize @accounts_invitation + if @accounts_invitation.update(invitation_params_update) render json: AccountsInvitationSerializer.new(@accounts_invitation).serializable_hash else @@ -34,17 +40,25 @@ def update end def destroy + authorize @accounts_invitation + @accounts_invitation.destroy render json: {status: 200}, status: :ok end def resend + authorize @accounts_invitation + @accounts_invitation.resend_invite render json: {status: 200}, status: :ok end private + def pundit_user + @account.accounts_shopkeepers.find_by!(shopkeeper: current_shopkeeper) + end + def set_account @account = current_shopkeeper.accounts.find(params[:account_id]) end @@ -65,11 +79,4 @@ def invitation_params_update .require(:accounts_invitation) .permit(:name, AccountsShopkeeper::ROLES) end - - def require_account_admin - accounts_shopkeeper = @account.accounts_shopkeepers.find_by(shopkeeper: current_shopkeeper) - return if accounts_shopkeeper&.admin? - - render json: {code: 401, error_message: I18n.t("api.shopkeeper.accounts.admin_required")}, status: :unauthorized - end end diff --git a/app/controllers/api/v1/shopkeeper/accounts_controller.rb b/app/controllers/api/v1/shopkeeper/accounts_controller.rb index ed2e196..15c8c6a 100644 --- a/app/controllers/api/v1/shopkeeper/accounts_controller.rb +++ b/app/controllers/api/v1/shopkeeper/accounts_controller.rb @@ -1,12 +1,11 @@ class Api::V1::Shopkeeper::AccountsController < Api::V1::Shopkeeper::BaseController before_action :set_account, only: %i[show update destroy] - before_action :require_account_admin, only: %i[update] - before_action :require_account_owner, only: %i[destroy] before_action :prevent_personal_account_deletion, only: %i[destroy] - skip_after_action :verify_authorized # GET /accounts def index + authorize Account + accounts = current_shopkeeper.accounts.sorted options = { params: {current_shopkeeper: current_shopkeeper} @@ -24,6 +23,8 @@ def index # GET /accounts/1 def show + authorize @account + options = { include: [:accounts_shopkeepers, :accounts_invitations], params: {current_shopkeeper: current_shopkeeper} @@ -33,6 +34,8 @@ def show # POST /accounts def create + authorize Account + account = Account.new(account_params.merge(owner: current_shopkeeper)) account.accounts_shopkeepers.new(shopkeeper: current_shopkeeper, admin: true) @@ -49,6 +52,8 @@ def create # PATCH/PUT /accounts/1 def update + authorize @account + if @account.update(account_params) options = { params: {current_shopkeeper: current_shopkeeper} @@ -61,6 +66,8 @@ def update # DELETE /accounts/1 def destroy + authorize @account + ActsAsTenant.without_tenant do @account.destroy end @@ -80,22 +87,17 @@ def account_params params.require(:account).permit(:name) end + def pundit_user + if @account + @account.accounts_shopkeepers.find_by!(shopkeeper: current_shopkeeper) + else + super + end + end + def prevent_personal_account_deletion return unless @account.personal? render json: {code: 422, error_message: I18n.t("api.shopkeeper.accounts.personal.cannot_delete")}, status: :unprocessable_entity end - - def require_account_admin - accounts_shopkeeper = @account.accounts_shopkeepers.find_by(shopkeeper: current_shopkeeper) - return if accounts_shopkeeper&.admin? - - render json: {code: 401, error_message: I18n.t("api.shopkeeper.accounts.admin_required")}, status: :unauthorized - end - - def require_account_owner - return if @account.owner?(current_shopkeeper) - - render json: {code: 401, error_message: I18n.t("api.shopkeeper.accounts.owner_required")}, status: :unauthorized - end end diff --git a/app/controllers/api/v1/shopkeeper/accounts_invitations_controller.rb b/app/controllers/api/v1/shopkeeper/accounts_invitations_controller.rb index c8b15d8..59e5f21 100644 --- a/app/controllers/api/v1/shopkeeper/accounts_invitations_controller.rb +++ b/app/controllers/api/v1/shopkeeper/accounts_invitations_controller.rb @@ -1,8 +1,9 @@ class Api::V1::Shopkeeper::AccountsInvitationsController < Api::V1::Shopkeeper::BaseController before_action :set_accounts_invitation - skip_after_action :verify_authorized def show + authorize @accounts_invitation, :show_by_token? + if @accounts_invitation.expired? render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone return @@ -14,6 +15,8 @@ def show end def update + authorize @accounts_invitation, :accept? + if @accounts_invitation.expired? render json: {code: 410, error_message: I18n.t("api.shopkeeper.accounts_invitations.expired")}, status: :gone return @@ -28,6 +31,8 @@ def update end def destroy + authorize @accounts_invitation, :reject? + @accounts_invitation.reject! render json: {status: 200}, status: :ok end diff --git a/app/controllers/api/v1/shopkeeper/accounts_shopkeepers_controller.rb b/app/controllers/api/v1/shopkeeper/accounts_shopkeepers_controller.rb index 4b38372..3763dff 100644 --- a/app/controllers/api/v1/shopkeeper/accounts_shopkeepers_controller.rb +++ b/app/controllers/api/v1/shopkeeper/accounts_shopkeepers_controller.rb @@ -2,11 +2,11 @@ class Api::V1::Shopkeeper::AccountsShopkeepersController < Api::V1::Shopkeeper:: before_action :set_account before_action :require_non_personal_account!, only: %i[show update destroy] before_action :set_accounts_shopkeeper, only: %i[show update destroy] - before_action :require_account_admin, except: %i[index show] before_action :safeguard_account_owner_deletion!, only: %i[destroy] - skip_after_action :verify_authorized def index + authorize AccountsShopkeeper + if @account.personal? render json: AccountsShopkeeperSerializer.new([]).serializable_hash and return end @@ -19,6 +19,8 @@ def index end def show + authorize @accounts_shopkeeper + options = {} options[:include] = [:account, :shopkeeper] @@ -26,6 +28,8 @@ def show end def update + authorize @accounts_shopkeeper + if @accounts_shopkeeper.update(accounts_shopkeeper_params) options = {} options[:include] = [:account, :shopkeeper] @@ -37,12 +41,18 @@ def update end def destroy + authorize @accounts_shopkeeper + @accounts_shopkeeper.destroy render json: {status: 200}, status: :ok end private + def pundit_user + @account.accounts_shopkeepers.find_by!(shopkeeper: current_shopkeeper) + end + def set_account @account = current_shopkeeper.accounts.find(params[:account_id]) end @@ -68,11 +78,4 @@ def safeguard_account_owner_deletion! render json: {code: 401, error_message: I18n.t("unauthorized")}, status: :unauthorized end - - def require_account_admin - accounts_shopkeeper = @account.accounts_shopkeepers.find_by(shopkeeper: current_shopkeeper) - return if accounts_shopkeeper&.admin? - - render json: {code: 401, error_message: I18n.t("api.shopkeeper.accounts.admin_required")}, status: :unauthorized - end end diff --git a/app/controllers/api/v1/shopkeeper/me_controller.rb b/app/controllers/api/v1/shopkeeper/me_controller.rb index 54f754f..a0cf7fb 100644 --- a/app/controllers/api/v1/shopkeeper/me_controller.rb +++ b/app/controllers/api/v1/shopkeeper/me_controller.rb @@ -1,14 +1,17 @@ class Api::V1::Shopkeeper::MeController < Api::V1::Shopkeeper::BaseController before_action :set_shopkeeper, only: %i[update_confirmed_privacy_version update_confirmed_terms_version] - skip_after_action :verify_authorized def update_confirmed_privacy_version + authorize :me + @shopkeeper.confirmed_privacy_version = PrivacyVersion.current_version @shopkeeper.save!(validate: false) render json: {status: 200}, status: :ok end def update_confirmed_terms_version + authorize :me + @shopkeeper.confirmed_terms_version = TermsVersion.current_version @shopkeeper.save!(validate: false) render json: {status: 200}, status: :ok diff --git a/app/policies/api/shopkeeper/account_policy.rb b/app/policies/api/shopkeeper/account_policy.rb new file mode 100644 index 0000000..f0318ac --- /dev/null +++ b/app/policies/api/shopkeeper/account_policy.rb @@ -0,0 +1,23 @@ +class Api::Shopkeeper::AccountPolicy < Api::Shopkeeper::BasePolicy + include Api::Shopkeeper::Concerns::Authorization + + def index? + true + end + + def show? + true + end + + def create? + true + end + + def update? + admin? + end + + def destroy? + owner? + end +end diff --git a/app/policies/api/shopkeeper/accounts_invitation_policy.rb b/app/policies/api/shopkeeper/accounts_invitation_policy.rb new file mode 100644 index 0000000..e1920f8 --- /dev/null +++ b/app/policies/api/shopkeeper/accounts_invitation_policy.rb @@ -0,0 +1,40 @@ +class Api::Shopkeeper::AccountsInvitationPolicy < Api::Shopkeeper::BasePolicy + include Api::Shopkeeper::Concerns::Authorization + + def index? + true + end + + def show? + true + end + + def create? + admin? + end + + def update? + admin? + end + + def destroy? + admin? + end + + def resend? + admin? + end + + # Token-based actions (any authenticated shopkeeper with the token) + def show_by_token? + true + end + + def accept? + true + end + + def reject? + true + end +end diff --git a/app/policies/api/shopkeeper/accounts_shopkeeper_policy.rb b/app/policies/api/shopkeeper/accounts_shopkeeper_policy.rb new file mode 100644 index 0000000..74c80d1 --- /dev/null +++ b/app/policies/api/shopkeeper/accounts_shopkeeper_policy.rb @@ -0,0 +1,19 @@ +class Api::Shopkeeper::AccountsShopkeeperPolicy < Api::Shopkeeper::BasePolicy + include Api::Shopkeeper::Concerns::Authorization + + def index? + true + end + + def show? + true + end + + def update? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/policies/api/shopkeeper/me_policy.rb b/app/policies/api/shopkeeper/me_policy.rb new file mode 100644 index 0000000..c9f594c --- /dev/null +++ b/app/policies/api/shopkeeper/me_policy.rb @@ -0,0 +1,9 @@ +class Api::Shopkeeper::MePolicy < Api::Shopkeeper::BasePolicy + def update_confirmed_privacy_version? + true + end + + def update_confirmed_terms_version? + true + end +end diff --git a/app/policies/api/shopkeeper/password_policy.rb b/app/policies/api/shopkeeper/password_policy.rb new file mode 100644 index 0000000..cc29cdb --- /dev/null +++ b/app/policies/api/shopkeeper/password_policy.rb @@ -0,0 +1,5 @@ +class Api::Shopkeeper::PasswordPolicy < Api::Shopkeeper::BasePolicy + def update? + true + end +end diff --git a/test/controllers/api/v1/shopkeeper/accounts_controller_test.rb b/test/controllers/api/v1/shopkeeper/accounts_controller_test.rb index ff8c7a7..416fb54 100644 --- a/test/controllers/api/v1/shopkeeper/accounts_controller_test.rb +++ b/test/controllers/api/v1/shopkeeper/accounts_controller_test.rb @@ -1,12 +1,91 @@ require "test_helper" class Api::V1::Shopkeeper::AccountsControllerTest < ActionDispatch::IntegrationTest - test "returns current shopkeeper accounts" do - shopkeeper = shopkeepers(:one) - shopkeeper.create_default_account + setup do + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first - get api_v1_shopkeeper_accounts_url, headers: shopkeeper.create_new_auth_token + @team_account = Account.create!(name: "Team Account", owner: @shopkeeper, personal: false) + AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: @shopkeeper, + admin: true + ) + end + + test "index returns current shopkeeper accounts" do + get api_v1_shopkeeper_accounts_url, headers: @shopkeeper.create_new_auth_token assert_response :success - assert_includes response.parsed_body["data"].map { |t| t["attributes"]["name"] }, shopkeeper.accounts.first.name + assert_includes response.parsed_body["data"].map { |t| t["attributes"]["name"] }, @account.name + end + + test "show returns account details" do + get api_v1_shopkeeper_account_url(@team_account), headers: @shopkeeper.create_new_auth_token + assert_response :success + assert_equal @team_account.id.to_s, response.parsed_body["data"]["id"] + end + + test "create creates a new account" do + assert_difference "Account.count", 1 do + post api_v1_shopkeeper_accounts_url, + params: {account: {name: "New Account"}}, + headers: @shopkeeper.create_new_auth_token + end + + assert_response :created + end + + test "update requires admin role" do + other_shopkeeper = shopkeepers(:two) + AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + patch api_v1_shopkeeper_account_url(@team_account), + params: {account: {name: "Updated"}}, + headers: other_shopkeeper.create_new_auth_token + + assert_response :unauthorized + end + + test "update succeeds for admin" do + patch api_v1_shopkeeper_account_url(@team_account), + params: {account: {name: "Updated Name"}}, + headers: @shopkeeper.create_new_auth_token + + assert_response :success + assert_equal "Updated Name", @team_account.reload.name + end + + test "destroy requires owner" do + other_shopkeeper = shopkeepers(:two) + AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: other_shopkeeper, + admin: true + ) + + delete api_v1_shopkeeper_account_url(@team_account), + headers: other_shopkeeper.create_new_auth_token + + assert_response :unauthorized + end + + test "destroy succeeds for owner" do + assert_difference "Account.count", -1 do + delete api_v1_shopkeeper_account_url(@team_account), + headers: @shopkeeper.create_new_auth_token + end + + assert_response :success + end + + test "requires authentication" do + get api_v1_shopkeeper_accounts_url + + assert_response :unauthorized end end diff --git a/test/controllers/api/v1/shopkeeper/accounts_invitations_controller_test.rb b/test/controllers/api/v1/shopkeeper/accounts_invitations_controller_test.rb index 2046897..f1cd44e 100644 --- a/test/controllers/api/v1/shopkeeper/accounts_invitations_controller_test.rb +++ b/test/controllers/api/v1/shopkeeper/accounts_invitations_controller_test.rb @@ -90,6 +90,28 @@ class Api::V1::Shopkeeper::AccountsInvitationsControllerTest < ActionDispatch::I assert_equal I18n.t("api.shopkeeper.accounts_invitations.expired"), response.parsed_body["error_message"] end + test "show is accessible by any authenticated shopkeeper" do + other_shopkeeper = shopkeepers(:two) + other_shopkeeper.create_default_account + + get api_v1_shopkeeper_accounts_invitation_url(@invitation.token), + headers: other_shopkeeper.create_new_auth_token + + assert_response :success + end + + test "destroy is accessible by any authenticated shopkeeper" do + other_shopkeeper = shopkeepers(:two) + other_shopkeeper.create_default_account + + assert_difference "AccountsInvitation.count", -1 do + delete api_v1_shopkeeper_accounts_invitation_url(@invitation.token), + headers: other_shopkeeper.create_new_auth_token + end + + assert_response :success + end + test "requires authentication" do get api_v1_shopkeeper_accounts_invitation_url(@invitation.token) diff --git a/test/policies/api/shopkeeper/account_policy_test.rb b/test/policies/api/shopkeeper/account_policy_test.rb new file mode 100644 index 0000000..c80e66e --- /dev/null +++ b/test/policies/api/shopkeeper/account_policy_test.rb @@ -0,0 +1,67 @@ +require "test_helper" + +class Api::Shopkeeper::AccountPolicyTest < ActiveSupport::TestCase + def setup + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first + end + + test "index? returns true for all users" do + accounts_shopkeeper = @account.accounts_shopkeepers.first + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert policy.index? + end + + test "show? returns true for all users" do + accounts_shopkeeper = @account.accounts_shopkeepers.first + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert policy.show? + end + + test "create? returns true for all users" do + accounts_shopkeeper = @account.accounts_shopkeepers.first + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert policy.create? + end + + test "update? returns true for admin" do + accounts_shopkeeper = @account.accounts_shopkeepers.first + accounts_shopkeeper.update!(admin: true) + + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert policy.update? + end + + test "update? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + accounts_shopkeeper = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + senior_manager: true + ) + + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert_not policy.update? + end + + test "destroy? returns true for account owner" do + accounts_shopkeeper = @account.accounts_shopkeepers.first + assert accounts_shopkeeper.account_owner? + + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert policy.destroy? + end + + test "destroy? returns false for non-owner" do + other_shopkeeper = shopkeepers(:two) + accounts_shopkeeper = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + admin: true + ) + + policy = Api::Shopkeeper::AccountPolicy.new(accounts_shopkeeper, @account) + assert_not policy.destroy? + end +end diff --git a/test/policies/api/shopkeeper/accounts_invitation_policy_test.rb b/test/policies/api/shopkeeper/accounts_invitation_policy_test.rb new file mode 100644 index 0000000..41af5b3 --- /dev/null +++ b/test/policies/api/shopkeeper/accounts_invitation_policy_test.rb @@ -0,0 +1,119 @@ +require "test_helper" + +class Api::Shopkeeper::AccountsInvitationPolicyTest < ActiveSupport::TestCase + def setup + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first + @accounts_shopkeeper = @account.accounts_shopkeepers.first + + @invitation = AccountsInvitation.create!( + account: @account, + name: "Invited User", + email: "invited@example.com", + invited_by: @shopkeeper, + junior_member: true + ) + end + + test "index? returns true for all users" do + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.index? + end + + test "show? returns true for all users" do + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.show? + end + + test "create? returns true for admin" do + @accounts_shopkeeper.update!(admin: true) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.create? + end + + test "create? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(member, @invitation) + assert_not policy.create? + end + + test "update? returns true for admin" do + @accounts_shopkeeper.update!(admin: true) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.update? + end + + test "update? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(member, @invitation) + assert_not policy.update? + end + + test "destroy? returns true for admin" do + @accounts_shopkeeper.update!(admin: true) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.destroy? + end + + test "destroy? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(member, @invitation) + assert_not policy.destroy? + end + + test "resend? returns true for admin" do + @accounts_shopkeeper.update!(admin: true) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.resend? + end + + test "resend? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(member, @invitation) + assert_not policy.resend? + end + + test "show_by_token? returns true for all users" do + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.show_by_token? + end + + test "accept? returns true for all users" do + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.accept? + end + + test "reject? returns true for all users" do + policy = Api::Shopkeeper::AccountsInvitationPolicy.new(@accounts_shopkeeper, @invitation) + assert policy.reject? + end +end diff --git a/test/policies/api/shopkeeper/accounts_shopkeeper_policy_test.rb b/test/policies/api/shopkeeper/accounts_shopkeeper_policy_test.rb new file mode 100644 index 0000000..1b4eac1 --- /dev/null +++ b/test/policies/api/shopkeeper/accounts_shopkeeper_policy_test.rb @@ -0,0 +1,67 @@ +require "test_helper" + +class Api::Shopkeeper::AccountsShopkeeperPolicyTest < ActiveSupport::TestCase + def setup + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first + + @team_account = Account.create!(name: "Team Account", owner: @shopkeeper, personal: false) + @team_accounts_shopkeeper = AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: @shopkeeper, + admin: true + ) + end + + test "index? returns true for all users" do + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(@team_accounts_shopkeeper, @team_accounts_shopkeeper) + assert policy.index? + end + + test "show? returns true for all users" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(member, @team_accounts_shopkeeper) + assert policy.show? + end + + test "update? returns true for admin" do + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(@team_accounts_shopkeeper, @team_accounts_shopkeeper) + assert policy.update? + end + + test "update? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(member, @team_accounts_shopkeeper) + assert_not policy.update? + end + + test "destroy? returns true for admin" do + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(@team_accounts_shopkeeper, @team_accounts_shopkeeper) + assert policy.destroy? + end + + test "destroy? returns false for non-admin" do + other_shopkeeper = shopkeepers(:two) + member = AccountsShopkeeper.create!( + account: @team_account, + shopkeeper: other_shopkeeper, + junior_member: true + ) + + policy = Api::Shopkeeper::AccountsShopkeeperPolicy.new(member, @team_accounts_shopkeeper) + assert_not policy.destroy? + end +end diff --git a/test/policies/api/shopkeeper/me_policy_test.rb b/test/policies/api/shopkeeper/me_policy_test.rb new file mode 100644 index 0000000..1df2664 --- /dev/null +++ b/test/policies/api/shopkeeper/me_policy_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class Api::Shopkeeper::MePolicyTest < ActiveSupport::TestCase + def setup + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first + @accounts_shopkeeper = @account.accounts_shopkeepers.first + end + + test "update_confirmed_privacy_version? returns true" do + policy = Api::Shopkeeper::MePolicy.new(@accounts_shopkeeper, :me) + assert policy.update_confirmed_privacy_version? + end + + test "update_confirmed_terms_version? returns true" do + policy = Api::Shopkeeper::MePolicy.new(@accounts_shopkeeper, :me) + assert policy.update_confirmed_terms_version? + end +end diff --git a/test/policies/api/shopkeeper/password_policy_test.rb b/test/policies/api/shopkeeper/password_policy_test.rb new file mode 100644 index 0000000..2451672 --- /dev/null +++ b/test/policies/api/shopkeeper/password_policy_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class Api::Shopkeeper::PasswordPolicyTest < ActiveSupport::TestCase + def setup + @shopkeeper = shopkeepers(:one) + @shopkeeper.create_default_account + @account = @shopkeeper.accounts.first + @accounts_shopkeeper = @account.accounts_shopkeepers.first + end + + test "update? returns true" do + policy = Api::Shopkeeper::PasswordPolicy.new(@accounts_shopkeeper, :password) + assert policy.update? + end +end From 7de815de3b0d0df762115eaefed7f6c3d4513284 Mon Sep 17 00:00:00 2001 From: dadachi Date: Sat, 14 Mar 2026 09:13:15 +0900 Subject: [PATCH 2/2] Change Render.com previews plan from starter to standard Co-Authored-By: Claude Opus 4.6 --- render.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.yaml b/render.yaml index 744b470..9aaf2c8 100644 --- a/render.yaml +++ b/render.yaml @@ -8,7 +8,7 @@ services: name: nativeapptemplateapi plan: standard previews: - plan: starter + plan: standard env: ruby region: singapore # the region must be consistent across all services for the internal keys to be read buildCommand: "./bin/render-build.sh"