mirror of
https://github.com/hanzoai/idv.git
synced 2026-10-30 18:33:39 +00:00
830 lines
23 KiB
Go
830 lines
23 KiB
Go
// Copyright 2024-2026 Lux Partners Limited. All rights reserved.
|
|
// Use of this source code is governed by the Apache 2.0
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// jumioSign computes the HMAC-SHA256 signature for test webhook payloads.
|
|
func jumioSign(body []byte, secret string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write(body)
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// --- Provider registry ---
|
|
|
|
func TestGetProviderJumio(t *testing.T) {
|
|
p, err := GetProvider("jumio", map[string]string{
|
|
"api_token": "tok",
|
|
"api_secret": "sec",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "jumio" {
|
|
t.Fatalf("expected 'jumio', got %q", p.Name())
|
|
}
|
|
}
|
|
|
|
func TestGetProviderOnfido(t *testing.T) {
|
|
p, err := GetProvider("onfido", map[string]string{
|
|
"api_token": "tok",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "onfido" {
|
|
t.Fatalf("expected 'onfido', got %q", p.Name())
|
|
}
|
|
}
|
|
|
|
func TestGetProviderPlaid(t *testing.T) {
|
|
p, err := GetProvider("plaid", map[string]string{
|
|
"client_id": "cid",
|
|
"secret": "sec",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "plaid" {
|
|
t.Fatalf("expected 'plaid', got %q", p.Name())
|
|
}
|
|
}
|
|
|
|
func TestGetProviderUnknown(t *testing.T) {
|
|
_, err := GetProvider("unknown", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown provider")
|
|
}
|
|
}
|
|
|
|
func TestListRegistered(t *testing.T) {
|
|
names := ListRegistered()
|
|
if len(names) < 3 {
|
|
t.Fatalf("expected at least 3 registered factories, got %d", len(names))
|
|
}
|
|
sort.Strings(names)
|
|
expected := []string{"jumio", "onfido", "plaid"}
|
|
for _, exp := range expected {
|
|
found := false
|
|
for _, n := range names {
|
|
if n == exp {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected %q in registered list", exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Jumio ---
|
|
|
|
func TestJumioName(t *testing.T) {
|
|
j := NewJumio(JumioConfig{})
|
|
if j.Name() != "jumio" {
|
|
t.Fatalf("expected 'jumio', got %q", j.Name())
|
|
}
|
|
}
|
|
|
|
func TestJumioDefaultBaseURL(t *testing.T) {
|
|
j := NewJumio(JumioConfig{})
|
|
if j.cfg.BaseURL != JumioSandboxv4 {
|
|
t.Fatalf("expected sandbox URL, got %q", j.cfg.BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestJumioCustomBaseURL(t *testing.T) {
|
|
j := NewJumio(JumioConfig{BaseURL: "https://custom.example.com"})
|
|
if j.cfg.BaseURL != "https://custom.example.com" {
|
|
t.Fatalf("expected custom URL, got %q", j.cfg.BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestJumioInitiateVerification(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/initiate" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"transactionReference": "txn-test-001",
|
|
"redirectUrl": "https://verify.jumio.com/abc",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
j := NewJumio(JumioConfig{
|
|
BaseURL: server.URL,
|
|
APIToken: "test-token",
|
|
APISecret: "test-secret",
|
|
})
|
|
|
|
resp, err := j.InitiateVerification(context.Background(), &VerificationRequest{
|
|
ApplicationID: "app-1",
|
|
Email: "[email protected]",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.VerificationID != "txn-test-001" {
|
|
t.Fatalf("expected 'txn-test-001', got %q", resp.VerificationID)
|
|
}
|
|
if resp.Provider != ProviderJumio {
|
|
t.Fatalf("expected 'jumio', got %q", resp.Provider)
|
|
}
|
|
if resp.RedirectURL != "https://verify.jumio.com/abc" {
|
|
t.Fatalf("unexpected redirect URL: %q", resp.RedirectURL)
|
|
}
|
|
}
|
|
|
|
func TestJumioCheckStatus(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/transactions/txn-001" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "DONE",
|
|
"verificationStatus": "APPROVED_VERIFIED",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
j := NewJumio(JumioConfig{BaseURL: server.URL})
|
|
result, err := j.CheckStatus(context.Background(), "txn-001")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestJumioParseWebhookApproved(t *testing.T) {
|
|
secret := "test-secret"
|
|
j := NewJumio(JumioConfig{APISecret: secret})
|
|
payload := []byte(`{
|
|
"transactionReference": "txn-001",
|
|
"customerInternalReference": "app-100",
|
|
"status": "DONE",
|
|
"verificationStatus": "APPROVED_VERIFIED",
|
|
"identityVerification": {"similarity": "MATCH", "validity": true}
|
|
}`)
|
|
|
|
sig := jumioSign(payload, secret)
|
|
event, err := j.ParseWebhook(payload, map[string]string{"Callback-Sig": sig})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", event.Status)
|
|
}
|
|
if event.VerificationID != "txn-001" {
|
|
t.Fatalf("expected 'txn-001', got %q", event.VerificationID)
|
|
}
|
|
if event.ApplicationID != "app-100" {
|
|
t.Fatalf("expected 'app-100', got %q", event.ApplicationID)
|
|
}
|
|
if len(event.Checks) < 2 {
|
|
t.Fatal("expected at least 2 checks")
|
|
}
|
|
if event.Checks[0].Type != "document" || event.Checks[0].Status != "clear" {
|
|
t.Fatalf("expected document/clear, got %s/%s", event.Checks[0].Type, event.Checks[0].Status)
|
|
}
|
|
if event.Checks[1].Type != "facial_similarity" || event.Checks[1].Status != "MATCH" {
|
|
t.Fatalf("expected facial_similarity/MATCH, got %s/%s", event.Checks[1].Type, event.Checks[1].Status)
|
|
}
|
|
}
|
|
|
|
func TestJumioParseWebhookDeclined(t *testing.T) {
|
|
cases := []string{
|
|
"DENIED_FRAUD",
|
|
"DENIED_UNSUPPORTED_ID_TYPE",
|
|
"DENIED_UNSUPPORTED_ID_COUNTRY",
|
|
"ERROR_NOT_READABLE_ID",
|
|
"NO_ID_UPLOADED",
|
|
}
|
|
|
|
secret := "decline-secret"
|
|
j := NewJumio(JumioConfig{APISecret: secret})
|
|
for _, vs := range cases {
|
|
t.Run(vs, func(t *testing.T) {
|
|
payload := []byte(fmt.Sprintf(`{
|
|
"transactionReference": "txn-dec",
|
|
"customerInternalReference": "app-dec",
|
|
"status": "FAILED",
|
|
"verificationStatus": %q
|
|
}`, vs))
|
|
sig := jumioSign(payload, secret)
|
|
event, err := j.ParseWebhook(payload, map[string]string{"Callback-Sig": sig})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusDeclined {
|
|
t.Fatalf("expected declined for %s, got %q", vs, event.Status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJumioParseWebhookPending(t *testing.T) {
|
|
secret := "pending-secret"
|
|
j := NewJumio(JumioConfig{APISecret: secret})
|
|
payload := []byte(`{
|
|
"transactionReference": "txn-p",
|
|
"customerInternalReference": "app-p",
|
|
"status": "PENDING",
|
|
"verificationStatus": "UNKNOWN"
|
|
}`)
|
|
sig := jumioSign(payload, secret)
|
|
event, err := j.ParseWebhook(payload, map[string]string{"Callback-Sig": sig})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusPending {
|
|
t.Fatalf("expected pending, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestJumioParseWebhookInvalidJSON(t *testing.T) {
|
|
secret := "json-secret"
|
|
j := NewJumio(JumioConfig{APISecret: secret})
|
|
body := []byte(`not-json`)
|
|
sig := jumioSign(body, secret)
|
|
_, err := j.ParseWebhook(body, map[string]string{"Callback-Sig": sig})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
// TestJumioWebhookValidSignature (RED-14) verifies correct HMAC passes.
|
|
func TestJumioWebhookValidSignature(t *testing.T) {
|
|
secret := "hmac-test-secret"
|
|
j := NewJumio(JumioConfig{APISecret: secret})
|
|
payload := []byte(`{"transactionReference":"txn-hmac","status":"DONE","verificationStatus":"APPROVED_VERIFIED"}`)
|
|
sig := jumioSign(payload, secret)
|
|
|
|
event, err := j.ParseWebhook(payload, map[string]string{"Callback-Sig": sig})
|
|
if err != nil {
|
|
t.Fatalf("RED-14: valid signature rejected: %v", err)
|
|
}
|
|
if event.VerificationID != "txn-hmac" {
|
|
t.Fatalf("expected txn-hmac, got %q", event.VerificationID)
|
|
}
|
|
}
|
|
|
|
// TestJumioWebhookInvalidSignature (RED-14) verifies wrong HMAC is rejected.
|
|
func TestJumioWebhookInvalidSignature(t *testing.T) {
|
|
j := NewJumio(JumioConfig{APISecret: "real-secret"})
|
|
payload := []byte(`{"transactionReference":"txn-bad"}`)
|
|
wrongSig := jumioSign(payload, "wrong-secret")
|
|
|
|
_, err := j.ParseWebhook(payload, map[string]string{"Callback-Sig": wrongSig})
|
|
if err == nil {
|
|
t.Fatal("RED-14: invalid signature should be rejected")
|
|
}
|
|
}
|
|
|
|
// TestJumioWebhookMissingSignature (RED-14) verifies missing header is rejected.
|
|
func TestJumioWebhookMissingSignature(t *testing.T) {
|
|
j := NewJumio(JumioConfig{APISecret: "some-secret"})
|
|
payload := []byte(`{"transactionReference":"txn-nosig"}`)
|
|
|
|
_, err := j.ParseWebhook(payload, map[string]string{})
|
|
if err == nil {
|
|
t.Fatal("RED-14: missing Callback-Sig header should be rejected")
|
|
}
|
|
}
|
|
|
|
// --- Onfido ---
|
|
|
|
func TestOnfidoName(t *testing.T) {
|
|
o := NewOnfido(OnfidoConfig{})
|
|
if o.Name() != "onfido" {
|
|
t.Fatalf("expected 'onfido', got %q", o.Name())
|
|
}
|
|
}
|
|
|
|
func TestOnfidoDefaultBaseURL(t *testing.T) {
|
|
o := NewOnfido(OnfidoConfig{})
|
|
if o.cfg.BaseURL != OnfidoSandboxAPIv3 {
|
|
t.Fatalf("expected sandbox URL, got %q", o.cfg.BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestOnfidoCheckStatus(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"id": "check-001",
|
|
"status": "complete",
|
|
"result": "clear",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
o := NewOnfido(OnfidoConfig{BaseURL: server.URL})
|
|
result, err := o.CheckStatus(context.Background(), "check-001")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestOnfidoParseWebhookClear(t *testing.T) {
|
|
o := NewOnfido(OnfidoConfig{})
|
|
payload := `{
|
|
"payload": {
|
|
"resource_type": "check",
|
|
"action": "check.completed",
|
|
"object": {
|
|
"id": "check-001",
|
|
"status": "complete",
|
|
"result": "clear",
|
|
"applicant_id": "app-001"
|
|
}
|
|
}
|
|
}`
|
|
event, err := o.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", event.Status)
|
|
}
|
|
if event.VerificationID != "check-001" {
|
|
t.Fatalf("expected 'check-001', got %q", event.VerificationID)
|
|
}
|
|
}
|
|
|
|
func TestOnfidoParseWebhookConsider(t *testing.T) {
|
|
o := NewOnfido(OnfidoConfig{})
|
|
payload := `{
|
|
"payload": {
|
|
"resource_type": "check",
|
|
"action": "check.completed",
|
|
"object": {
|
|
"id": "check-002",
|
|
"status": "complete",
|
|
"result": "consider"
|
|
}
|
|
}
|
|
}`
|
|
event, err := o.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusDeclined {
|
|
t.Fatalf("expected declined, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestOnfidoParseWebhookInvalidJSON(t *testing.T) {
|
|
o := NewOnfido(OnfidoConfig{})
|
|
_, err := o.ParseWebhook([]byte(`{bad`), nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
// --- Plaid ---
|
|
|
|
func TestPlaidName(t *testing.T) {
|
|
p := NewPlaid(PlaidConfig{})
|
|
if p.Name() != "plaid" {
|
|
t.Fatalf("expected 'plaid', got %q", p.Name())
|
|
}
|
|
}
|
|
|
|
func TestPlaidDefaultBaseURL(t *testing.T) {
|
|
p := NewPlaid(PlaidConfig{})
|
|
if p.cfg.BaseURL != PlaidSandbox {
|
|
t.Fatalf("expected sandbox URL, got %q", p.cfg.BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestPlaidCheckStatus(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "success",
|
|
"steps": map[string]string{
|
|
"verify_sms": "success",
|
|
"documentary_verification": "success",
|
|
"selfie_check": "success",
|
|
"kyc_check": "success",
|
|
"risk_check": "success",
|
|
},
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
p := NewPlaid(PlaidConfig{BaseURL: server.URL, ClientID: "test", Secret: "test"})
|
|
result, err := p.CheckStatus(context.Background(), "idv-001")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", result.Status)
|
|
}
|
|
if len(result.Checks) != 4 {
|
|
t.Fatalf("expected 4 checks, got %d", len(result.Checks))
|
|
}
|
|
}
|
|
|
|
func TestPlaidParseWebhookStepCompleted(t *testing.T) {
|
|
p := NewPlaid(PlaidConfig{})
|
|
payload := `{
|
|
"webhook_type": "IDENTITY_VERIFICATION",
|
|
"webhook_code": "STEP_COMPLETED",
|
|
"identity_verification_id": "idv-step-001"
|
|
}`
|
|
event, err := p.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusPending {
|
|
t.Fatalf("expected pending, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestPlaidParseWebhookExpired(t *testing.T) {
|
|
p := NewPlaid(PlaidConfig{})
|
|
payload := `{
|
|
"webhook_type": "IDENTITY_VERIFICATION",
|
|
"webhook_code": "VERIFICATION_EXPIRED",
|
|
"identity_verification_id": "idv-exp-001"
|
|
}`
|
|
event, err := p.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusExpired {
|
|
t.Fatalf("expected expired, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestPlaidParseWebhookCompleted(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"status": "success"})
|
|
}))
|
|
defer server.Close()
|
|
|
|
p := NewPlaid(PlaidConfig{BaseURL: server.URL, ClientID: "test", Secret: "test"})
|
|
payload := `{
|
|
"webhook_type": "IDENTITY_VERIFICATION",
|
|
"webhook_code": "VERIFICATION_COMPLETED",
|
|
"identity_verification_id": "idv-done-001"
|
|
}`
|
|
event, err := p.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusApproved {
|
|
t.Fatalf("expected approved, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestPlaidParseWebhookCompletedFailed(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"status": "failed"})
|
|
}))
|
|
defer server.Close()
|
|
|
|
p := NewPlaid(PlaidConfig{BaseURL: server.URL, ClientID: "test", Secret: "test"})
|
|
payload := `{
|
|
"webhook_type": "IDENTITY_VERIFICATION",
|
|
"webhook_code": "VERIFICATION_COMPLETED",
|
|
"identity_verification_id": "idv-fail-001"
|
|
}`
|
|
event, err := p.ParseWebhook([]byte(payload), nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if event.Status != StatusDeclined {
|
|
t.Fatalf("expected declined, got %q", event.Status)
|
|
}
|
|
}
|
|
|
|
func TestPlaidParseWebhookInvalidJSON(t *testing.T) {
|
|
p := NewPlaid(PlaidConfig{})
|
|
_, err := p.ParseWebhook([]byte(`not valid`), nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
// --- Status constants ---
|
|
|
|
func TestStatusConstants(t *testing.T) {
|
|
if StatusPending != "pending" {
|
|
t.Fatalf("StatusPending = %q", StatusPending)
|
|
}
|
|
if StatusApproved != "approved" {
|
|
t.Fatalf("StatusApproved = %q", StatusApproved)
|
|
}
|
|
if StatusDeclined != "declined" {
|
|
t.Fatalf("StatusDeclined = %q", StatusDeclined)
|
|
}
|
|
if StatusExpired != "expired" {
|
|
t.Fatalf("StatusExpired = %q", StatusExpired)
|
|
}
|
|
if StatusError != "error" {
|
|
t.Fatalf("StatusError = %q", StatusError)
|
|
}
|
|
}
|
|
|
|
func TestNewID(t *testing.T) {
|
|
id1 := newID()
|
|
id2 := newID()
|
|
if id1 == id2 {
|
|
t.Fatal("expected unique IDs")
|
|
}
|
|
if len(id1) != 32 {
|
|
t.Fatalf("expected 32-char hex ID, got %d chars", len(id1))
|
|
}
|
|
}
|
|
|
|
// --- Provider registry: additional coverage ---
|
|
|
|
func TestRegisterFactoryOverwrite(t *testing.T) {
|
|
// Register a custom factory, then overwrite it
|
|
customCalled := false
|
|
RegisterFactory("custom-test-provider", func(config map[string]string) (Provider, error) {
|
|
customCalled = true
|
|
return NewJumio(JumioConfig{BaseURL: config["base_url"]}), nil
|
|
})
|
|
|
|
p, err := GetProvider("custom-test-provider", map[string]string{"base_url": "https://example.com"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !customCalled {
|
|
t.Fatal("custom factory should have been called")
|
|
}
|
|
if p == nil {
|
|
t.Fatal("expected non-nil provider")
|
|
}
|
|
|
|
// Overwrite with a different factory
|
|
overwriteCalled := false
|
|
RegisterFactory("custom-test-provider", func(config map[string]string) (Provider, error) {
|
|
overwriteCalled = true
|
|
return NewOnfido(OnfidoConfig{}), nil
|
|
})
|
|
|
|
p2, err := GetProvider("custom-test-provider", nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !overwriteCalled {
|
|
t.Fatal("overwritten factory should have been called")
|
|
}
|
|
if p2.Name() != "onfido" {
|
|
t.Fatalf("expected onfido from overwritten factory, got %q", p2.Name())
|
|
}
|
|
|
|
// Clean up: re-register nothing to avoid polluting other tests
|
|
// (not strictly necessary since test factories don't conflict with init ones)
|
|
}
|
|
|
|
func TestGetProviderWithNilConfig(t *testing.T) {
|
|
// All init-registered providers should handle nil config gracefully
|
|
for _, name := range []string{"jumio", "onfido", "plaid"} {
|
|
p, err := GetProvider(name, nil)
|
|
if err != nil {
|
|
t.Fatalf("GetProvider(%q, nil): unexpected error: %v", name, err)
|
|
}
|
|
if p.Name() != name {
|
|
t.Fatalf("expected %q, got %q", name, p.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetProviderErrorMessage(t *testing.T) {
|
|
_, err := GetProvider("nonexistent-provider-xyz", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "nonexistent-provider-xyz") {
|
|
t.Fatalf("error should mention provider name, got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "not registered") {
|
|
t.Fatalf("error should say 'not registered', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRegisterFactoryReturningError(t *testing.T) {
|
|
RegisterFactory("failing-factory", func(config map[string]string) (Provider, error) {
|
|
return nil, fmt.Errorf("config validation failed: missing api_key")
|
|
})
|
|
|
|
_, err := GetProvider("failing-factory", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error from failing factory")
|
|
}
|
|
if !strings.Contains(err.Error(), "config validation failed") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Verification request field coverage ---
|
|
|
|
func TestVerificationRequestFields(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"transactionReference": "txn-fields-001",
|
|
"redirectUrl": "https://verify.example.com/test",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
j := NewJumio(JumioConfig{
|
|
BaseURL: server.URL,
|
|
APIToken: "tok",
|
|
APISecret: "sec",
|
|
})
|
|
|
|
// Test with all optional fields populated
|
|
resp, err := j.InitiateVerification(context.Background(), &VerificationRequest{
|
|
ApplicationID: "app-full",
|
|
GivenName: "John",
|
|
FamilyName: "Doe",
|
|
DateOfBirth: "1990-01-15",
|
|
Email: "[email protected]",
|
|
Phone: "+1-555-0100",
|
|
Country: "US",
|
|
IPAddress: "192.168.1.1",
|
|
Street: []string{"123 Main St", "Apt 4"},
|
|
City: "San Francisco",
|
|
State: "CA",
|
|
PostalCode: "94102",
|
|
TaxID: "123-45-6789",
|
|
TaxIDType: "ssn",
|
|
DocumentType: "passport",
|
|
DocumentID: "P123456",
|
|
Provider: "jumio",
|
|
Workflow: "wf-001",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.VerificationID != "txn-fields-001" {
|
|
t.Fatalf("expected txn-fields-001, got %q", resp.VerificationID)
|
|
}
|
|
if resp.Status != StatusPending {
|
|
t.Fatalf("expected pending, got %q", resp.Status)
|
|
}
|
|
}
|
|
|
|
// --- Jumio edge cases ---
|
|
|
|
func TestJumioInitiateVerificationHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(`{"error": "internal server error"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
j := NewJumio(JumioConfig{BaseURL: server.URL, APIToken: "tok", APISecret: "sec"})
|
|
_, err := j.InitiateVerification(context.Background(), &VerificationRequest{
|
|
ApplicationID: "app-err",
|
|
Email: "[email protected]",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for HTTP 500")
|
|
}
|
|
}
|
|
|
|
func TestJumioCheckStatusHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
j := NewJumio(JumioConfig{BaseURL: server.URL})
|
|
_, err := j.CheckStatus(context.Background(), "txn-notfound")
|
|
if err == nil {
|
|
t.Fatal("expected error for HTTP 404")
|
|
}
|
|
}
|
|
|
|
// --- Onfido edge cases ---
|
|
|
|
func TestOnfidoInitiateVerification(t *testing.T) {
|
|
// Mock: create applicant then create check
|
|
callCount := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if strings.Contains(r.URL.Path, "checks") {
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"id": "chk-001",
|
|
"status": "in_progress",
|
|
})
|
|
} else {
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"id": "applicant-001",
|
|
})
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
o := NewOnfido(OnfidoConfig{BaseURL: server.URL, APIToken: "tok"})
|
|
resp, err := o.InitiateVerification(context.Background(), &VerificationRequest{
|
|
ApplicationID: "app-onfido",
|
|
GivenName: "Jane",
|
|
FamilyName: "Doe",
|
|
Email: "[email protected]",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Provider != ProviderOnfido {
|
|
t.Fatalf("expected onfido, got %q", resp.Provider)
|
|
}
|
|
if resp.Status != StatusPending {
|
|
t.Fatalf("expected pending, got %q", resp.Status)
|
|
}
|
|
}
|
|
|
|
// --- Plaid edge cases ---
|
|
|
|
func TestPlaidInitiateVerification(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": "idv-plaid-001",
|
|
"shareable_url": "https://plaid.com/verify/abc",
|
|
"status": "active",
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
p := NewPlaid(PlaidConfig{BaseURL: server.URL, ClientID: "cid", Secret: "sec"})
|
|
resp, err := p.InitiateVerification(context.Background(), &VerificationRequest{
|
|
ApplicationID: "app-plaid",
|
|
GivenName: "Bob",
|
|
FamilyName: "Smith",
|
|
Email: "[email protected]",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.Provider != ProviderPlaid {
|
|
t.Fatalf("expected plaid, got %q", resp.Provider)
|
|
}
|
|
}
|
|
|
|
// --- Provider constant coverage ---
|
|
|
|
func TestProviderNameConstants(t *testing.T) {
|
|
if ProviderJumio != "jumio" {
|
|
t.Fatalf("ProviderJumio = %q", ProviderJumio)
|
|
}
|
|
if ProviderOnfido != "onfido" {
|
|
t.Fatalf("ProviderOnfido = %q", ProviderOnfido)
|
|
}
|
|
if ProviderPlaid != "plaid" {
|
|
t.Fatalf("ProviderPlaid = %q", ProviderPlaid)
|
|
}
|
|
}
|
|
|
|
func TestListRegisteredContainsAllInit(t *testing.T) {
|
|
names := ListRegistered()
|
|
nameSet := make(map[string]bool)
|
|
for _, n := range names {
|
|
nameSet[n] = true
|
|
}
|
|
|
|
for _, expected := range []string{ProviderJumio, ProviderOnfido, ProviderPlaid} {
|
|
if !nameSet[expected] {
|
|
t.Errorf("ListRegistered() missing %q", expected)
|
|
}
|
|
}
|
|
}
|