Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ public HttpClient CreateAuthenticatedClient(string baseUrl, string apiKey, strin
{
InnerHandler = innerHandler
};

return new HttpClient(authenticator) { BaseAddress = new Uri(baseUrl) };
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void GenerateJwt_WithDifferentUris_ProducesConsistentBehavior()
// Act & Assert - Both should fail consistently with invalid key
var act1 = () => _sut.GenerateJwt(uri1, TestApiKey, invalidKey);
var act2 = () => _sut.GenerateJwt(uri2, TestApiKey, invalidKey);

act1.Should().Throw<Exception>();
act2.Should().Throw<Exception>();
}
Expand Down Expand Up @@ -100,7 +100,7 @@ public void GenerateJwt_ParameterValidation_RequiresAllParameters()
{
// This test verifies the method signature and basic parameter handling
// without needing real cryptographic operations

// Arrange
var uri = "https://api.coinbase.com/api/v3/brokerage/accounts";
var apiKey = "test-key";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
// Assert
result.Should().NotBeNull();
result.IsSuccess.Should().BeTrue();
result.Data!.SuccessResponse.OrderId.Should().Be("success-after-retry");

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 10.0.x)

Dereference of a possibly null reference.

Check failure on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / code-quality

Dereference of a possibly null reference.

Check failure on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / code-quality

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (macos-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 181 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (macos-latest, 10.0.x)

Dereference of a possibly null reference.

await _mockApi.Received(3).PlaceOrder(orderRequest);
}
Expand Down Expand Up @@ -520,7 +520,7 @@
// Assert
result.Should().NotBeNull();
result.IsSuccess.Should().BeTrue();
result.Data!.SuccessResponse.OrderId.Should().Be("close-position-order-id");

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 10.0.x)

Dereference of a possibly null reference.

Check failure on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / code-quality

Dereference of a possibly null reference.

Check failure on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / code-quality

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (macos-latest, 10.0.x)

Dereference of a possibly null reference.

Check warning on line 523 in Coinbase.AdvancedTrade.Client.Tests/CoinbaseAdvancedTradeClientTests.cs

View workflow job for this annotation

GitHub Actions / test (macos-latest, 10.0.x)

Dereference of a possibly null reference.

await _mockApi.Received(1).ClosePosition(request);
}
Expand Down Expand Up @@ -569,4 +569,266 @@
}

#endregion

#region CancelOrdersAsync Tests

[Fact]
public async Task CancelOrdersAsync_WithSuccessfulCancellation_ReturnsSuccess()
{
// Arrange
var orderIds = new List<string> { "order-1", "order-2" };
var expectedResponse = new CancelOrdersResponse
{
Results = new List<CancelOrderResult>
{
new CancelOrderResult { Success = true, OrderId = "order-1" },
new CancelOrderResult { Success = true, OrderId = "order-2" }
}
};

_mockApi.CancelOrders(Arg.Any<CancelOrdersRequest>()).Returns(expectedResponse);

// Act
var result = await _sut.CancelOrdersAsync(orderIds);

// Assert
result.IsSuccess.Should().BeTrue();
result.Data!.Results.Should().HaveCount(2);
result.Data.Results.Should().AllSatisfy(r => r.Success.Should().BeTrue());
}

[Fact]
public async Task CancelOrdersAsync_WithPartialFailure_ReturnsResultsWithFailures()
{
// Arrange
var orderIds = new List<string> { "order-1", "order-2" };
var expectedResponse = new CancelOrdersResponse
{
Results = new List<CancelOrderResult>
{
new CancelOrderResult { Success = true, OrderId = "order-1" },
new CancelOrderResult { Success = false, OrderId = "order-2", FailureReason = "UNKNOWN_CANCEL_FAILURE_REASON" }
}
};

_mockApi.CancelOrders(Arg.Any<CancelOrdersRequest>()).Returns(expectedResponse);

// Act
var result = await _sut.CancelOrdersAsync(orderIds);

// Assert
result.IsSuccess.Should().BeTrue();
result.Data!.Results.Should().HaveCount(2);
result.Data.Results[0].Success.Should().BeTrue();
result.Data.Results[1].Success.Should().BeFalse();
result.Data.Results[1].FailureReason.Should().Be("UNKNOWN_CANCEL_FAILURE_REASON");
}

[Fact]
public async Task CancelOrdersAsync_WithApiException_ReturnsError()
{
// Arrange
var orderIds = new List<string> { "order-1" };
var apiException = await ApiException.Create(
new HttpRequestMessage(HttpMethod.Post, "https://api.coinbase.com"),
HttpMethod.Post,
new HttpResponseMessage(HttpStatusCode.Unauthorized),
new RefitSettings()
);

_mockApi.CancelOrders(Arg.Any<CancelOrdersRequest>()).Throws(apiException);

// Act
var result = await _sut.CancelOrdersAsync(orderIds);

// Assert
result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("Authentication failed");
}

#endregion

#region GetOrderAsync Tests

[Fact]
public async Task GetOrderAsync_WithValidOrderId_ReturnsOrder()
{
// Arrange
var orderId = "test-order-id";
var expectedResponse = new GetOrderResponse
{
Order = new OrderV3
{
OrderId = orderId,
ProductId = "BTC-USD",
UserId = "user-1",
OrderConfiguration = new OrderConfiguration(),
Side = "BUY",
ClientOrderId = "client-order-1",
Status = "FILLED",
TimeInForce = "GOOD_UNTIL_CANCELLED",
CreatedTime = "2024-01-01T00:00:00Z",
CompletionPercentage = "100",
FilledSize = "0.5",
AverageFilledPrice = "50000",
NumberOfFills = "1"
}
};

_mockApi.GetOrder(orderId).Returns(expectedResponse);

// Act
var result = await _sut.GetOrderAsync(orderId);

// Assert
result.IsSuccess.Should().BeTrue();
result.Data!.Order.OrderId.Should().Be(orderId);
result.Data.Order.Status.Should().Be("FILLED");
}

[Fact]
public async Task GetOrderAsync_WithNotFoundOrderId_ReturnsError()
{
// Arrange
var orderId = "nonexistent-order";
var notFoundException = await ApiException.Create(
new HttpRequestMessage(HttpMethod.Get, "https://api.coinbase.com"),
HttpMethod.Get,
new HttpResponseMessage(HttpStatusCode.NotFound),
new RefitSettings()
);

_mockApi.GetOrder(orderId).Throws(notFoundException);

// Act
var result = await _sut.GetOrderAsync(orderId);

// Assert
result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("not found");
}

#endregion

#region GetProductAsync Tests

[Fact]
public async Task GetProductAsync_WithValidProductId_ReturnsProduct()
{
// Arrange
var productId = "BTC-USD";
var expectedProduct = new AdvancedTradeProduct
{
ProductId = productId,
Price = "50000.00",
PricePercentageChange24h = "2.5",
Volume24h = "1000",
VolumePercentageChange24h = "5.0",
BaseIncrement = "0.00000001",
QuoteIncrement = "0.01",
QuoteMinSize = "1",
QuoteMaxSize = "10000000",
BaseMinSize = "0.00000001",
BaseMaxSize = "1000",
BaseCurrencyId = "BTC",
QuoteCurrencyId = "USD",
DisplayName = "BTC/USD",
Status = "online"
};

_mockApi.GetProduct(productId).Returns(expectedProduct);

// Act
var result = await _sut.GetProductAsync(productId);

// Assert
result.IsSuccess.Should().BeTrue();
result.Data!.ProductId.Should().Be(productId);
result.Data.Price.Should().Be("50000.00");
}

[Fact]
public async Task GetProductAsync_WithNotFoundProduct_ReturnsError()
{
// Arrange
var productId = "INVALID-PAIR";
var notFoundException = await ApiException.Create(
new HttpRequestMessage(HttpMethod.Get, "https://api.coinbase.com"),
HttpMethod.Get,
new HttpResponseMessage(HttpStatusCode.NotFound),
new RefitSettings()
);

_mockApi.GetProduct(productId).Throws(notFoundException);

// Act
var result = await _sut.GetProductAsync(productId);

// Assert
result.IsSuccess.Should().BeFalse();
result.ErrorMessage.Should().Contain("not found");
}

#endregion

#region CancellationToken Tests

[Fact]
public async Task PlaceOrderAsync_CancellationToken_IsPassedToApi()
{
// Arrange
var orderRequest = new OrderRequest
{
ClientOrderId = Guid.NewGuid().ToString(),
ProductId = "BTC-USD",
Side = "BUY",
OrderConfiguration = new OrderConfiguration
{
MarketMarketIoc = new MarketMarketIoc { QuoteSize = "100" }
}
};

var expectedResponse = new OrderInformation
{
Success = true,
SuccessResponse = new SuccessResponse { OrderId = "test-order-id" }
};

using var cts = new CancellationTokenSource();
var token = cts.Token;

_mockApi.PlaceOrder(Arg.Any<OrderRequest>(), Arg.Any<CancellationToken>()).Returns(expectedResponse);

// Act
var result = await _sut.PlaceOrderAsync(orderRequest, token);

// Assert
result.IsSuccess.Should().BeTrue();
await _mockApi.Received(1).PlaceOrder(Arg.Any<OrderRequest>(), Arg.Any<CancellationToken>());
}

[Fact]
public async Task ListAccountsAsync_CancellationToken_IsPassedToApi()
{
// Arrange
var expectedResponse = new AccountsResponse
{
Accounts = new List<CoinbaseAccount>(),
HasNext = false
};

using var cts = new CancellationTokenSource();
var token = cts.Token;

_mockApi.ListAccounts(Arg.Any<CancellationToken>()).Returns(expectedResponse);

// Act
var result = await _sut.ListAccountsAsync(token);

// Assert
result.IsSuccess.Should().BeTrue();
await _mockApi.Received(1).ListAccounts(Arg.Any<CancellationToken>());
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,102 @@ public void Models_HandleEmptyArrays_Correctly()
}

#endregion

#region CancelOrders Serialization Tests

[Fact]
public void CancelOrdersRequest_Serialization_ProducesCorrectJson()
{
// Arrange
var request = new CancelOrdersRequest
{
OrderIds = new List<string> { "order-1", "order-2" }
};

// Act
var json = JsonSerializer.Serialize(request, _jsonOptions);
var deserialized = JsonSerializer.Deserialize<CancelOrdersRequest>(json, _jsonOptions);

// Assert
deserialized.Should().NotBeNull();
deserialized!.OrderIds.Should().HaveCount(2);
deserialized.OrderIds.Should().Contain("order-1");
deserialized.OrderIds.Should().Contain("order-2");
json.Should().Contain("order_ids");
}

[Fact]
public void CancelOrdersResponse_Deserialization_ParsesCorrectly()
{
// Arrange
var json = @"{
""results"": [
{
""success"": true,
""failure_reason"": null,
""order_id"": ""order-1""
},
{
""success"": false,
""failure_reason"": ""UNKNOWN_CANCEL_FAILURE_REASON"",
""order_id"": ""order-2""
}
]
}";

// Act
var result = JsonSerializer.Deserialize<CancelOrdersResponse>(json, _jsonOptions);

// Assert
result.Should().NotBeNull();
result!.Results.Should().HaveCount(2);
result.Results[0].Success.Should().BeTrue();
result.Results[0].OrderId.Should().Be("order-1");
result.Results[1].Success.Should().BeFalse();
result.Results[1].FailureReason.Should().Be("UNKNOWN_CANCEL_FAILURE_REASON");
}

#endregion

#region GetOrderResponse Serialization Tests

[Fact]
public void GetOrderResponse_Deserialization_ParsesCorrectly()
{
// Arrange
var json = @"{
""order"": {
""order_id"": ""test-order-123"",
""product_id"": ""BTC-USD"",
""user_id"": ""user-1"",
""order_configuration"": {},
""side"": ""BUY"",
""client_order_id"": ""client-1"",
""status"": ""FILLED"",
""time_in_force"": ""GOOD_UNTIL_CANCELLED"",
""created_time"": ""2024-01-01T00:00:00Z"",
""completion_percentage"": ""100"",
""filled_size"": ""0.5"",
""average_filled_price"": ""50000.00"",
""number_of_fills"": ""1"",
""total_fees"": ""25.00"",
""settled"": true
}
}";

// Act
var result = JsonSerializer.Deserialize<GetOrderResponse>(json, _jsonOptions);

// Assert
result.Should().NotBeNull();
result!.Order.Should().NotBeNull();
result.Order.OrderId.Should().Be("test-order-123");
result.Order.ProductId.Should().Be("BTC-USD");
result.Order.Status.Should().Be("FILLED");
result.Order.FilledSize.Should().Be("0.5");
result.Order.TotalFees.Should().Be("25.00");
result.Order.Settled.Should().BeTrue();
}

#endregion
}
Loading
Loading