headers('post', '/v3/example?b=2&a=1', $body); $contentMd5 = base64_encode(md5($body, true)); $stringToSign = "POST\n*/*\n{$contentMd5}\napplication/json; charset=UTF-8\n\n/v3/example?a=1&b=2"; $expected = base64_encode(hash_hmac('sha256', $stringToSign, 'secret-456', true)); $this->assertSame('app-123', $headers['X-Tsign-Open-App-Id']); $this->assertSame('Signature', $headers['X-Tsign-Open-Auth-Mode']); $this->assertSame($contentMd5, $headers['Content-MD5']); $this->assertSame($expected, $headers['X-Tsign-Open-Ca-Signature']); $this->assertMatchesRegularExpression('/^\d{13}$/', $headers['X-Tsign-Open-Ca-Timestamp']); } public function test_it_sorts_query_keys_and_keeps_only_the_first_repeated_value(): void { $signer = new EsignRequestSigner('app', 'secret'); $this->assertSame( '/v3/files?empty&keyword=first&z=9', $signer->canonicalPath('/v3/files?z=9&keyword=first&empty=&keyword=second'), ); } public function test_it_rejects_non_api_paths(): void { $this->expectException(InvalidArgumentException::class); (new EsignRequestSigner('app', 'secret'))->canonicalPath('v3/files'); } }