Skip to content

Commit 609a753

Browse files
committed
WOOTAX-258: address PR review — @SInCE stamps and cache-hit coverage
Review follow-ups on #2983: - The five new private cache-key helpers were stamped `@since 3.4.0`; the release in progress is 3.6.10. - `calculate_backend_totals()` passes the order-item-ID-keyed map from `get_backend_line_items()` straight into `calculate_tax()` without going through `group_items_by_location()`, so `array_values()` is the only thing keeping `line_items` a JSON array rather than an object on every admin-side request. The one existing `calculate_tax()` test returns at the cross-state guard and never reaches it. Adds a test that captures the encoded body and asserts a zero-indexed list, checking the raw JSON as well as the decoded array since `json_decode()` hides the array/object distinction. - `smartcalcs_cache_request()` had no coverage at all: the existing tests compare `get_cache_signature()` outputs in isolation, verifying the ingredients but never the result. Adds a test that counts `smartcalcs_request()` invocations across two equivalent-but-byte-different bodies and asserts one HTTP call plus a populated signature transient. It asserts specifically on `tj_tax_<md5(signature)>` and on the *absence* of the `tj_tax_<zip>_<state>` key, which only ever caches 400 zip-to-state mismatches and would otherwise mask a broken signature. Both new tests were verified to fail against mutated guards.
1 parent a3a1130 commit 609a753

2 files changed

Lines changed: 151 additions & 5 deletions

File tree

classes/class-wc-connect-taxjar-integration.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,7 @@ protected function get_line_item( $id, $line_items ) {
11141114
* identical lines — an order can legitimately hold the same product twice at the
11151115
* same price — distinct, so neither loses its rate.
11161116
*
1117-
* @since 3.4.0
1117+
* @since 3.6.10
11181118
*
11191119
* @param array $line_items Line items whose 'id' is currently the bare product ID.
11201120
*
@@ -1156,7 +1156,7 @@ private function assign_canonical_line_item_ids( array $line_items ): array {
11561156
* cache keys and line item fingerprints — never to build the request sent to
11571157
* TaxJar, which keeps the merchant's values verbatim.
11581158
*
1159-
* @since 3.4.0
1159+
* @since 3.6.10
11601160
*
11611161
* @param mixed $value Value to normalize.
11621162
*
@@ -1186,7 +1186,7 @@ private function normalize_cache_string( $value ): string {
11861186
* silently reinterpreted — notably ZIP codes, where "01234" must never become
11871187
* "1234".
11881188
*
1189-
* @since 3.4.0
1189+
* @since 3.6.10
11901190
*
11911191
* @param mixed $value Value to normalize.
11921192
*
@@ -1223,7 +1223,7 @@ private function normalize_cache_number( $value ): string {
12231223
*
12241224
* Only the cache key is derived from this. The body sent to TaxJar is untouched.
12251225
*
1226-
* @since 3.4.0
1226+
* @since 3.6.10
12271227
*
12281228
* @param string $json Encoded TaxJar request body.
12291229
*
@@ -1246,7 +1246,7 @@ private function get_cache_signature( $json ): string {
12461246
* value, because several address fields hold digit-only strings that must keep
12471247
* their exact form (a leading-zero ZIP above all).
12481248
*
1249-
* @since 3.4.0
1249+
* @since 3.6.10
12501250
*
12511251
* @param mixed $value Value to canonicalize.
12521252
* @param string $key Key the value was found under.

tests/php/test-class-wc-connect-taxjar-integration.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,4 +2185,150 @@ public function test_cache_signature_falls_back_for_undecodable_body() {
21852185
$this->assertSame( 'not json', $signature );
21862186
$this->assertNotSame( $signature, $other );
21872187
}
2188+
2189+
// -------------------------------------------------------------------------
2190+
// Encoded request body and cache-hit tests
2191+
// -------------------------------------------------------------------------
2192+
2193+
/**
2194+
* calculate_backend_totals() hands calculate_tax() the order-item-ID-keyed map
2195+
* from get_backend_line_items() without re-indexing it, so array_values() in
2196+
* calculate_tax() is the only thing keeping 'line_items' a JSON array. A
2197+
* string-keyed PHP array encodes as a JSON object, which TaxJar would reject —
2198+
* silently, on every admin-side request.
2199+
*/
2200+
public function test_calculate_tax_encodes_keyed_line_items_as_json_list() {
2201+
$captured = null;
2202+
2203+
$integration = $this->getMockBuilder( 'WC_Connect_TaxJar_Integration' )
2204+
->disableOriginalConstructor()
2205+
->onlyMethods( array( 'smartcalcs_cache_request', 'get_store_settings', '_log' ) )
2206+
->getMock();
2207+
2208+
// Same state as the destination below, so the cross-state guard does not return early.
2209+
$integration->method( 'get_store_settings' )->willReturn(
2210+
array(
2211+
'country' => 'US',
2212+
'state' => 'CA',
2213+
'postcode' => '94110',
2214+
'city' => 'San Francisco',
2215+
'street' => '1 Main St',
2216+
)
2217+
);
2218+
2219+
$integration->method( 'smartcalcs_cache_request' )->willReturnCallback(
2220+
function ( $json ) use ( &$captured ) {
2221+
$captured = $json;
2222+
// The encoded body is all this test needs; bail before any HTTP work.
2223+
return false;
2224+
}
2225+
);
2226+
2227+
WC()->customer->set_is_vat_exempt( false );
2228+
2229+
$integration->calculate_tax(
2230+
array(
2231+
'to_country' => 'US',
2232+
'to_state' => 'CA',
2233+
'to_zip' => '94110',
2234+
'to_city' => 'San Francisco',
2235+
'to_street' => '2 Other St',
2236+
'shipping_amount' => 0,
2237+
// Keyed by order item ID, exactly as get_backend_line_items() returns it.
2238+
'line_items' => array(
2239+
'12' => array(
2240+
'id' => '42-abc123def456-0',
2241+
'quantity' => 1,
2242+
'unit_price' => '25.00',
2243+
),
2244+
'34' => array(
2245+
'id' => '43-bbbbbbbbbbbb-0',
2246+
'quantity' => 2,
2247+
'unit_price' => '10.00',
2248+
),
2249+
),
2250+
)
2251+
);
2252+
2253+
$this->assertNotNull( $captured, 'calculate_tax() returned before encoding a request body.' );
2254+
2255+
$decoded = json_decode( $captured, true );
2256+
2257+
$this->assertArrayHasKey( 'line_items', $decoded );
2258+
// A JSON object would decode to keys '12' and '34' instead.
2259+
$this->assertSame( array( 0, 1 ), array_keys( $decoded['line_items'] ) );
2260+
$this->assertSame( '42-abc123def456-0', $decoded['line_items'][0]['id'] );
2261+
$this->assertSame( '43-bbbbbbbbbbbb-0', $decoded['line_items'][1]['id'] );
2262+
// Assert on the raw JSON too, since json_decode() hides the array/object distinction.
2263+
$this->assertStringContainsString( '"line_items":[{', $captured );
2264+
}
2265+
2266+
/**
2267+
* The point of the canonical signature: two bodies that differ only in
2268+
* formatting must share one cache entry, so the second request never reaches
2269+
* the API. Exercises smartcalcs_cache_request() end to end rather than
2270+
* comparing get_cache_signature() outputs in isolation.
2271+
*/
2272+
public function test_smartcalcs_cache_request_serves_equivalent_bodies_from_one_entry() {
2273+
$call_count = 0;
2274+
2275+
$api_client = $this->getMockBuilder( 'WC_Connect_API_Client' )
2276+
->disableOriginalConstructor()
2277+
->getMock();
2278+
2279+
$logger = $this->getMockBuilder( 'WC_Connect_Logger' )
2280+
->disableOriginalConstructor()
2281+
->getMock();
2282+
2283+
$tracks = $this->getMockBuilder( 'WC_Connect_Tracks' )
2284+
->disableOriginalConstructor()
2285+
->getMock();
2286+
2287+
// Real notifier: clear_notices() is static (so it cannot be stubbed) and is a
2288+
// no-op without a WC session, which the unit-test context does not set up.
2289+
$notifier = new Automattic\WCServices\StoreNotices\StoreNoticesNotifier( false );
2290+
2291+
// Real constructor, so cache_time is populated and the transient is written with it.
2292+
$integration = $this->getMockBuilder( 'WC_Connect_TaxJar_Integration' )
2293+
->setConstructorArgs( array( $api_client, $logger, 'https://example.com', $tracks, $notifier ) )
2294+
->onlyMethods( array( 'smartcalcs_request' ) )
2295+
->getMock();
2296+
2297+
$api_response = array(
2298+
'response' => array( 'code' => 200 ),
2299+
'body' => wp_json_encode( array( 'tax' => array( 'amount_to_collect' => 2.5 ) ) ),
2300+
);
2301+
2302+
$integration->method( 'smartcalcs_request' )->willReturnCallback(
2303+
function () use ( &$call_count, $api_response ) {
2304+
++$call_count;
2305+
return $api_response;
2306+
}
2307+
);
2308+
2309+
$baseline = $this->get_taxjar_request_body();
2310+
$variant = $this->get_taxjar_request_body(
2311+
array(
2312+
'to_city' => ' beverly hills ',
2313+
'shipping' => '5',
2314+
)
2315+
);
2316+
2317+
// Documents the pre-fix baseline: without canonicalization these are two cache entries.
2318+
$this->assertNotSame( $baseline, $variant, 'The two bodies must differ byte-wise or this test proves nothing.' );
2319+
2320+
// from_state 'CA' skips the California nexus check on both the fresh and cached path.
2321+
$first = $integration->smartcalcs_cache_request( $baseline, 'CA' );
2322+
$second = $integration->smartcalcs_cache_request( $variant, 'CA' );
2323+
2324+
$this->assertSame( 1, $call_count, 'The equivalent second body should have been served from the cache.' );
2325+
$this->assertEquals( $api_response, $first );
2326+
$this->assertEquals( $api_response, $second );
2327+
2328+
// The hit must come from the signature key, not the zip/state key — that one
2329+
// only ever caches 400 zip-to-state mismatches and would mask a broken signature.
2330+
$signature = $this->invoke_protected_method( 'get_cache_signature', array( $baseline ) );
2331+
$this->assertEquals( $api_response, get_transient( 'tj_tax_' . hash( 'md5', $signature ) ) );
2332+
$this->assertFalse( get_transient( 'tj_tax_90210_ca' ) );
2333+
}
21882334
}

0 commit comments

Comments
 (0)