This class contains unit tests for the NetworkStorage service. It tests rate limit detection, delay calculation, and how requests are retried without losing original content.
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Sandbox;
[TestClass]
public class NetworkStorageRateLimitTests
{
[TestMethod]
public void IsRetryableRateLimit_RetriesReadsAndOnlyRetrySafeWrites()
{
// A real HTTP 429 hides its body in s&box, so a read may retry and a write may not (retrySafe unknown).
Assert.IsTrue( NetworkStorage.IsRetryableRateLimit( "GET", 429, "" ) );
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "POST", 429, "" ) );
// Endpoint wrapper: HTTP 200 carrying status 429 and a RATE_LIMITED code.
const string limited = "{\"ok\":false,\"status\":429,\"error\":{\"code\":\"RATE_LIMITED\",\"message\":\"slow down\"}}";
const string limitedRetrySafe = "{\"ok\":false,\"status\":429,\"error\":{\"code\":\"RATE_LIMITED\",\"message\":\"slow down\"},\"retrySafe\":true}";
Assert.IsTrue( NetworkStorage.IsRetryableRateLimit( "GET", 200, limited ) );
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "POST", 200, limited ) );
Assert.IsTrue( NetworkStorage.IsRetryableRateLimit( "POST", 200, limitedRetrySafe ) );
}
[TestMethod]
public void IsRetryableRateLimit_IgnoresAnswersThatAreNotTransient()
{
// Plan or quota limits also answer 429 but are not transient.
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "GET", 429, "{\"ok\":false,\"error\":{\"code\":\"PLAN_LIMIT_EXCEEDED\"}}" ) );
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "GET", 200, "{\"ok\":false,\"status\":429,\"error\":{\"code\":\"PLAN_LIMIT_EXCEEDED\"}}" ) );
// Successes and other statuses are never retried.
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "GET", 200, "{\"ok\":true,\"data\":{}}" ) );
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "GET", 200, "{\"ok\":false,\"status\":409,\"error\":{\"code\":\"PLAYER_EXISTS\"}}" ) );
Assert.IsFalse( NetworkStorage.IsRetryableRateLimit( "GET", 500, "" ) );
}
[TestMethod]
public void IsRetryableRateLimit_AcceptsPlainRateLimitErrorString()
{
// Request limits answer with a plain error string rather than an error object.
Assert.IsTrue( NetworkStorage.IsRetryableRateLimit( "GET", 429, "{\"error\":\"RATE_LIMITED\"}" ) );
Assert.IsTrue( NetworkStorage.IsRetryableRateLimit( "GET", 429, "not json" ) );
}
[TestMethod]
public void RateLimitDelaySeconds_PrefersHeaderThenBodyThenDefault()
{
Assert.AreEqual( 2.0, NetworkStorage.RateLimitDelaySeconds( "2", "" ), 0.0 );
Assert.AreEqual( 0.0, NetworkStorage.RateLimitDelaySeconds( "0", "" ), 0.0 );
Assert.AreEqual( 3.0, NetworkStorage.RateLimitDelaySeconds( "soon", "{\"retryAfterSec\":3}" ), 0.0 );
Assert.AreEqual( 1.0, NetworkStorage.RateLimitDelaySeconds( "-1", "" ), 0.0 );
Assert.AreEqual( 1.0, NetworkStorage.RateLimitDelaySeconds( null, "not json" ), 0.0 );
Assert.AreEqual( 1.0, NetworkStorage.RateLimitDelaySeconds( null, "" ), 0.0 );
}
[TestMethod]
public async Task SendWithRetries_RetriedPostStillSendsOriginalBytes()
{
const string payload = "{\"ore\":\"moon_ore\",\"kg\":5}";
const string limited = "{\"ok\":false,\"status\":429,\"error\":{\"code\":\"RATE_LIMITED\"},\"retrySafe\":true,\"retryAfterSec\":0}";
const string accepted = "{\"ok\":true,\"data\":{\"minedKg\":5}}";
var received = new List<string>();
var sent = new List<HttpContent>();
var reply = await NetworkStorage.SendWithRetriesAsync( "POST", () => new StringContent( payload ),
async content =>
{
// Like the engine, each send consumes and disposes the request body it was given.
Assert.IsFalse( sent.Exists( c => ReferenceEquals( c, content ) ) );
sent.Add( content );
received.Add( await content.ReadAsStringAsync() );
content.Dispose();
return received.Count == 1
? new HttpResponseMessage( HttpStatusCode.TooManyRequests ) { Content = new StringContent( limited ) }
: new HttpResponseMessage( HttpStatusCode.OK ) { Content = new StringContent( accepted ) };
} );
Assert.AreEqual( 200, reply.Status );
Assert.AreEqual( accepted, reply.Body );
Assert.AreEqual( 2, received.Count );
Assert.AreEqual( payload, received[0] );
Assert.AreEqual( payload, received[1] );
}
}