Files
ubicloud/spec/model/invoice_spec.rb
Jeremy Evans 096ac0111b Switch access control to use AccessControlEntry and {Subject,Action,Object}Tag
Previously, access control was implemented via AccessPolicy and {Access/AppliedTag}.
This doesn't remove AccessPolicy/AppliedTag yet, as there are still references to
them.  They will be removed in a later commit.

In order for the new access control to work, the authorization methods need to be
passed the project_id, to force the access query to operate on a single project.
That part isn't very invasive, since authorization is mostly centralized in
helpers/general.rb, but it did require a couple other places to be changed:

* model/invoice.rb is the only caller of Authorization.authorize outside of
  the Clover helpers.  This is probably a layer violation that should be fixed
  later, but for now, add the project_id manually to the call.

* In the github route, set @project so that the helpers can access it.

Add Project#dissociate_subject, which handles remove the subject tags and
direct access control entries when removing a user or token from a project.
Call this when removing uses or tokens from a project.

Remove Authorization.authorized_resources_dataset, as it was only
needed by Dataset#authorize.  This inlines the code into Dataset#authorize.
This was done because it doesn't make sense to use the method in isolation,
and the query needed in Dataset#authorize depends on the dataset, which
isn't passed to Authorization.authorized_resources_dataset.

Remove Authorization.expand_actions.  This method has no equivalent in the
new design, and it is no longer needed.

For the api specs that use a personal access token, this adds the
token to the Admin subject tag (which has full access).  This has
similar behavior as applying the admin managed policy had previously.

The authorization queries do not currently support nested tags.  Support
for nested tags will be added later.

While here, fix parameter name for Clover#all_permissions.
This method is only currently called with @project.id as the argument,
so it doesn't technically need an argument, but it could potentially
be used later to get all permissions on an object other than the
current project.
2025-01-09 09:55:55 -08:00

143 lines
8.5 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
RSpec.describe Invoice do
subject(:invoice) { described_class.new(id: "50d5aae4-311c-843b-b500-77fbc7778050", begin_time: Time.now, end_time: Time.now, created_at: Time.now, content: {"cost" => 10, "subtotal" => 11, "credit" => 1, "discount" => 0, "resources" => []}, status: "unpaid") }
let(:billing_info) { BillingInfo.create_with_id(stripe_id: "cs_1234567890") }
before do
allow(invoice).to receive(:reload)
allow(invoice).to receive(:project).and_return(instance_double(Project, path: "/project/p1", accounts: []))
allow(Config).to receive(:stripe_secret_key).and_return("secret_key")
end
describe ".send_failure_email" do
it "sends failure email to accounts with billing permissions in addition to the provided billing email" do
project = Project.create_with_id(name: "cool-project").tap { |p| p.associate_with_project(p) }
accounts = (0..2).map { Account.create_with_id(email: "account#{_1}@example.com").tap { |a| a.associate_with_project(project) } }
AccessControlEntry.create_with_id(project_id: project.id, subject_id: accounts[0].id)
AccessControlEntry.create_with_id(project_id: project.id, subject_id: accounts[1].id, action_id: ActionType::NAME_MAP["Vm:view"])
AccessControlEntry.create_with_id(project_id: project.id, subject_id: accounts[2].id, action_id: ActionType::NAME_MAP["Project:billing"])
invoice = described_class.create_with_id(project_id: project.id, invoice_number: "001", begin_time: Time.now, end_time: Time.now, content: {
"billing_info" => {"email" => "billing@example.com"},
"resources" => [],
"subtotal" => 0.0,
"credit" => 0.0,
"discount" => 0.0,
"cost" => 0.0
})
invoice.send_failure_email([])
expect(Mail::TestMailer.deliveries.first.to).to contain_exactly("billing@example.com", accounts[0].email, accounts[2].email)
end
end
describe ".charge" do
it "not charge if Stripe not enabled" do
allow(Config).to receive(:stripe_secret_key).and_return(nil)
expect(Clog).to receive(:emit).with("Billing is not enabled. Set STRIPE_SECRET_KEY to enable billing.").and_call_original
expect(invoice.charge).to be true
end
it "not charge if already charged" do
expect(Clog).to receive(:emit).with("Invoice already charged.").and_call_original
invoice.status = "paid"
expect(invoice.charge).to be true
end
it "not charge if less than minimum charge threshold" do
invoice.content["billing_info"] = {"id" => billing_info.id, "email" => "customer@example.com"}
invoice.content["cost"] = 0.4
expect(invoice).to receive(:update).with(status: "below_minimum_threshold")
expect(Clog).to receive(:emit).with("Invoice cost is less than minimum charge cost.").and_call_original
expect(invoice.charge).to be true
expect(Mail::TestMailer.deliveries.length).to eq 1
end
it "not charge if doesn't have billing info" do
expect(Clog).to receive(:emit).with("Invoice doesn't have billing info.").and_call_original
expect(invoice.charge).to be false
end
it "not charge if no payment methods" do
invoice.content["billing_info"] = {"id" => billing_info.id}
expect(Clog).to receive(:emit).with("Invoice doesn't have billing info.").and_call_original
expect(invoice.charge).to be false
end
it "not charge if all payment methods fails" do
invoice.content["billing_info"] = {"id" => billing_info.id}
payment_method1 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_1", order: 1)
payment_method2 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_2", order: 2)
# rubocop:disable RSpec/VerifiedDoubles
expect(Stripe::PaymentIntent).to receive(:create).with(hash_including(amount: 1000, customer: billing_info.stripe_id, payment_method: payment_method1.stripe_id))
.and_raise(Stripe::CardError.new("Unsufficient funds", {}))
expect(Stripe::PaymentIntent).to receive(:create).with(hash_including(amount: 1000, customer: billing_info.stripe_id, payment_method: payment_method2.stripe_id))
.and_raise(Stripe::CardError.new("Card declined", {}))
# rubocop:enable RSpec/VerifiedDoubles
expect(Clog).to receive(:emit).with("Invoice couldn't charged.").and_call_original.twice
expect(Clog).to receive(:emit).with("Invoice couldn't charged with any payment method.").and_call_original
expect(invoice.charge).to be false
expect(Mail::TestMailer.deliveries.length).to eq 1
end
it "fails if PaymentIntent does not raise an exception in case of failure" do
invoice.content["billing_info"] = {"id" => billing_info.id}
payment_method = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_1", order: 1)
# rubocop:disable RSpec/VerifiedDoubles
expect(Stripe::PaymentIntent).to receive(:create).with(hash_including(amount: 1000, customer: billing_info.stripe_id, payment_method: payment_method.stripe_id))
.and_return(double(Stripe::PaymentIntent, id: "payment-intent-id", status: "failed"))
# rubocop:enable RSpec/VerifiedDoubles
expect(Clog).to receive(:emit).with("BUG: payment intent should succeed here").and_call_original
expect(Clog).to receive(:emit).with("Invoice couldn't charged with any payment method.").and_call_original
expect(invoice.charge).to be false
end
it "can charge from a correct payment method even some of them are not working" do
invoice.content["billing_info"] = {"id" => billing_info.id, "email" => "customer@example.com"}
invoice.content["resources"] = [{"cost" => 4.3384, "line_items" => [{"cost" => 4.3384, "amount" => 5423.0, "duration" => 1, "location" => "global", "description" => "standard-2 GitHub Runner", "resource_type" => "GitHubRunnerMinutes", "resource_family" => "standard-2"}], "resource_id" => "ed0b26bf-53c4-82d2-9a00-21e5f05dc364", "resource_name" => "Daily Usage 2024-02-26"}]
payment_method1 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_1", order: 1)
payment_method2 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_2", order: 2)
payment_method3 = PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_3", order: 3)
# rubocop:disable RSpec/VerifiedDoubles
expect(Stripe::PaymentIntent).to receive(:create).with(hash_including(amount: 1000, customer: billing_info.stripe_id, payment_method: payment_method1.stripe_id))
.and_raise(Stripe::CardError.new("Declined", {}))
expect(Stripe::PaymentIntent).to receive(:create).with(hash_including(amount: 1000, customer: billing_info.stripe_id, payment_method: payment_method2.stripe_id))
.and_return(double(Stripe::PaymentIntent, status: "succeeded", id: "pi_1234567890"))
expect(Stripe::PaymentIntent).not_to receive(:create).with(hash_including(payment_method: payment_method3.stripe_id))
# rubocop:enable RSpec/VerifiedDoubles
expect(invoice).to receive(:save).with(columns: [:status, :content])
expect(Clog).to receive(:emit).with("Invoice couldn't charged.").and_call_original
expect(Clog).to receive(:emit).with("Invoice charged.").and_call_original
project = instance_double(Project)
expect(project).to receive(:update).with(reputation: "verified")
expect(invoice).to receive(:project).and_return(project)
expect(invoice.charge).to be true
expect(invoice.status).to eq("paid")
expect(invoice.content["payment_method"]["id"]).to eq(payment_method2.id)
expect(invoice.content["payment_intent"]).to eq("pi_1234567890")
expect(Mail::TestMailer.deliveries.length).to eq 1
expect(Mail::TestMailer.deliveries.first.attachments.length).to eq 1
end
it "does not update project reputation if cost is less than 5" do
invoice.content["cost"] = 4
invoice.content["billing_info"] = {"id" => billing_info.id}
PaymentMethod.create_with_id(billing_info_id: billing_info.id, stripe_id: "pm_1", order: 1)
# rubocop:disable RSpec/VerifiedDoubles
expect(Stripe::PaymentIntent).to receive(:create).and_return(double(Stripe::PaymentIntent, status: "succeeded", id: "pi_1234567890"))
# rubocop:enable RSpec/VerifiedDoubles
expect(invoice).to receive(:save).with(columns: [:status, :content])
expect(invoice).to receive(:send_success_email)
expect(invoice).not_to receive(:project)
expect(invoice.charge).to be true
end
end
end