Merge pull request #15 from exstake/feature/create-exchange-order-mutations

Feature/create exchange order mutations
This commit is contained in:
João Geonizeli
2021-08-15 11:58:23 -03:00
committed by GitHub
36 changed files with 858 additions and 45 deletions

View File

@@ -8,6 +8,7 @@ ruby "2.7.4"
gem "pg", "~> 1.1"
gem "puma", "~> 5.0"
gem "rails", "~> 6.1.4"
gem "rails-i18n", "~> 6.0"
gem "sass-rails", ">= 6"
gem "turbolinks", "~> 5"
gem "webpacker", "~> 5.0"

View File

@@ -216,6 +216,9 @@ GEM
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.1.4)
actionpack (= 6.1.4)
activesupport (= 6.1.4)
@@ -351,6 +354,7 @@ DEPENDENCIES
pundit
rails (~> 6.1.4)
rails-erd
rails-i18n (~> 6.0)
rspec-graphql_matchers (~> 1.3)
rspec-rails
rubocop-rails

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Inputs
class CreateBuyCryptoOrderAttributesInput < Types::BaseInputObject
argument :currency_id, ID, required: true
argument :amount_cents, Integer, "Amount to be paid", required: true
end
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Inputs
class CreateSellCryptoOrderAttributesInput < Types::BaseInputObject
argument :currency_id, ID, required: true
argument :amount, String, "Amount to be paid", required: true
end
end

View File

@@ -6,8 +6,16 @@ module Mutations
input_object_class Types::BaseInputObject
object_class Types::BaseObject
field :errors, [String],
field :errors, [Types::RecordInvalidType],
null: true,
description: "Errors encountered during execution of the mutation."
def current_user
context[:current_user]
end
def decode_id(encoded_id)
GraphQL::Schema::UniqueWithinType.decode(encoded_id).last
end
end
end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Mutations
class CreateBuyCryptoOrder < BaseMutation
field :order, Types::BuyCryptoOrderType, null: true
argument :order, Inputs::CreateBuyCryptoOrderAttributesInput, required: true
def resolve(order:)
currency_id = decode_id(order[:currency_id])
ActiveRecord::Base.transaction do
current_user.fiat_balance.withdrawal!(order[:amount_cents])
record = BuyCryptoOrder.create!(
paid_amount_cents: order[:amount_cents],
currency_id: currency_id,
user_id: current_user.id,
)
{ order: record }
rescue ActiveRecord::RecordInvalid => e
{ errors: Resolvers::ModelErrors.from_active_record_model(e.record) }
end
end
end
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Mutations
class CreateSellCryptoOrder < BaseMutation
field :order, Types::SellCryptoOrderType, null: true
argument :order, Inputs::CreateSellCryptoOrderAttributesInput, required: true
def resolve(order:)
currency_id = decode_id(order[:currency_id])
amount = BigDecimal(order[:amount])
ActiveRecord::Base.transaction do
current_user.balances.find_by!(currency_id: currency_id)
.withdrawal!(amount)
record = SellCryptoOrder.create(
paid_amount: amount,
currency_id: currency_id,
user_id: current_user.id,
)
{ order: record }
rescue ActiveRecord::RecordInvalid => e
{ errors: Resolvers::ModelErrors.from_active_record_model(e.record) }
end
end
end
end

View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
module Resolvers
class ModelErrors
attr_reader :full_messages, :field_name, :messages, :path
def initialize(args)
@full_messages = args[:full_messages]
@field_name = args[:field_name]
@messages = args[:messages]
@path = args[:path]
end
def self.from_active_record_model(model)
return if model&.errors.blank?
model.errors.messages.map do |field, messages|
new(
full_messages: model.errors.full_messages_for(field),
field_name: field,
messages: messages,
path: ["attributes", field]
)
end
end
def self.from_active_record_model_errors(errors)
return if errors.blank?
errors.messages.map do |field, messages|
new(
full_messages: errors.full_messages_for(field),
field_name: field,
messages: messages,
path: ["attributes", field]
)
end
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Types
class BuyCryptoOrderType < Types::BaseObject
implements GraphQL::Types::Relay::Node
global_id_field :id
graphql_name "BuyCryptoOrder"
field :id, ID, null: false
field :currency, CurrencyType, null: false
def currency
dataloader.with(Dataloader::Source, Currency).load(object.currency_id)
end
field :status, ProcessStatusEnum, null: false
field :paid_amount_cents, Integer, null: false
field :received_amount, String, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end

View File

@@ -9,7 +9,5 @@ module Types
field :id, ID, null: false
field :amount_cents, Integer, null: false
field :amount_currency, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end

View File

@@ -1,5 +1,7 @@
# frozen_string_literal: true
module Types
class MutationType < Types::BaseObject
field :create_sell_crypto_order, mutation: Mutations::CreateSellCryptoOrder
field :create_buy_crypto_order, mutation: Mutations::CreateBuyCryptoOrder
end
end

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
module Types
class ProcessStatusEnum < Types::BaseEnum
graphql_name "ProcessStatus"
value "PROCESSING", value: "processing"
value "COMPLETED", value: "completed"
value "CANCELED", value: "canceled"
end
end

View File

@@ -18,5 +18,15 @@ module Types
def fiat_balances
Pundit.policy_scope(current_user, FiatBalance)
end
field :sell_crypto_orders, SellCryptoOrderType.connection_type, null: false
def sell_crypto_orders
[]
end
field :buy_crypto_orders, BuyCryptoOrderType.connection_type, null: false
def buy_crypto_orders
[]
end
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module Types
class RecordInvalidType < Types::BaseObject
graphql_name "RecordInvalid"
field :full_messages, [String], null: false
field :field_name, String, null: true
field :messages, [String], null: true
field :path, [String], null: true
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Types
class SellCryptoOrderType < Types::BaseObject
implements GraphQL::Types::Relay::Node
global_id_field :id
graphql_name "SellCryptoOrder"
field :id, ID, null: false
field :currency, CurrencyType, null: false
def currency
dataloader.with(Dataloader::Source, Currency).load(object.currency_id)
end
field :status, ProcessStatusEnum, null: false
field :paid_amount, String, null: false
field :received_amount_cents, Integer, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class XStakeSchema < GraphQL::Schema
# mutation(Types::MutationType)
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Dataloader

View File

@@ -34,19 +34,24 @@ type BalanceEdge {
node: Balance!
}
type Currency implements Node {
type BuyCryptoOrder implements Node {
createdAt: ISO8601DateTime!
currency: Currency!
id: ID!
name: String!
paidAmountCents: Int!
receivedAmount: Float
status: ProcessStatus!
updatedAt: ISO8601DateTime!
}
"""
The connection type for Currency.
The connection type for BuyCryptoOrder.
"""
type CurrencyConnection {
type BuyCryptoOrderConnection {
"""
A list of edges.
"""
edges: [CurrencyEdge!]!
edges: [BuyCryptoOrderEdge!]!
"""
Information to aid in pagination.
@@ -57,7 +62,7 @@ type CurrencyConnection {
"""
An edge in a connection.
"""
type CurrencyEdge {
type BuyCryptoOrderEdge {
"""
A cursor for use in pagination.
"""
@@ -66,7 +71,82 @@ type CurrencyEdge {
"""
The item at the end of the edge.
"""
node: Currency!
node: BuyCryptoOrder!
}
input CreateBuyCryptoOrderAttributesInput {
"""
Amount to be paid
"""
amount: Float!
currencyId: ID!
}
"""
Autogenerated input type of CreateBuyCryptoOrder
"""
input CreateBuyCryptoOrderInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
order: CreateBuyCryptoOrderAttributesInput!
}
"""
Autogenerated return type of CreateBuyCryptoOrder
"""
type CreateBuyCryptoOrderPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]
order: BuyCryptoOrder!
}
input CreateSellCryptoOrderAttributesInput {
"""
Amount to be paid
"""
amount: Float!
currencyId: ID!
}
"""
Autogenerated input type of CreateSellCryptoOrder
"""
input CreateSellCryptoOrderInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
order: CreateSellCryptoOrderAttributesInput!
}
"""
Autogenerated return type of CreateSellCryptoOrder
"""
type CreateSellCryptoOrderPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]
order: SellCryptoOrder!
}
type Currency implements Node {
id: ID!
name: String!
}
type FiatBalance implements Node {
@@ -112,6 +192,21 @@ An ISO 8601-encoded datetime
"""
scalar ISO8601DateTime
type Mutation {
createBuyCryptoOrder(
"""
Parameters for CreateBuyCryptoOrder
"""
input: CreateBuyCryptoOrderInput!
): CreateBuyCryptoOrderPayload
createSellCryptoOrder(
"""
Parameters for CreateSellCryptoOrder
"""
input: CreateSellCryptoOrderInput!
): CreateSellCryptoOrderPayload
}
"""
An object with an ID.
"""
@@ -147,6 +242,12 @@ type PageInfo {
startCursor: String
}
enum ProcessStatus {
CANCELED
COMPLETED
PROCESSING
}
type Query {
balances(
"""
@@ -169,7 +270,7 @@ type Query {
"""
last: Int
): BalanceConnection!
currencies(
buyCryptoOrders(
"""
Returns the elements in the list that come after the specified cursor.
"""
@@ -189,7 +290,7 @@ type Query {
Returns the last _n_ elements from the list.
"""
last: Int
): CurrencyConnection!
): BuyCryptoOrderConnection!
currentUser: User
fiatBalances(
"""
@@ -232,6 +333,67 @@ type Query {
"""
ids: [ID!]!
): [Node]!
sellCryptoOrders(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): SellCryptoOrderConnection!
}
type SellCryptoOrder implements Node {
createdAt: ISO8601DateTime!
currency: Currency!
id: ID!
paidAmount: Float!
receivedAmountCents: Int
status: ProcessStatus!
updatedAt: ISO8601DateTime!
}
"""
The connection type for SellCryptoOrder.
"""
type SellCryptoOrderConnection {
"""
A list of edges.
"""
edges: [SellCryptoOrderEdge!]!
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type SellCryptoOrderEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: SellCryptoOrder!
}
type User {

View File

@@ -25,5 +25,13 @@ class Balance < ApplicationRecord
belongs_to :user
belongs_to :currency
validates :amount, presence: true
validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
def withdrawal!(value)
update!(amount: amount - value)
end
def deposit!(value)
update!(amount: amount + value)
end
end

View File

@@ -24,7 +24,17 @@ class FiatBalance < ApplicationRecord
monetize :amount_cents
validates :amount_cents, numericality: { greater_than_or_equal_to: 0 }
def amount_formatted
amount.format
end
def withdrawal!(value)
update!(amount_cents: amount_cents - value)
end
def deposit!(value)
update!(amount_cents: amount_cents + value)
end
end

View File

@@ -25,7 +25,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :validatable
has_many :documents, class_name: "UserDocument", dependent: :destroy
has_one :balance, dependent: :restrict_with_error
has_many :balances, dependent: :restrict_with_error
has_one :fiat_balance, dependent: :restrict_with_error
validates :first_name, :last_name, :email, presence: true

View File

@@ -32,6 +32,18 @@ pt-BR:
fiat_balance:
amount_formatted: Quantia
amount_cents: Quantia
currency:
name: Nome
errors:
models:
balance:
attributes:
amount:
greater_than_or_equal_to: "saldo insuficiente"
fiat_balance:
attributes:
amount_cents:
greater_than_or_equal_to: "saldo insuficiente"

View File

@@ -23,6 +23,6 @@ FactoryBot.define do
factory :fiat_balance do
association :user
amount_currency { "BRL" }
amount_cents { Faker::Number.number(digits: 10) }
amount_cents { Faker::Number.number(digits: 4) }
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
require "rails_helper"
describe Inputs::CreateBuyCryptoOrderAttributesInput do
subject { described_class }
describe "fields" do
it { is_expected.to(accept_argument("currency_id").of_type("ID!")) }
it { is_expected.to(accept_argument("amount_cents").of_type("Int!")) }
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
require "rails_helper"
describe Inputs::CreateSellCryptoOrderAttributesInput do
subject { described_class }
describe "fields" do
it { is_expected.to(accept_argument("currency_id").of_type("ID!")) }
it { is_expected.to(accept_argument("amount").of_type("String!")) }
end
end

View File

@@ -0,0 +1,121 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(Mutations::CreateBuyCryptoOrder, type: :mutation) do
let(:query_string) do
<<~GQL
mutation($currencyId: ID!, $amountCents: Int!) {
createBuyCryptoOrder(input: {
order: {
currencyId: $currencyId,
amountCents: $amountCents,
}
}) {
errors {
fullMessages
fieldName
messages
path
}
order {
status
paidAmountCents
receivedAmount
currency {
name
}
}
}
}
GQL
end
context "when the user has enough balance" do
it "withdraws from his account and creates a buy order" do
currency = create(:currency)
user = create(
:user,
fiat_balance: build(
:fiat_balance,
amount_cents: 100_00
),
)
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = {
"currencyId": currency_global_id,
"amountCents": 90_00,
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createBuyCryptoOrder" => {
"errors" => nil,
"order" => {
"status" => "PROCESSING",
"paidAmountCents" => 90_00,
"receivedAmount" => nil,
"currency" => {
"name" => "CAKE",
},
},
},
},
}))
expect(user.fiat_balance.reload.amount_cents).to(eq(10_00))
end
end
context "when the user does not have enough balance" do
it "returns withdrawl error" do
currency = create(:currency)
user = create(
:user,
fiat_balance: build(
:fiat_balance,
amount_cents: 80_00
),
)
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = {
"currencyId": currency_global_id,
"amountCents": 90_00,
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createBuyCryptoOrder" => {
"errors" => [{
"fullMessages" => ["Quantia saldo insuficiente"],
"fieldName" => "amount_cents",
"messages" => ["saldo insuficiente"],
"path" => ["attributes", "amount_cents"],
}],
"order" => nil,
},
},
}))
end
end
end

View File

@@ -0,0 +1,121 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(Mutations::CreateSellCryptoOrder, type: :mutation) do
let(:query_string) do
<<~GQL
mutation($currencyId: ID!, $amount: String!) {
createSellCryptoOrder(input: {
order: {
currencyId: $currencyId,
amount: $amount,
}
}) {
errors {
fullMessages
fieldName
messages
path
}
order {
status
paidAmount
receivedAmountCents
currency {
name
}
}
}
}
GQL
end
context "when the user has enough balance" do
it "withdraws from his account and creates a buy order" do
currency = create(:currency)
user = create(
:user,
balances: [
build(:balance, currency: currency, amount: 1.0034),
]
)
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = {
"currencyId": currency_global_id,
"amount": "0.80",
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createSellCryptoOrder" => {
"errors" => nil,
"order" => {
"status" => "PROCESSING",
"paidAmount" => "0.8",
"receivedAmountCents" => nil,
"currency" => {
"name" => "CAKE",
},
},
},
},
}))
expect(user.balances.first.reload.amount.to_s).to(eq("0.2034"))
end
end
context "when the user does not have enough balance" do
it "returns withdrawl error" do
currency = create(:currency)
user = create(
:user,
balances: [
build(:balance, currency: currency, amount: 0.0034),
]
)
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = {
"currencyId": currency_global_id,
"amount": "0.80",
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createSellCryptoOrder" => {
"errors" => [{
"fullMessages" => ["Quantia saldo insuficiente"],
"fieldName" => "amount",
"messages" => ["saldo insuficiente"],
"path" => ["attributes", "amount"],
}],
"order" => nil,
},
},
}))
expect(user.balances.first.reload.amount.to_s).to(eq("0.0034"))
end
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(Types::BuyCryptoOrderType) do
subject { described_class }
describe "arguments" do
it { is_expected.to(have_a_field(:id).of_type("ID!")) }
it { is_expected.to(have_a_field(:currency).of_type("Currency!")) }
it { is_expected.to(have_a_field(:paid_amount_cents).of_type("Int!")) }
it { is_expected.to(have_a_field(:received_amount).of_type("String")) }
it { is_expected.to(have_a_field(:status).of_type("ProcessStatus!")) }
it { is_expected.to(have_a_field(:created_at).of_type("ISO8601DateTime!")) }
it { is_expected.to(have_a_field(:updated_at).of_type("ISO8601DateTime!")) }
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
require "rails_helper"
describe Types::ProcessStatusEnum do
describe "values" do
it { expect(described_class.values.keys).to(match(["PROCESSING", "COMPLETED", "CANCELED"])) }
end
end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
require "rails_helper"
describe Types::RecordInvalidType do
subject { described_class }
describe "fields" do
it { is_expected.to(have_field(:full_messages).of_type("[String!]!")) }
it { is_expected.to(have_field(:field_name).of_type("String")) }
it { is_expected.to(have_field(:messages).of_type("[String!]")) }
it { is_expected.to(have_field(:path).of_type("[String!]")) }
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(Types::SellCryptoOrderType) do
subject { described_class }
describe "arguments" do
it { is_expected.to(have_a_field(:id).of_type("ID!")) }
it { is_expected.to(have_a_field(:currency).of_type("Currency!")) }
it { is_expected.to(have_a_field(:paid_amount).of_type("String!")) }
it { is_expected.to(have_a_field(:received_amount_cents).of_type("Int")) }
it { is_expected.to(have_a_field(:status).of_type("ProcessStatus!")) }
it { is_expected.to(have_a_field(:created_at).of_type("ISO8601DateTime!")) }
it { is_expected.to(have_a_field(:updated_at).of_type("ISO8601DateTime!")) }
end
end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: admin_users
#
# id :bigint not null, primary key
# email :string default(""), not null
# encrypted_password :string default(""), not null
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_admin_users_on_email (email) UNIQUE
# index_admin_users_on_reset_password_token (reset_password_token) UNIQUE
#
require "rails_helper"
RSpec.describe(AdminUser, type: :model) do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@@ -32,4 +32,32 @@ RSpec.describe(Balance, type: :model) do
it { is_expected.to(belong_to(:user)) }
it { is_expected.to(belong_to(:currency)) }
end
describe ".withdrwal!" do
context "when value is greater than the balance" do
it "raise ActiveRecord::RecordInvalid" do
balance = build(:balance, amount: 70.342)
expect { balance.withdrawal!(80) }.to(
raise_error(ActiveRecord::RecordInvalid, "A validação falhou: Quantia saldo insuficiente")
)
end
end
context "when value is equals to the balance" do
it "returns true" do
balance = build(:balance, amount: 70.342)
expect(balance.withdrawal!(70.342)).to(eq(true))
end
end
context "when value is smaller than the balance" do
it "returns true" do
balance = build(:balance, amount: 70.342)
expect(balance.withdrawal!(20)).to(eq(true))
end
end
end
end

View File

@@ -25,4 +25,32 @@ RSpec.describe(FiatBalance, type: :model) do
describe "associations" do
it { is_expected.to(belong_to(:user)) }
end
describe ".withdrwal!" do
context "when value is greater than the balance" do
it "raise ActiveRecord::RecordInvalid" do
balance = build(:fiat_balance, amount_cents: 100_00)
expect { balance.withdrawal!(100_50) }.to(
raise_error(ActiveRecord::RecordInvalid, "A validação falhou: Quantia saldo insuficiente")
)
end
end
context "when value is equals to the balance" do
it "returns true" do
balance = build(:fiat_balance, amount_cents: 100_00)
expect(balance.withdrawal!(100_00)).to(eq(true))
end
end
context "when value is smaller than the balance" do
it "returns true" do
balance = build(:fiat_balance, amount_cents: 100_00)
expect(balance.withdrawal!(90_00)).to(eq(true))
end
end
end
end

View File

@@ -31,7 +31,7 @@ RSpec.describe(User, type: :model) do
describe "associations" do
it { is_expected.to(have_many(:documents)) }
it { is_expected.to(have_one(:balance)) }
it { is_expected.to(have_many(:balances)) }
it { is_expected.to(have_one(:fiat_balance)) }
end
end

View File

@@ -2,5 +2,41 @@
require "rails_helper"
RSpec.describe(BalancePolicy, type: :policy) do
pending "add some examples to (or delete) #{__FILE__}"
context "when user has balances" do
it "return only balances from a user" do
create(:balance)
create(:balance)
user = build(:user)
balance = create(:balance, user: user)
balances = BalancePolicy::Scope.new(user, Balance).resolve
expect(balances).to(eq([balance]))
end
end
context "when user has not balances" do
it "return empty array" do
create(:balance)
create(:balance)
user = build(:user)
balances = BalancePolicy::Scope.new(user, Balance).resolve
expect(balances).to(eq([]))
end
end
context "when user is nil" do
it "return empty array" do
create(:balance)
create(:balance)
balances = BalancePolicy::Scope.new(nil, Balance).resolve
expect(balances).to(eq([]))
end
end
end

View File

@@ -2,5 +2,43 @@
require "rails_helper"
RSpec.describe(FiatBalancePolicy, type: :policy) do
pending "add some examples to (or delete) #{__FILE__}"
describe "::Scope" do
context "when user has balances" do
it "return only balances from a user" do
create(:fiat_balance)
create(:fiat_balance)
user = build(:user)
balance = create(:fiat_balance, user: user)
balances = FiatBalancePolicy::Scope.new(user, FiatBalance).resolve
expect(balances).to(eq([balance]))
end
end
context "when user has not balances" do
it "return empty array" do
create(:fiat_balance)
create(:fiat_balance)
user = build(:user)
balances = FiatBalancePolicy::Scope.new(user, FiatBalance).resolve
expect(balances).to(eq([]))
end
end
context "when user is nil" do
it "return empty array" do
create(:fiat_balance)
create(:fiat_balance)
balances = FiatBalancePolicy::Scope.new(nil, FiatBalance).resolve
expect(balances).to(eq([]))
end
end
end
end