mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-06-19 18:28:18 +08:00
Feat/monorepo (#734)
* copied everything from repos back to ocelot repo * added src projects to sln * removed all test projects that have no tests * added all test projects to sln * removed test not on master * merged unit tests * merged acceptance tests * merged integration tests * fixed namepaces * build script creates packages for all projects * updated docs to make sure no references to external repos that we will remove * +semver: breaking
This commit is contained in:
259
test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs
Normal file
259
test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs
Normal file
@ -0,0 +1,259 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using Butterfly.Client.AspNetCore;
|
||||
using Configuration.File;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Rafty.Infrastructure;
|
||||
using Shouldly;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
public class ButterflyTracingTests : IDisposable
|
||||
{
|
||||
private IWebHost _serviceOneBuilder;
|
||||
private IWebHost _serviceTwoBuilder;
|
||||
private IWebHost _fakeButterfly;
|
||||
private readonly Steps _steps;
|
||||
private string _downstreamPathOne;
|
||||
private string _downstreamPathTwo;
|
||||
private int _butterflyCalled;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ButterflyTracingTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_steps = new Steps();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_forward_tracing_information_from_ocelot_and_downstream_services()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/values",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51887,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/api001/values",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||
{
|
||||
UseTracing = true
|
||||
}
|
||||
},
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/values",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51388,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/api002/values",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||
{
|
||||
UseTracing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var butterflyUrl = "http://localhost:9618";
|
||||
|
||||
this.Given(x => GivenFakeButterfly(butterflyUrl))
|
||||
.And(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl))
|
||||
.And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||
.BDDfy();
|
||||
|
||||
var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => _butterflyCalled >= 4);
|
||||
|
||||
_output.WriteLine($"_butterflyCalled is {_butterflyCalled}");
|
||||
|
||||
commandOnAllStateMachines.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_tracing_header()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/values",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51387,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/api001/values",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||
{
|
||||
UseTracing = true
|
||||
},
|
||||
DownstreamHeaderTransform = new Dictionary<string, string>()
|
||||
{
|
||||
{"Trace-Id", "{TraceId}"},
|
||||
{"Tom", "Laura"}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var butterflyUrl = "http://localhost:9618";
|
||||
|
||||
this.Given(x => GivenFakeButterfly(butterflyUrl))
|
||||
.And(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id"))
|
||||
.And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl)
|
||||
{
|
||||
_serviceOneBuilder = new WebHostBuilder()
|
||||
.UseUrls(baseUrl)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.ConfigureServices(services => {
|
||||
services.AddButterfly(option =>
|
||||
{
|
||||
option.CollectorUrl = butterflyUrl;
|
||||
option.Service = "Service One";
|
||||
option.IgnoredRoutesRegexPatterns = new string[0];
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UsePathBase(basePath);
|
||||
app.Run(async context =>
|
||||
{
|
||||
_downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||
|
||||
if(_downstreamPathOne != basePath)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_serviceOneBuilder.Start();
|
||||
}
|
||||
|
||||
private void GivenFakeButterfly(string baseUrl)
|
||||
{
|
||||
_fakeButterfly = new WebHostBuilder()
|
||||
.UseUrls(baseUrl)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
{
|
||||
_butterflyCalled++;
|
||||
await context.Response.WriteAsync("OK...");
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_fakeButterfly.Start();
|
||||
}
|
||||
|
||||
private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl)
|
||||
{
|
||||
_serviceTwoBuilder = new WebHostBuilder()
|
||||
.UseUrls(baseUrl)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.ConfigureServices(services => {
|
||||
services.AddButterfly(option =>
|
||||
{
|
||||
option.CollectorUrl = butterflyUrl;
|
||||
option.Service = "Service Two";
|
||||
option.IgnoredRoutesRegexPatterns = new string[0];
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UsePathBase(basePath);
|
||||
app.Run(async context =>
|
||||
{
|
||||
_downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||
|
||||
if(_downstreamPathTwo != basePath)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_serviceTwoBuilder.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceOneBuilder?.Dispose();
|
||||
_serviceTwoBuilder?.Dispose();
|
||||
_fakeButterfly?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
137
test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs
Normal file
137
test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs
Normal file
@ -0,0 +1,137 @@
|
||||
namespace Ocelot.AcceptanceTests.Caching
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using CacheManager.Core;
|
||||
using CacheManager.Core.Internal;
|
||||
using CacheManager.Core.Logging;
|
||||
using CacheManager.Core.Utility;
|
||||
|
||||
public class InMemoryJsonHandle<TCacheValue> : BaseCacheHandle<TCacheValue>
|
||||
{
|
||||
private readonly ICacheSerializer _serializer;
|
||||
private readonly ConcurrentDictionary<string, Tuple<Type, byte[]>> _cache;
|
||||
|
||||
public InMemoryJsonHandle(
|
||||
ICacheManagerConfiguration managerConfiguration,
|
||||
CacheHandleConfiguration configuration,
|
||||
ICacheSerializer serializer,
|
||||
ILoggerFactory loggerFactory) : base(managerConfiguration, configuration)
|
||||
{
|
||||
_cache = new ConcurrentDictionary<string, Tuple<Type, byte[]>>();
|
||||
_serializer = serializer;
|
||||
Logger = loggerFactory.CreateLogger(this);
|
||||
}
|
||||
|
||||
public override int Count => _cache.Count;
|
||||
|
||||
protected override ILogger Logger { get; }
|
||||
|
||||
public override void Clear() => _cache.Clear();
|
||||
|
||||
public override void ClearRegion(string region)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(region, nameof(region));
|
||||
|
||||
var key = string.Concat(region, ":");
|
||||
foreach (var item in _cache.Where(p => p.Key.StartsWith(key, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_cache.TryRemove(item.Key, out Tuple<Type, byte[]> val);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Exists(string key)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(key, nameof(key));
|
||||
|
||||
return _cache.ContainsKey(key);
|
||||
}
|
||||
|
||||
public override bool Exists(string key, string region)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(region, nameof(region));
|
||||
var fullKey = GetKey(key, region);
|
||||
return _cache.ContainsKey(fullKey);
|
||||
}
|
||||
|
||||
protected override bool AddInternalPrepared(CacheItem<TCacheValue> item)
|
||||
{
|
||||
Guard.NotNull(item, nameof(item));
|
||||
|
||||
var key = GetKey(item.Key, item.Region);
|
||||
|
||||
var serializedItem = _serializer.SerializeCacheItem(item);
|
||||
|
||||
return _cache.TryAdd(key, new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem));
|
||||
}
|
||||
|
||||
protected override CacheItem<TCacheValue> GetCacheItemInternal(string key) => GetCacheItemInternal(key, null);
|
||||
|
||||
protected override CacheItem<TCacheValue> GetCacheItemInternal(string key, string region)
|
||||
{
|
||||
var fullKey = GetKey(key, region);
|
||||
|
||||
CacheItem<TCacheValue> deserializedResult = null;
|
||||
|
||||
if (_cache.TryGetValue(fullKey, out Tuple<Type, byte[]> result))
|
||||
{
|
||||
deserializedResult = _serializer.DeserializeCacheItem<TCacheValue>(result.Item2, result.Item1);
|
||||
|
||||
if (deserializedResult.ExpirationMode != ExpirationMode.None && IsExpired(deserializedResult, DateTime.UtcNow))
|
||||
{
|
||||
_cache.TryRemove(fullKey, out Tuple<Type, byte[]> removeResult);
|
||||
TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, deserializedResult.Value);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return deserializedResult;
|
||||
}
|
||||
|
||||
protected override void PutInternalPrepared(CacheItem<TCacheValue> item)
|
||||
{
|
||||
Guard.NotNull(item, nameof(item));
|
||||
|
||||
var serializedItem = _serializer.SerializeCacheItem<TCacheValue>(item);
|
||||
|
||||
_cache[GetKey(item.Key, item.Region)] = new Tuple<Type, byte[]>(item.Value.GetType(), serializedItem);
|
||||
}
|
||||
|
||||
protected override bool RemoveInternal(string key) => RemoveInternal(key, null);
|
||||
|
||||
protected override bool RemoveInternal(string key, string region)
|
||||
{
|
||||
var fullKey = GetKey(key, region);
|
||||
return _cache.TryRemove(fullKey, out Tuple<Type, byte[]> val);
|
||||
}
|
||||
|
||||
private static string GetKey(string key, string region)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(key, nameof(key));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(region))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
return string.Concat(region, ":", key);
|
||||
}
|
||||
|
||||
private static bool IsExpired(CacheItem<TCacheValue> item, DateTime now)
|
||||
{
|
||||
if (item.ExpirationMode == ExpirationMode.Absolute
|
||||
&& item.CreatedUtc.Add(item.ExpirationTimeout) < now)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (item.ExpirationMode == ExpirationMode.Sliding
|
||||
&& item.LastAccessedUtc.Add(item.ExpirationTimeout) < now)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
225
test/Ocelot.AcceptanceTests/CachingTests.cs
Normal file
225
test/Ocelot.AcceptanceTests/CachingTests.cs
Normal file
@ -0,0 +1,225 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using Configuration.File;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class CachingTests : IDisposable
|
||||
{
|
||||
private readonly Steps _steps;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public CachingTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_cached_response()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51899,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
FileCacheOptions = new FileCacheOptions
|
||||
{
|
||||
TtlSeconds = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunning())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.And(x => _steps.ThenTheContentLengthIs(16))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_cached_response_with_expires_header()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 52839,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
FileCacheOptions = new FileCacheOptions
|
||||
{
|
||||
TtlSeconds = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1"))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunning())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom"))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.And(x => _steps.ThenTheContentLengthIs(16))
|
||||
.And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_cached_response_when_using_jsonserialized_cache()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51899,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
FileCacheOptions = new FileCacheOptions
|
||||
{
|
||||
TtlSeconds = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_not_return_cached_response_as_ttl_expires()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51899,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
FileCacheOptions = new FileCacheOptions
|
||||
{
|
||||
TtlSeconds = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunning())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
|
||||
.And(x => x.GivenTheCacheExpires())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenTheCacheExpires()
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody)
|
||||
{
|
||||
_serviceHandler.Dispose();
|
||||
GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null);
|
||||
}
|
||||
|
||||
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key))
|
||||
{
|
||||
context.Response.Headers.Add(key, value);
|
||||
}
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
176
test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs
Normal file
176
test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs
Normal file
@ -0,0 +1,176 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Configuration.File;
|
||||
using Consul;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class ConfigurationInConsulTests : IDisposable
|
||||
{
|
||||
private IWebHost _builder;
|
||||
private readonly Steps _steps;
|
||||
private IWebHost _fakeConsulBuilder;
|
||||
private FileConfiguration _config;
|
||||
private readonly List<ServiceEntry> _consulServices;
|
||||
|
||||
public ConfigurationInConsulTests()
|
||||
{
|
||||
_consulServices = new List<ServiceEntry>();
|
||||
_steps = new Steps();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51779,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 9502
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var fakeConsulServiceDiscoveryUrl = "http://localhost:9502";
|
||||
|
||||
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura"))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||
{
|
||||
_fakeConsulBuilder = new WebHostBuilder()
|
||||
.UseUrls(url)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseUrls(url)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
{
|
||||
if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_config);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
|
||||
var kvp = new FakeConsulGetResponse(base64);
|
||||
|
||||
await context.Response.WriteJsonAsync(new FakeConsulGetResponse[] { kvp });
|
||||
}
|
||||
else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new StreamReader(context.Request.Body);
|
||||
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
_config = JsonConvert.DeserializeObject<FileConfiguration>(json);
|
||||
|
||||
var response = JsonConvert.SerializeObject(true);
|
||||
|
||||
await context.Response.WriteAsync(response);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||
{
|
||||
await context.Response.WriteJsonAsync(_consulServices);
|
||||
}
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_fakeConsulBuilder.Start();
|
||||
}
|
||||
|
||||
public class FakeConsulGetResponse
|
||||
{
|
||||
public FakeConsulGetResponse(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public int CreateIndex => 100;
|
||||
public int ModifyIndex => 200;
|
||||
public int LockIndex => 200;
|
||||
public string Key => "InternalConfiguration";
|
||||
public int Flags => 0;
|
||||
public string Value { get; private set; }
|
||||
public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e";
|
||||
}
|
||||
|
||||
private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody)
|
||||
{
|
||||
_builder = new WebHostBuilder()
|
||||
.UseUrls(url)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseUrls(url)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UsePathBase(basePath);
|
||||
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_builder.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_builder?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
470
test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs
Normal file
470
test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs
Normal file
@ -0,0 +1,470 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Cache;
|
||||
using Configuration.File;
|
||||
using Consul;
|
||||
using Infrastructure;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Shouldly;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class ConsulConfigurationInConsulTests : IDisposable
|
||||
{
|
||||
private IWebHost _builder;
|
||||
private readonly Steps _steps;
|
||||
private IWebHost _fakeConsulBuilder;
|
||||
private FileConfiguration _config;
|
||||
private readonly List<ServiceEntry> _consulServices;
|
||||
|
||||
public ConsulConfigurationInConsulTests()
|
||||
{
|
||||
_consulServices = new List<ServiceEntry>();
|
||||
_steps = new Steps();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_return_response_200_with_simple_url()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51779,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 9500
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var fakeConsulServiceDiscoveryUrl = "http://localhost:9500";
|
||||
|
||||
this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura"))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_load_configuration_out_of_consul()
|
||||
{
|
||||
var consulPort = 8500;
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
|
||||
var consulConfig = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/status",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51779,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/cs/status",
|
||||
UpstreamHttpMethod = new List<string> {"Get"}
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||
.And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "/status", 200, "Hello from Laura"))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_load_configuration_out_of_consul_if_it_is_changed()
|
||||
{
|
||||
var consulPort = 8506;
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
|
||||
var consulConfig = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/status",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51780,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/cs/status",
|
||||
UpstreamHttpMethod = new List<string> {"Get"}
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var secondConsulConfig = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/status",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51780,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/cs/status/awesome",
|
||||
UpstreamHttpMethod = new List<string> {"Get"}
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||
.And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, ""))
|
||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51780", "/status", 200, "Hello from Laura"))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.When(x => GivenTheConsulConfigurationIs(secondConsulConfig))
|
||||
.Then(x => ThenTheConfigIsUpdatedInOcelot())
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit()
|
||||
{
|
||||
const int consulPort = 8523;
|
||||
const string serviceName = "web";
|
||||
const int downstreamServicePort = 8187;
|
||||
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = downstreamServicePort,
|
||||
ID = "web_90_0_2_224_8080",
|
||||
Tags = new[] { "version-v1" }
|
||||
},
|
||||
};
|
||||
|
||||
var consulConfig = new FileConfiguration
|
||||
{
|
||||
DynamicReRoutes = new List<FileDynamicReRoute>
|
||||
{
|
||||
new FileDynamicReRoute
|
||||
{
|
||||
ServiceName = serviceName,
|
||||
RateLimitRule = new FileRateLimitRule()
|
||||
{
|
||||
EnableRateLimiting = true,
|
||||
ClientWhitelist = new List<string>(),
|
||||
Limit = 3,
|
||||
Period = "1s",
|
||||
PeriodTimespan = 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
},
|
||||
RateLimitOptions = new FileRateLimitOptions()
|
||||
{
|
||||
ClientIdHeader = "ClientId",
|
||||
DisableRateLimitHeaders = false,
|
||||
QuotaExceededMessage = "",
|
||||
RateLimitCounterPrefix = "",
|
||||
HttpStatusCode = 428
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
}
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
GlobalConfiguration = new FileGlobalConfiguration
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura"))
|
||||
.And(x => GivenTheConsulConfigurationIs(consulConfig))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(428))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void ThenTheConfigIsUpdatedInOcelot()
|
||||
{
|
||||
var result = Wait.WaitFor(20000).Until(() => {
|
||||
try
|
||||
{
|
||||
_steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome");
|
||||
_steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK);
|
||||
_steps.ThenTheResponseBodyShouldBe("Hello from Laura");
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
|
||||
private void GivenTheConsulConfigurationIs(FileConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||
{
|
||||
foreach (var serviceEntry in serviceEntries)
|
||||
{
|
||||
_consulServices.Add(serviceEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||
{
|
||||
_fakeConsulBuilder = new WebHostBuilder()
|
||||
.UseUrls(url)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseUrls(url)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
{
|
||||
if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_config);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
|
||||
var kvp = new FakeConsulGetResponse(base64);
|
||||
json = JsonConvert.SerializeObject(new FakeConsulGetResponse[] { kvp });
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration")
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new StreamReader(context.Request.Body);
|
||||
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
_config = JsonConvert.DeserializeObject<FileConfiguration>(json);
|
||||
|
||||
var response = JsonConvert.SerializeObject(true);
|
||||
|
||||
await context.Response.WriteAsync(response);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_consulServices);
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_fakeConsulBuilder.Start();
|
||||
}
|
||||
|
||||
public class FakeConsulGetResponse
|
||||
{
|
||||
public FakeConsulGetResponse(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public int CreateIndex => 100;
|
||||
public int ModifyIndex => 200;
|
||||
public int LockIndex => 200;
|
||||
public string Key => "InternalConfiguration";
|
||||
public int Flags => 0;
|
||||
public string Value { get; private set; }
|
||||
public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e";
|
||||
}
|
||||
|
||||
private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody)
|
||||
{
|
||||
_builder = new WebHostBuilder()
|
||||
.UseUrls(url)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseUrls(url)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UsePathBase(basePath);
|
||||
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
_builder.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_builder?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
|
||||
class FakeCache : IOcelotCache<FileConfiguration>
|
||||
{
|
||||
public void Add(string key, FileConfiguration value, TimeSpan ttl, string region)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public FileConfiguration Get(string key, string region)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void ClearRegion(string region)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
339
test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs
Normal file
339
test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs
Normal file
@ -0,0 +1,339 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Configuration.File;
|
||||
using Consul;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Shouldly;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class ConsulWebSocketTests : IDisposable
|
||||
{
|
||||
private readonly List<string> _secondRecieved;
|
||||
private readonly List<string> _firstRecieved;
|
||||
private readonly List<ServiceEntry> _serviceEntries;
|
||||
private readonly Steps _steps;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public ConsulWebSocketTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
_firstRecieved = new List<string>();
|
||||
_secondRecieved = new List<string>();
|
||||
_serviceEntries = new List<ServiceEntry>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_proxy_websocket_input_to_downstream_service_and_use_service_discovery_and_load_balancer()
|
||||
{
|
||||
var downstreamPort = 5007;
|
||||
var downstreamHost = "localhost";
|
||||
|
||||
var secondDownstreamPort = 5008;
|
||||
var secondDownstreamHost = "localhost";
|
||||
|
||||
var serviceName = "websockets";
|
||||
var consulPort = 8509;
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = downstreamHost,
|
||||
Port = downstreamPort,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
var serviceEntryTwo = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = secondDownstreamHost,
|
||||
Port = secondDownstreamPort,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
|
||||
var config = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
UpstreamPathTemplate = "/",
|
||||
DownstreamPathTemplate = "/ws",
|
||||
DownstreamScheme = "ws",
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" },
|
||||
ServiceName = serviceName,
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort,
|
||||
Type = "consul"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(_ => _steps.GivenThereIsAConfiguration(config))
|
||||
.And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul())
|
||||
.And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||
.And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws"))
|
||||
.And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws"))
|
||||
.When(_ => WhenIStartTheClients())
|
||||
.Then(_ => ThenBothDownstreamServicesAreCalled())
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void ThenBothDownstreamServicesAreCalled()
|
||||
{
|
||||
_firstRecieved.Count.ShouldBe(10);
|
||||
_firstRecieved.ForEach(x =>
|
||||
{
|
||||
x.ShouldBe("test");
|
||||
});
|
||||
|
||||
_secondRecieved.Count.ShouldBe(10);
|
||||
_secondRecieved.ForEach(x =>
|
||||
{
|
||||
x.ShouldBe("chocolate");
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||
{
|
||||
foreach (var serviceEntry in serviceEntries)
|
||||
{
|
||||
_serviceEntries.Add(serviceEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_serviceEntries);
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task WhenIStartTheClients()
|
||||
{
|
||||
var firstClient = StartClient("ws://localhost:5000/");
|
||||
|
||||
var secondClient = StartSecondClient("ws://localhost:5000/");
|
||||
|
||||
await Task.WhenAll(firstClient, secondClient);
|
||||
}
|
||||
|
||||
private async Task StartClient(string url)
|
||||
{
|
||||
var client = new ClientWebSocket();
|
||||
|
||||
await client.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||
|
||||
var sending = Task.Run(async () =>
|
||||
{
|
||||
string line = "test";
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line);
|
||||
|
||||
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true,
|
||||
CancellationToken.None);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
});
|
||||
|
||||
var receiving = Task.Run(async () =>
|
||||
{
|
||||
var buffer = new byte[1024 * 4];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
_firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(sending, receiving);
|
||||
}
|
||||
|
||||
private async Task StartSecondClient(string url)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
|
||||
var client = new ClientWebSocket();
|
||||
|
||||
await client.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||
|
||||
var sending = Task.Run(async () =>
|
||||
{
|
||||
string line = "test";
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line);
|
||||
|
||||
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true,
|
||||
CancellationToken.None);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
});
|
||||
|
||||
var receiving = Task.Run(async () =>
|
||||
{
|
||||
var buffer = new byte[1024 * 4];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
_secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(sending, receiving);
|
||||
}
|
||||
|
||||
private async Task StartFakeDownstreamService(string url, string path)
|
||||
{
|
||||
await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path == path)
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
await Echo(webSocket);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task StartSecondFakeDownstreamService(string url, string path)
|
||||
{
|
||||
await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path == path)
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
await Message(webSocket);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task Echo(WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buffer = new byte[1024 * 4];
|
||||
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
while (!result.CloseStatus.HasValue)
|
||||
{
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
|
||||
|
||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
}
|
||||
|
||||
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Message(WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buffer = new byte[1024 * 4];
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes("chocolate");
|
||||
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
while (!result.CloseStatus.HasValue)
|
||||
{
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None);
|
||||
|
||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
}
|
||||
|
||||
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
283
test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs
Normal file
283
test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs
Normal file
@ -0,0 +1,283 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Configuration.File;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Steeltoe.Common.Discovery;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class EurekaServiceDiscoveryTests : IDisposable
|
||||
{
|
||||
private readonly Steps _steps;
|
||||
private readonly List<IServiceInstance> _eurekaInstances;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public EurekaServiceDiscoveryTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
_eurekaInstances = new List<IServiceInstance>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_use_eureka_service_discovery_and_make_request()
|
||||
{
|
||||
var eurekaPort = 8761;
|
||||
var serviceName = "product";
|
||||
var downstreamServicePort = 50371;
|
||||
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||
var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}";
|
||||
|
||||
var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false,
|
||||
new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary<string, string>());
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Type = "Eureka"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl))
|
||||
.And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithEureka())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests)))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances)
|
||||
{
|
||||
foreach (var instance in serviceInstances)
|
||||
{
|
||||
_eurekaInstances.Add(instance);
|
||||
}
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
if (context.Request.Path.Value == "/eureka/apps/")
|
||||
{
|
||||
var apps = new List<Application>();
|
||||
|
||||
foreach (var serviceInstance in _eurekaInstances)
|
||||
{
|
||||
var a = new Application
|
||||
{
|
||||
name = serviceName,
|
||||
instance = new List<Instance>
|
||||
{
|
||||
new Instance
|
||||
{
|
||||
instanceId = $"{serviceInstance.Host}:{serviceInstance}",
|
||||
hostName = serviceInstance.Host,
|
||||
app = serviceName,
|
||||
ipAddr = "127.0.0.1",
|
||||
status = "UP",
|
||||
overriddenstatus = "UNKNOWN",
|
||||
port = new Port {value = serviceInstance.Port, enabled = "true"},
|
||||
securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"},
|
||||
countryId = 1,
|
||||
dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"},
|
||||
leaseInfo = new LeaseInfo
|
||||
{
|
||||
renewalIntervalInSecs = 30,
|
||||
durationInSecs = 90,
|
||||
registrationTimestamp = 1457714988223,
|
||||
lastRenewalTimestamp= 1457716158319,
|
||||
evictionTimestamp = 0,
|
||||
serviceUpTimestamp = 1457714988223
|
||||
},
|
||||
metadata = new Metadata
|
||||
{
|
||||
value = "java.util.Collections$EmptyMap"
|
||||
},
|
||||
homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||
statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||
healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}",
|
||||
vipAddress = serviceName,
|
||||
isCoordinatingDiscoveryServer = "false",
|
||||
lastUpdatedTimestamp = "1457714988223",
|
||||
lastDirtyTimestamp = "1457714988172",
|
||||
actionType = "ADDED"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
apps.Add(a);
|
||||
}
|
||||
|
||||
var applications = new EurekaApplications
|
||||
{
|
||||
applications = new Applications
|
||||
{
|
||||
application = apps,
|
||||
apps__hashcode = "UP_1_",
|
||||
versions__delta = "1"
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(applications);
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenEurekaProductServiceOneIsRunning(string url)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await context.Response.WriteAsync(exception.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeEurekaService : IServiceInstance
|
||||
{
|
||||
public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary<string, string> metadata)
|
||||
{
|
||||
ServiceId = serviceId;
|
||||
Host = host;
|
||||
Port = port;
|
||||
IsSecure = isSecure;
|
||||
Uri = uri;
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public string ServiceId { get; }
|
||||
public string Host { get; }
|
||||
public int Port { get; }
|
||||
public bool IsSecure { get; }
|
||||
public Uri Uri { get; }
|
||||
public IDictionary<string, string> Metadata { get; }
|
||||
}
|
||||
|
||||
public class Port
|
||||
{
|
||||
[JsonProperty("$")]
|
||||
public int value { get; set; }
|
||||
|
||||
[JsonProperty("@enabled")]
|
||||
public string enabled { get; set; }
|
||||
}
|
||||
|
||||
public class SecurePort
|
||||
{
|
||||
[JsonProperty("$")]
|
||||
public int value { get; set; }
|
||||
|
||||
[JsonProperty("@enabled")]
|
||||
public string enabled { get; set; }
|
||||
}
|
||||
|
||||
public class DataCenterInfo
|
||||
{
|
||||
[JsonProperty("@class")]
|
||||
public string value { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class LeaseInfo
|
||||
{
|
||||
public int renewalIntervalInSecs { get; set; }
|
||||
|
||||
public int durationInSecs { get; set; }
|
||||
|
||||
public long registrationTimestamp { get; set; }
|
||||
|
||||
public long lastRenewalTimestamp { get; set; }
|
||||
|
||||
public int evictionTimestamp { get; set; }
|
||||
|
||||
public long serviceUpTimestamp { get; set; }
|
||||
}
|
||||
|
||||
public class Metadata
|
||||
{
|
||||
[JsonProperty("@class")]
|
||||
public string value { get; set; }
|
||||
}
|
||||
|
||||
public class Instance
|
||||
{
|
||||
public string instanceId { get; set; }
|
||||
public string hostName { get; set; }
|
||||
public string app { get; set; }
|
||||
public string ipAddr { get; set; }
|
||||
public string status { get; set; }
|
||||
public string overriddenstatus { get; set; }
|
||||
public Port port { get; set; }
|
||||
public SecurePort securePort { get; set; }
|
||||
public int countryId { get; set; }
|
||||
public DataCenterInfo dataCenterInfo { get; set; }
|
||||
public LeaseInfo leaseInfo { get; set; }
|
||||
public Metadata metadata { get; set; }
|
||||
public string homePageUrl { get; set; }
|
||||
public string statusPageUrl { get; set; }
|
||||
public string healthCheckUrl { get; set; }
|
||||
public string vipAddress { get; set; }
|
||||
public string isCoordinatingDiscoveryServer { get; set; }
|
||||
public string lastUpdatedTimestamp { get; set; }
|
||||
public string lastDirtyTimestamp { get; set; }
|
||||
public string actionType { get; set; }
|
||||
}
|
||||
|
||||
public class Application
|
||||
{
|
||||
public string name { get; set; }
|
||||
public List<Instance> instance { get; set; }
|
||||
}
|
||||
|
||||
public class Applications
|
||||
{
|
||||
public string versions__delta { get; set; }
|
||||
public string apps__hashcode { get; set; }
|
||||
public List<Application> application { get; set; }
|
||||
}
|
||||
|
||||
public class EurekaApplications
|
||||
{
|
||||
public Applications applications { get; set; }
|
||||
}
|
||||
}
|
@ -25,7 +25,12 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj" />
|
||||
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
|
||||
<ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" />
|
||||
<ProjectReference Include="..\..\src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj" />
|
||||
<ProjectReference Include="..\..\src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj" />
|
||||
<ProjectReference Include="..\..\src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj" />
|
||||
<ProjectReference Include="..\Ocelot.ManualTest\Ocelot.ManualTest.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@ -54,5 +59,10 @@
|
||||
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
|
||||
<PackageReference Include="IdentityServer4" Version="2.2.0" />
|
||||
<PackageReference Include="Consul" Version="0.7.2.6" />
|
||||
<PackageReference Include="Rafty" Version="0.4.4" />
|
||||
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.1.2" />
|
||||
<PackageReference Include="CacheManager.Serialization.Json" Version="1.1.2" />
|
||||
<PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
274
test/Ocelot.AcceptanceTests/PollyQoSTests.cs
Normal file
274
test/Ocelot.AcceptanceTests/PollyQoSTests.cs
Normal file
@ -0,0 +1,274 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Configuration.File;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class PollyQoSTests : IDisposable
|
||||
{
|
||||
private readonly Steps _steps;
|
||||
private int _requestCount;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public PollyQoSTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_not_timeout()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51569,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Post" },
|
||||
QoSOptions = new FileQoSOptions
|
||||
{
|
||||
TimeoutValue = 1000,
|
||||
ExceptionsAllowedBeforeBreaking = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||
.And(x => _steps.GivenThePostHasContent("postContent"))
|
||||
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_timeout()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51579,
|
||||
}
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Post" },
|
||||
QoSOptions = new FileQoSOptions
|
||||
{
|
||||
TimeoutValue = 10,
|
||||
ExceptionsAllowedBeforeBreaking = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||
.And(x => _steps.GivenThePostHasContent("postContent"))
|
||||
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_open_circuit_breaker_then_close()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51892,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
QoSOptions = new FileQoSOptions
|
||||
{
|
||||
ExceptionsAllowedBeforeBreaking = 1,
|
||||
TimeoutValue = 500,
|
||||
DurationOfBreak = 1000
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura"))
|
||||
.Given(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.Given(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.Given(x => x.GivenIWaitMilliseconds(3000))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void open_circuit_should_not_effect_different_reRoute()
|
||||
{
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51872,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
QoSOptions = new FileQoSOptions
|
||||
{
|
||||
ExceptionsAllowedBeforeBreaking = 1,
|
||||
TimeoutValue = 500,
|
||||
DurationOfBreak = 1000
|
||||
}
|
||||
},
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 51880,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/working",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura"))
|
||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom", 0))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithPolly())
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/working"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
|
||||
.And(x => x.GivenIWaitMilliseconds(3000))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenIWaitMilliseconds(int ms)
|
||||
{
|
||||
Thread.Sleep(ms);
|
||||
}
|
||||
|
||||
private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
//circuit starts closed
|
||||
if (_requestCount == 0)
|
||||
{
|
||||
_requestCount++;
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
return;
|
||||
}
|
||||
|
||||
//request one times out and polly throws exception, circuit opens
|
||||
if (_requestCount == 1)
|
||||
{
|
||||
_requestCount++;
|
||||
await Task.Delay(1000);
|
||||
context.Response.StatusCode = 200;
|
||||
return;
|
||||
}
|
||||
|
||||
//after break closes we return 200 OK
|
||||
if (_requestCount == 2)
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
Thread.Sleep(timeout);
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
583
test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs
Normal file
583
test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs
Normal file
@ -0,0 +1,583 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Configuration.File;
|
||||
using Consul;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Shouldly;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class ServiceDiscoveryTests : IDisposable
|
||||
{
|
||||
private readonly Steps _steps;
|
||||
private readonly List<ServiceEntry> _consulServices;
|
||||
private int _counterOne;
|
||||
private int _counterTwo;
|
||||
private static readonly object SyncLock = new object();
|
||||
private string _downstreamPath;
|
||||
private string _receivedToken;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public ServiceDiscoveryTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
_consulServices = new List<ServiceEntry>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_use_consul_service_discovery_and_load_balance_request()
|
||||
{
|
||||
var consulPort = 8502;
|
||||
var serviceName = "product";
|
||||
var downstreamServiceOneUrl = "http://localhost:50881";
|
||||
var downstreamServiceTwoUrl = "http://localhost:50882";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 50881,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
var serviceEntryTwo = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 50882,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50))
|
||||
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50))
|
||||
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_handle_request_to_consul_for_downstream_service_and_make_request()
|
||||
{
|
||||
const int consulPort = 8505;
|
||||
const string serviceName = "web";
|
||||
const string downstreamServiceOneUrl = "http://localhost:8080";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 8080,
|
||||
ID = "web_90_0_2_224_8080",
|
||||
Tags = new[] { "version-v1" }
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/home",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/home",
|
||||
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/home"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes()
|
||||
{
|
||||
const int consulPort = 8513;
|
||||
const string serviceName = "web";
|
||||
const int downstreamServicePort = 8087;
|
||||
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = downstreamServicePort,
|
||||
ID = "web_90_0_2_224_8080",
|
||||
Tags = new[] { "version-v1" }
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
GlobalConfiguration = new FileGlobalConfiguration
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
},
|
||||
DownstreamScheme = "http",
|
||||
HttpHandlerOptions = new FileHttpHandlerOptions
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
UseCookieContainer = true,
|
||||
UseTracing = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura"))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes()
|
||||
{
|
||||
var consulPort = 8510;
|
||||
var serviceName = "product";
|
||||
var serviceOnePort = 50888;
|
||||
var serviceTwoPort = 50889;
|
||||
var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}";
|
||||
var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = serviceOnePort,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
var serviceEntryTwo = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = serviceTwoPort,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
},
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
DownstreamScheme = "http"
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50))
|
||||
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50))
|
||||
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_use_token_to_make_request_to_consul()
|
||||
{
|
||||
var token = "abctoken";
|
||||
var consulPort = 8515;
|
||||
var serviceName = "web";
|
||||
var downstreamServiceOneUrl = "http://localhost:8081";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 8081,
|
||||
ID = "web_90_0_2_224_8080",
|
||||
Tags = new[] { "version-v1" }
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/home",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/home",
|
||||
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort,
|
||||
Token = token
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||
.And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||
.And(_ => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(_ => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home"))
|
||||
.Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.And(_ => _receivedToken.ShouldBe(token))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_send_request_to_service_after_it_becomes_available_in_consul()
|
||||
{
|
||||
var consulPort = 8501;
|
||||
var serviceName = "product";
|
||||
var downstreamServiceOneUrl = "http://localhost:50879";
|
||||
var downstreamServiceTwoUrl = "http://localhost:50880";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 50879,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
var serviceEntryTwo = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = 50880,
|
||||
ID = Guid.NewGuid().ToString(),
|
||||
Tags = new string[0]
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||
.And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10))
|
||||
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6))
|
||||
.And(x => WhenIRemoveAService(serviceEntryTwo))
|
||||
.And(x => GivenIResetCounters())
|
||||
.And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||
.And(x => ThenOnlyOneServiceHasBeenCalled())
|
||||
.And(x => WhenIAddAServiceBackIn(serviceEntryTwo))
|
||||
.And(x => GivenIResetCounters())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10))
|
||||
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10))
|
||||
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request()
|
||||
{
|
||||
const int consulPort = 8518;
|
||||
const string serviceName = "web";
|
||||
const int downstreamServicePort = 8082;
|
||||
var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
var serviceEntryOne = new ServiceEntry()
|
||||
{
|
||||
Service = new AgentService()
|
||||
{
|
||||
Service = serviceName,
|
||||
Address = "localhost",
|
||||
Port = downstreamServicePort,
|
||||
ID = $"web_90_0_2_224_{downstreamServicePort}",
|
||||
Tags = new[] { "version-v1" }
|
||||
},
|
||||
};
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/home",
|
||||
DownstreamScheme = "http",
|
||||
UpstreamPathTemplate = "/home",
|
||||
UpstreamHttpMethod = new List<string> { "Get", "Options" },
|
||||
ServiceName = serviceName,
|
||||
LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort,
|
||||
Type = "PollConsul",
|
||||
PollingInterval = 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura"))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName))
|
||||
.And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunningWithConsul())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo)
|
||||
{
|
||||
_consulServices.Add(serviceEntryTwo);
|
||||
}
|
||||
|
||||
private void ThenOnlyOneServiceHasBeenCalled()
|
||||
{
|
||||
_counterOne.ShouldBe(10);
|
||||
_counterTwo.ShouldBe(0);
|
||||
}
|
||||
|
||||
private void WhenIRemoveAService(ServiceEntry serviceEntryTwo)
|
||||
{
|
||||
_consulServices.Remove(serviceEntryTwo);
|
||||
}
|
||||
|
||||
private void GivenIResetCounters()
|
||||
{
|
||||
_counterOne = 0;
|
||||
_counterTwo = 0;
|
||||
}
|
||||
|
||||
private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top)
|
||||
{
|
||||
_counterOne.ShouldBeInRange(bottom, top);
|
||||
_counterOne.ShouldBeInRange(bottom, top);
|
||||
}
|
||||
|
||||
private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected)
|
||||
{
|
||||
var total = _counterOne + _counterTwo;
|
||||
total.ShouldBe(expected);
|
||||
}
|
||||
|
||||
private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries)
|
||||
{
|
||||
foreach (var serviceEntry in serviceEntries)
|
||||
{
|
||||
_consulServices.Add(serviceEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
if (context.Request.Path.Value == $"/v1/health/service/{serviceName}")
|
||||
{
|
||||
if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values))
|
||||
{
|
||||
_receivedToken = values.First();
|
||||
}
|
||||
var json = JsonConvert.SerializeObject(_consulServices);
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenProductServiceOneIsRunning(string url, int statusCode)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string response;
|
||||
lock (SyncLock)
|
||||
{
|
||||
_counterOne++;
|
||||
response = _counterOne.ToString();
|
||||
}
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(response);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await context.Response.WriteAsync(exception.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenProductServiceTwoIsRunning(string url, int statusCode)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string response;
|
||||
lock (SyncLock)
|
||||
{
|
||||
_counterTwo++;
|
||||
response = _counterTwo.ToString();
|
||||
}
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(response);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await context.Response.WriteAsync(exception.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||
{
|
||||
_downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||
|
||||
if (_downstreamPath != basePath)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@
|
||||
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Caching;
|
||||
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
|
||||
using Ocelot.Middleware.Multiplexer;
|
||||
using static Ocelot.Infrastructure.Wait;
|
||||
@ -32,6 +33,13 @@
|
||||
using Requester;
|
||||
using CookieHeaderValue = System.Net.Http.Headers.CookieHeaderValue;
|
||||
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
|
||||
using global::CacheManager.Core;
|
||||
using Ocelot.Cache.CacheManager;
|
||||
using Ocelot.Provider.Consul;
|
||||
using Ocelot.Provider.Eureka;
|
||||
using Ocelot.Infrastructure;
|
||||
using Ocelot.Provider.Polly;
|
||||
using Ocelot.Tracing.Butterfly;
|
||||
|
||||
public class Steps : IDisposable
|
||||
{
|
||||
@ -98,6 +106,42 @@
|
||||
await _ocelotHost.StartAsync();
|
||||
}
|
||||
|
||||
public async Task StartFakeOcelotWithWebSocketsWithConsul()
|
||||
{
|
||||
_ocelotBuilder = new WebHostBuilder();
|
||||
_ocelotBuilder.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton(_ocelotBuilder);
|
||||
s.AddOcelot().AddConsul();
|
||||
});
|
||||
_ocelotBuilder.UseKestrel()
|
||||
.UseUrls("http://localhost:5000")
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", false, false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureLogging((hostingContext, logging) =>
|
||||
{
|
||||
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
|
||||
logging.AddConsole();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseWebSockets();
|
||||
app.UseOcelot().Wait();
|
||||
})
|
||||
.UseIISIntegration();
|
||||
_ocelotHost = _ocelotBuilder.Build();
|
||||
await _ocelotHost.StartAsync();
|
||||
}
|
||||
|
||||
|
||||
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
|
||||
{
|
||||
var configurationPath = TestConfiguration.ConfigurationPath;
|
||||
@ -124,6 +168,12 @@
|
||||
File.WriteAllText(configurationPath, jsonConfiguration);
|
||||
}
|
||||
|
||||
public void ThenTheResponseBodyHeaderIs(string key, string value)
|
||||
{
|
||||
var header = _response.Content.Headers.GetValues(key);
|
||||
header.First().ShouldBe(value);
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningReloadingConfig(bool shouldReload)
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
@ -183,6 +233,203 @@
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningWithConsul()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", false, false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot().AddConsul();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
public void ThenTheTraceHeaderIsSet(string key)
|
||||
{
|
||||
var header = _response.Headers.GetValues(key);
|
||||
header.First().ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl)
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot()
|
||||
.AddButterfly(option =>
|
||||
{
|
||||
//this is the url that the butterfly collector server is running on...
|
||||
option.CollectorUrl = butterflyUrl;
|
||||
option.Service = "Ocelot";
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
await next.Invoke();
|
||||
});
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot()
|
||||
.AddCacheManager((x) =>
|
||||
{
|
||||
x.WithMicrosoftLogging(log =>
|
||||
{
|
||||
log.AddConsole(LogLevel.Debug);
|
||||
})
|
||||
.WithJsonSerializer()
|
||||
.WithHandle(typeof(InMemoryJsonHandle<>));
|
||||
})
|
||||
.AddConsul()
|
||||
.AddConfigStoredInConsul();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningUsingConsulToStoreConfig()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot().AddConsul().AddConfigStoredInConsul();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url)
|
||||
{
|
||||
var result = Wait.WaitFor(2000).Until(() => {
|
||||
try
|
||||
{
|
||||
_response = _ocelotClient.GetAsync(url).Result;
|
||||
_response.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
|
||||
|
||||
public void GivenOcelotIsRunningUsingJsonSerializedCache()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", false, false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot()
|
||||
.AddCacheManager((x) =>
|
||||
{
|
||||
x.WithMicrosoftLogging(log =>
|
||||
{
|
||||
log.AddConsole(LogLevel.Debug);
|
||||
})
|
||||
.WithJsonSerializer()
|
||||
.WithHandle(typeof(InMemoryJsonHandle<>));
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache)
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
@ -578,6 +825,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningWithEureka()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", false, false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot()
|
||||
.AddEureka();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot().Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
public void GivenOcelotIsRunningWithPolly()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||
config.AddJsonFile("ocelot.json", false, false);
|
||||
config.AddEnvironmentVariables();
|
||||
})
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddOcelot()
|
||||
.AddPolly();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseOcelot()
|
||||
.Wait();
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder);
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void WhenIGetUrlOnTheApiGateway(string url)
|
||||
{
|
||||
_response = _ocelotClient.GetAsync(url).Result;
|
||||
@ -696,6 +1004,11 @@
|
||||
{
|
||||
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
|
||||
}
|
||||
|
||||
public void ThenTheContentLengthIs(int expected)
|
||||
{
|
||||
_response.Content.Headers.ContentLength.ShouldBe(expected);
|
||||
}
|
||||
|
||||
public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||
{
|
||||
|
154
test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs
Normal file
154
test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs
Normal file
@ -0,0 +1,154 @@
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Configuration.File;
|
||||
using Consul;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using TestStack.BDDfy;
|
||||
using Xunit;
|
||||
|
||||
public class TwoDownstreamServicesTests : IDisposable
|
||||
{
|
||||
private readonly Steps _steps;
|
||||
private readonly List<ServiceEntry> _serviceEntries;
|
||||
private string _downstreamPathOne;
|
||||
private string _downstreamPathTwo;
|
||||
private readonly ServiceHandler _serviceHandler;
|
||||
|
||||
public TwoDownstreamServicesTests()
|
||||
{
|
||||
_serviceHandler = new ServiceHandler();
|
||||
_steps = new Steps();
|
||||
_serviceEntries = new List<ServiceEntry>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_fix_issue_194()
|
||||
{
|
||||
var consulPort = 8503;
|
||||
var downstreamServiceOneUrl = "http://localhost:8362";
|
||||
var downstreamServiceTwoUrl = "http://localhost:8330";
|
||||
var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}";
|
||||
|
||||
var configuration = new FileConfiguration
|
||||
{
|
||||
ReRoutes = new List<FileReRoute>
|
||||
{
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/user/{user}",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 8362,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/api/user/{user}",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
},
|
||||
new FileReRoute
|
||||
{
|
||||
DownstreamPathTemplate = "/api/product/{product}",
|
||||
DownstreamScheme = "http",
|
||||
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||
{
|
||||
new FileHostAndPort
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 8330,
|
||||
}
|
||||
},
|
||||
UpstreamPathTemplate = "/api/product/{product}",
|
||||
UpstreamHttpMethod = new List<string> { "Get" },
|
||||
}
|
||||
},
|
||||
GlobalConfiguration = new FileGlobalConfiguration()
|
||||
{
|
||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider()
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = consulPort
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, "/api/user/info", 200, "user"))
|
||||
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, "/api/product/info", 200, "product"))
|
||||
.And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl))
|
||||
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||
.And(x => _steps.GivenOcelotIsRunning())
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/user/info?id=1"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("user"))
|
||||
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/product/info?id=1"))
|
||||
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||
.And(x => _steps.ThenTheResponseBodyShouldBe("product"))
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>
|
||||
{
|
||||
if (context.Request.Path.Value == "/v1/health/service/product")
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(_serviceEntries);
|
||||
context.Response.Headers.Add("Content-Type", "application/json");
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenProductServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||
{
|
||||
_downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value)
|
||||
? context.Request.PathBase.Value
|
||||
: context.Request.Path.Value;
|
||||
|
||||
if (_downstreamPathOne != basePath)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenProductServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody)
|
||||
{
|
||||
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
|
||||
{
|
||||
_downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
|
||||
|
||||
if (_downstreamPathTwo != basePath)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync("downstream path didnt match base path");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceHandler?.Dispose();
|
||||
_steps.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user