pushing stuff that was worked on

This commit is contained in:
twooey committed 2026-08-28 15:17:46 -04:00
1 parent 773883a0f3
commit 197938f482
22 files changed
+1251 -124

No files matched your search

@@ -0,0 +1,115 @@
<?php
namespace Bookstore\Core\Tests\Unit\Payments;
use Bookstore\Core\Payments\HelcimApiException;
use Bookstore\Core\Payments\HelcimClient;
use Bookstore\Core\Tests\TestCase;
use Brain\Monkey\Functions;
/**
* HelcimGatewayConfig::token() reads getenv('HELCIM_API_TOKEN_FILE') —
* mocking that directly is what switches HelcimClient between fake mode
* (no token: every call returns a synthetic approved response, no
* network call) and real mode (a token: real wp_remote_post calls with
* the shape confirmed against Helcim's current developer docs).
*/
class HelcimClientTest extends TestCase
{
public function test_fake_mode_never_calls_the_real_api(): void
{
Functions\when('getenv')->justReturn(false);
Functions\when('wp_generate_uuid4')->justReturn('fake-uuid');
Functions\expect('wp_remote_post')->never();
$init = HelcimClient::initialize_checkout(42.00, 'USD', 'preauth');
$this->assertTrue($init['fake']);
$this->assertNotEmpty($init['checkoutToken']);
$this->assertNotEmpty($init['secretToken']);
$capture = HelcimClient::capture(123456, 42.00, '127.0.0.1');
$this->assertSame('APPROVED', $capture['status']);
$this->assertIsInt($capture['transactionId']);
$this->assertNotEmpty($capture['cardToken']);
$reverse = HelcimClient::reverse(123456, '127.0.0.1');
$this->assertSame('APPROVED', $reverse['status']);
}
public function test_real_mode_initialize_sends_the_documented_request_shape(): void
{
Functions\when('getenv')->justReturn('/fake/path/to/token');
Functions\when('is_readable')->justReturn(true);
Functions\when('file_get_contents')->justReturn('real-token-abc');
Functions\when('wp_json_encode')->alias('json_encode');
Functions\when('is_wp_error')->justReturn(false);
Functions\when('wp_remote_retrieve_response_code')->justReturn(200);
Functions\when('wp_remote_retrieve_body')->justReturn(json_encode([
'checkoutToken' => 'real-checkout-token',
'secretToken' => 'real-secret-token',
]));
Functions\expect('wp_remote_post')
->once()
->withArgs(function ($url, $args) {
$this->assertSame('https://api.helcim.com/v2/helcim-pay/initialize', $url);
$this->assertSame('real-token-abc', $args['headers']['api-token']);
$this->assertArrayNotHasKey('idempotency-key', $args['headers']);
$body = json_decode($args['body'], true);
$this->assertSame('preauth', $body['paymentType']);
$this->assertEquals(42.0, $body['amount']);
$this->assertSame('USD', $body['currency']);
return true;
})
->andReturn(['response' => ['code' => 200]]);
$result = HelcimClient::initialize_checkout(42.00, 'USD', 'preauth');
$this->assertSame('real-checkout-token', $result['checkoutToken']);
}
public function test_real_mode_capture_includes_an_idempotency_key(): void
{
Functions\when('getenv')->justReturn('/fake/path/to/token');
Functions\when('is_readable')->justReturn(true);
Functions\when('file_get_contents')->justReturn('real-token-abc');
Functions\when('wp_json_encode')->alias('json_encode');
Functions\when('wp_generate_uuid4')->justReturn('idem-key-123');
Functions\when('is_wp_error')->justReturn(false);
Functions\when('wp_remote_retrieve_response_code')->justReturn(200);
Functions\when('wp_remote_retrieve_body')->justReturn(json_encode(['transactionId' => 999, 'status' => 'APPROVED']));
Functions\expect('wp_remote_post')
->once()
->withArgs(function ($url, $args) {
$this->assertSame('https://api.helcim.com/v2/payment/capture', $url);
$this->assertSame('idem-key-123', $args['headers']['idempotency-key']);
$body = json_decode($args['body'], true);
$this->assertSame(555, $body['preAuthTransactionId']);
$this->assertEquals(42.0, $body['amount']);
$this->assertSame('127.0.0.1', $body['ipAddress']);
return true;
})
->andReturn(['response' => ['code' => 200]]);
$result = HelcimClient::capture(555, 42.00, '127.0.0.1');
$this->assertSame(999, $result['transactionId']);
}
public function test_real_mode_throws_on_an_error_response(): void
{
Functions\when('getenv')->justReturn('/fake/path/to/token');
Functions\when('is_readable')->justReturn(true);
Functions\when('file_get_contents')->justReturn('real-token-abc');
Functions\when('wp_json_encode')->alias('json_encode');
Functions\when('wp_generate_uuid4')->justReturn('idem-key-123');
Functions\when('is_wp_error')->justReturn(false);
Functions\when('wp_remote_retrieve_response_code')->justReturn(422);
Functions\when('wp_remote_retrieve_body')->justReturn(json_encode(['errors' => ['card declined']]));
Functions\when('wp_remote_post')->justReturn(['response' => ['code' => 422]]);
$this->expectException(HelcimApiException::class);
$this->expectExceptionMessageMatches('/card declined/');
HelcimClient::capture(555, 42.00, '127.0.0.1');
}
}
@@ -0,0 +1,84 @@
<?php
namespace Bookstore\Core\Tests\Unit\Payments;
use Bookstore\Core\Order\OrderStatuses;
use Bookstore\Core\Payments\HelcimGateway;
use Bookstore\Core\Tests\TestCase;
use Brain\Monkey\Functions;
use Mockery;
/**
* process_payment()'s branching: pick up the preauth CheckoutController
* already stashed (Payments\CheckoutSession, backed by get/set/delete_
* transient — mocked at that WP-function layer, not by mocking
* CheckoutSession itself, matching this suite's established pattern),
* run the (moved-here) fraud check, and either authorize the order or
* reverse the hold and route to review. HelcimClient runs in fake mode
* here (no token env) so no real network call is involved either way.
*/
class HelcimGatewayTest extends TestCase
{
private function stub_pending_preauth(?array $data): void
{
Functions\when('get_transient')->justReturn($data);
Functions\when('delete_transient')->justReturn(true);
Functions\when('getenv')->justReturn(false); // HelcimClient stays in fake mode
Functions\when('wp_generate_uuid4')->justReturn('fake-uuid');
Functions\when('WC')->justReturn((object) ['session' => null]); // CheckoutSession::identity() falls back to session_id()
}
private function mock_order(): \WC_Order
{
$order = Mockery::mock(\WC_Order::class);
$order->shouldReceive('update_meta_data')->withAnyArgs();
$order->shouldReceive('save');
$order->shouldReceive('add_order_note')->withAnyArgs();
$order->shouldReceive('get_meta')->withAnyArgs()->andReturn('');
return $order;
}
public function test_no_pending_preauth_fails_without_touching_the_order(): void
{
$this->stub_pending_preauth(null);
Functions\when('wc_get_order')->justReturn(Mockery::mock(\WC_Order::class));
Functions\expect('wc_add_notice')->once();
$gateway = new HelcimGateway();
$result = $gateway->process_payment(123);
$this->assertSame('failure', $result['result']);
}
public function test_fraud_pass_authorizes_the_order(): void
{
$this->stub_pending_preauth(['preauth_transaction_id' => 555, 'card_token' => 'tok_1', 'amount' => 42.00]);
Functions\when('apply_filters')->justReturn(['pass' => true, 'reason' => 'no-op: fraud plugin not installed']);
$order = $this->mock_order();
$order->shouldReceive('update_status')->once()->with(OrderStatuses::AUTHORIZED, Mockery::type('string'));
Functions\when('wc_get_order')->justReturn($order);
$gateway = new HelcimGateway();
$result = $gateway->process_payment(123);
$this->assertSame('success', $result['result']);
$this->assertArrayHasKey('redirect', $result);
}
public function test_fraud_fail_reverses_the_hold_and_routes_to_exception(): void
{
$this->stub_pending_preauth(['preauth_transaction_id' => 555, 'card_token' => 'tok_1', 'amount' => 42.00]);
Functions\when('apply_filters')->justReturn(['pass' => false, 'reason' => 'high risk score']);
Functions\expect('wc_add_notice')->once();
$order = $this->mock_order();
$order->shouldReceive('update_status')->once()->with(OrderStatuses::EXCEPTION, Mockery::type('string'));
Functions\when('wc_get_order')->justReturn($order);
$gateway = new HelcimGateway();
$result = $gateway->process_payment(123);
$this->assertSame('failure', $result['result']);
}
}
@@ -27,6 +27,63 @@ if (!class_exists(\WC_Cart::class)) {
}
}
// Payments\HelcimGateway extends this. Minimal stand-in for the real
// WC_Payment_Gateway's settings-API plumbing (init_settings()/
// get_option()) so the constructor doesn't fatal in a unit test — real
// WooCommerce's version does much more (admin fields rendering, etc.),
// none of which the gateway's process_payment() branching depends on.
if (!class_exists(\WC_Payment_Gateway::class)) {
class WC_Payment_Gateway
{
public $id;
public $method_title;
public $method_description;
public $has_fields;
public $supports = [];
public $title;
public $description;
public $enabled;
public $form_fields = [];
public $settings = [];
public function init_settings(): void
{
$this->settings = [];
}
public function get_option($key, $default = '')
{
return $this->settings[$key] ?? $default;
}
public function get_return_url($order)
{
return 'https://example.test/order-received/';
}
public function process_admin_options(): void
{
}
}
}
if (!class_exists(\WP_Error::class)) {
class WP_Error
{
private string $message;
public function __construct($code = '', $message = '')
{
$this->message = $message;
}
public function get_error_message(): string
{
return $this->message;
}
}
}
// Membership\Tier calls $wpdb->get_row(..., ARRAY_A) — these are real
// WordPress core constants (wp-includes/wp-db.php) that a full WP
// bootstrap would define; this one deliberately doesn't load WordPress,