mirror of
https://github.com/we-promise/sure.git
synced 2026-06-05 18:59:04 +00:00
Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's hosted /auth page actually coordinates decoupled SCA and redirects back with a code, so route these banks through it instead: - Provider#start_authorization accepts and forwards an auth_method param - EnableBankingItem#select_auth_method picks the best method (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden methods - Shared begin_authorization! re-fetches ASPSP metadata on each authorize and reauthorize, so the method is always re-derived (no persistence required) - Remove the DECOUPLED block in the controller Also stop the integration from constantly reporting "session expired": - Only a session-level GET /sessions 401/404 flips the connection to requires_update; per-account 401/404 are retried and no longer kill the whole connection - Reconcile session_expires_at from the API's access.valid_until on every sync - Treat an expired session as a graceful requires_update state instead of raising a bare error No schema changes. Adds covering tests.
107 lines
3.3 KiB
Ruby
107 lines
3.3 KiB
Ruby
require "test_helper"
|
|
|
|
class EnableBankingItemTest < ActiveSupport::TestCase
|
|
setup do
|
|
@item = EnableBankingItem.new(
|
|
family: families(:dylan_family),
|
|
name: "Test",
|
|
country_code: "DE",
|
|
application_id: "app",
|
|
client_certificate: "cert"
|
|
)
|
|
end
|
|
|
|
test "select_auth_method prefers REDIRECT over DECOUPLED and EMBEDDED" do
|
|
aspsp = {
|
|
auth_methods: [
|
|
{ name: "decoupled_app", approach: "DECOUPLED" },
|
|
{ name: "redirect_web", approach: "REDIRECT" },
|
|
{ name: "embedded_form", approach: "EMBEDDED" }
|
|
]
|
|
}.with_indifferent_access
|
|
|
|
selected = @item.send(:select_auth_method, aspsp, "personal")
|
|
|
|
assert_equal "redirect_web", selected[:name]
|
|
assert_equal "REDIRECT", selected[:approach]
|
|
end
|
|
|
|
test "select_auth_method falls back to DECOUPLED when no REDIRECT exists" do
|
|
aspsp = {
|
|
auth_methods: [
|
|
{ name: "embedded_form", approach: "EMBEDDED" },
|
|
{ name: "decoupled_app", approach: "DECOUPLED" }
|
|
]
|
|
}.with_indifferent_access
|
|
|
|
selected = @item.send(:select_auth_method, aspsp, "personal")
|
|
|
|
assert_equal "decoupled_app", selected[:name]
|
|
assert_equal "DECOUPLED", selected[:approach]
|
|
end
|
|
|
|
test "select_auth_method filters by psu_type when methods declare one" do
|
|
aspsp = {
|
|
auth_methods: [
|
|
{ name: "business_redirect", approach: "REDIRECT", psu_type: "business" },
|
|
{ name: "personal_decoupled", approach: "DECOUPLED", psu_type: "personal" }
|
|
]
|
|
}.with_indifferent_access
|
|
|
|
selected = @item.send(:select_auth_method, aspsp, "personal")
|
|
|
|
assert_equal "personal_decoupled", selected[:name]
|
|
end
|
|
|
|
test "select_auth_method ignores hidden methods" do
|
|
aspsp = {
|
|
auth_methods: [
|
|
{ name: "hidden_redirect", approach: "REDIRECT", hidden_method: true },
|
|
{ name: "decoupled_app", approach: "DECOUPLED" }
|
|
]
|
|
}.with_indifferent_access
|
|
|
|
selected = @item.send(:select_auth_method, aspsp, "personal")
|
|
|
|
assert_equal "decoupled_app", selected[:name]
|
|
end
|
|
|
|
test "select_auth_method returns nil when no auth methods present" do
|
|
assert_nil @item.send(:select_auth_method, { auth_methods: [] }.with_indifferent_access, "personal")
|
|
end
|
|
|
|
test "select_auth_method returns nil when every method is hidden" do
|
|
aspsp = {
|
|
auth_methods: [
|
|
{ name: "hidden_a", approach: "REDIRECT", hidden_method: true },
|
|
{ name: "hidden_b", approach: "DECOUPLED", hidden_method: true }
|
|
]
|
|
}.with_indifferent_access
|
|
|
|
# All methods hidden -> fall back to the ASPSP default rather than forcing one.
|
|
assert_nil @item.send(:select_auth_method, aspsp, "personal")
|
|
end
|
|
|
|
test "reconcile_session_expiry! updates session_expires_at from access.valid_until" do
|
|
@item.session_id = "sess"
|
|
@item.session_expires_at = 1.day.from_now
|
|
@item.save!
|
|
new_expiry = 60.days.from_now.change(usec: 0)
|
|
|
|
@item.reconcile_session_expiry!({ access: { valid_until: new_expiry.iso8601 } })
|
|
|
|
assert_equal new_expiry.to_i, @item.reload.session_expires_at.to_i
|
|
end
|
|
|
|
test "reconcile_session_expiry! is a no-op when valid_until is missing" do
|
|
@item.session_id = "sess"
|
|
original = 1.day.from_now.change(usec: 0)
|
|
@item.session_expires_at = original
|
|
@item.save!
|
|
|
|
@item.reconcile_session_expiry!({ access: {} })
|
|
|
|
assert_equal original.to_i, @item.reload.session_expires_at.to_i
|
|
end
|
|
end
|