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.
143 lines
8.5 KiB
Ruby
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
|