Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/pod.git

Refactors tests, fixes broken address, adds new tests

AuthorDave Jarvis <email>
Date2026-01-25 20:13:02 GMT-0800
Commit93d6ee2480434d5d4a78982e9ee25c2f43d6679f
Parent2c48ab6
.gitignore
-openapi_public.yml
+api.yml
Address.php
private const REGION_MAP = [
+ // Australia
+ 'NEWSOUTHWALES' => 'NSW',
+ 'VICTORIA' => 'VIC',
+ 'QUEENSLAND' => 'QLD',
+ 'WESTERNAUSTRALIA' => 'WA',
+ 'SOUTHAUSTRALIA' => 'SA',
+ 'TASMANIA' => 'TAS',
+ 'AUSTRALIANCAPITALTERRITORY' => 'ACT',
+ 'NORTHERNTERRITORY' => 'NT',
+ // Canada
'ALBERTA' => 'AB',
'BRITISHCOLUMBIA' => 'BC',
'SASKATCHEWAN' => 'SK',
'YUKON' => 'YT',
+ // United States
'ALABAMA' => 'AL',
'ALASKA' => 'AK',
'ARIZONA' => 'AZ',
'ARKANSAS' => 'AR',
'CALIFORNIA' => 'CA',
'COLORADO' => 'CO',
'CONNECTICUT' => 'CT',
'DELAWARE' => 'DE',
+ 'DISTRICTOFCOLUMBIA' => 'DC',
'FLORIDA' => 'FL',
'GEORGIA' => 'GA',
'WESTVIRGINIA' => 'WV',
'WISCONSIN' => 'WI',
- 'WYOMING' => 'WY',
- 'DISTRICTOFCOLUMBIA' => 'DC'
+ 'WYOMING' => 'WY'
];
/**
- * Initializes the address object with raw input data.
+ * Initializes and automatically validates the address.
*
- * @param array $input Raw address field data from form submission.
+ * @param array $input Raw address field data.
*/
public function __construct( array $input ) {
$this->fields = $input;
+ $this->process();
}
* @return bool True if all required fields are present, false otherwise.
*/
- public function process( array $required ) {
+ public function process( array $required = [] ): bool {
foreach( $required as $field ) {
if( empty( $this->fields[ $field ] ) ) {
Order.php
private $summary = [];
private $errorMessage = '';
- private $statusCode = 0;
+ private $renderMethod;
public function process( array $response, int $statusCode ) {
- $this->statusCode = $statusCode;
-
if( $statusCode === 201 ) {
- $this->summary = $response;
+ $this->processSuccess( $response );
} else {
- $this->errorMessage = $response[ 'shipping_address' ][ 'detail' ]
- [ 'errors' ][ 0 ][ 'message' ]
- ?? $response[ 'detail' ]
- ?? "An unexpected error occurred (HTTP $statusCode).";
+ $this->processError( $response, $statusCode );
}
+ }
+
+ protected function processSuccess( array $response ) {
+ $this->summary = $response;
+ $this->renderMethod = 'renderSummary';
+ }
+
+ protected function processError( array $response, int $statusCode ) {
+ $this->errorMessage = $response[ 'shipping_address' ][ 'detail' ]
+ [ 'errors' ][ 0 ][ 'message' ]
+ ?? $response[ 'detail' ]
+ ?? "An unexpected error occurred (HTTP $statusCode).";
+ $this->renderMethod = 'renderError';
}
public function render() {
- if( $this->statusCode === 201 ) {
- $this->renderSummary();
- } else {
- $this->renderError();
- }
+ $this->{$this->renderMethod}();
}
Publisher.php
/**
- * Sets up the publisher with API credentials and order
- * specifications.
+ * Sets up the publisher with API credentials and order specifications.
*
* @param string $key Client authentication key for API access.
* @param string $base Base URL for the API endpoints.
* @param array $specs Order specifications including package, page
- * count, quantity, and shipping method.
+ * count, quantity, and shipping method.
*/
public function initialize( string $key, string $base, array $specs ) {
public function process( Address $address, Order $order ) {
$token = $this->authenticate();
+ $body = [ 'detail' => 'Authentication failed.' ];
+ $code = 401;
- if( !$token ) {
- $order->process( [ 'detail' => 'Authentication failed.' ], 401 );
- return;
+ if( $token ) {
+ $payload = $this->buildPayload( $address );
+ $res = $this->request( self::PATH_COST, $payload, $token );
+ $body = $res['body'];
+ $code = $res['code'];
+
+ $this->infuse( $body, $payload, $code );
}
+
+ $order->process( $body, $code );
+ }
+ /**
+ * Builds the request payload from specifications and address.
+ *
+ * @param Address $address Address object to configure.
+ * @return array The formatted API payload.
+ */
+ private function buildPayload( Address $address ): array {
$payload = [
'line_items' => [ [
'page_count' => $this->specs[ 'pages' ],
'quantity' => $this->specs[ 'quantity' ],
'pod_package_id' => $this->specs[ 'package' ]
] ],
'shipping_option' => $this->specs[ 'shipping' ]
];
-
$country = $address->configure( $payload );
$payload[ 'currency' ] = self::CURRENCY_MAP[ $country ] ?? 'USD';
- $context = stream_context_create( [ 'http' => [
- 'method' => 'POST',
- 'header' => "Authorization: Bearer $token\r\nContent-Type: " .
- "application/json\r\n",
- 'content' => json_encode( $payload ),
- 'ignore_errors' => true
- ] ] );
+ return $payload;
+ }
- $raw = @file_get_contents( $this->base . self::PATH_COST, false, $context );
- $body = json_decode( $raw, true ) ?? [];
+ /**
+ * Merges original address fields into the response.
+ *
+ * @param array $body Reference to the response body array.
+ * @param array $payload The original request payload.
+ * @param int $code The HTTP response code.
+ */
+ private function infuse(array &$body, array $payload, int $code) {
+ if( $code === 201 &&
+ isset( $body['shipping_address'], $payload['shipping_address'] ) ) {
+ $address = &$body['shipping_address'];
+ $sent = $payload['shipping_address'];
+
+ $address['name'] = $address['name'] ?? $sent['name'] ?? '';
+ $address['country_code'] = $address['country_code'] ??
+ $sent['country_code'] ?? '';
+ }
+ }
+
+ /**
+ * Obtains an access token from the authentication endpoint.
+ *
+ * @return string|null The access token or null on failure.
+ */
+ private function authenticate() {
+ $header = "Authorization: Basic " . base64_encode( $this->key ) .
+ "\r\nContent-Type: application/x-www-form-urlencoded\r\n";
+ $content = http_build_query( [ 'grant_type' => 'client_credentials' ] );
+ $res = $this->post( self::PATH_AUTH, $content, $header );
+ return $res['body'][ 'access_token' ] ?? null;
+ }
+
+ /**
+ * Sends the cost calculation request to the API.
+ *
+ * @param string $path The API endpoint path.
+ * @param array $data The payload to send.
+ * @param string $token The bearer token.
+ * @return array Response body and HTTP status code.
+ */
+ private function request( string $path, array $data, string $token ) {
+ $header = "Authorization: Bearer $token\r\nContent-Type: " .
+ "application/json\r\n";
+ return $this->post( $path, json_encode( $data ), $header );
+ }
+
+ /**
+ * Executes the HTTP POST request.
+ *
+ * @param string $path The API endpoint path.
+ * @param string $content The raw request body.
+ * @param string $header The HTTP headers.
+ * @return array Decoded JSON body and status code.
+ */
+ protected function post( string $path, string $content, string $header ) {
+ $context = stream_context_create( [
+ 'http' => [
+ 'method' => 'POST',
+ 'header' => $header,
+ 'content' => $content,
+ 'ignore_errors' => true
+ ]
+ ] );
+ $raw = @file_get_contents( $this->base . $path, false, $context );
$code = 0;
if( !empty( $http_response_header ) ) {
preg_match( "/\d{3}/", $http_response_header[ 0 ], $matches );
$code = intval( $matches[ 0 ] );
- }
-
- // Preserve the name and country code for the order summary.
- if( $code === 201 && isset( $body[ 'shipping_address' ] ) ) {
- $sentName = $payload[ 'shipping_address' ][ 'name' ];
- $sentCountry = $payload[ 'shipping_address' ][ 'country_code' ];
-
- if( empty( $body[ 'shipping_address' ][ 'name' ] ) ) {
- $body[ 'shipping_address' ][ 'name' ] = $sentName;
- }
- if( empty( $body[ 'shipping_address' ][ 'country_code' ] ) ) {
- $body[ 'shipping_address' ][ 'country_code' ] = $sentCountry;
- }
}
-
- $order->process( $body, $code );
- }
-
- private function authenticate() {
- $context = stream_context_create( [ 'http' => [
- 'method' => 'POST',
- 'header' => "Authorization: Basic " . base64_encode( $this->key ) .
- "\r\nContent-Type: application/x-www-form-urlencoded\r\n",
- 'content' => http_build_query( [ 'grant_type' => 'client_credentials' ] )
- ] ] );
- $response = @file_get_contents(
- $this->base . self::PATH_AUTH, false, $context
- );
- return $response
- ? json_decode( $response, true )[ 'access_token' ] ?? null
- : null;
+ return [ 'body' => json_decode( $raw, true ) ?? [], 'code' => $code ];
}
}
tests/Main.php
<?php
-require_once 'TestSuite.php';
+require_once 'TestSession.php';
require_once 'TestAddress.php';
require_once 'TestExchangeRate.php';
require_once 'TestMoney.php';
-require_once 'TestSession.php';
+require_once 'TestPublisher.php';
+require_once 'TestOrder.php';
+require_once 'TestSuite.php';
/**
* Runs the entire test suite.
*
* Usage: php Main.php
*
* Return: 0 means no tests failed; 1 means at least one test failed.
*/
class Main {
- public function run() {
- // Test sessions first to ensure headers are sent before any output occurs.
+ public function run(): int {
+ // Test sessions first to ensure headers are sent before any output
+ // occurs.
$tests = [
new TestSession(),
new TestAddress(),
new TestExchangeRate(),
- new TestMoney()
+ new TestMoney(),
+ new TestPublisher(),
+ new TestOrder()
];
$suite = new TestSuite();
tests/TestHarness.php
+<?php
+require_once '../Address.php';
+
+class TestHarness {
+ public const ADDRESSES = [
+ 'GB' => [
+ 'name' => 'The Ritz London',
+ 'street1' => '150 Piccadilly',
+ 'street2' => '520 Jermyn Street',
+ 'city' => 'London',
+ 'state' => 'Greater London',
+ 'postcode' => 'SW1Y 6LX',
+ 'country' => 'GB',
+ 'phone' => '+44 20 7493 8181',
+ 'email' => 'test@example.org'
+ ],
+ 'AU' => [
+ 'name' => 'Fitzroy Mills Market',
+ 'street1' => '45 Brunswick Street',
+ 'street2' => 'Fitzroy Mills Market',
+ 'city' => 'Fitzroy',
+ 'state' => 'Victoria',
+ 'postcode' => '3065',
+ 'country' => 'AU',
+ 'phone' => '+61 3 9419 4444',
+ 'email' => 'test@example.org'
+ ],
+ 'CA' => [
+ 'name' => 'Mount Dennis Community',
+ 'street1' => '42 Weston Road',
+ 'street2' => 'Mount Dennis',
+ 'city' => 'Toronto',
+ 'state' => 'Ontario',
+ 'postcode' => 'm6n5h3',
+ 'country' => 'CA',
+ 'phone' => '1-416-555-0142',
+ 'email' => 'test@example.org'
+ ],
+ 'US' => [
+ 'name' => 'Cal State Fullerton',
+ 'street1' => '800 N State College Blvd',
+ 'street2' => 'Cal State Fullerton',
+ 'city' => 'Fullerton',
+ 'state' => 'CA',
+ 'postcode' => '92831',
+ 'country' => 'US',
+ 'phone' => '+1 657-278-2011',
+ 'email' => 'test@example.org'
+ ]
+ ];
+
+ /**
+ * Creates an Address object for the specified country.
+ *
+ * @param string $country The country code (US, CA, GB, AU).
+ *
+ * @return Address The address object.
+ */
+ public function createAddress( string $country ): Address {
+ return new Address( self::ADDRESSES[$country] );
+ }
+}
tests/TestOrder.php
+<?php
+require_once '../Order.php';
+require_once '../Publisher.php';
+require_once '../Address.php';
+require_once '../Configuration.php';
+require_once 'TestHarness.php';
+require_once 'TestableOrder.php';
+
+class TestOrder {
+ public function run( $callback ) {
+ $callback( [
+ 'test_Order_Process_WithCode201_ProcessesSuccessfully',
+ 'test_Order_Process_WithCode400_ProcessesError',
+ 'test_Order_Process_WithCode500_ProcessesError'
+ ] );
+ }
+
+ public function test_Order_Process_WithCode201_ProcessesSuccessfully() {
+ return $this->runOrderTest( 201 );
+ }
+
+ public function test_Order_Process_WithCode400_ProcessesError() {
+ return $this->runOrderTest( 400 );
+ }
+
+ public function test_Order_Process_WithCode500_ProcessesError() {
+ return $this->runOrderTest( 500 );
+ }
+
+ private function runOrderTest( int $expectedCode ): bool {
+ $harness = new TestHarness();
+ $publisher = $this->createPublisher( $expectedCode );
+ $address = $harness->createAddress( 'US' );
+ $order = new TestableOrder();
+
+ $publisher->process( $address, $order );
+
+ return $order->processed( $expectedCode );
+ }
+
+ private function createPublisher( int $statusCode ): Publisher {
+ $config = new Configuration();
+ $publisher = new TestablePublisher( $statusCode );
+
+ $config->load();
+ $config->configure( $publisher );
+
+ return $publisher;
+ }
+}
+
+/**
+ * Testable Publisher subclass that returns controlled responses.
+ */
+class TestablePublisher extends Publisher {
+ private $statusCode;
+
+ public function __construct( int $statusCode ) {
+ $this->statusCode = $statusCode;
+ }
+
+ protected function post( string $path, string $content, string $header ) {
+ return [ 'body' => [], 'code' => $this->statusCode ];
+ }
+}
tests/TestPublisher.php
+<?php
+require_once '../Publisher.php';
+require_once '../Address.php';
+require_once '../Order.php';
+require_once '../Configuration.php';
+require_once 'TestableOrder.php';
+require_once 'TestHarness.php';
+
+class TestPublisher {
+ public function run( $callback ): void
+ {
+ $callback( [
+ 'test_Publisher_Process_WithUSAddress_UsesUSD',
+ 'test_Publisher_Process_WithCanadianAddress_UsesCAD',
+ 'test_Publisher_Process_WithUKAddress_UsesGBP',
+ 'test_Publisher_Process_WithAustralianAddress_UsesAUD'
+ ] );
+ }
+
+ public function test_Publisher_Process_WithUSAddress_UsesUSD() {
+ return $this->runPublisherTest( 'US' );
+ }
+
+ public function test_Publisher_Process_WithCanadianAddress_UsesCAD() {
+ return $this->runPublisherTest( 'CA' );
+ }
+
+ public function test_Publisher_Process_WithUKAddress_UsesGBP() {
+ return $this->runPublisherTest( 'GB' );
+ }
+
+ public function test_Publisher_Process_WithAustralianAddress_UsesAUD() {
+ return $this->runPublisherTest( 'AU' );
+ }
+
+ private function runPublisherTest( string $country ): bool {
+ $harness = new TestHarness();
+ $publisher = $this->createPublisher();
+ $address = $harness->createAddress( $country );
+ $order = new TestableOrder();
+
+ $publisher->process( $address, $order );
+
+ return $order->success();
+ }
+
+ private function createPublisher(): Publisher {
+ $config = new Configuration();
+ $publisher = new Publisher();
+
+ $config->load();
+ $config->configure( $publisher );
+
+ return $publisher;
+ }
+}
tests/TestableOrder.php
+<?php
+require_once '../Order.php';
+
+/**
+ * Testable Order subclass that tracks processing.
+ */
+class TestableOrder extends Order {
+ private bool $successful = false;
+ private int $processedCode = 0;
+
+ protected function processSuccess( array $response ): void {
+ $this->successful = true;
+ parent::processSuccess( $response );
+ }
+
+ protected function processError( array $response, int $statusCode ): void {
+ $this->successful = false;
+ parent::processError( $response, $statusCode );
+ }
+
+ public function process( array $response, int $statusCode ): void {
+ $this->processedCode = $statusCode;
+ parent::process( $response, $statusCode );
+ }
+
+ /**
+ * Checks if processing was successful.
+ *
+ * @return bool True if processing was successful.
+ */
+ public function success(): bool {
+ return $this->successful;
+ }
+
+ /**
+ * Checks if the order was processed with a specific status code.
+ *
+ * @param int $code The expected HTTP status code.
+ * @return bool True if processed with the expected code.
+ */
+ public function processed( int $code ): bool {
+ return $this->processedCode === $code;
+ }
+}
Delta360 lines added, 70 lines removed, 290-line increase