From 9f87f01fe85890604cd21c6e3e3d83cbd1f0ce2b Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 17:44:04 -0300 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20adiciona=20associa=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20usu=C3=A1rios=20aos=20recursos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 2 +- app/controllers/api/v1/groups_controller.rb | 2 +- app/controllers/api/v1/terrains_controller.rb | 2 +- app/models/feed.rb | 2 ++ app/models/group.rb | 2 ++ app/models/terrain.rb | 2 ++ app/models/user.rb | 3 +++ app/services/feeds/create.rb | 4 ++-- app/services/groups/create.rb | 4 ++-- app/services/terrains/create.rb | 6 +++--- ...20261024160000_add_user_to_domain_resources.rb | 7 +++++++ db/schema.rb | 11 ++++++++++- spec/factories/feeds.rb | 1 + spec/factories/groups.rb | 1 + spec/factories/occupations.rb | 2 +- spec/factories/terrains.rb | 1 + spec/models/feed_spec.rb | 15 +++++++++++++++ spec/models/group_spec.rb | 15 +++++++++++++++ spec/models/terrain_spec.rb | 15 +++++++++++++++ spec/models/user_spec.rb | 11 +++++++++++ spec/requests/api/v1/feeds/create_spec.rb | 1 + spec/requests/api/v1/groups/create_spec.rb | 1 + spec/requests/api/v1/terrains/create_spec.rb | 1 + spec/services/feeds/create_spec.rb | 6 ++++-- spec/services/groups/create_spec.rb | 10 ++++++---- spec/services/terrains/create_spec.rb | 10 ++++++---- 26 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20261024160000_add_user_to_domain_resources.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index 13bf07d..c4442ed 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -4,7 +4,7 @@ class FeedsController < BaseController before_action :authenticate_request! def create - feed = Feeds::Create.call(name: feed_params[:name], initial_stock_quantity: feed_params[:initial_stock_quantity]) + feed = Feeds::Create.call(user: current_user, name: feed_params[:name], initial_stock_quantity: feed_params[:initial_stock_quantity]) render json: feed, serializer: FeedSerializer, status: :created end diff --git a/app/controllers/api/v1/groups_controller.rb b/app/controllers/api/v1/groups_controller.rb index e3a2917..584811f 100644 --- a/app/controllers/api/v1/groups_controller.rb +++ b/app/controllers/api/v1/groups_controller.rb @@ -4,7 +4,7 @@ class GroupsController < BaseController before_action :authenticate_request! def create - group = Groups::Create.call(attributes: group_params) + group = Groups::Create.call(user: current_user, attributes: group_params) render json: group, serializer: GroupSerializer, status: :created end diff --git a/app/controllers/api/v1/terrains_controller.rb b/app/controllers/api/v1/terrains_controller.rb index bec63cb..9029815 100644 --- a/app/controllers/api/v1/terrains_controller.rb +++ b/app/controllers/api/v1/terrains_controller.rb @@ -4,7 +4,7 @@ class TerrainsController < BaseController before_action :authenticate_request! def create - terrain = Terrains::Create.call(attributes: terrain_params) + terrain = Terrains::Create.call(user: current_user, attributes: terrain_params) render json: terrain, serializer: TerrainSerializer, status: :created end diff --git a/app/models/feed.rb b/app/models/feed.rb index 82250d8..a01afe3 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,4 +1,6 @@ class Feed < ApplicationRecord + belongs_to :user + has_many :feed_movements, dependent: :restrict_with_error validates :name, presence: true validates :stock_quantity, presence: true, numericality: { greater_than_or_equal_to: 0 } diff --git a/app/models/group.rb b/app/models/group.rb index cbaaee8..33a617c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,4 +1,6 @@ class Group < ApplicationRecord + belongs_to :user + has_many :occupations, dependent: :restrict_with_error validates :name, presence: true diff --git a/app/models/terrain.rb b/app/models/terrain.rb index 65c54d3..5588eae 100644 --- a/app/models/terrain.rb +++ b/app/models/terrain.rb @@ -1,4 +1,6 @@ class Terrain < ApplicationRecord + belongs_to :user + has_many :occupations, dependent: :restrict_with_error enum :status, { diff --git a/app/models/user.rb b/app/models/user.rb index 213b79b..95aeff5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,9 @@ class User < ApplicationRecord :recoverable, :validatable has_many :sessions, dependent: :destroy + has_many :groups, dependent: :destroy + has_many :terrains, dependent: :destroy + has_many :feeds, dependent: :destroy validates :name, presence: true diff --git a/app/services/feeds/create.rb b/app/services/feeds/create.rb index c233481..88637c4 100644 --- a/app/services/feeds/create.rb +++ b/app/services/feeds/create.rb @@ -1,8 +1,8 @@ module Feeds class Create class << self - def call(name:, initial_stock_quantity:) - Feed.create!(name:, stock_quantity: initial_stock_quantity) + def call(user:, name:, initial_stock_quantity:) + user.feeds.create!(name:, stock_quantity: initial_stock_quantity) end end end diff --git a/app/services/groups/create.rb b/app/services/groups/create.rb index babf188..d280dbb 100644 --- a/app/services/groups/create.rb +++ b/app/services/groups/create.rb @@ -1,8 +1,8 @@ module Groups class Create class << self - def call(attributes:) - Group.create!(attributes) + def call(user:, attributes:) + user.groups.create!(attributes.merge(user:)) end end end diff --git a/app/services/terrains/create.rb b/app/services/terrains/create.rb index bda4fd5..80d6e46 100644 --- a/app/services/terrains/create.rb +++ b/app/services/terrains/create.rb @@ -1,9 +1,9 @@ module Terrains class Create class << self - def call(attributes:) - Terrain.create!( - attributes.merge(status: :available) + def call(user:, attributes:) + user.terrains.create!( + attributes.merge(status: :available, user:) ) end end diff --git a/db/migrate/20261024160000_add_user_to_domain_resources.rb b/db/migrate/20261024160000_add_user_to_domain_resources.rb new file mode 100644 index 0000000..76346cb --- /dev/null +++ b/db/migrate/20261024160000_add_user_to_domain_resources.rb @@ -0,0 +1,7 @@ +class AddUserToDomainResources < ActiveRecord::Migration[8.1] + def change + add_reference :groups, :user, null: false, foreign_key: true, type: :uuid + add_reference :terrains, :user, null: false, foreign_key: true, type: :uuid + add_reference :feeds, :user, null: false, foreign_key: true, type: :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index 0c080fb..8af8199 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_10_24_125351) do +ActiveRecord::Schema[8.1].define(version: 2026_10_24_160000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -35,7 +35,9 @@ t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "user_id", null: false t.index ["deleted_at"], name: "index_feeds_on_deleted_at" + t.index ["user_id"], name: "index_feeds_on_user_id" end create_table "groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -44,7 +46,9 @@ t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "user_id", null: false t.index ["deleted_at"], name: "index_groups_on_deleted_at" + t.index ["user_id"], name: "index_groups_on_user_id" end create_table "occupations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -78,8 +82,10 @@ t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.uuid "user_id", null: false t.index ["deleted_at"], name: "index_terrains_on_deleted_at" t.index ["status"], name: "index_terrains_on_status" + t.index ["user_id"], name: "index_terrains_on_user_id" end create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -95,7 +101,10 @@ end add_foreign_key "feed_movements", "feeds" + add_foreign_key "feeds", "users" + add_foreign_key "groups", "users" add_foreign_key "occupations", "groups" add_foreign_key "occupations", "terrains" add_foreign_key "sessions", "users" + add_foreign_key "terrains", "users" end diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb index c3583d5..dee6892 100644 --- a/spec/factories/feeds.rb +++ b/spec/factories/feeds.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :feed do + association :user name { Faker::Food.ingredient } stock_quantity { Faker::Number.decimal(l_digits: 3, r_digits: 2) } deleted_at { nil } diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 8eafb54..61f03e1 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :group do + association :user name { Faker::Creature::Animal.name } animal_count { Faker::Number.between(from: 0, to: 100) } deleted_at { nil } diff --git a/spec/factories/occupations.rb b/spec/factories/occupations.rb index 4a0dc75..8e57577 100644 --- a/spec/factories/occupations.rb +++ b/spec/factories/occupations.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :occupation do association :group - association :terrain + terrain { association :terrain, user: group&.user || association(:user) } started_at { 1.days.ago } ended_at { nil } diff --git a/spec/factories/terrains.rb b/spec/factories/terrains.rb index f8de0ba..bae8289 100644 --- a/spec/factories/terrains.rb +++ b/spec/factories/terrains.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :terrain do + association :user name { Faker::Address.community } rest_days { Faker::Number.between(from: 0, to: 60) } status { :available } diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index acb146d..d78d144 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -1,6 +1,21 @@ require 'rails_helper' RSpec.describe Feed, type: :model do + describe 'ownership' do + it "belongs to a user" do + user = create(:user) + feed = create(:feed, user:) + expect(user.feeds).to contain_exactly(feed) + expect(feed.user).to eq(user) + end + + it "requires a user" do + feed = build(:feed, user: nil) + expect(feed).not_to be_valid + expect(feed.errors[:user]).to be_present + end + end + describe 'validations' do it "is valid with valid attributes" do feed = build(:feed) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 81b7e8c..b1bc421 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,6 +1,21 @@ require 'rails_helper' RSpec.describe Group, type: :model do + describe 'ownership' do + it "belongs to a user" do + user = create(:user) + group = create(:group, user:) + expect(user.groups).to contain_exactly(group) + expect(group.user).to eq(user) + end + + it "requires a user" do + group = build(:group, user: nil) + expect(group).not_to be_valid + expect(group.errors[:user]).to be_present + end + end + describe 'validations' do it "is valid with valid attributes" do group = build(:group) diff --git a/spec/models/terrain_spec.rb b/spec/models/terrain_spec.rb index 70c1fbf..ef965b3 100644 --- a/spec/models/terrain_spec.rb +++ b/spec/models/terrain_spec.rb @@ -1,6 +1,21 @@ require 'rails_helper' RSpec.describe Terrain, type: :model do + describe 'ownership' do + it "belongs to a user" do + user = create(:user) + terrain = create(:terrain, user:) + expect(user.terrains).to contain_exactly(terrain) + expect(terrain.user).to eq(user) + end + + it "requires a user" do + terrain = build(:terrain, user: nil) + expect(terrain).not_to be_valid + expect(terrain.errors[:user]).to be_present + end + end + it "is valid with valid attributes" do expect(build(:terrain)).to be_valid end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f7ba09d..5d42e50 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -50,6 +50,17 @@ end describe 'associations' do + [ :group, :terrain, :feed ].each do |resource| + it "destroys the associated #{resource} when the user is destroyed" do + user = create(:user) + record = create(resource, user:) + + user.destroy! + expect(User.exists?(user.id)).to be false + expect(record.class.exists?(record.id)).to be false + end + end + it "destroy associated sessions when the user is destroyed" do user = create(:user) session = create(:session, user: user) diff --git a/spec/requests/api/v1/feeds/create_spec.rb b/spec/requests/api/v1/feeds/create_spec.rb index 0de2756..5a79be6 100644 --- a/spec/requests/api/v1/feeds/create_spec.rb +++ b/spec/requests/api/v1/feeds/create_spec.rb @@ -15,6 +15,7 @@ it "creates a feed with valid attributes" do post "/api/v1/feeds", params: valid_params, headers: headers, as: :json expect(response).to have_http_status(:created) + expect(Feed.find(response.parsed_body["id"]).user).to eq(user) expect(response.parsed_body).to include( "id" => be_present, "name" => "Feed A", diff --git a/spec/requests/api/v1/groups/create_spec.rb b/spec/requests/api/v1/groups/create_spec.rb index fbd454f..8126720 100644 --- a/spec/requests/api/v1/groups/create_spec.rb +++ b/spec/requests/api/v1/groups/create_spec.rb @@ -15,6 +15,7 @@ it "creates a group with valid attributes" do post "/api/v1/groups", params: { group: attributes }, headers: headers, as: :json expect(response).to have_http_status(:created) + expect(Group.find(response.parsed_body["id"]).user).to eq(user) expect(response.parsed_body).to include( "id" => be_present, "name" => attributes[:name], diff --git a/spec/requests/api/v1/terrains/create_spec.rb b/spec/requests/api/v1/terrains/create_spec.rb index 32e21df..b5e83fa 100644 --- a/spec/requests/api/v1/terrains/create_spec.rb +++ b/spec/requests/api/v1/terrains/create_spec.rb @@ -15,6 +15,7 @@ it "creates a terrain with valid attributes" do post "/api/v1/terrains", params: { terrain: attributes }, headers: headers, as: :json expect(response).to have_http_status(:created) + expect(Terrain.find(response.parsed_body["id"]).user).to eq(user) expect(response.parsed_body).to include( "id" => be_present, "name" => attributes[:name], diff --git a/spec/services/feeds/create_spec.rb b/spec/services/feeds/create_spec.rb index b24ab3c..8400b91 100644 --- a/spec/services/feeds/create_spec.rb +++ b/spec/services/feeds/create_spec.rb @@ -2,16 +2,18 @@ RSpec.describe Feeds::Create, type: :service do describe '.call' do + let(:user) { create(:user) } it "creates a feed with initial stock quantity" do - feed = described_class.call(name: "Feed A", initial_stock_quantity: 100) + feed = described_class.call(user: user, name: "Feed A", initial_stock_quantity: 100) expect(feed).to be_persisted + expect(feed.user).to eq(user) expect(feed.name).to eq("Feed A") expect(feed.stock_quantity).to eq(100) end it "raises an error if the feed cannot be created" do expect { - described_class.call(name: nil, initial_stock_quantity: 100) + described_class.call(user: user, name: nil, initial_stock_quantity: 100) }.to raise_error(ActiveRecord::RecordInvalid) end end diff --git a/spec/services/groups/create_spec.rb b/spec/services/groups/create_spec.rb index 4cce71a..6cf7471 100644 --- a/spec/services/groups/create_spec.rb +++ b/spec/services/groups/create_spec.rb @@ -2,27 +2,29 @@ RSpec.describe Groups::Create, type: :service do describe '.call' do + let(:user) { create(:user) } let(:attributes) { attributes_for(:group) } it "creates a group" do - expect { described_class.call(attributes: attributes) }.to change(Group, :count).by(1) + expect { described_class.call(user: user, attributes: attributes) }.to change(Group, :count).by(1) end it "returns the created group" do - group = described_class.call(attributes: attributes) + group = described_class.call(user: user, attributes: attributes) expect(group).to be_a(Group) expect(group).to be_persisted + expect(group.user).to eq(user) expect(group).to have_attributes(name: attributes[:name], animal_count: attributes[:animal_count]) end it "raises an error when the attributes are invalid" do invalid_attributes = attributes.merge(name: nil) - expect { described_class.call(attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.call(user: user, attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) end it "does not create a group when the attributes are invalid" do invalid_attributes = attributes.merge(animal_count: -1) - expect { described_class.call(attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.call(user: user, attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) expect(Group.count).to eq(0) end end diff --git a/spec/services/terrains/create_spec.rb b/spec/services/terrains/create_spec.rb index 7a22aad..f93798f 100644 --- a/spec/services/terrains/create_spec.rb +++ b/spec/services/terrains/create_spec.rb @@ -2,28 +2,30 @@ RSpec.describe Terrains::Create, type: :service do describe '.call' do + let(:user) { create(:user) } let(:attributes) { attributes_for(:terrain) } it "creates a terrain" do - expect { described_class.call(attributes: attributes) }.to change(Terrain, :count).by(1) + expect { described_class.call(user: user, attributes: attributes) }.to change(Terrain, :count).by(1) end it "returns the created terrain" do - terrain = described_class.call(attributes: attributes) + terrain = described_class.call(user: user, attributes: attributes) expect(terrain).to be_a(Terrain) expect(terrain).to be_persisted + expect(terrain.user).to eq(user) expect(terrain).to have_attributes(name: attributes[:name], rest_days: attributes[:rest_days]) expect(terrain).to be_available end it "raises an error when the attributes are invalid" do invalid_attributes = attributes.merge(name: nil) - expect { described_class.call(attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.call(user: user, attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) end it "does not create a terrain when the attributes are invalid" do invalid_attributes = attributes.merge(rest_days: -1) - expect { described_class.call(attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.call(user: user, attributes: invalid_attributes) }.to raise_error(ActiveRecord::RecordInvalid) expect(Terrain.count).to eq(0) end end From 25fd7559112e72f1866f2b5d1294ecbb8a529047 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 17:49:54 -0300 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20isola=20dados=20de=20grupos=20p?= =?UTF-8?q?or=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/groups_controller.rb | 7 ++-- .../api/v1/occupations_controller.rb | 4 +- app/queries/groups/query.rb | 4 +- app/services/groups/deactivate.rb | 4 +- app/services/groups/find.rb | 4 +- app/services/groups/list.rb | 4 +- app/services/groups/update.rb | 4 +- spec/requests/api/v1/groups/create_spec.rb | 9 +++++ spec/requests/api/v1/groups/destroy_spec.rb | 15 +++++-- spec/requests/api/v1/groups/index_spec.rb | 13 +++--- spec/requests/api/v1/groups/show_spec.rb | 15 ++++++- spec/requests/api/v1/groups/update_spec.rb | 14 +++++-- .../api/v1/occupations/create_spec.rb | 4 +- .../v1/occupations/history_by_group_spec.rb | 12 +++++- spec/services/groups/deactivate_spec.rb | 40 +++++++++++++------ spec/services/groups/find_spec.rb | 18 ++++++--- spec/services/groups/list_spec.rb | 39 +++++++++--------- spec/services/groups/update_spec.rb | 20 +++++----- 18 files changed, 152 insertions(+), 78 deletions(-) diff --git a/app/controllers/api/v1/groups_controller.rb b/app/controllers/api/v1/groups_controller.rb index 584811f..2c9f59a 100644 --- a/app/controllers/api/v1/groups_controller.rb +++ b/app/controllers/api/v1/groups_controller.rb @@ -10,6 +10,7 @@ def create def index result = Groups::List.call( + user: current_user, filters: params.permit(:name, :min_animal_count, :max_animal_count).to_h, sort: params[:sort], direction: params[:direction], @@ -18,17 +19,17 @@ def index end def show - group = Groups::Find.call(id: params[:id]) + group = Groups::Find.call(user: current_user, id: params[:id]) render json: group, serializer: GroupSerializer, status: :ok end def update - group = Groups::Update.call(id: params[:id], attributes: group_params) + group = Groups::Update.call(user: current_user, id: params[:id], attributes: group_params) render json: group, serializer: GroupSerializer, status: :ok end def destroy - Groups::Deactivate.call(id: params[:id]) + Groups::Deactivate.call(user: current_user, id: params[:id]) head :no_content end diff --git a/app/controllers/api/v1/occupations_controller.rb b/app/controllers/api/v1/occupations_controller.rb index d5ac4b8..7d13efe 100644 --- a/app/controllers/api/v1/occupations_controller.rb +++ b/app/controllers/api/v1/occupations_controller.rb @@ -4,7 +4,7 @@ class OccupationsController < BaseController before_action :authenticate_request! def create - group = Groups::Find.call(id: occupation_params[:group_id]) + group = Groups::Find.call(user: current_user, id: occupation_params[:group_id]) destination = Terrains::Find.call(id: occupation_params[:terrain_id]) occupation = Occupations::Move.call(group:, destination:) @@ -13,7 +13,7 @@ def create end def by_group - group = Groups::Find.call(id: params[:id]) + group = Groups::Find.call(user: current_user, id: params[:id]) result = Occupations::HistoryByGroup.call(group:, page: params[:page], per_page: params[:per_page]) paginated = PaginatedCollectionSerializer.call(result:, serializer: GroupOccupationHistorySerializer) diff --git a/app/queries/groups/query.rb b/app/queries/groups/query.rb index 8c8556f..01bd563 100644 --- a/app/queries/groups/query.rb +++ b/app/queries/groups/query.rb @@ -15,8 +15,8 @@ class Query DEFAULT_SORT = { updated_at: :desc }.freeze class << self - def call(filters: {}, sort: nil, direction: nil) - scope = Filtering::Filterer.call(scope: Group.active, filters:, definitions: FILTERS) + def call(scope:, filters: {}, sort: nil, direction: nil) + scope = Filtering::Filterer.call(scope:, filters:, definitions: FILTERS) Sorting::Sorter.call(scope: scope, sort:, direction:, allowed: SORTS, default: DEFAULT_SORT) end end diff --git a/app/services/groups/deactivate.rb b/app/services/groups/deactivate.rb index 1579898..6d29d46 100644 --- a/app/services/groups/deactivate.rb +++ b/app/services/groups/deactivate.rb @@ -1,9 +1,9 @@ module Groups class Deactivate class << self - def call(id:) + def call(user:, id:) ActiveRecord::Base.transaction do - group = Group.active.lock.find(id) + group = user.groups.active.lock.find(id) occupation = group.occupations.active.lock.first close_occupation(occupation) if occupation group.soft_delete! diff --git a/app/services/groups/find.rb b/app/services/groups/find.rb index ee941eb..6282bef 100644 --- a/app/services/groups/find.rb +++ b/app/services/groups/find.rb @@ -1,8 +1,8 @@ module Groups class Find class << self - def call(id:) - Group.active.find(id) + def call(user:, id:) + user.groups.active.find(id) end end end diff --git a/app/services/groups/list.rb b/app/services/groups/list.rb index e3042ce..a146449 100644 --- a/app/services/groups/list.rb +++ b/app/services/groups/list.rb @@ -1,8 +1,8 @@ module Groups class List class << self - def call(filters: {}, sort: nil, direction: nil, page:, per_page:) - scope = Groups::Query.call(filters:, sort:, direction:) + def call(user:, filters: {}, sort: nil, direction: nil, page:, per_page:) + scope = Groups::Query.call(scope: user.groups.active, filters:, sort:, direction:) Pagination::Paginator.call( scope: scope, page:, diff --git a/app/services/groups/update.rb b/app/services/groups/update.rb index 8591f6c..4ea91e1 100644 --- a/app/services/groups/update.rb +++ b/app/services/groups/update.rb @@ -1,8 +1,8 @@ module Groups class Update class << self - def call(id:, attributes:) - group = Groups::Find.call(id:) + def call(user:, id:, attributes:) + group = Groups::Find.call(user:, id:) group.update!(attributes) group end diff --git a/spec/requests/api/v1/groups/create_spec.rb b/spec/requests/api/v1/groups/create_spec.rb index 8126720..5cf41ba 100644 --- a/spec/requests/api/v1/groups/create_spec.rb +++ b/spec/requests/api/v1/groups/create_spec.rb @@ -33,4 +33,13 @@ post "/api/v1/groups", params: { group: attributes }, as: :json expect(response).to have_http_status(:unauthorized) end + + it "ignores ownership supplied by the client" do + other_user = create(:user) + post "/api/v1/groups", params: { group: attributes.merge(user_id: other_user.id) }, headers: headers, as: :json + + expect(response).to have_http_status(:created) + expect(Group.find(response.parsed_body["id"]).user).to eq(user) + expect(response.parsed_body).not_to have_key("user_id") + end end diff --git a/spec/requests/api/v1/groups/destroy_spec.rb b/spec/requests/api/v1/groups/destroy_spec.rb index c230899..07839bc 100644 --- a/spec/requests/api/v1/groups/destroy_spec.rb +++ b/spec/requests/api/v1/groups/destroy_spec.rb @@ -9,7 +9,7 @@ end it "soft deletes the group" do - group = create(:group) + group = create(:group, user:) delete "/api/v1/groups/#{group.id}", headers: headers expect(response).to have_http_status(:no_content) expect(group.reload).to be_deleted @@ -21,8 +21,8 @@ end it "removes the group from its current terrain" do - group = create(:group) - terrain = create(:terrain, :occupied) + group = create(:group, user:) + terrain = create(:terrain, :occupied, user:) occupation = create(:occupation, group:, terrain:) delete "/api/v1/groups/#{group.id}", headers: headers expect(response).to have_http_status(:no_content) @@ -30,8 +30,15 @@ end it "returns unauthorized when no token is provided" do - group = create(:group) + group = create(:group, user:) delete "/api/v1/groups/#{group.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's group" do + group = create(:group) + delete "/api/v1/groups/#{group.id}", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/groups/index_spec.rb b/spec/requests/api/v1/groups/index_spec.rb index 5ed1da1..57f6e88 100644 --- a/spec/requests/api/v1/groups/index_spec.rb +++ b/spec/requests/api/v1/groups/index_spec.rb @@ -9,8 +9,8 @@ end it "filters groups" do - matching_group = create(:group, name: "North Herd") - create(:group, name: "South Herd") + matching_group = create(:group, user: user, name: "North Herd") + create(:group, user: user, name: "South Herd") get "/api/v1/groups", headers: headers, params: { name: "North" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -18,8 +18,8 @@ end it "sorts groups" do - small_group = create(:group, animal_count: 5) - large_group = create(:group, animal_count: 20) + small_group = create(:group, user: user, animal_count: 5) + large_group = create(:group, user: user, animal_count: 20) get "/api/v1/groups", headers: headers, params: { sort: "animal_count", direction: "desc" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -27,7 +27,8 @@ end it 'returns a paginated list of groups' do - create_list(:group, 3) + create_list(:group, 3, user: user) + create_list(:group, 2) get "/api/v1/groups", headers: headers, params: { page: 1, per_page: 2 } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -41,7 +42,7 @@ end it "returns serialized group attributes" do - group = create(:group) + group = create(:group, user:) get "/api/v1/groups", headers: headers serialized_group = response.parsed_body['data'].first expect(serialized_group).to include( diff --git a/spec/requests/api/v1/groups/show_spec.rb b/spec/requests/api/v1/groups/show_spec.rb index afc19bb..a2a3c45 100644 --- a/spec/requests/api/v1/groups/show_spec.rb +++ b/spec/requests/api/v1/groups/show_spec.rb @@ -9,7 +9,7 @@ end it "returns the group with the given id" do - group = create(:group) + group = create(:group, user:) get "/api/v1/groups/#{group.id}", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) @@ -26,8 +26,19 @@ end it "returns unauthorized when no token is provided" do - group = create(:group) + group = create(:group, user:) get "/api/v1/groups/#{group.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns the same not found response for another user's group and a missing group" do + group = create(:group) + get "/api/v1/groups/#{group.id}", headers: headers + expect(response).to have_http_status(:not_found) + foreign_response = response.parsed_body + + get "/api/v1/groups/#{SecureRandom.uuid}", headers: headers + expect(response).to have_http_status(:not_found) + expect(response.parsed_body).to eq(foreign_response) + end end diff --git a/spec/requests/api/v1/groups/update_spec.rb b/spec/requests/api/v1/groups/update_spec.rb index 4bf282c..cf9afbd 100644 --- a/spec/requests/api/v1/groups/update_spec.rb +++ b/spec/requests/api/v1/groups/update_spec.rb @@ -9,7 +9,7 @@ end it "updates the group" do - group = create(:group) + group = create(:group, user:) patch "/api/v1/groups/#{group.id}", params: { group: { name: "Updated Name", animal_count: 20 } }, headers: headers, as: :json body = response.parsed_body expect(response).to have_http_status(:ok) @@ -21,7 +21,7 @@ end it "returns validation errors when the attributes are invalid" do - group = create(:group) + group = create(:group, user:) patch "/api/v1/groups/#{group.id}", params: { group: { animal_count: -10 } }, headers: headers, as: :json expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body["errors"]["animal_count"]).to be_present @@ -33,8 +33,16 @@ end it "returns unauthorized when no token is provided" do - group = create(:group) + group = create(:group, user:) patch "/api/v1/groups/#{group.id}", params: { group: { name: "Updated Name" } }, as: :json expect(response).to have_http_status(:unauthorized) end + + it "returns not found without updating another user's group" do + group = create(:group, name: "Original Name") + patch "/api/v1/groups/#{group.id}", params: { group: { name: "Updated Name" } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + expect(group.reload.name).to eq("Original Name") + end end diff --git a/spec/requests/api/v1/occupations/create_spec.rb b/spec/requests/api/v1/occupations/create_spec.rb index f94c22d..f168a87 100644 --- a/spec/requests/api/v1/occupations/create_spec.rb +++ b/spec/requests/api/v1/occupations/create_spec.rb @@ -10,8 +10,8 @@ } end - let(:group) { create(:group) } - let(:terrain) { create(:terrain) } + let(:group) { create(:group, user:) } + let(:terrain) { create(:terrain, user:) } it "creates the current occupation" do post "/api/v1/occupations", diff --git a/spec/requests/api/v1/occupations/history_by_group_spec.rb b/spec/requests/api/v1/occupations/history_by_group_spec.rb index 3578c2c..e38327e 100644 --- a/spec/requests/api/v1/occupations/history_by_group_spec.rb +++ b/spec/requests/api/v1/occupations/history_by_group_spec.rb @@ -11,7 +11,7 @@ end it "returns the occupations for the group" do - group = create(:group) + group = create(:group, user:) occupation = create(:occupation, group:) get "/api/v1/groups/#{group.id}/occupations", headers: headers body = response.parsed_body @@ -20,8 +20,16 @@ end it "returns unauthorized without valid authentication" do - group = create(:group) + group = create(:group, user:) get "/api/v1/groups/#{group.id}/occupations" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's group" do + group = create(:group) + create(:occupation, group:) + get "/api/v1/groups/#{group.id}/occupations", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/services/groups/deactivate_spec.rb b/spec/services/groups/deactivate_spec.rb index 269e367..e75d57f 100644 --- a/spec/services/groups/deactivate_spec.rb +++ b/spec/services/groups/deactivate_spec.rb @@ -2,40 +2,56 @@ RSpec.describe Groups::Deactivate, type: :service do describe '.call' do + let(:user) { create(:user) } + it "deactivates the group" do - group = create(:group) - result = described_class.call(id: group.id) + group = create(:group, user:) + result = described_class.call(user: user, id: group.id) expect(result.reload).to be_deleted end it "closes the current occupation when the group is occupying a terrain" do - group = create(:group) - terrain = create(:terrain, :occupied) + group = create(:group, user:) + terrain = create(:terrain, :occupied, user:) occupation = create(:occupation, group:, terrain:) - described_class.call(id: group.id) + described_class.call(user: user, id: group.id) expect(occupation.reload).to be_finished end it "starts the terrain rest when the group is removed" do - group = create(:group) - terrain = create(:terrain, :occupied, rest_days: 10) + group = create(:group, user:) + terrain = create(:terrain, :occupied, user: user, rest_days: 10) create(:occupation, group:, terrain:) - described_class.call(id: group.id) + described_class.call(user: user, id: group.id) expect(terrain.reload).to be_resting end it "makes the terrain available when rest_days is 0" do - group = create(:group) - terrain = create(:terrain, :occupied, rest_days: 0) + group = create(:group, user:) + terrain = create(:terrain, :occupied, user: user, rest_days: 0) create(:occupation, group:, terrain:) - described_class.call(id: group.id) + described_class.call(user: user, id: group.id) expect(terrain.reload).to be_available end it "raises an error when the group does not exist" do expect { - described_class.call(id: SecureRandom.uuid) + described_class.call(user: user, id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) end + + it "does not change another user's group or its occupation and terrain" do + group = create(:group) + terrain = create(:terrain, :occupied, user: group.user, rest_days: 10) + occupation = create(:occupation, group:, terrain:) + + expect { + described_class.call(user:, id: group.id) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(group.reload).not_to be_deleted + expect(occupation.reload).to be_active + expect(terrain.reload).to be_occupied + end end end diff --git a/spec/services/groups/find_spec.rb b/spec/services/groups/find_spec.rb index 3536851..d30da4c 100644 --- a/spec/services/groups/find_spec.rb +++ b/spec/services/groups/find_spec.rb @@ -2,19 +2,27 @@ RSpec.describe Groups::Find, type: :service do describe '.call' do + let(:user) { create(:user) } + it 'returns an active group' do - group = create(:group) - result = described_class.call(id: group.id) + group = create(:group, user:) + result = described_class.call(user: user, id: group.id) expect(result).to eq(group) end it "raises an error when the group is deleted" do - group = create(:group, :deleted) - expect { described_class.call(id: group.id) }.to raise_error(ActiveRecord::RecordNotFound) + group = create(:group, :deleted, user:) + expect { described_class.call(user: user, id: group.id) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error when the group does not exist" do - expect { described_class.call(id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.call(user: user, id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises an error when the group belongs to another user" do + group = create(:group) + + expect { described_class.call(user:, id: group.id) }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/services/groups/list_spec.rb b/spec/services/groups/list_spec.rb index 63fb70e..02bfe71 100644 --- a/spec/services/groups/list_spec.rb +++ b/spec/services/groups/list_spec.rb @@ -2,38 +2,40 @@ RSpec.describe Groups::List, type: :service do describe '.call' do + let(:user) { create(:user) } + it "returns only active groups" do - active_group = create(:group) - create(:group, :deleted) - result = described_class.call(page: 1, per_page: 10) + active_group = create(:group, user:) + create(:group, :deleted, user:) + result = described_class.call(user: user, page: 1, per_page: 10) expect(result.records).to contain_exactly(active_group) end it "filters groups by name" do - matching_group = create(:group, name: "North Herd") - create(:group, name: "South Herd") - result = described_class.call(filters: { name: "North" }, page: 1, per_page: 10) + matching_group = create(:group, user: user, name: "North Herd") + create(:group, user: user, name: "South Herd") + result = described_class.call(user: user, filters: { name: "North" }, page: 1, per_page: 10) expect(result.records).to contain_exactly(matching_group) end it "filters groups by animal count range" do - matching_group = create(:group, animal_count: 15) - create(:group, animal_count: 5) - create(:group, animal_count: 25) - result = described_class.call(filters: { min_animal_count: 10, max_animal_count: 20 }, page: 1, per_page: 10) + matching_group = create(:group, user: user, animal_count: 15) + create(:group, user: user, animal_count: 5) + create(:group, user: user, animal_count: 25) + result = described_class.call(user: user, filters: { min_animal_count: 10, max_animal_count: 20 }, page: 1, per_page: 10) expect(result.records).to contain_exactly(matching_group) end it "sorts groups by animal count" do - small_group = create(:group, animal_count: 5) - large_group = create(:group, animal_count: 20) - result = described_class.call(sort: "animal_count", direction: "desc", page: 1, per_page: 10) + small_group = create(:group, user: user, animal_count: 5) + large_group = create(:group, user: user, animal_count: 20) + result = described_class.call(user: user, sort: "animal_count", direction: "desc", page: 1, per_page: 10) expect(result.records).to eq([ large_group, small_group ]) end it "returns a paginated result" do - create_list(:group, 15) - result = described_class.call(page: 2, per_page: 10) + create_list(:group, 15, user: user) + result = described_class.call(user: user, page: 2, per_page: 10) expect(result.records.size).to eq(5) expect(result.page).to eq(2) expect(result.per_page).to eq(10) @@ -42,9 +44,10 @@ end it "paginates the filtered query results" do - create_list(:group, 15, name: "North Group") - create_list(:group, 10, name: "South Herd") - result = described_class.call(filters: { name: "Group" }, page: 1, per_page: 10) + create_list(:group, 15, user: user, name: "North Group") + create_list(:group, 10, user: user, name: "South Herd") + create_list(:group, 3, name: "North Group") + result = described_class.call(user: user, filters: { name: "Group" }, page: 1, per_page: 10) expect(result.records.size).to eq(10) expect(result.total_items).to eq(15) expect(result.total_pages).to eq(2) diff --git a/spec/services/groups/update_spec.rb b/spec/services/groups/update_spec.rb index aeef418..3b8d537 100644 --- a/spec/services/groups/update_spec.rb +++ b/spec/services/groups/update_spec.rb @@ -2,35 +2,37 @@ RSpec.describe Groups::Update, type: :service do describe '.call' do + let(:user) { create(:user) } + it "updates the group" do - group = create(:group) - result = described_class.call(id: group.id, attributes: { name: "New Name", animal_count: 50 }) + group = create(:group, user:) + result = described_class.call(user: user, id: group.id, attributes: { name: "New Name", animal_count: 50 }) expect(result).to have_attributes(name: "New Name", animal_count: 50) end it "persists the changes" do - group = create(:group) - described_class.call(id: group.id, attributes: { name: "New Name" }) + group = create(:group, user:) + described_class.call(user: user, id: group.id, attributes: { name: "New Name" }) expect(group.reload.name).to eq("New Name") end it "raises an error if the attributes are invalid" do - group = create(:group) + group = create(:group, user:) expect { - described_class.call(id: group.id, attributes: { animal_count: -10 }) + described_class.call(user: user, id: group.id, attributes: { animal_count: -10 }) }.to raise_error(ActiveRecord::RecordInvalid) end it "raises an error if the group is deleted" do - group = create(:group, :deleted) + group = create(:group, :deleted, user:) expect { - described_class.call(id: group.id, attributes: { name: "New Name" }) + described_class.call(user: user, id: group.id, attributes: { name: "New Name" }) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error if the group is not found" do expect { - described_class.call(id: 999, attributes: { name: "New Name" }) + described_class.call(user: user, id: 999, attributes: { name: "New Name" }) }.to raise_error(ActiveRecord::RecordNotFound) end end From 7ecd3ed1fa626482ea2f2e286a19ec09959cec0f Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 17:56:21 -0300 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20isola=20dados=20de=20grupos=20p?= =?UTF-8?q?or=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/occupations_controller.rb | 4 +- app/controllers/api/v1/terrains_controller.rb | 7 +-- app/queries/terrains/query.rb | 4 +- app/services/terrains/deactivate.rb | 4 +- app/services/terrains/find.rb | 4 +- app/services/terrains/list.rb | 4 +- app/services/terrains/update.rb | 4 +- .../api/v1/occupations/create_spec.rb | 9 ++++ .../v1/occupations/history_by_terrain_spec.rb | 14 ++++-- spec/requests/api/v1/terrains/create_spec.rb | 13 ++++++ spec/requests/api/v1/terrains/destroy_spec.rb | 15 +++++-- spec/requests/api/v1/terrains/index_spec.rb | 13 +++--- spec/requests/api/v1/terrains/show_spec.rb | 17 +++++-- spec/requests/api/v1/terrains/update_spec.rb | 27 ++++++++++-- spec/services/terrains/deactivate_spec.rb | 44 +++++++++++++------ spec/services/terrains/find_spec.rb | 18 +++++--- spec/services/terrains/list_spec.rb | 40 +++++++++-------- spec/services/terrains/update_spec.rb | 20 +++++---- 18 files changed, 181 insertions(+), 80 deletions(-) diff --git a/app/controllers/api/v1/occupations_controller.rb b/app/controllers/api/v1/occupations_controller.rb index 7d13efe..0da3157 100644 --- a/app/controllers/api/v1/occupations_controller.rb +++ b/app/controllers/api/v1/occupations_controller.rb @@ -5,7 +5,7 @@ class OccupationsController < BaseController def create group = Groups::Find.call(user: current_user, id: occupation_params[:group_id]) - destination = Terrains::Find.call(id: occupation_params[:terrain_id]) + destination = Terrains::Find.call(user: current_user, id: occupation_params[:terrain_id]) occupation = Occupations::Move.call(group:, destination:) @@ -21,7 +21,7 @@ def by_group end def by_terrain - terrain = Terrains::Find.call(id: params[:id]) + terrain = Terrains::Find.call(user: current_user, id: params[:id]) result = Occupations::HistoryByTerrain.call(terrain:, page: params[:page], per_page: params[:per_page]) paginated = PaginatedCollectionSerializer.call(result:, serializer: TerrainOccupationHistorySerializer) diff --git a/app/controllers/api/v1/terrains_controller.rb b/app/controllers/api/v1/terrains_controller.rb index 9029815..73e6977 100644 --- a/app/controllers/api/v1/terrains_controller.rb +++ b/app/controllers/api/v1/terrains_controller.rb @@ -10,6 +10,7 @@ def create def index result = Terrains::List.call( + user: current_user, filters: params.permit(:name, :status, :min_rest_days, :max_rest_days).to_h, sort: params[:sort], direction: params[:direction], @@ -18,17 +19,17 @@ def index end def show - terrain = Terrains::Find.call(id: params[:id]) + terrain = Terrains::Find.call(user: current_user, id: params[:id]) render json: terrain, serializer: TerrainSerializer, status: :ok end def update - terrain = Terrains::Update.call(id: params[:id], attributes: terrain_params) + terrain = Terrains::Update.call(user: current_user, id: params[:id], attributes: terrain_params) render json: terrain, serializer: TerrainSerializer, status: :ok end def destroy - Terrains::Deactivate.call(id: params[:id]) + Terrains::Deactivate.call(user: current_user, id: params[:id]) head :no_content end diff --git a/app/queries/terrains/query.rb b/app/queries/terrains/query.rb index aa53abb..14a46f0 100644 --- a/app/queries/terrains/query.rb +++ b/app/queries/terrains/query.rb @@ -16,8 +16,8 @@ class Query DEFAULT_SORT = { updated_at: :desc }.freeze class << self - def call(filters: {}, sort: nil, direction: nil) - scope = Filtering::Filterer.call(scope: Terrain.active, filters:, definitions: FILTERS) + def call(scope:, filters: {}, sort: nil, direction: nil) + scope = Filtering::Filterer.call(scope:, filters:, definitions: FILTERS) Sorting::Sorter.call(scope: scope, sort:, direction:, allowed: SORTS, default: DEFAULT_SORT) end end diff --git a/app/services/terrains/deactivate.rb b/app/services/terrains/deactivate.rb index f943040..4729c4d 100644 --- a/app/services/terrains/deactivate.rb +++ b/app/services/terrains/deactivate.rb @@ -1,9 +1,9 @@ module Terrains class Deactivate class << self - def call(id:) + def call(user:, id:) ActiveRecord::Base.transaction do - terrain = Terrain.active.lock.find(id) + terrain = user.terrains.active.lock.find(id) validate_deactivation!(terrain) terrain.soft_delete! terrain diff --git a/app/services/terrains/find.rb b/app/services/terrains/find.rb index 93ba2d8..35041b4 100644 --- a/app/services/terrains/find.rb +++ b/app/services/terrains/find.rb @@ -1,8 +1,8 @@ module Terrains class Find class << self - def call(id:) - Terrain.active.find(id) + def call(user:, id:) + user.terrains.active.find(id) end end end diff --git a/app/services/terrains/list.rb b/app/services/terrains/list.rb index 31f81bc..22d01a0 100644 --- a/app/services/terrains/list.rb +++ b/app/services/terrains/list.rb @@ -1,8 +1,8 @@ module Terrains class List class << self - def call(filters: {}, sort: nil, direction: nil, page: nil, per_page: nil) - scope = Terrains::Query.call(filters:, sort:, direction:) + def call(user:, filters: {}, sort: nil, direction: nil, page: nil, per_page: nil) + scope = Terrains::Query.call(scope: user.terrains.active, filters:, sort:, direction:) Pagination::Paginator.call(scope: scope, page:, per_page:) end end diff --git a/app/services/terrains/update.rb b/app/services/terrains/update.rb index fc9b8e8..ca9eaa6 100644 --- a/app/services/terrains/update.rb +++ b/app/services/terrains/update.rb @@ -1,8 +1,8 @@ module Terrains class Update class << self - def call(id:, attributes:) - terrain = Terrains::Find.call(id:) + def call(user:, id:, attributes:) + terrain = Terrains::Find.call(user:, id:) terrain.update!(attributes) terrain end diff --git a/spec/requests/api/v1/occupations/create_spec.rb b/spec/requests/api/v1/occupations/create_spec.rb index f168a87..da20b1b 100644 --- a/spec/requests/api/v1/occupations/create_spec.rb +++ b/spec/requests/api/v1/occupations/create_spec.rb @@ -62,4 +62,13 @@ expect(response).to have_http_status(:unauthorized) end + + it "returns not found for a destination belonging to another user" do + destination = create(:terrain, :occupied) + post "/api/v1/occupations", params: { + occupation: { group_id: group.id, terrain_id: destination.id } + }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/occupations/history_by_terrain_spec.rb b/spec/requests/api/v1/occupations/history_by_terrain_spec.rb index eeab8c3..029c3a3 100644 --- a/spec/requests/api/v1/occupations/history_by_terrain_spec.rb +++ b/spec/requests/api/v1/occupations/history_by_terrain_spec.rb @@ -11,8 +11,8 @@ end it "returns the occupations for the terrain" do - terrain = create(:terrain) - occupation = create(:occupation, terrain:) + terrain = create(:terrain, user:) + occupation = create(:occupation, terrain:, group: create(:group, user:)) get "/api/v1/terrains/#{terrain.id}/occupations", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) @@ -20,8 +20,16 @@ end it "returns unauthorized without valid authentication" do - terrain = create(:terrain) + terrain = create(:terrain, user:) get "/api/v1/terrains/#{terrain.id}/occupations" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's terrain" do + terrain = create(:terrain) + create(:occupation, terrain:, group: create(:group, user: terrain.user)) + get "/api/v1/terrains/#{terrain.id}/occupations", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/terrains/create_spec.rb b/spec/requests/api/v1/terrains/create_spec.rb index b5e83fa..23b1afd 100644 --- a/spec/requests/api/v1/terrains/create_spec.rb +++ b/spec/requests/api/v1/terrains/create_spec.rb @@ -34,4 +34,17 @@ post "/api/v1/terrains", params: { terrain: attributes }, as: :json expect(response).to have_http_status(:unauthorized) end + + it "ignores ownership and status supplied by the client" do + other_user = create(:user) + post "/api/v1/terrains", params: { + terrain: attributes.merge(user_id: other_user.id, status: "occupied") + }, headers: headers, as: :json + + expect(response).to have_http_status(:created) + terrain = Terrain.find(response.parsed_body["id"]) + expect(terrain.user).to eq(user) + expect(terrain).to be_available + expect(response.parsed_body).not_to have_key("user_id") + end end diff --git a/spec/requests/api/v1/terrains/destroy_spec.rb b/spec/requests/api/v1/terrains/destroy_spec.rb index 8928f29..4e314bb 100644 --- a/spec/requests/api/v1/terrains/destroy_spec.rb +++ b/spec/requests/api/v1/terrains/destroy_spec.rb @@ -11,7 +11,7 @@ end it "deletes a terrain" do - terrain = create(:terrain) + terrain = create(:terrain, user:) delete "/api/v1/terrains/#{terrain.id}", headers: headers expect(response).to have_http_status(:no_content) end @@ -22,16 +22,23 @@ end it "does not deactivate an occupied terrain" do - terrain = create(:terrain, :occupied) - create(:occupation, terrain:) + terrain = create(:terrain, :occupied, user:) + create(:occupation, terrain:, group: create(:group, user:)) delete "/api/v1/terrains/#{terrain.id}", headers: headers expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body["errors"]["base"]).to be_present end it "returns unauthorized when no token is provided" do - terrain = create(:terrain) + terrain = create(:terrain, user:) delete "/api/v1/terrains/#{terrain.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's terrain" do + terrain = create(:terrain) + delete "/api/v1/terrains/#{terrain.id}", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/terrains/index_spec.rb b/spec/requests/api/v1/terrains/index_spec.rb index 3d07eed..6bd19de 100644 --- a/spec/requests/api/v1/terrains/index_spec.rb +++ b/spec/requests/api/v1/terrains/index_spec.rb @@ -11,8 +11,8 @@ end it "filters terrains" do - matching_terrain = create(:terrain, name: "North Pasture") - create(:terrain, name: "South Pasture") + matching_terrain = create(:terrain, user: user, name: "North Pasture") + create(:terrain, user: user, name: "South Pasture") get "/api/v1/terrains", headers: headers, params: { name: "North" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -20,8 +20,8 @@ end it "sorts terrains" do - shorter_rest = create(:terrain, rest_days: 5) - longer_rest = create(:terrain, rest_days: 20) + shorter_rest = create(:terrain, user: user, rest_days: 5) + longer_rest = create(:terrain, user: user, rest_days: 20) get "/api/v1/terrains", headers: headers, params: { sort: "rest_days", direction: "desc" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -29,7 +29,8 @@ end it "returns a paginated list of terrains" do - create_list(:terrain, 3) + create_list(:terrain, 3, user: user) + create_list(:terrain, 2) get "/api/v1/terrains", headers: headers, params: { page: 1, per_page: 2 } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -43,7 +44,7 @@ end it "returns serialized terrain attributes" do - terrain = create(:terrain) + terrain = create(:terrain, user:) get "/api/v1/terrains", headers: headers serialized_terrain = response.parsed_body['data'].first expect(serialized_terrain).to include( diff --git a/spec/requests/api/v1/terrains/show_spec.rb b/spec/requests/api/v1/terrains/show_spec.rb index 76f86f7..527b942 100644 --- a/spec/requests/api/v1/terrains/show_spec.rb +++ b/spec/requests/api/v1/terrains/show_spec.rb @@ -11,7 +11,7 @@ end it "returns a terrain" do - terrain = create(:terrain) + terrain = create(:terrain, user:) get "/api/v1/terrains/#{terrain.id}", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) @@ -24,7 +24,7 @@ end it "returns not found when the terrain is deleted" do - terrain = create(:terrain, :deleted) + terrain = create(:terrain, :deleted, user:) get "/api/v1/terrains/#{terrain.id}", headers: headers expect(response).to have_http_status(:not_found) end @@ -35,8 +35,19 @@ end it "returns unauthorized when no token is provided" do - terrain = create(:terrain) + terrain = create(:terrain, user:) get "/api/v1/terrains/#{terrain.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns the same not found response for another user's terrain and a missing terrain" do + terrain = create(:terrain) + get "/api/v1/terrains/#{terrain.id}", headers: headers + expect(response).to have_http_status(:not_found) + foreign_response = response.parsed_body + + get "/api/v1/terrains/#{SecureRandom.uuid}", headers: headers + expect(response).to have_http_status(:not_found) + expect(response.parsed_body).to eq(foreign_response) + end end diff --git a/spec/requests/api/v1/terrains/update_spec.rb b/spec/requests/api/v1/terrains/update_spec.rb index a4f9406..8da41a7 100644 --- a/spec/requests/api/v1/terrains/update_spec.rb +++ b/spec/requests/api/v1/terrains/update_spec.rb @@ -11,7 +11,7 @@ end it "updates a terrain" do - terrain = create(:terrain) + terrain = create(:terrain, user:) patch "/api/v1/terrains/#{terrain.id}", params: { terrain: { name: "Updated Terrain" } }, headers: headers, as: :json body = response.parsed_body expect(response).to have_http_status(:ok) @@ -24,7 +24,7 @@ end it "return validation errors when the attributes are invalid" do - terrain = create(:terrain) + terrain = create(:terrain, user:) patch "/api/v1/terrains/#{terrain.id}", params: { terrain: { rest_days: -5 } }, headers: headers, as: :json expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body["errors"]["rest_days"]).to be_present @@ -36,8 +36,29 @@ end it "returns unauthorized when no token is provided" do - terrain = create(:terrain) + terrain = create(:terrain, user:) patch "/api/v1/terrains/#{terrain.id}", params: { terrain: { name: "Updated Terrain" } }, as: :json expect(response).to have_http_status(:unauthorized) end + + it "returns not found without updating another user's terrain" do + terrain = create(:terrain, name: "Original Name") + patch "/api/v1/terrains/#{terrain.id}", params: { terrain: { name: "Updated Terrain" } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + expect(terrain.reload.name).to eq("Original Name") + end + + it "ignores ownership and status supplied by the client" do + terrain = create(:terrain, :resting, user:) + other_user = create(:user) + patch "/api/v1/terrains/#{terrain.id}", params: { + terrain: { name: "Updated Terrain", user_id: other_user.id, status: "available" } + }, headers: headers, as: :json + + expect(response).to have_http_status(:ok) + expect(terrain.reload.name).to eq("Updated Terrain") + expect(terrain.user).to eq(user) + expect(terrain).to be_resting + end end diff --git a/spec/services/terrains/deactivate_spec.rb b/spec/services/terrains/deactivate_spec.rb index 2896cef..9314fd3 100644 --- a/spec/services/terrains/deactivate_spec.rb +++ b/spec/services/terrains/deactivate_spec.rb @@ -2,50 +2,66 @@ RSpec.describe Terrains::Deactivate, type: :service do describe ".call" do + let(:user) { create(:user) } + it "deactivates an available terrain" do - terrain = create(:terrain) - result = described_class.call(id: terrain.id) + terrain = create(:terrain, user:) + result = described_class.call(user: user, id: terrain.id) expect(result.deleted?).to be true expect(result.deleted_at).to be_present end it "keeps the terrain persisted" do - terrain = create(:terrain) - expect { described_class.call(id: terrain.id) }.not_to change(Terrain, :count) + terrain = create(:terrain, user:) + expect { described_class.call(user: user, id: terrain.id) }.not_to change(Terrain, :count) end it "deactivates a resting terrain" do - terrain = create(:terrain, :resting) - result = described_class.call(id: terrain.id) + terrain = create(:terrain, :resting, user:) + result = described_class.call(user: user, id: terrain.id) expect(result.reload).to be_deleted end it "does not deactivate a terrain with an active occupation" do - terrain = create(:terrain, :occupied) - create(:occupation, terrain:) + terrain = create(:terrain, :occupied, user:) + create(:occupation, terrain:, group: create(:group, user:)) expect { - described_class.call(id: terrain.id) + described_class.call(user: user, id: terrain.id) }.to raise_error(Occupations::DeactivationError) expect(terrain.reload).not_to be_deleted end it "removes the terrain from the active scope" do - terrain = create(:terrain) - described_class.call(id: terrain.id) + terrain = create(:terrain, user:) + described_class.call(user: user, id: terrain.id) expect(Terrain.active).not_to include(terrain) end it "raises an error if the terrain is already deleted" do - terrain = create(:terrain, :deleted) + terrain = create(:terrain, :deleted, user:) expect { - described_class.call(id: terrain.id) + described_class.call(user: user, id: terrain.id) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error if the terrain is not found" do expect { - described_class.call(id: SecureRandom.uuid) + described_class.call(user: user, id: SecureRandom.uuid) + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "does not change another user's occupied terrain or its occupation" do + terrain = create(:terrain, :occupied) + group = create(:group, user: terrain.user) + occupation = create(:occupation, group:, terrain:) + + expect { + described_class.call(user:, id: terrain.id) }.to raise_error(ActiveRecord::RecordNotFound) + + expect(terrain.reload).not_to be_deleted + expect(terrain).to be_occupied + expect(occupation.reload).to be_active end end end diff --git a/spec/services/terrains/find_spec.rb b/spec/services/terrains/find_spec.rb index 18e54b4..2de12d5 100644 --- a/spec/services/terrains/find_spec.rb +++ b/spec/services/terrains/find_spec.rb @@ -2,19 +2,27 @@ RSpec.describe Terrains::Find, type: :service do describe ".call" do + let(:user) { create(:user) } + it "returns an active terrain" do - terrain = create(:terrain) - result = described_class.call(id: terrain.id) + terrain = create(:terrain, user:) + result = described_class.call(user: user, id: terrain.id) expect(result).to eq(terrain) end it "raises an error when the terrain is deleted" do - terrain = create(:terrain, :deleted) - expect { described_class.call(id: terrain.id) }.to raise_error(ActiveRecord::RecordNotFound) + terrain = create(:terrain, :deleted, user:) + expect { described_class.call(user: user, id: terrain.id) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error when the terrain does not exist" do - expect { described_class.call(id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.call(user: user, id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises an error when the terrain belongs to another user" do + terrain = create(:terrain) + + expect { described_class.call(user:, id: terrain.id) }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/services/terrains/list_spec.rb b/spec/services/terrains/list_spec.rb index 0e73eae..8097835 100644 --- a/spec/services/terrains/list_spec.rb +++ b/spec/services/terrains/list_spec.rb @@ -2,45 +2,49 @@ RSpec.describe Terrains::List, type: :service do describe '.call' do + let(:user) { create(:user) } + it "returns only active terrains" do - active_terrain = create(:terrain) - create(:terrain, :deleted) - result = described_class.call(page: 1, per_page: 10) + active_terrain = create(:terrain, user:) + create(:terrain, :deleted, user:) + result = described_class.call(user: user, page: 1, per_page: 10) expect(result.records).to contain_exactly(active_terrain) end it "filters terrains by name" do - matching_terrain = create(:terrain, name: "North Pasture") - create(:terrain, name: "South Pasture") - result = described_class.call(filters: { name: "North" }, page: 1, per_page: 10) + matching_terrain = create(:terrain, user: user, name: "North Pasture") + create(:terrain, user: user, name: "South Pasture") + result = described_class.call(user: user, filters: { name: "North" }, page: 1, per_page: 10) expect(result.records).to contain_exactly(matching_terrain) end it "filters terrains by rest days range" do - matching_terrain = create(:terrain, rest_days: 15) - create(:terrain, rest_days: 5) - create(:terrain, rest_days: 25) - result = described_class.call(filters: { min_rest_days: 10, max_rest_days: 20 }, page: 1, per_page: 10) + matching_terrain = create(:terrain, user: user, rest_days: 15) + create(:terrain, user: user, rest_days: 5) + create(:terrain, user: user, rest_days: 25) + result = described_class.call(user: user, filters: { min_rest_days: 10, max_rest_days: 20 }, page: 1, per_page: 10) expect(result.records).to contain_exactly(matching_terrain) end it "filters terrains by status" do - active_terrain = create(:terrain, status: :available) - create(:terrain, status: :occupied) - result = described_class.call(filters: { status: "available" }, page: 1, per_page: 10) + active_terrain = create(:terrain, user: user, status: :available) + create(:terrain, user: user, status: :occupied) + create(:terrain, status: :available) + result = described_class.call(user: user, filters: { status: "available" }, page: 1, per_page: 10) expect(result.records).to contain_exactly(active_terrain) end it "sorts terrains by rest days" do - shorter_rest = create(:terrain, rest_days: 5) - longer_rest = create(:terrain, rest_days: 20) - result = described_class.call(sort: "rest_days", direction: "desc", page: 1, per_page: 10) + shorter_rest = create(:terrain, user: user, rest_days: 5) + longer_rest = create(:terrain, user: user, rest_days: 20) + result = described_class.call(user: user, sort: "rest_days", direction: "desc", page: 1, per_page: 10) expect(result.records).to eq([ longer_rest, shorter_rest ]) end it "returns a paginated result" do - create_list(:terrain, 15) - result = described_class.call(page: 2, per_page: 10) + create_list(:terrain, 15, user: user) + create_list(:terrain, 3) + result = described_class.call(user: user, page: 2, per_page: 10) expect(result.records.size).to eq(5) expect(result.page).to eq(2) expect(result.per_page).to eq(10) diff --git a/spec/services/terrains/update_spec.rb b/spec/services/terrains/update_spec.rb index b218e41..c2ff07b 100644 --- a/spec/services/terrains/update_spec.rb +++ b/spec/services/terrains/update_spec.rb @@ -2,35 +2,37 @@ RSpec.describe Terrains::Update, type: :service do describe ".call" do + let(:user) { create(:user) } + it "updates the terrain" do - terrain = create(:terrain) - result = described_class.call(id: terrain.id, attributes: { name: "Updated Terrain", rest_days: 20 }) + terrain = create(:terrain, user:) + result = described_class.call(user: user, id: terrain.id, attributes: { name: "Updated Terrain", rest_days: 20 }) expect(result).to have_attributes(name: "Updated Terrain", rest_days: 20) end it "persists the changes" do - terrain = create(:terrain) - described_class.call(id: terrain.id, attributes: { name: "Updated Terrain" }) + terrain = create(:terrain, user:) + described_class.call(user: user, id: terrain.id, attributes: { name: "Updated Terrain" }) expect(terrain.reload.name).to eq("Updated Terrain") end it "raises an error if the attributes are invalid" do - terrain = create(:terrain) + terrain = create(:terrain, user:) expect { - described_class.call(id: terrain.id, attributes: { rest_days: -5 }) + described_class.call(user: user, id: terrain.id, attributes: { rest_days: -5 }) }.to raise_error(ActiveRecord::RecordInvalid) end it "raises an error if the terrain is deleted" do - terrain = create(:terrain, :deleted) + terrain = create(:terrain, :deleted, user:) expect { - described_class.call(id: terrain.id, attributes: { name: "Updated Terrain" }) + described_class.call(user: user, id: terrain.id, attributes: { name: "Updated Terrain" }) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error if the terrain does not exist" do expect { - described_class.call(id: SecureRandom.uuid, attributes: { name: "Updated Terrain" }) + described_class.call(user: user, id: SecureRandom.uuid, attributes: { name: "Updated Terrain" }) }.to raise_error(ActiveRecord::RecordNotFound) end end From 9119e4e890c5944d55ff3f65545c287e6c337a36 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 18:01:41 -0300 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20isola=20dados=20de=20ra=C3=A7?= =?UTF-8?q?=C3=B5es=20por=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/feed_movements_controller.rb | 3 +- app/controllers/api/v1/feeds_controller.rb | 7 ++-- app/queries/feeds/query.rb | 4 +- app/services/feed_movements/history.rb | 4 +- app/services/feeds/deactivate.rb | 4 +- app/services/feeds/find.rb | 4 +- app/services/feeds/list.rb | 4 +- app/services/feeds/update.rb | 4 +- .../api/v1/feed_movements/index_spec.rb | 14 +++++-- spec/requests/api/v1/feeds/create_spec.rb | 11 +++++ spec/requests/api/v1/feeds/destroy_spec.rb | 13 ++++-- spec/requests/api/v1/feeds/index_spec.rb | 11 ++--- spec/requests/api/v1/feeds/show_spec.rb | 15 ++++++- spec/requests/api/v1/feeds/update_spec.rb | 18 +++++--- spec/services/feed_movements/history_spec.rb | 14 ++++--- spec/services/feeds/deactivate_spec.rb | 25 ++++++++--- spec/services/feeds/find_spec.rb | 18 +++++--- spec/services/feeds/list_spec.rb | 42 ++++++++++++------- spec/services/feeds/update_spec.rb | 20 +++++---- 19 files changed, 162 insertions(+), 73 deletions(-) diff --git a/app/controllers/api/v1/feed_movements_controller.rb b/app/controllers/api/v1/feed_movements_controller.rb index b3bf65a..d91b569 100644 --- a/app/controllers/api/v1/feed_movements_controller.rb +++ b/app/controllers/api/v1/feed_movements_controller.rb @@ -24,8 +24,9 @@ def create_exit end def index - feed = Feed.active.find(params[:feed_id]) + feed = Feeds::Find.call(user: current_user, id: params[:feed_id]) result = FeedMovements::History.call( + user: current_user, feed_id: feed.id, page: params[:page], per_page: params[:per_page] ) diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index c4442ed..435111b 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -10,6 +10,7 @@ def create def index result = Feeds::List.call( + user: current_user, filters: params.permit(:name, :min_stock_quantity, :max_stock_quantity).to_h, sort: params[:sort], direction: params[:direction], @@ -18,17 +19,17 @@ def index end def show - feed = Feeds::Find.call(id: params[:id]) + feed = Feeds::Find.call(user: current_user, id: params[:id]) render json: feed, serializer: FeedSerializer end def update - feed = Feeds::Update.call(id: params[:id], name: feed_params[:name]) + feed = Feeds::Update.call(user: current_user, id: params[:id], name: feed_params[:name]) render json: feed, serializer: FeedSerializer end def destroy - Feeds::Deactivate.call(id: params[:id]) + Feeds::Deactivate.call(user: current_user, id: params[:id]) head :no_content end diff --git a/app/queries/feeds/query.rb b/app/queries/feeds/query.rb index aac288c..ff4647d 100644 --- a/app/queries/feeds/query.rb +++ b/app/queries/feeds/query.rb @@ -15,8 +15,8 @@ class Query DEFAULT_SORT = { updated_at: :desc }.freeze class << self - def call(filters: {}, sort: nil, direction: nil) - scope = Filtering::Filterer.call(scope: Feed.active, filters:, definitions: FILTERS) + def call(scope:, filters: {}, sort: nil, direction: nil) + scope = Filtering::Filterer.call(scope:, filters:, definitions: FILTERS) Sorting::Sorter.call(scope: scope, sort:, direction:, allowed: SORTS, default: DEFAULT_SORT) end end diff --git a/app/services/feed_movements/history.rb b/app/services/feed_movements/history.rb index 54a2091..ee82edd 100644 --- a/app/services/feed_movements/history.rb +++ b/app/services/feed_movements/history.rb @@ -1,8 +1,8 @@ module FeedMovements class History class << self - def call(feed_id:, page:, per_page:) - feed = Feeds::Find.call(id: feed_id) + def call(user:, feed_id:, page:, per_page:) + feed = Feeds::Find.call(user:, id: feed_id) scope = feed.feed_movements.order(occurred_on: :desc, created_at: :desc) Pagination::Paginator.call( scope:, diff --git a/app/services/feeds/deactivate.rb b/app/services/feeds/deactivate.rb index 02b6198..8407e4f 100644 --- a/app/services/feeds/deactivate.rb +++ b/app/services/feeds/deactivate.rb @@ -1,8 +1,8 @@ module Feeds class Deactivate class << self - def call(id:) - feed = Feeds::Find.call(id:) + def call(user:, id:) + feed = Feeds::Find.call(user:, id:) feed.soft_delete! feed end diff --git a/app/services/feeds/find.rb b/app/services/feeds/find.rb index 37df0fb..6389c65 100644 --- a/app/services/feeds/find.rb +++ b/app/services/feeds/find.rb @@ -1,8 +1,8 @@ module Feeds class Find class << self - def call(id:) - Feed.active.find(id) + def call(user:, id:) + user.feeds.active.find(id) end end end diff --git a/app/services/feeds/list.rb b/app/services/feeds/list.rb index 335607e..9aef945 100644 --- a/app/services/feeds/list.rb +++ b/app/services/feeds/list.rb @@ -1,8 +1,8 @@ module Feeds class List class << self - def call(filters: {}, sort: nil, direction: nil, page:, per_page:) - scope = Feeds::Query.call(filters:, sort:, direction:) + def call(user:, filters: {}, sort: nil, direction: nil, page:, per_page:) + scope = Feeds::Query.call(scope: user.feeds.active, filters:, sort:, direction:) Pagination::Paginator.call( scope: scope, page:, diff --git a/app/services/feeds/update.rb b/app/services/feeds/update.rb index b096554..3c1d13f 100644 --- a/app/services/feeds/update.rb +++ b/app/services/feeds/update.rb @@ -1,8 +1,8 @@ module Feeds class Update class << self - def call(id:, name:) - feed = Feeds::Find.call(id:) + def call(user:, id:, name:) + feed = Feeds::Find.call(user:, id:) feed.update!(name:) feed end diff --git a/spec/requests/api/v1/feed_movements/index_spec.rb b/spec/requests/api/v1/feed_movements/index_spec.rb index 7432595..1f7c3c6 100644 --- a/spec/requests/api/v1/feed_movements/index_spec.rb +++ b/spec/requests/api/v1/feed_movements/index_spec.rb @@ -11,7 +11,7 @@ end it "returns a list of feed movements for the given feed" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) create(:feed_movement, feed:, movement_type: "entry", quantity: 50, value: 200, occurred_on: Date.current) create(:feed_movement, feed:, movement_type: "exit", quantity: 30, reason: "Sold", occurred_on: Date.current) get "/api/v1/feeds/#{feed.id}/movements", headers: headers @@ -22,7 +22,7 @@ end it "returns a paginated list of feed movements" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) create_list(:feed_movement, 3, feed:, movement_type: "entry", quantity: 50, value: 200, occurred_on: Date.current) get "/api/v1/feeds/#{feed.id}/movements", headers: headers, params: { page: 1, per_page: 2 } body = response.parsed_body @@ -42,8 +42,16 @@ end it "returns unauthorized when no token is provided" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) get "/api/v1/feeds/#{feed.id}/movements" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's feed" do + feed = create(:feed) + create(:feed_movement, feed:) + get "/api/v1/feeds/#{feed.id}/movements", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/feeds/create_spec.rb b/spec/requests/api/v1/feeds/create_spec.rb index 5a79be6..255471d 100644 --- a/spec/requests/api/v1/feeds/create_spec.rb +++ b/spec/requests/api/v1/feeds/create_spec.rb @@ -33,4 +33,15 @@ post "/api/v1/feeds", params: valid_params, as: :json expect(response).to have_http_status(:unauthorized) end + + it "ignores ownership supplied by the client" do + other_user = create(:user) + post "/api/v1/feeds", params: { + feed: valid_params[:feed].merge(user_id: other_user.id) + }, headers: headers, as: :json + + expect(response).to have_http_status(:created) + expect(Feed.find(response.parsed_body["id"]).user).to eq(user) + expect(response.parsed_body).not_to have_key("user_id") + end end diff --git a/spec/requests/api/v1/feeds/destroy_spec.rb b/spec/requests/api/v1/feeds/destroy_spec.rb index ed49e66..508ac5a 100644 --- a/spec/requests/api/v1/feeds/destroy_spec.rb +++ b/spec/requests/api/v1/feeds/destroy_spec.rb @@ -11,7 +11,7 @@ end it "soft deletes the feed" do - feed = create(:feed) + feed = create(:feed, user:) delete "/api/v1/feeds/#{feed.id}", headers: headers expect(response).to have_http_status(:no_content) expect(feed.reload).to be_deleted @@ -23,14 +23,21 @@ end it "returns not found when the feed is already deleted" do - feed = create(:feed, :deleted) + feed = create(:feed, :deleted, user:) delete "/api/v1/feeds/#{feed.id}", headers: headers expect(response).to have_http_status(:not_found) end it "returns unauthorized when no token is provided" do - feed = create(:feed) + feed = create(:feed, user:) delete "/api/v1/feeds/#{feed.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's feed" do + feed = create(:feed) + delete "/api/v1/feeds/#{feed.id}", headers: headers + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/feeds/index_spec.rb b/spec/requests/api/v1/feeds/index_spec.rb index 608cac5..1f12603 100644 --- a/spec/requests/api/v1/feeds/index_spec.rb +++ b/spec/requests/api/v1/feeds/index_spec.rb @@ -11,8 +11,8 @@ end it "filters feeds" do - matching_feed = create(:feed, name: "Premium Feed") - create(:feed, name: "Basic Feed") + matching_feed = create(:feed, user: user, name: "Premium Feed") + create(:feed, user: user, name: "Basic Feed") get "/api/v1/feeds", headers: headers, params: { name: "Premium" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -20,8 +20,8 @@ end it "sorts feeds" do - low_stock_feed = create(:feed, stock_quantity: 5) - high_stock_feed = create(:feed, stock_quantity: 20) + low_stock_feed = create(:feed, user: user, stock_quantity: 5) + high_stock_feed = create(:feed, user: user, stock_quantity: 20) get "/api/v1/feeds", headers: headers, params: { sort: "stock_quantity", direction: "desc" } body = response.parsed_body expect(response).to have_http_status(:ok) @@ -29,7 +29,8 @@ end it "returns a paginated list of feeds" do - create_list(:feed, 3) + create_list(:feed, 3, user: user) + create_list(:feed, 2) get "/api/v1/feeds", headers: headers, params: { page: 1, per_page: 2 } body = response.parsed_body expect(response).to have_http_status(:ok) diff --git a/spec/requests/api/v1/feeds/show_spec.rb b/spec/requests/api/v1/feeds/show_spec.rb index 765a254..f2b423c 100644 --- a/spec/requests/api/v1/feeds/show_spec.rb +++ b/spec/requests/api/v1/feeds/show_spec.rb @@ -11,7 +11,7 @@ end it "returns a feed with the given id" do - feed = create(:feed) + feed = create(:feed, user:) get "/api/v1/feeds/#{feed.id}", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) @@ -28,8 +28,19 @@ end it "returns unauthorized when no token is provided" do - feed = create(:feed) + feed = create(:feed, user:) get "/api/v1/feeds/#{feed.id}" expect(response).to have_http_status(:unauthorized) end + + it "returns the same not found response for another user's feed and a missing feed" do + feed = create(:feed) + get "/api/v1/feeds/#{feed.id}", headers: headers + expect(response).to have_http_status(:not_found) + foreign_response = response.parsed_body + + get "/api/v1/feeds/#{SecureRandom.uuid}", headers: headers + expect(response).to have_http_status(:not_found) + expect(response.parsed_body).to eq(foreign_response) + end end diff --git a/spec/requests/api/v1/feeds/update_spec.rb b/spec/requests/api/v1/feeds/update_spec.rb index 45214b5..67f45b0 100644 --- a/spec/requests/api/v1/feeds/update_spec.rb +++ b/spec/requests/api/v1/feeds/update_spec.rb @@ -11,7 +11,7 @@ end it "updates the feed name" do - feed = create(:feed, name: "Old Name") + feed = create(:feed, user: user, name: "Old Name") patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "New Name" } }, headers: headers, as: :json body = response.parsed_body expect(response).to have_http_status(:ok) @@ -24,8 +24,8 @@ end it "does not update the stock quantity directly" do - feed = create(:feed, stock_quantity: 10) - patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: feed.name, stock_quantity: 20 } }, headers: headers, as: :json + feed = create(:feed, user: user, stock_quantity: 10) + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: feed.name, stock_quantity: 20, initial_stock_quantity: 30 } }, headers: headers, as: :json body = response.parsed_body expect(response).to have_http_status(:ok) expect(body).to include( @@ -37,7 +37,7 @@ end it "returns validation errors when the attributes are invalid" do - feed = create(:feed) + feed = create(:feed, user:) patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "" } }, headers: headers, as: :json expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["name"]).to be_present @@ -49,8 +49,16 @@ end it "returns unauthorized when no token is provided" do - feed = create(:feed) + feed = create(:feed, user:) patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "New Name" } }, as: :json expect(response).to have_http_status(:unauthorized) end + + it "returns not found without updating another user's feed" do + feed = create(:feed, name: "Original Name") + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "New Name" } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + expect(feed.reload.name).to eq("Original Name") + end end diff --git a/spec/services/feed_movements/history_spec.rb b/spec/services/feed_movements/history_spec.rb index a2662a4..906b3f1 100644 --- a/spec/services/feed_movements/history_spec.rb +++ b/spec/services/feed_movements/history_spec.rb @@ -2,23 +2,25 @@ RSpec.describe FeedMovements::History do describe '.call' do + let(:user) { create(:user) } + it "returns the history of feed movements for a given feed" do - feed = create(:feed) - other_feed = create(:feed) + feed = create(:feed, user:) + other_feed = create(:feed, user:) entry_movement = create(:feed_movement, :entry, feed:, quantity: 50, value: 200, occurred_on: Date.current - 1.day) exit_movement = create(:feed_movement, :exit, feed:, quantity: 30, reason: "Sold", occurred_on: Date.current) create(:feed_movement, :entry, feed: other_feed, quantity: 20, value: 100, occurred_on: Date.current) - result = described_class.call(feed_id: feed.id, page: 1, per_page: 10) + result = described_class.call(user: user, feed_id: feed.id, page: 1, per_page: 10) expect(result.records).to eq([ exit_movement, entry_movement ]) end it "raises an error when the feed does not exist" do - expect { described_class.call(feed_id: SecureRandom.uuid, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.call(user: user, feed_id: SecureRandom.uuid, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) end it "raises an error when the feed is deleted" do - feed = create(:feed, :deleted) - expect { described_class.call(feed_id: feed.id, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) + feed = create(:feed, :deleted, user:) + expect { described_class.call(user: user, feed_id: feed.id, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/services/feeds/deactivate_spec.rb b/spec/services/feeds/deactivate_spec.rb index 07831e3..4b94624 100644 --- a/spec/services/feeds/deactivate_spec.rb +++ b/spec/services/feeds/deactivate_spec.rb @@ -2,23 +2,38 @@ RSpec.describe Feeds::Deactivate, type: :service do describe '.call' do + let(:user) { create(:user) } + it 'deactivates the feed' do - feed = create(:feed) - result = described_class.call(id: feed.id) + feed = create(:feed, user:) + result = described_class.call(user: user, id: feed.id) expect(result.reload).to be_deleted end it 'raises an error when the feed is already deleted' do - feed = create(:feed, :deleted) + feed = create(:feed, :deleted, user:) expect { - described_class.call(id: feed.id) + described_class.call(user: user, id: feed.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises an error when the feed does not exist' do expect { - described_class.call(id: SecureRandom.uuid) + described_class.call(user: user, id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) end + + it "does not change another user's feed or its history" do + feed = create(:feed, stock_quantity: 100) + movement = create(:feed_movement, feed:) + + expect { + described_class.call(user:, id: feed.id) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(feed.reload).not_to be_deleted + expect(feed.stock_quantity).to eq(100) + expect(feed.feed_movements).to contain_exactly(movement) + end end end diff --git a/spec/services/feeds/find_spec.rb b/spec/services/feeds/find_spec.rb index 643d4e8..3d9512b 100644 --- a/spec/services/feeds/find_spec.rb +++ b/spec/services/feeds/find_spec.rb @@ -2,19 +2,27 @@ RSpec.describe Feeds::Find, type: :service do describe '.call' do + let(:user) { create(:user) } + it 'returns an active feed' do - feed = create(:feed) - result = described_class.call(id: feed.id) + feed = create(:feed, user:) + result = described_class.call(user: user, id: feed.id) expect(result).to eq(feed) end it 'raises an error when feed deleted' do - feed = create(:feed, :deleted) - expect { described_class.call(id: feed.id) }.to raise_error(ActiveRecord::RecordNotFound) + feed = create(:feed, :deleted, user:) + expect { described_class.call(user: user, id: feed.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises an error when feed does not exist' do - expect { described_class.call(id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.call(user: user, id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises an error when the feed belongs to another user" do + feed = create(:feed) + + expect { described_class.call(user:, id: feed.id) }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/services/feeds/list_spec.rb b/spec/services/feeds/list_spec.rb index c58f551..65e9112 100644 --- a/spec/services/feeds/list_spec.rb +++ b/spec/services/feeds/list_spec.rb @@ -2,30 +2,32 @@ RSpec.describe Feeds::List, type: :service do describe '.call' do + let(:user) { create(:user) } + it "returns only active feeds" do - active_feed = create(:feed) - create(:feed, :deleted) - result = described_class.call(page: 1, per_page: 10) + active_feed = create(:feed, user:) + create(:feed, :deleted, user:) + result = described_class.call(user: user, page: 1, per_page: 10) expect(result.records).to contain_exactly(active_feed) end it "filters feeds by name" do - matching_feed = create(:feed, name: "Premium Feed") - create(:feed, name: "Basic Feed") - result = described_class.call(filters: { name: "Premium" }, page: 1, per_page: 10) + matching_feed = create(:feed, user: user, name: "Premium Feed") + create(:feed, user: user, name: "Basic Feed") + result = described_class.call(user: user, filters: { name: "Premium" }, page: 1, per_page: 10) expect(result.records).to contain_exactly(matching_feed) end it "sorts feeds by stock quantity" do - low_stock_feed = create(:feed, stock_quantity: 5) - high_stock_feed = create(:feed, stock_quantity: 20) - result = described_class.call(sort: "stock_quantity", direction: "desc", page: 1, per_page: 10) + low_stock_feed = create(:feed, user: user, stock_quantity: 5) + high_stock_feed = create(:feed, user: user, stock_quantity: 20) + result = described_class.call(user: user, sort: "stock_quantity", direction: "desc", page: 1, per_page: 10) expect(result.records).to eq([ high_stock_feed, low_stock_feed ]) end it "returns a paginated result" do - create_list(:feed, 15) - result = described_class.call(page: 2, per_page: 10) + create_list(:feed, 15, user: user) + result = described_class.call(user: user, page: 2, per_page: 10) expect(result.records.size).to eq(5) expect(result.page).to eq(2) expect(result.per_page).to eq(10) @@ -34,12 +36,24 @@ end it "paginates the filtered query results" do - create_list(:feed, 15, name: "Premium Feed") - create_list(:feed, 10, name: "Basic Feed") - result = described_class.call(filters: { name: "Premium" }, page: 1, per_page: 10) + create_list(:feed, 15, user: user, name: "Premium Feed") + create_list(:feed, 10, user: user, name: "Basic Feed") + create_list(:feed, 3, name: "Premium Feed") + result = described_class.call(user: user, filters: { name: "Premium" }, page: 1, per_page: 10) expect(result.records.size).to eq(10) expect(result.total_items).to eq(15) expect(result.total_pages).to eq(2) end + + it "filters the user's feeds by stock quantity range" do + matching_feed = create(:feed, user:, stock_quantity: 15) + create(:feed, user:, stock_quantity: 5) + create(:feed, user:, stock_quantity: 25) + create(:feed, stock_quantity: 15) + + result = described_class.call(user:, filters: { min_stock_quantity: 10, max_stock_quantity: 20 }, page: 1, per_page: 10) + + expect(result.records).to contain_exactly(matching_feed) + end end end diff --git a/spec/services/feeds/update_spec.rb b/spec/services/feeds/update_spec.rb index 9ed4e4b..4d795c2 100644 --- a/spec/services/feeds/update_spec.rb +++ b/spec/services/feeds/update_spec.rb @@ -2,35 +2,37 @@ RSpec.describe Feeds::Update, type: :service do describe '.call' do + let(:user) { create(:user) } + it 'updates the feed name' do - feed = create(:feed, name: "Old Name") - result = described_class.call(id: feed.id, name: "New Name") + feed = create(:feed, user: user, name: "Old Name") + result = described_class.call(user: user, id: feed.id, name: "New Name") expect(result.name).to eq("New Name") end it "persists the changes" do - feed = create(:feed, name: "Old Name") - described_class.call(id: feed.id, name: "New Name") + feed = create(:feed, user: user, name: "Old Name") + described_class.call(user: user, id: feed.id, name: "New Name") expect(feed.reload.name).to eq("New Name") end it "raises an error if the name is invalid" do - feed = create(:feed) + feed = create(:feed, user:) expect { - described_class.call(id: feed.id, name: "") + described_class.call(user: user, id: feed.id, name: "") }.to raise_error(ActiveRecord::RecordInvalid) end it 'raises an error when the feed is deleted' do - feed = create(:feed, :deleted) + feed = create(:feed, :deleted, user:) expect { - described_class.call(id: feed.id, name: "New Name") + described_class.call(user: user, id: feed.id, name: "New Name") }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises an error when the feed does not exist' do expect { - described_class.call(id: SecureRandom.uuid, name: "New Name") + described_class.call(user: user, id: SecureRandom.uuid, name: "New Name") }.to raise_error(ActiveRecord::RecordNotFound) end end From 6e548385782018db7eccc487088d6dcc94e2ccd1 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 18:14:05 -0300 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20isola=20ocupa=C3=A7=C3=B5es=20p?= =?UTF-8?q?or=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/occupations_controller.rb | 2 +- app/services/groups/deactivate.rb | 8 +-- app/services/occupations/move.rb | 11 +-- app/services/occupations/open.rb | 2 + .../api/v1/occupations/create_spec.rb | 9 +++ .../v1/occupations/history_by_group_spec.rb | 3 + .../v1/occupations/history_by_terrain_spec.rb | 3 + spec/services/groups/deactivate_spec.rb | 14 ++++ spec/services/occupations/move_spec.rb | 71 +++++++++++++++---- spec/services/occupations/open_spec.rb | 12 +++- 10 files changed, 110 insertions(+), 25 deletions(-) diff --git a/app/controllers/api/v1/occupations_controller.rb b/app/controllers/api/v1/occupations_controller.rb index 0da3157..399b5b6 100644 --- a/app/controllers/api/v1/occupations_controller.rb +++ b/app/controllers/api/v1/occupations_controller.rb @@ -7,7 +7,7 @@ def create group = Groups::Find.call(user: current_user, id: occupation_params[:group_id]) destination = Terrains::Find.call(user: current_user, id: occupation_params[:terrain_id]) - occupation = Occupations::Move.call(group:, destination:) + occupation = Occupations::Move.call(user: current_user, group:, destination:) render json: occupation, serializer: OccupationSerializer, status: :created end diff --git a/app/services/groups/deactivate.rb b/app/services/groups/deactivate.rb index 6d29d46..bc8c43e 100644 --- a/app/services/groups/deactivate.rb +++ b/app/services/groups/deactivate.rb @@ -5,7 +5,7 @@ def call(user:, id:) ActiveRecord::Base.transaction do group = user.groups.active.lock.find(id) occupation = group.occupations.active.lock.first - close_occupation(occupation) if occupation + close_occupation(user:, occupation:) if occupation group.soft_delete! group end @@ -13,10 +13,10 @@ def call(user:, id:) private - def close_occupation(occupation) - occupation.terrain.lock! + def close_occupation(user:, occupation:) + terrain = user.terrains.lock.find(occupation.terrain_id) Occupations::Close.call(occupation:) - Terrains::StartRest.call(terrain: occupation.terrain) + Terrains::StartRest.call(terrain:) end end end diff --git a/app/services/occupations/move.rb b/app/services/occupations/move.rb index 3339bc9..abf0750 100644 --- a/app/services/occupations/move.rb +++ b/app/services/occupations/move.rb @@ -1,12 +1,12 @@ module Occupations class Move class << self - def call(group:, destination:) + def call(user:, group:, destination:) ActiveRecord::Base.transaction do - group.lock! + group = user.groups.lock.find(group.id) current_occupation = group.occupations.active.lock.first - lock_terrains!(current_occupation, destination) + destination = lock_terrains!(user:, current_occupation:, destination:) MovementPolicy.validate!(group:, current_occupation:, destination:) @@ -28,9 +28,10 @@ def close_current_occupation(occupation) Terrains::StartRest.call(terrain: occupation.terrain) end - def lock_terrains!(current_occupation, destination) + def lock_terrains!(user:, current_occupation:, destination:) terrains_ids = [ current_occupation&.terrain_id, destination.id ].compact.uniq.sort - Terrain.where(id: terrains_ids).order(:id).lock.load + terrains = user.terrains.order(:id).lock.find(terrains_ids) + terrains.find { |terrain| terrain.id == destination.id } end end end diff --git a/app/services/occupations/open.rb b/app/services/occupations/open.rb index 396fcfa..cad5fd0 100644 --- a/app/services/occupations/open.rb +++ b/app/services/occupations/open.rb @@ -2,6 +2,8 @@ module Occupations class Open class << self def call(group:, terrain:) + raise ActiveRecord::RecordNotFound unless group.user_id == terrain.user_id + Occupation.create!( group:, terrain:, diff --git a/spec/requests/api/v1/occupations/create_spec.rb b/spec/requests/api/v1/occupations/create_spec.rb index da20b1b..d10607a 100644 --- a/spec/requests/api/v1/occupations/create_spec.rb +++ b/spec/requests/api/v1/occupations/create_spec.rb @@ -71,4 +71,13 @@ expect(response).to have_http_status(:not_found) end + + it "returns not found for a group belonging to another user" do + other_group = create(:group) + post "/api/v1/occupations", params: { + occupation: { group_id: other_group.id, terrain_id: terrain.id } + }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/occupations/history_by_group_spec.rb b/spec/requests/api/v1/occupations/history_by_group_spec.rb index e38327e..2eaf1c2 100644 --- a/spec/requests/api/v1/occupations/history_by_group_spec.rb +++ b/spec/requests/api/v1/occupations/history_by_group_spec.rb @@ -13,10 +13,13 @@ it "returns the occupations for the group" do group = create(:group, user:) occupation = create(:occupation, group:) + create(:occupation) get "/api/v1/groups/#{group.id}/occupations", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) expect(body["data"].pluck("id")).to eq([ occupation.id ]) + expect(body["group"]["id"]).to eq(group.id) + expect(body["meta"]["total_items"]).to eq(1) end it "returns unauthorized without valid authentication" do diff --git a/spec/requests/api/v1/occupations/history_by_terrain_spec.rb b/spec/requests/api/v1/occupations/history_by_terrain_spec.rb index 029c3a3..02ec29b 100644 --- a/spec/requests/api/v1/occupations/history_by_terrain_spec.rb +++ b/spec/requests/api/v1/occupations/history_by_terrain_spec.rb @@ -13,10 +13,13 @@ it "returns the occupations for the terrain" do terrain = create(:terrain, user:) occupation = create(:occupation, terrain:, group: create(:group, user:)) + create(:occupation) get "/api/v1/terrains/#{terrain.id}/occupations", headers: headers body = response.parsed_body expect(response).to have_http_status(:ok) expect(body["data"].pluck("id")).to eq([ occupation.id ]) + expect(body["terrain"]["id"]).to eq(terrain.id) + expect(body["meta"]["total_items"]).to eq(1) end it "returns unauthorized without valid authentication" do diff --git a/spec/services/groups/deactivate_spec.rb b/spec/services/groups/deactivate_spec.rb index e75d57f..8adc355 100644 --- a/spec/services/groups/deactivate_spec.rb +++ b/spec/services/groups/deactivate_spec.rb @@ -53,5 +53,19 @@ expect(occupation.reload).to be_active expect(terrain.reload).to be_occupied end + + it "does not deactivate a group whose occupation references another user's terrain" do + group = create(:group, user:) + terrain = create(:terrain, :occupied, rest_days: 10) + occupation = create(:occupation, group:, terrain:) + + expect { + described_class.call(user:, id: group.id) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(group.reload).not_to be_deleted + expect(occupation.reload).to be_active + expect(terrain.reload).to be_occupied + end end end diff --git a/spec/services/occupations/move_spec.rb b/spec/services/occupations/move_spec.rb index 9b3b36a..968b3c2 100644 --- a/spec/services/occupations/move_spec.rb +++ b/spec/services/occupations/move_spec.rb @@ -2,21 +2,22 @@ RSpec.describe Occupations::Move, type: :service do describe '.call' do - let (:group) { create(:group) } + let(:user) { create(:user) } + let (:group) { create(:group, user:) } it "moves the occupation without a current occupation to a terrain" do - destination = create(:terrain) - occupation = described_class.call(group:, destination:) + destination = create(:terrain, user:) + occupation = described_class.call(user: user, group:, destination:) expect(occupation).to be_persisted expect(occupation).to have_attributes(group:, terrain: destination, ended_at: nil) expect(destination.reload).to be_occupied end it "moves a group from one terrain to another" do - origin = create(:terrain, :occupied, rest_days: 10) - destination = create(:terrain) + origin = create(:terrain, :occupied, user: user, rest_days: 10) + destination = create(:terrain, user:) previous_occupation = create(:occupation, group:, terrain: origin) - occupation = described_class.call(group:, destination:) + occupation = described_class.call(user: user, group:, destination:) expect(previous_occupation.reload).to be_finished expect(origin.reload).to be_resting expect(occupation).to be_active @@ -25,29 +26,71 @@ end it "makes the origin terrain when it does not require rest" do - origin = create(:terrain, :occupied, rest_days: 0) - destination = create(:terrain) + origin = create(:terrain, :occupied, user: user, rest_days: 0) + destination = create(:terrain, user:) create(:occupation, group:, terrain: origin) - described_class.call(group:, destination:) + described_class.call(user: user, group:, destination:) expect(origin.reload).to be_available end it "allows movement to a resting terrain" do - destination = create(:terrain, :resting) - occupation = described_class.call(group:, destination:) + destination = create(:terrain, :resting, user:) + occupation = described_class.call(user: user, group:, destination:) expect(occupation.terrain).to eq(destination) expect(destination.reload).to be_occupied end it "does not change the current occupation when the movement is invalid" do - origin = create(:terrain, :occupied, rest_days: 10) - destination = create(:terrain, :occupied) + origin = create(:terrain, :occupied, user: user, rest_days: 10) + destination = create(:terrain, :occupied, user:) current_occupation = create(:occupation, group:, terrain: origin) expect { - described_class.call(group:, destination:) + described_class.call(user: user, group:, destination:) }.to raise_error(Occupations::MovementError) expect(current_occupation.reload).to be_active expect(origin.reload).to be_occupied end + + it "does not move another user's group even when the destination has the same owner" do + other_group = create(:group) + destination = create(:terrain, user: other_group.user) + + expect { + described_class.call(user:, group: other_group, destination:) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(other_group.occupations).to be_empty + expect(destination.reload).to be_available + end + + it "does not change the current occupation when the destination belongs to another user" do + origin = create(:terrain, :occupied, user:, rest_days: 10) + destination = create(:terrain) + occupation = create(:occupation, group:, terrain: origin) + + expect { + described_class.call(user:, group:, destination:) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(group.occupations).to contain_exactly(occupation) + expect(occupation.reload).to be_active + expect(origin.reload).to be_occupied + expect(destination.reload).to be_available + end + + it "does not move a group whose current occupation references another user's terrain" do + origin = create(:terrain, :occupied) + destination = create(:terrain, user:) + occupation = create(:occupation, group:, terrain: origin) + + expect { + described_class.call(user:, group:, destination:) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(group.occupations).to contain_exactly(occupation) + expect(occupation.reload).to be_active + expect(origin.reload).to be_occupied + expect(destination.reload).to be_available + end end end diff --git a/spec/services/occupations/open_spec.rb b/spec/services/occupations/open_spec.rb index 42779e0..a689fab 100644 --- a/spec/services/occupations/open_spec.rb +++ b/spec/services/occupations/open_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Occupations::Open, type: :service do describe '.call' do let(:group) { create(:group) } - let(:terrain) { create(:terrain) } + let(:terrain) { create(:terrain, user: group.user) } it 'creates an occupation' do expect { @@ -17,5 +17,15 @@ expect(occupation).to have_attributes(group:, terrain:, ended_at: nil) expect(occupation.started_at).to be_present end + + it "does not create an occupation between resources of different users" do + other_terrain = create(:terrain) + + expect { + described_class.call(group:, terrain: other_terrain) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(Occupation.count).to eq(0) + end end end From fe3e6e366a3447a300f2385171abba427496073a Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 18:27:03 -0300 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20isola=20movimenta=C3=A7=C3=B5es?= =?UTF-8?q?=20de=20ra=C3=A7=C3=A3o=20por=20usu=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/feed_movements_controller.rb | 2 ++ app/services/feed_movements/create_entry.rb | 4 +-- app/services/feed_movements/create_exit.rb | 4 +-- .../v1/feed_movements/create_entry_spec.rb | 17 ++++++++-- .../api/v1/feed_movements/create_exit_spec.rb | 19 ++++++++--- .../api/v1/feed_movements/index_spec.rb | 1 + .../feed_movements/create_entry_spec.rb | 26 ++++++++++++--- .../feed_movements/create_exit_spec.rb | 32 +++++++++++++++---- 8 files changed, 84 insertions(+), 21 deletions(-) diff --git a/app/controllers/api/v1/feed_movements_controller.rb b/app/controllers/api/v1/feed_movements_controller.rb index d91b569..dc8e5bb 100644 --- a/app/controllers/api/v1/feed_movements_controller.rb +++ b/app/controllers/api/v1/feed_movements_controller.rb @@ -5,6 +5,7 @@ class FeedMovementsController < BaseController def create_entry movement = FeedMovements::CreateEntry.call( + user: current_user, feed_id: params[:feed_id], quantity: entry_params[:quantity], value: entry_params[:value], @@ -15,6 +16,7 @@ def create_entry def create_exit movement = FeedMovements::CreateExit.call( + user: current_user, feed_id: params[:feed_id], quantity: exit_params[:quantity], reason: exit_params[:reason], diff --git a/app/services/feed_movements/create_entry.rb b/app/services/feed_movements/create_entry.rb index aa81d6f..4c124f3 100644 --- a/app/services/feed_movements/create_entry.rb +++ b/app/services/feed_movements/create_entry.rb @@ -1,9 +1,9 @@ module FeedMovements class CreateEntry class << self - def call(feed_id:, quantity:, value:, occurred_on:) + def call(user:, feed_id:, quantity:, value:, occurred_on:) ActiveRecord::Base.transaction do - feed = Feed.active.lock.find(feed_id) + feed = user.feeds.active.lock.find(feed_id) movement = FeedMovement.create!( feed:, diff --git a/app/services/feed_movements/create_exit.rb b/app/services/feed_movements/create_exit.rb index 434793d..8c0e626 100644 --- a/app/services/feed_movements/create_exit.rb +++ b/app/services/feed_movements/create_exit.rb @@ -1,9 +1,9 @@ module FeedMovements class CreateExit class << self - def call(feed_id:, quantity:, reason:, occurred_on:) + def call(user:, feed_id:, quantity:, reason:, occurred_on:) ActiveRecord::Base.transaction do - feed = Feed.active.lock.find(feed_id) + feed = user.feeds.active.lock.find(feed_id) movement = FeedMovement.new( feed:, diff --git a/spec/requests/api/v1/feed_movements/create_entry_spec.rb b/spec/requests/api/v1/feed_movements/create_entry_spec.rb index 128a5d9..7089fb3 100644 --- a/spec/requests/api/v1/feed_movements/create_entry_spec.rb +++ b/spec/requests/api/v1/feed_movements/create_entry_spec.rb @@ -11,7 +11,7 @@ end it "creates a entry movement for the feed" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { quantity: 50, value: 200, @@ -31,7 +31,7 @@ end it "returns unprocessable content when attributes are invalid" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { quantity: -50, value: 200, @@ -53,7 +53,7 @@ end it "returns unauthorized when no token is provided" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { quantity: 50, value: 200, @@ -62,4 +62,15 @@ expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's feed" do + feed = create(:feed) + post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { + quantity: 50, + value: 200, + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/feed_movements/create_exit_spec.rb b/spec/requests/api/v1/feed_movements/create_exit_spec.rb index 4c1ef45..bf9fdd8 100644 --- a/spec/requests/api/v1/feed_movements/create_exit_spec.rb +++ b/spec/requests/api/v1/feed_movements/create_exit_spec.rb @@ -11,7 +11,7 @@ end it "creates an exit movement for a feed" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { quantity: 50, reason: "Sold", @@ -31,7 +31,7 @@ end it "returns unprocessable content when attributes are invalid" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { quantity: 150, reason: "Sold", @@ -43,7 +43,7 @@ end it "returns unprocessable content when stock quantity is insufficient" do - feed = create(:feed, stock_quantity: 30) + feed = create(:feed, user: user, stock_quantity: 30) post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { quantity: 50, reason: "Sold", @@ -65,7 +65,7 @@ end it "returns unauthorized when no token is provided" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { quantity: 50, reason: "Sold", @@ -74,4 +74,15 @@ expect(response).to have_http_status(:unauthorized) end + + it "returns not found for another user's feed even when stock is insufficient" do + feed = create(:feed, stock_quantity: 0) + post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { + quantity: 50, + reason: "Sold", + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end end diff --git a/spec/requests/api/v1/feed_movements/index_spec.rb b/spec/requests/api/v1/feed_movements/index_spec.rb index 1f7c3c6..f554bcc 100644 --- a/spec/requests/api/v1/feed_movements/index_spec.rb +++ b/spec/requests/api/v1/feed_movements/index_spec.rb @@ -24,6 +24,7 @@ it "returns a paginated list of feed movements" do feed = create(:feed, user: user, stock_quantity: 100) create_list(:feed_movement, 3, feed:, movement_type: "entry", quantity: 50, value: 200, occurred_on: Date.current) + create_list(:feed_movement, 2) get "/api/v1/feeds/#{feed.id}/movements", headers: headers, params: { page: 1, per_page: 2 } body = response.parsed_body expect(response).to have_http_status(:ok) diff --git a/spec/services/feed_movements/create_entry_spec.rb b/spec/services/feed_movements/create_entry_spec.rb index 82522dc..e53b113 100644 --- a/spec/services/feed_movements/create_entry_spec.rb +++ b/spec/services/feed_movements/create_entry_spec.rb @@ -2,9 +2,12 @@ RSpec.describe FeedMovements::CreateEntry, type: :service do describe '.call' do + let(:user) { create(:user) } + it "creates a entry movement" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) movement = described_class.call( + user: user, feed_id: feed.id, quantity: 50, value: 200, @@ -19,8 +22,9 @@ end it "increases the feed stock quantity" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) described_class.call( + user: user, feed_id: feed.id, quantity: 50, value: 200, @@ -30,9 +34,10 @@ end it "does not create a movement when attributes are invalid" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) expect { described_class.call( + user: user, feed_id: feed.id, quantity: -50, value: 200, @@ -44,9 +49,10 @@ end it "raises an error when the feed is deleted" do - feed = create(:feed, :deleted) + feed = create(:feed, :deleted, user:) expect { described_class.call( + user: user, feed_id: feed.id, quantity: 50, value: 200, @@ -54,5 +60,17 @@ ) }.to raise_error(ActiveRecord::RecordNotFound) end + + it "does not change another user's stock or history" do + feed = create(:feed, stock_quantity: 100) + existing_movement = create(:feed_movement, feed:) + + expect { + described_class.call(user:, feed_id: feed.id, quantity: 50, value: 200, occurred_on: Date.current) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(feed.reload.stock_quantity).to eq(100) + expect(feed.feed_movements).to contain_exactly(existing_movement) + end end end diff --git a/spec/services/feed_movements/create_exit_spec.rb b/spec/services/feed_movements/create_exit_spec.rb index f3791d8..6c82fdc 100644 --- a/spec/services/feed_movements/create_exit_spec.rb +++ b/spec/services/feed_movements/create_exit_spec.rb @@ -2,9 +2,12 @@ RSpec.describe FeedMovements::CreateExit, type: :service do describe '.call' do + let(:user) { create(:user) } + it "creates an exit movement" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) movement = described_class.call( + user: user, feed_id: feed.id, quantity: 50, reason: "Sold", @@ -19,8 +22,9 @@ end it "decreases the feed stock quantity" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) described_class.call( + user: user, feed_id: feed.id, quantity: 50, reason: "Sold", @@ -30,8 +34,9 @@ end it "allows removing the entire available stock" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) described_class.call( + user: user, feed_id: feed.id, quantity: 100, reason: "Sold", @@ -41,9 +46,10 @@ end it "does not allow exit movement when stock is insufficient" do - feed = create(:feed, stock_quantity: 30) + feed = create(:feed, user: user, stock_quantity: 30) expect { described_class.call( + user: user, feed_id: feed.id, quantity: 50, reason: "Sold", @@ -55,9 +61,10 @@ end it "does not create a movement when attributes are invalid" do - feed = create(:feed, stock_quantity: 100) + feed = create(:feed, user: user, stock_quantity: 100) expect { described_class.call( + user: user, feed_id: feed.id, quantity: -50, reason: "Sold", @@ -69,9 +76,10 @@ end it "raises an error when the feed is deleted" do - feed = create(:feed, :deleted) + feed = create(:feed, :deleted, user:) expect { described_class.call( + user: user, feed_id: feed.id, quantity: 50, reason: "Sold", @@ -79,5 +87,17 @@ ) }.to raise_error(ActiveRecord::RecordNotFound) end + + it "does not change another user's stock or history" do + feed = create(:feed, stock_quantity: 100) + existing_movement = create(:feed_movement, feed:) + + expect { + described_class.call(user:, feed_id: feed.id, quantity: 50, reason: "Sold", occurred_on: Date.current) + }.to raise_error(ActiveRecord::RecordNotFound) + + expect(feed.reload.stock_quantity).to eq(100) + expect(feed.feed_movements).to contain_exactly(existing_movement) + end end end From cdf075ce6f3c08c2f3d7c3531d642e7ce48da135 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 18:34:05 -0300 Subject: [PATCH 7/7] test: substitui unprocessable_entity por unprocessable_content --- spec/requests/api/v1/auth/register_spec.rb | 2 +- spec/requests/api/v1/groups/create_spec.rb | 2 +- spec/requests/api/v1/groups/update_spec.rb | 2 +- spec/requests/api/v1/password/reset_spec.rb | 2 +- spec/requests/api/v1/terrains/create_spec.rb | 2 +- spec/requests/api/v1/terrains/destroy_spec.rb | 2 +- spec/requests/api/v1/terrains/update_spec.rb | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/requests/api/v1/auth/register_spec.rb b/spec/requests/api/v1/auth/register_spec.rb index 1ac83f3..452059a 100644 --- a/spec/requests/api/v1/auth/register_spec.rb +++ b/spec/requests/api/v1/auth/register_spec.rb @@ -31,7 +31,7 @@ it "returns validation errors for invalid user attributes" do post "/api/v1/auth/register", params: { user: attributes.merge(email: nil) }, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["email"]).to be_present end end diff --git a/spec/requests/api/v1/groups/create_spec.rb b/spec/requests/api/v1/groups/create_spec.rb index 5cf41ba..cfd1e78 100644 --- a/spec/requests/api/v1/groups/create_spec.rb +++ b/spec/requests/api/v1/groups/create_spec.rb @@ -25,7 +25,7 @@ it "returns validations errors with invalid attributes" do post "/api/v1/groups", params: { group: attributes.merge(name: nil) }, headers: headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["name"]).to be_present end diff --git a/spec/requests/api/v1/groups/update_spec.rb b/spec/requests/api/v1/groups/update_spec.rb index cf9afbd..ffc5cd9 100644 --- a/spec/requests/api/v1/groups/update_spec.rb +++ b/spec/requests/api/v1/groups/update_spec.rb @@ -23,7 +23,7 @@ it "returns validation errors when the attributes are invalid" do group = create(:group, user:) patch "/api/v1/groups/#{group.id}", params: { group: { animal_count: -10 } }, headers: headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["animal_count"]).to be_present end diff --git a/spec/requests/api/v1/password/reset_spec.rb b/spec/requests/api/v1/password/reset_spec.rb index 03522e2..67dd3e8 100644 --- a/spec/requests/api/v1/password/reset_spec.rb +++ b/spec/requests/api/v1/password/reset_spec.rb @@ -29,7 +29,7 @@ it "returns validation errors for an invalid password" do patch "/api/v1/password/reset", params: { reset_token:, password: "short", password_confirmation: "short" }, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["password"]).to be_present end diff --git a/spec/requests/api/v1/terrains/create_spec.rb b/spec/requests/api/v1/terrains/create_spec.rb index 23b1afd..4127637 100644 --- a/spec/requests/api/v1/terrains/create_spec.rb +++ b/spec/requests/api/v1/terrains/create_spec.rb @@ -26,7 +26,7 @@ it "returns validations errors with invalid attributes" do post "/api/v1/terrains", params: { terrain: attributes.merge(name: nil) }, headers: headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["name"]).to be_present end diff --git a/spec/requests/api/v1/terrains/destroy_spec.rb b/spec/requests/api/v1/terrains/destroy_spec.rb index 4e314bb..25bbc23 100644 --- a/spec/requests/api/v1/terrains/destroy_spec.rb +++ b/spec/requests/api/v1/terrains/destroy_spec.rb @@ -25,7 +25,7 @@ terrain = create(:terrain, :occupied, user:) create(:occupation, terrain:, group: create(:group, user:)) delete "/api/v1/terrains/#{terrain.id}", headers: headers - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["base"]).to be_present end diff --git a/spec/requests/api/v1/terrains/update_spec.rb b/spec/requests/api/v1/terrains/update_spec.rb index 8da41a7..858a0d3 100644 --- a/spec/requests/api/v1/terrains/update_spec.rb +++ b/spec/requests/api/v1/terrains/update_spec.rb @@ -26,7 +26,7 @@ it "return validation errors when the attributes are invalid" do terrain = create(:terrain, user:) patch "/api/v1/terrains/#{terrain.id}", params: { terrain: { rest_days: -5 } }, headers: headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.parsed_body["errors"]["rest_days"]).to be_present end