Prepare production deploy

This commit is contained in:
2025-10-27 19:23:12 +07:00
parent ce43bbf31f
commit ffe1bed051
12 changed files with 71 additions and 1178 deletions

View File

@@ -1,504 +0,0 @@
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Managing.Infrastructure.Evm;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Models.Privy;
using Moq;
using Moq.Protected;
using Org.Webpki.JsonCanonicalizer;
using Xunit;
namespace Managing.Infrastructure.Tests;
public class PrivyServiceTests
{
private readonly PrivyService _privyService;
public PrivyServiceTests()
{
_privyService = new PrivyService(new PrivySettings()
{
AppId = "cm7u09v0u002zrkuf2yjjr58p",
AppSecret = "25wwYu5AgxArU7djgvQEuioc9YSdGY3WN3r1dmXftPfH33KfGVfzopW3vqoPFjy1b8wS2gkDDZ9iQ8yxSo9Vi4iN",
AuthorizationKey =
"wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggpJ65PCo4E6NYpY867AyE6p1KxOrs8LJqHZw+t+076yhRANCAAS2EM23CtIfQRmHWTxcqb1j5yfrVePjZyBOZZ2RoPZHb9bDGLos206fTuVA3zgLVomlOoHTeYifkBASCn9Mfg3b"
});
}
[Fact]
public async Task Should_sign_message()
{
// Arrange
var walletId = "cm7vxs99f0007blcl8cmzv74t";
var message = "Hello, Ethereum";
var address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78";
// Act
var signature = await _privyService.SignMessageAsync(address, message);
// Assert
Assert.NotNull(signature);
}
// GsyIYCt202K339eTNmB8ZG/bziRiGUcwFoXwfq85Wf+kpZgaSvJSJ/zO6TSEbDdqrb6JEWVyv4zaBV6j28w2SQ==
// MEUCIQCxx3BVsVu+GyFI/vIYm2x4hloHojKhpFrnj4KjfypgkgIgFnrlQ8CmJ479qgXpY+wdt1D5ki5+SFzXXBzMd+ckIAM=
[Fact]
public async Task Should_Get_User_Wallets()
{
var did = "did:privy:cm7vxs99f0007blcl8cmzv74t";
var result = await _privyService.GetUserWalletsAsync(did);
Assert.NotNull(result);
}
[Fact]
public async Task Should_Get_User_Wallets_With_Delegation_Status()
{
// Arrange
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
var testDid = "did:privy:test123456789";
// Sample JSON response matching the actual API response structure
var mockResponse = new
{
did = testDid,
linked_accounts = new object[]
{
new
{
type = "wallet",
address = "0x123abc456def789ghi",
chain_type = "ethereum",
chain_id = "eip155:1",
delegated = true,
verified = true,
wallet_index = 0,
wallet_client = "privy",
wallet_client_type = "privy",
connector_type = "embedded",
imported = false,
recovery_method = "privy",
verified_at = 1741180715,
first_verified_at = 1741180715,
latest_verified_at = 1741180715,
id = "abc123"
},
new
{
type = "wallet",
address = "0x987zyx654wvu321tsr",
chain_type = "ethereum",
chain_id = "eip155:1",
delegated = false,
verified = true,
wallet_index = 1,
wallet_client = "privy",
wallet_client_type = "privy",
connector_type = "embedded",
imported = false,
recovery_method = "privy",
verified_at = 1741180715,
first_verified_at = 1741180715,
latest_verified_at = 1741180715,
id = "def456"
},
new
{
type = "email",
address = "test@example.com",
verified = true
}
}
};
var mockResponseJson = JsonSerializer.Serialize(mockResponse);
// Setup mock HttpClient to return our mock response
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(mockResponseJson, Encoding.UTF8, "application/json")
});
var httpClient = new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new Uri("https://auth.privy.io/")
};
// Create mock settings
var mockSettings = new Mock<IPrivySettings>();
mockSettings.Setup(s => s.AppId).Returns("test-app-id");
mockSettings.Setup(s => s.AppSecret).Returns("test-app-secret");
mockSettings.Setup(s => s.AuthorizationKey).Returns("wallet-auth:test-auth-key");
// Create PrivyService with mocked dependencies
var privyService = new PrivyService(mockSettings.Object);
// Use reflection to set the private _privyClient field
var privyClientField = typeof(PrivyService).GetField("_privyClient",
BindingFlags.Instance | BindingFlags.NonPublic);
privyClientField.SetValue(privyService, httpClient);
// Act
var result = await privyService.GetUserWalletsAsync(testDid);
// Assert
Assert.NotNull(result);
Assert.Equal(testDid, result.Did);
Assert.Equal(3, result.LinkedAccounts.Count);
// Verify wallet accounts correctly processed
var delegatedWallets = result.LinkedAccounts.Where(a => a.Type == "wallet" && a.Delegated).ToList();
Assert.Single(delegatedWallets);
Assert.Equal("0x123abc456def789ghi", delegatedWallets[0].Address);
Assert.Equal("privy", delegatedWallets[0].WalletClient);
Assert.Equal(0, delegatedWallets[0].WalletIndex);
Assert.Equal(1741180715, delegatedWallets[0].VerifiedAtTimestamp);
Assert.NotNull(delegatedWallets[0].VerifiedAt); // Verify timestamp conversion works
// Verify non-delegated wallet
var nonDelegatedWallets = result.LinkedAccounts.Where(a => a.Type == "wallet" && !a.Delegated).ToList();
Assert.Single(nonDelegatedWallets);
Assert.Equal("0x987zyx654wvu321tsr", nonDelegatedWallets[0].Address);
// Verify non-wallet account
var nonWalletAccounts = result.LinkedAccounts.Where(a => a.Type != "wallet").ToList();
Assert.Single(nonWalletAccounts);
Assert.Equal("email", nonWalletAccounts[0].Type);
// Verify HTTP request made correctly
mockHttpMessageHandler.Protected().Verify(
"SendAsync",
Times.Once(),
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get &&
req.RequestUri.ToString().Contains($"/api/v1/users/{testDid}")),
ItExpr.IsAny<CancellationToken>()
);
}
[Fact]
public void Should_Generate_Correct_Authorization_Signature()
{
// Arrange - Use the exact values from Privy documentation
var url = "https://api.privy.io/v1/wallets";
var body = new { chain_type = "ethereum" };
var httpMethod = "POST";
// Expected signature from Privy documentation
var expectedSignature =
"MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck=";
// Create a PrivyService instance with the sample auth key from the docs
var settings = new PrivySettings
{
AppId = "cm4db8x9t000ccn87pctvcg9j", // Sample app ID from Privy docs
AppSecret = "test-app-secret", // Not used for signature generation
AuthorizationKey =
"wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqOBE+hZld+PCaj051uOl0XpEwe3tKBC5tsYsKdnPymGhRANCAAQ2HyYUbLRcfj9obpViwjYU/S7FdNUehkcfjYdd+R2gH/1q0ZJx7mOF1zpiEbbBNRLuXzP0NPN6nonkI8umzLXZ"
};
var privyService = new PrivyService(settings);
// Act
var actualSignature = privyService.GenerateAuthorizationSignature(url, body, httpMethod);
// Assert
Assert.Equal(expectedSignature, actualSignature);
Console.WriteLine($"Signature verification passed! Generated: {actualSignature}");
}
[Fact]
public void Should_Generate_Same_Signature_For_Identical_Requests()
{
// Arrange
var url = "https://auth.privy.io/v1/wallets/rpc";
var body = new
{
address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78",
chain_type = "ethereum",
method = "personal_sign",
@params = new
{
message = "Hello, Ethereum",
encoding = "utf-8"
}
};
// Act
var signature1 = _privyService.GenerateAuthorizationSignature(url, body);
var signature2 = _privyService.GenerateAuthorizationSignature(url, body);
// Assert
Assert.NotNull(signature1);
Assert.NotNull(signature2);
Assert.Equal(signature1, signature2); // Signatures should be deterministic for the same input
}
[Fact]
public void Should_Generate_Different_Signatures_For_Different_Inputs()
{
// Arrange
var url = "https://auth.privy.io/v1/wallets/rpc";
var body1 = new
{
address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78",
chain_type = "ethereum",
method = "personal_sign",
@params = new
{
message = "Hello, Ethereum",
encoding = "utf-8"
}
};
var body2 = new
{
address = "0x932167388dD9aad41149b3cA23eBD489E2E2DD78",
chain_type = "ethereum",
method = "personal_sign",
@params = new
{
message = "Different message", // Only the message is different
encoding = "utf-8"
}
};
var differentUrl = "https://auth.privy.io/v1/wallets/different";
// Act
var signature1 = _privyService.GenerateAuthorizationSignature(url, body1);
var signature2 = _privyService.GenerateAuthorizationSignature(url, body2);
var signature3 = _privyService.GenerateAuthorizationSignature(differentUrl, body1);
// Assert
Assert.NotEqual(signature1, signature2); // Different message should produce different signature
Assert.NotEqual(signature1, signature3); // Different URL should produce different signature
Assert.NotEqual(signature2, signature3); // Different URL and message should produce different signature
}
[Fact]
public void Should_Generate_Correct_Signature_Payload_Structure()
{
// This test validates that we're constructing the payload structure correctly
// Arrange - Use the same sample data as the signature test
var url = "https://api.privy.io/v1/wallets";
var body = new { chain_type = "ethereum" };
var appId = "cm4db8x9t000ccn87pctvcg9j";
var httpMethod = "POST";
// Expected payload structure from the Privy documentation
var expectedPayloadStructure = new
{
version = 1,
method = "POST",
url = "https://api.privy.io/v1/wallets",
body = new { chain_type = "ethereum" },
headers = new Dictionary<string, string> { { "privy-app-id", appId } }
};
// Capture the actual payload by overriding the signature generation
string capturedPayload = null;
// We need to create a testable service that lets us inspect the payload
// Here's a simple mock that captures the JSON payload by inheriting from PrivyService
var testService = new TestPrivyService(new PrivySettings
{
AppId = appId,
AppSecret = "test-secret",
AuthorizationKey = "wallet-auth:test-key"
});
// Act - Generate the signature (this will capture the payload internally)
var payload = testService.GetSignaturePayload(url, body, httpMethod);
// Assert - verify payload matches expected structure
// Convert both to dictionaries for easier comparison
var expectedDict = ConvertAnonymousObjectToDictionary(expectedPayloadStructure);
var actualDict = ConvertAnonymousObjectToDictionary(payload);
// Check top-level properties
Assert.Equal(expectedDict["version"], actualDict["version"]);
Assert.Equal(expectedDict["method"], actualDict["method"]);
Assert.Equal(expectedDict["url"], actualDict["url"]);
// Compare body properties
var expectedBody = ConvertAnonymousObjectToDictionary(expectedDict["body"]);
var actualBody = ConvertAnonymousObjectToDictionary(actualDict["body"]);
Assert.Equal(expectedBody["chain_type"], actualBody["chain_type"]);
// Compare headers
var expectedHeaders = (Dictionary<string, string>)expectedDict["headers"];
var actualHeaders = (Dictionary<string, string>)actualDict["headers"];
Assert.Equal(expectedHeaders["privy-app-id"], actualHeaders["privy-app-id"]);
Console.WriteLine("Payload structure verification passed!");
}
[Fact]
public async Task Should_Use_n8n_To_sign()
{
var httpClient = new HttpClient();
var url = "https://n8n.kaigen.managing.live/webhook-test/3d931e75-40c8-403d-a26e-6200b328ca85";
httpClient.BaseAddress = new Uri(url);
var privyUrl = "https://api.privy.io/v1/wallets";
var body = new { chain_type = "ethereum" };
var httpMethod = "POST";
// Expected signature from Privy documentation
var expectedSignature =
"MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck=";
// Use Privy docs data for the request body
var signaturePayload = new Dictionary<string, object>
{
["method"] = "POST",
["url"] = privyUrl, // Use the FULL URL for signature calculation as per Privy docs
["body"] = body,
};
// Serialize the payload to JSON
var jsonPayload = JsonSerializer.Serialize(signaturePayload);
// Create the request
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json")
};
// Act
var response = await httpClient.SendAsync(request);
// Assert
Assert.NotNull(response);
Assert.True(response.IsSuccessStatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedSignature, responseContent);
}
[Fact]
public void Should_Sign_Correctly()
{
// Arrange
var url = "https://api.privy.io/v1/wallets";
var body = new { chain_type = "ethereum" };
var httpMethod = "POST";
var expectedSignature =
"MEUCIQDvwv0Ci+A+7bqi1x0UNBS7of5tV3dmon8hO3sbD3stSgIgEBGn1EdMukw7IrFS4WgYXbgZhXkp3NXL7O0T7dnO8Ck=";
var settings = new PrivySettings
{
AppId = "cm4db8x9t000ccn87pctvcg9j",
AuthorizationKey =
"wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqOBE+hZld+PCaj051uOl0XpEwe3tKBC5tsYsKdnPymGhRANCAAQ2HyYUbLRcfj9obpViwjYU/S7FdNUehkcfjYdd+R2gH/1q0ZJx7mOF1zpiEbbBNRLuXzP0NPN6nonkI8umzLXZ"
};
var payload = new
{
version = 1,
method = httpMethod,
url = url.TrimEnd('/'),
body = body,
headers = new
{
privy_app_id = settings.AppId
}
};
string serializedPayload = JsonSerializer.Serialize(payload);
JsonCanonicalizer jsonCanonicalizer = new JsonCanonicalizer(serializedPayload);
byte[] canonicalizedBytes = jsonCanonicalizer.GetEncodedUTF8();
Console.WriteLine($"Canonicalized JSON: {Encoding.UTF8.GetString(canonicalizedBytes)}");
byte[] privateKeyBytes = Convert.FromBase64String(settings.AuthorizationKey.Replace("wallet-auth:", ""));
using var privateKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
privateKey.ImportPkcs8PrivateKey(privateKeyBytes, out _);
byte[] signatureBuffer = privateKey.SignData(canonicalizedBytes, HashAlgorithmName.SHA256);
string signature = Convert.ToBase64String(signatureBuffer);
Console.WriteLine($"Generated Signature: {signature}");
Assert.Equal(expectedSignature, signature);
}
// Helper class for testing the payload structure
private class TestPrivyService : PrivyService
{
public TestPrivyService(IPrivySettings settings) : base(settings)
{
}
public object GetSignaturePayload(string url, object body, string httpMethod)
{
// Ensure we have a full, absolute URL for signature calculation
string fullUrl;
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
// Already a full URL
fullUrl = url;
}
else
{
// It's a relative path, so construct the full URL using the base address
string relativePath = url.StartsWith("/") ? url.Substring(1) : url;
fullUrl = new Uri(new Uri("https://auth.privy.io/"), relativePath).ToString();
}
// Create the signature payload structure exactly as in the GenerateAuthorizationSignature method
var headers = new Dictionary<string, string> { { "privy-app-id", GetPrivyAppId() } };
return new
{
version = 1,
method = httpMethod,
url = fullUrl,
body = body,
headers = headers
};
}
// Helper to expose the private _appId
private string GetPrivyAppId()
{
// Use reflection to get the private field
var field = typeof(PrivyService).GetField("_appId",
BindingFlags.Instance | BindingFlags.NonPublic);
return (string)field.GetValue(this);
}
}
// Helper for converting anonymous objects to dictionaries for easier comparison
private Dictionary<string, object> ConvertAnonymousObjectToDictionary(object obj)
{
if (obj is Dictionary<string, object> dict)
return dict;
var result = new Dictionary<string, object>();
foreach (var prop in obj.GetType().GetProperties())
{
result[prop.Name] = prop.GetValue(obj);
}
return result;
}
}