This commit is contained in:
Shirleen Sharma 2020-12-18 15:24:10 -08:00
Родитель d91685a5fd
Коммит a241d802d2
88 изменённых файлов: 5940 добавлений и 37 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -348,3 +348,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/*/src/local.settings.json
/*.sln

14
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,14 @@
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to
agree to a Contributor License Agreement (CLA) declaring that you have the right to,
and actually do, grant us the rights to use your contribution. For details, visit
https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need
to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnPlugins</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnPlugins_Test</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MultiCdnApi</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnLibrary</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnLibrary_Test</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MultiCdnApi_Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.15.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.2" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,21 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using Microsoft.Azure.WebJobs;
using System.Collections.Generic;
public interface ICdnPlugin<T>
{
bool ProcessPartnerRequest(T partnerRequest, ICollector<ICdnRequest> queue);
void AddMessagesToSendQueue(ICdnRequest cdnRequest, ICollector<ICdnRequest> msg);
IList<ICdnRequest> SplitRequestIntoBatches(T partnerRequest, int maxNumUrl);
bool ValidPartnerRequest(string inputRequest, string resourceID, out T partnerRequest);
}
}

Просмотреть файл

@ -0,0 +1,25 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using Microsoft.Azure.Storage.Queue;
using System.Threading.Tasks;
internal interface ICdnQueueProcessor<T>
{
Task<CloudQueueMessage> ProcessPollRequest(T queueMsg, int maxRetry);
CloudQueueMessage CompletePollRequest(IRequestInfo requestInfo, T queueMsg);
Task<CloudQueueMessage> ProcessPurgeRequest(T queueMsg, int maxRetry);
CloudQueueMessage CompletePurgeRequest(IRequestInfo requestInfo, T queueMsg);
void AddMessageToQueue(CloudQueue outputQueue, CloudQueueMessage message, T queueMsg);
CloudQueueMessage CreateCloudQueueMessage(T request, IRequestInfo requestInfo);
}
}

Просмотреть файл

@ -0,0 +1,40 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System.Net.Http;
using System.Threading.Tasks;
public class HttpHandler : IHttpHandler
{
private readonly HttpClient httpClient;
public HttpHandler()
{
this.httpClient = new HttpClient();
}
public HttpHandler(DelegatingHandler delegatingHandler)
{
this.httpClient = new HttpClient(delegatingHandler);
}
public Task<HttpResponseMessage> GetAsync(string endpoint)
{
return this.httpClient.GetAsync(endpoint);
}
public Task<HttpResponseMessage> PostAsync(string endpoint, StringContent urls)
{
return this.httpClient.PostAsync(endpoint, urls);
}
public void Dispose()
{
this.httpClient.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Net.Http;
using System.Threading.Tasks;
public interface IHttpHandler : IDisposable
{
Task<HttpResponseMessage> PostAsync(string endpoint, StringContent urls);
Task<HttpResponseMessage> GetAsync(string endpoint);
}
}

Просмотреть файл

@ -0,0 +1,35 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System.Collections.Generic;
using System.Text.Json;
public class CdnConfiguration
{
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize JSON
public string Hostname { get; set; }
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize JSON
public IDictionary<string, string> CdnWithCredentials { get; set; }
public CdnConfiguration()
{
CdnWithCredentials = new Dictionary<string, string>();
}
public CdnConfiguration(string rawCdnConfiguration)
{
var cdnConfiguration = JsonSerializer.Deserialize<CdnConfiguration>(rawCdnConfiguration);
Hostname = cdnConfiguration.Hostname;
CdnWithCredentials = cdnConfiguration.CdnWithCredentials;
}
public override string ToString()
{
return $"{nameof(Hostname)}: {Hostname}, {nameof(CdnWithCredentials)}: <skipping credentials...>";
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
public abstract class CosmosDbEntity : ICosmosDbEntity
{
public string id { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,30 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
public interface ICdnRequest : ICosmosDbEntity
{
/// <summary>
/// Id of the PartnerRequest this CdnRequest was batched from
/// </summary>
public string PartnerRequestID { get; set; }
public string[] Urls { get; set; }
public string Status { get; set; }
public int NumTimesProcessed { get; set; }
public string RequestBody { get; set; }
public string Endpoint { get; set; }
/// <summary>
/// Id returned by CDN once cache purge is submitted
/// </summary>
public string CdnRequestId { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,16 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System.Diagnostics.CodeAnalysis;
public interface ICosmosDbEntity
{
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Needs to be lowercase for CosmosDB to use this value as the id of the item")]
// ReSharper disable once InconsistentNaming
public string id { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,27 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System.Collections.Generic;
public interface IPartnerRequest : ICosmosDbEntity
{
/// <summary>
/// Id of the UserRequest this PartnerRequest was created from
/// </summary>
public string UserRequestID { get; set; }
public string CDN { get; set; }
public string Status { get; set; }
public int NumTotalCdnRequests { get; set; }
public int NumCompletedCdnRequests { get; set; }
public ISet<string> Urls { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
internal interface IRequestInfo
{
public string RequestID { get; set; }
public RequestStatus RequestStatus { get; set; }
}
public enum RequestStatus
{
Error,
Unauthorized,
Throttled,
Unknown,
BatchCreated,
MaxRetry,
PurgeSubmitted,
PurgeCompleted,
};
}

Просмотреть файл

@ -0,0 +1,52 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Collections.Generic;
public class Partner : CosmosDbEntity
{
public string TenantId {
get;
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize Cosmos DB objects
set;
} // todo: sync: do plugins really need this too?
public string Name {
get;
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize Cosmos DB objects
set;
}
public string ContactEmail {
get;
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize Cosmos DB objects
set;
}
public string NotifyContactEmail
{
get;
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - used to deserialize Cosmos DB objects
set;
}
public IEnumerable<CdnConfiguration> CdnConfigurations {
get;
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global, MemberCanBePrivate.Global - used to deserialize Cosmos DB objects
set;
}
public Partner(string tenant, string name, string contactEmail, string notifyContactEmail,
CdnConfiguration[] cdnConfigurations)
{
id = Guid.NewGuid().ToString();
Name = name;
TenantId = tenant;
ContactEmail = contactEmail;
NotifyContactEmail = notifyContactEmail;
CdnConfigurations = cdnConfigurations;
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Collections.Generic;
public class UserRequest : CosmosDbEntity
{
public UserRequest(string partnerId, string description, string ticketId, string hostname, ISet<string> urls)
{
PartnerId = partnerId;
Description = description;
TicketId = ticketId;
Hostname = hostname;
Urls = urls;
id = Guid.NewGuid().ToString();
}
public string PartnerId { get; }
public string Description { get; }
/// <summary>
/// TicketId is an id of an item in a task-tracking system
/// </summary>
public string TicketId { get; }
public string Hostname { get; }
public ISet<string> Urls { get; }
public int NumTotalPartnerRequests { get; set; }
public int NumCompletedPartnerRequests { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,101 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
public abstract class CosmosDbEntityClient: IDisposable
{
private readonly string cosmosDbId;
private readonly string containerId;
private readonly string partitionKey = "/id";
private readonly CosmosClient cosmosClient;
protected Container Container;
protected CosmosDbEntityClient(string connectionString, string databaseId, string containerId)
{
this.containerId = containerId;
this.cosmosDbId = databaseId;
CosmosClientOptions clientOptions = new CosmosClientOptions()
{
SerializerOptions = new CosmosSerializationOptions()
{
IgnoreNullValues = true
}
};
cosmosClient = new CosmosClient(connectionString, clientOptions);
}
protected CosmosDbEntityClient(Container container)
{
Container = container;
}
protected CosmosDbEntityClient(string connectionString, string databaseId, string containerId, string partitionKey) : this(connectionString, databaseId, containerId)
{
this.partitionKey = $"/{partitionKey}";
}
protected async Task Create<T>(T item)
{
if (Container == null)
{
await CreateContainer();
}
await Container.CreateItemAsync(item);
}
protected async Task Upsert<T>(T item)
{
if (Container == null)
{
await CreateContainer();
}
await Container.UpsertItemAsync(item);
}
protected async Task<T> SelectFirstByIdAsync<T>(string id, string indexColumnName = "id")
{
if (Container == null)
{
await CreateContainer();
}
using var queryIterator = Container.GetItemQueryIterator<T>(
$"SELECT * FROM {containerId} c WHERE c.{indexColumnName} = '{id}'");
if (queryIterator != null && queryIterator.HasMoreResults)
{
var response = await queryIterator.ReadNextAsync();
if (response.Count > 0)
{
return response.First();
}
}
return default;
}
protected async Task CreateContainer()
{
var database = await cosmosClient.CreateDatabaseIfNotExistsAsync(cosmosDbId);
Container = await database.Database.CreateContainerIfNotExistsAsync(containerId, partitionKey);
}
public void Dispose()
{
if (cosmosClient != null)
{
cosmosClient.Dispose();
}
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Threading.Tasks;
public interface ICdnRequestTableManager<in T> : IDisposable where T : Enum
{
public Task CreateCdnRequest(ICdnRequest request, T cdn);
public Task UpdateCdnRequest(ICdnRequest request, T cdn);
}
}

Просмотреть файл

@ -0,0 +1,19 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Threading.Tasks;
public interface IPartnerRequestTableManager<in T> : IDisposable where T : Enum
{
public Task CreatePartnerRequest(IPartnerRequest partnerRequest, T cdn);
public Task UpdatePartnerRequest(IPartnerRequest partnerRequest, T cdn);
public Task<IPartnerRequest> GetPartnerRequest(string id, T cdn);
}
}

Просмотреть файл

@ -0,0 +1,22 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CachePurgeLibrary
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public interface IRequestTable<T>: IDisposable
{
public Task CreateItem(T request);
public Task UpsertItem(T request);
public Task<T> GetItem(string id);
public Task<IEnumerable<T>> GetItems();
}
}

Просмотреть файл

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\CachePurgeLibrary\CachePurgeLibrary.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.8" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnPlugins_Test</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnPlugins</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnLibrary_Test</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MultiCdnApi</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,132 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text.Json;
internal class AfdPlugin : ICdnPlugin<AfdPartnerRequest>
{
private static readonly string baseUri = Environment.GetEnvironmentVariable("Afd_BaseUri") ?? "https://www.afdcp.com/api/v2.0/Tenants/";
private static readonly string BatchCreated = RequestStatus.BatchCreated.ToString();
private readonly int maxNumUrl = EnvironmentConfig.AfdBatchSize;
private readonly ILogger logger;
public AfdPlugin(ILogger logger)
{
this.logger = logger;
}
public AfdPlugin(ILogger logger, int maxNumUrl)
{
this.logger = logger;
this.maxNumUrl = maxNumUrl;
}
public bool ProcessPartnerRequest(AfdPartnerRequest partnerRequest, ICollector<ICdnRequest> queue)
{
if (TryGetEndpoint(partnerRequest, out var endpoint))
{
// Split messages into batches and add to queue
var batchedRequests = SplitRequestIntoBatches(partnerRequest, maxNumUrl);
foreach (var req in batchedRequests)
{
req.Endpoint = endpoint;
AddMessagesToSendQueue(req, queue);
}
partnerRequest.Status = BatchCreated;
return true;
}
return false;
}
public void AddMessagesToSendQueue(ICdnRequest cdnRequest, ICollector<ICdnRequest> msg)
{
string desc = $"CachePurge_{DateTime.UtcNow}";
if (cdnRequest is AfdRequest afdRequest && !string.IsNullOrEmpty(afdRequest.Description))
{
desc = afdRequest.Description;
}
if (cdnRequest.Urls.Length > 0)
{
try
{
var afdRequestBody = new AfdRequestBody()
{
Urls = cdnRequest.Urls,
Description = desc
};
cdnRequest.RequestBody = JsonSerializer.Serialize(afdRequestBody, CdnPluginHelper.JsonSerializerOptions);
msg.Add(cdnRequest);
}
catch (Exception e)
{
logger.LogError($"AfdPlugin: Exception serializing cdnRequest id={cdnRequest.id}, {e.Message}");
}
}
}
public IList<ICdnRequest> SplitRequestIntoBatches(AfdPartnerRequest partnerRequest, int maxNumUrl)
{
var partnerRequests = new List<ICdnRequest>();
var urlLists = CdnPluginHelper.SplitUrlListIntoBatches(partnerRequest.Urls, maxNumUrl);
partnerRequest.NumTotalCdnRequests = urlLists.Count;
foreach (var list in urlLists)
{
var req = new AfdRequest(partnerRequest.id, partnerRequest.TenantID, partnerRequest.PartnerID, partnerRequest.Description, list);
partnerRequests.Add(req);
}
return partnerRequests;
}
public bool TryGetEndpoint(AfdPartnerRequest partnerRequest, out string endpoint)
{
endpoint = string.Empty;
if (string.IsNullOrEmpty(partnerRequest.TenantID) || string.IsNullOrEmpty(partnerRequest.PartnerID))
{
return false;
}
endpoint = baseUri + $"{partnerRequest.TenantID}/Partners/{partnerRequest.PartnerID}/CachePurges";
return true;
}
public bool ValidPartnerRequest(string inputRequest, string resourceID, out AfdPartnerRequest partnerRequest)
{
partnerRequest = null;
try
{
partnerRequest = JsonSerializer.Deserialize<AfdPartnerRequest>(inputRequest, CdnPluginHelper.JsonSerializerOptions);
return CdnPluginHelper.IsValidRequest(partnerRequest);
}
catch (Exception e)
{
logger.LogError($"AfdPlugin: Exception reading resource ID={resourceID}, {e.Message}");
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,127 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text.Json;
internal class AkamaiPlugin : ICdnPlugin<AkamaiPartnerRequest>
{
private const string Production = "production";
private const string Staging = "staging";
private static readonly string baseUri = Environment.GetEnvironmentVariable("Akamai_BaseUri") ?? "https://fakeUri";
private static readonly string BatchCreated = RequestStatus.BatchCreated.ToString();
private readonly int maxNumUrl = EnvironmentConfig.AkamaiBatchSize;
private readonly ILogger logger;
// TODO: Implement for delete
public AkamaiPlugin(ILogger logger)
{
this.logger = logger;
}
public AkamaiPlugin(ILogger logger, int maxNumUrl)
{
this.logger = logger;
this.maxNumUrl = maxNumUrl;
}
public bool ProcessPartnerRequest(AkamaiPartnerRequest partnerRequest, ICollector<ICdnRequest> queue)
{
var endpoint = GetEndpoint(partnerRequest);
// Split messages into batches and add to queue
var batchedRequests = SplitRequestIntoBatches(partnerRequest, maxNumUrl);
foreach (var req in batchedRequests)
{
req.Endpoint = endpoint;
AddMessagesToSendQueue(req, queue);
}
partnerRequest.Status = BatchCreated;
return true;
}
public void AddMessagesToSendQueue(ICdnRequest cdnRequest, ICollector<ICdnRequest> msg)
{
if (cdnRequest.Urls.Length > 0)
{
try
{
var akamaiRequestBody = new AkamaiRequestBody()
{
Objects = cdnRequest.Urls,
};
cdnRequest.RequestBody = JsonSerializer.Serialize(akamaiRequestBody, CdnPluginHelper.JsonSerializerOptions);
msg.Add(cdnRequest);
}
catch (Exception e)
{
logger.LogError($"AkamaiPlugin: Exception serializing cdnRequest id={cdnRequest.id}, {e.Message}");
}
}
}
public IList<ICdnRequest> SplitRequestIntoBatches(AkamaiPartnerRequest partnerRequest, int maxNumUrl)
{
var partnerRequests = new List<ICdnRequest>();
var urlLists = CdnPluginHelper.SplitUrlListIntoBatches(partnerRequest.Urls, maxNumUrl);
partnerRequest.NumTotalCdnRequests = urlLists.Count;
foreach (var list in urlLists)
{
var req = new AkamaiRequest(partnerRequest.id, list);
partnerRequests.Add(req);
}
return partnerRequests;
}
public string GetEndpoint(AkamaiPartnerRequest partnerRequest)
{
// Use staging as default
var endpoint = $"{baseUri}{Staging}";
if (!string.IsNullOrEmpty(partnerRequest.Network) && partnerRequest.Network.Equals(Production, StringComparison.OrdinalIgnoreCase))
{
endpoint = $"{baseUri}{Production}";
}
return endpoint;
}
public bool ValidPartnerRequest(string inputRequest, string resourceID, out AkamaiPartnerRequest partnerRequest)
{
partnerRequest = null;
try
{
partnerRequest = JsonSerializer.Deserialize<AkamaiPartnerRequest>(inputRequest, CdnPluginHelper.JsonSerializerOptions);
return CdnPluginHelper.IsValidRequest(partnerRequest);
}
catch (Exception e)
{
logger.LogError($"AkamaiPlugin: Exception reading resource ID={resourceID}, {e.Message}");
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,142 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Storage.Queue;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
internal class AfdQueueProcessor : ICdnQueueProcessor<AfdRequest>
{
private readonly ILogger logger;
private static readonly double RequestWaitTime = EnvironmentConfig.RequestWaitTime;
public AfdQueueProcessor(ILogger logger)
{
this.logger = logger;
}
public void AddMessageToQueue(CloudQueue outputQueue, CloudQueueMessage message, AfdRequest queueMsg)
{
// increase backoff based on # dequeues
var nextVisibleTime = queueMsg.NumTimesProcessed > 0 ? TimeSpan.FromSeconds(queueMsg.NumTimesProcessed * RequestWaitTime) : TimeSpan.FromSeconds(RequestWaitTime);
outputQueue.AddMessage(message, null, nextVisibleTime);
}
public CloudQueueMessage CreateCloudQueueMessage(AfdRequest request, IRequestInfo requestInfo)
{
if (string.IsNullOrEmpty(request.Endpoint))
{
logger.LogWarning($"AfdQueueProcessor: Request with id={request.id} has empty endpoint");
return null;
}
request.Status = requestInfo.RequestStatus.ToString();
return new CloudQueueMessage(JsonSerializer.Serialize(request));
}
public async Task<CloudQueueMessage> ProcessPurgeRequest(AfdRequest queueMsg, int maxRetry)
{
if (queueMsg.Urls == null || queueMsg.Urls.Length == 0)
{
logger.LogWarning($"AfdQueueProcessor: Dropping purge msg with no urls");
return null;
}
var requestInfo = new AfdRequestInfo() { RequestStatus = RequestStatus.MaxRetry };
try
{
if (queueMsg.NumTimesProcessed < maxRetry)
{
requestInfo = await AfdRequestProcessor.SendPurgeRequest(queueMsg.Endpoint, new StringContent(queueMsg.RequestBody, Encoding.UTF8, "application/json"), logger);
return CompletePurgeRequest(requestInfo, queueMsg);
}
else
{
logger.LogWarning($"AfdQueueProcessor: Dropping purge msg: queueMsg with num NumTimesProcessed = {queueMsg.NumTimesProcessed} is greater than maxRetry={maxRetry}");
}
}
finally
{
queueMsg.Status = requestInfo.RequestStatus.ToString();
}
return null;
}
public CloudQueueMessage CompletePurgeRequest(IRequestInfo requestInfo, AfdRequest queueMsg)
{
if (requestInfo.RequestStatus == RequestStatus.PurgeSubmitted && !string.IsNullOrEmpty(requestInfo.RequestID))
{
// If the request was submitted, add to queue as a poll request
queueMsg.NumTimesProcessed = 0;
queueMsg.CdnRequestId = requestInfo.RequestID;
}
else if (requestInfo.RequestStatus == RequestStatus.Throttled || requestInfo.RequestStatus == RequestStatus.Error)
{
requestInfo.RequestID = null;
queueMsg.NumTimesProcessed++;
}
else if (requestInfo.RequestStatus == RequestStatus.Unauthorized || requestInfo.RequestStatus == RequestStatus.Unknown)
{
logger.LogError($"AfdQueueProcessor: Request Status {requestInfo.RequestStatus}, {queueMsg.id}");
return null;
}
return CreateCloudQueueMessage(queueMsg, requestInfo);
}
public async Task<CloudQueueMessage> ProcessPollRequest(AfdRequest queueMsg, int maxRetry)
{
var requestInfo = new AfdRequestInfo() { RequestStatus = RequestStatus.MaxRetry };
try
{
if (queueMsg.NumTimesProcessed < maxRetry)
{
requestInfo.RequestStatus = await AfdRequestProcessor.SendPollRequest(queueMsg.Endpoint, queueMsg.CdnRequestId, logger);
return CompletePollRequest(requestInfo, queueMsg);
}
else
{
logger.LogWarning($"AfdQueueProcessor: Dropping poll msg: queueMsg with num NumTimesProcessed = {queueMsg.NumTimesProcessed} is greater than maxRetry={maxRetry}");
}
}
finally
{
queueMsg.Status = requestInfo.RequestStatus.ToString();
}
return null;
}
public CloudQueueMessage CompletePollRequest(IRequestInfo requestInfo, AfdRequest queueMsg)
{
if (requestInfo.RequestStatus == RequestStatus.PurgeCompleted)
{
logger.LogInformation($"AfdQueueProcessor: Completed purge request RequestID={queueMsg.CdnRequestId}, NumTimesProcessed={queueMsg.NumTimesProcessed}, {queueMsg.RequestBody}");
return null;
}
else if (requestInfo.RequestStatus == RequestStatus.Throttled || requestInfo.RequestStatus == RequestStatus.Error)
{
// Only increase if the request is throttled or has an error
// Otherwise, we can keep polling at the current rate until the purge is done
queueMsg.NumTimesProcessed++;
}
//re-make polling message and re-add it to the queue
return CreateCloudQueueMessage(queueMsg, requestInfo);
}
}
}

Просмотреть файл

@ -0,0 +1,111 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CachePurgeLibrary;
using Microsoft.Azure.Storage.Queue;
using Microsoft.Extensions.Logging;
internal class AkamaiQueueProcessor : ICdnQueueProcessor<AkamaiRequest>
{
private readonly ILogger logger;
private static readonly double RequestWaitTime = EnvironmentConfig.RequestWaitTime;
public AkamaiQueueProcessor(ILogger logger)
{
this.logger = logger;
}
public CloudQueueMessage CreateCloudQueueMessage(AkamaiRequest request, IRequestInfo requestInfo)
{
if (string.IsNullOrEmpty(request.Endpoint))
{
logger.LogWarning($"AkamaiQueueProcessor: Request with id={request.id} has empty endpoint");
return null;
}
request.Status = requestInfo.RequestStatus.ToString();
return new CloudQueueMessage(JsonSerializer.Serialize(request));
}
public void AddMessageToQueue(CloudQueue outputQueue, CloudQueueMessage message, AkamaiRequest queueMsg)
{
// increase backoff based on # dequeues
var nextVisibleTime = queueMsg.NumTimesProcessed > 0 ? TimeSpan.FromSeconds(queueMsg.NumTimesProcessed * RequestWaitTime) : TimeSpan.FromSeconds(RequestWaitTime);
outputQueue.AddMessage(message, null, nextVisibleTime);
}
public async Task<CloudQueueMessage> ProcessPurgeRequest(AkamaiRequest queueMsg, int maxRetry)
{
if (queueMsg.Urls == null || queueMsg.Urls.Length == 0)
{
logger.LogWarning($"AkamaiQueueProcessor: Dropping purge msg with no urls");
return null;
}
var requestInfo = new AkamaiRequestInfo() { RequestStatus = RequestStatus.MaxRetry };
try
{
if (queueMsg.NumTimesProcessed < maxRetry)
{
requestInfo = await AkamaiRequestProcessor.SendPurgeRequest(queueMsg.Endpoint, new StringContent(queueMsg.RequestBody, Encoding.UTF8, "application/json"), logger);
return CompletePurgeRequest(requestInfo, queueMsg);
}
else
{
logger.LogWarning($"AkamaiQueueProcessor: Dropping purge msg: queueMsg with num NumTimesProcessed = {queueMsg.NumTimesProcessed} is greater than maxRetry={maxRetry}");
}
}
finally
{
queueMsg.Status = requestInfo.RequestStatus.ToString();
}
return null;
}
public CloudQueueMessage CompletePurgeRequest(IRequestInfo requestInfo, AkamaiRequest queueMsg)
{
if (requestInfo.RequestStatus == RequestStatus.PurgeCompleted && !string.IsNullOrEmpty(requestInfo.RequestID))
{
// Don't need to poll Akamai to verify purge completion
queueMsg.CdnRequestId = requestInfo.RequestID;
if (requestInfo is AkamaiRequestInfo akamaiRequestInfo)
{
queueMsg.SupportId = akamaiRequestInfo.SupportID;
}
return null;
}
else if (requestInfo.RequestStatus == RequestStatus.Throttled || requestInfo.RequestStatus == RequestStatus.Error)
{
requestInfo.RequestID = null;
queueMsg.NumTimesProcessed++;
}
else if (requestInfo.RequestStatus == RequestStatus.Unauthorized || requestInfo.RequestStatus == RequestStatus.Unknown)
{
logger.LogError($"AkamaiQueueProcessor: Request Status {requestInfo.RequestStatus}, {queueMsg.id}");
return null;
}
return CreateCloudQueueMessage(queueMsg, requestInfo);
}
public CloudQueueMessage CompletePollRequest(IRequestInfo requestInfo, AkamaiRequest queueMsg) => throw new NotImplementedException();
public Task<CloudQueueMessage> ProcessPollRequest(AkamaiRequest queueMsg, int maxRetry) => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,18 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
public class AfdPartnerRequest : PartnerRequest
{
public string TenantID { get; set; }
public string PartnerID { get; set; }
public string Description { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System;
public class AfdRequest : CosmosDbEntity, ICdnRequest
{
public AfdRequest() { }
public AfdRequest(string partnerRequestID, string tenantId, string partnerId, string description, string[] urls)
{
this.id = Guid.NewGuid().ToString();
PartnerRequestID = partnerRequestID;
TenantID = tenantId;
PartnerID = partnerId;
Description = description;
Urls = urls;
}
public string CdnRequestId { get; set; }
public string PartnerRequestID { get; set; }
public string TenantID { get; set; }
public string PartnerID { get; set; }
public string Description { get; set; }
public string[] Urls { get; set; }
public string Status { get; set; } = string.Empty;
public int NumTimesProcessed { get; set; }
public string Endpoint { get; set; }
public string RequestBody { get; set; }
}
internal class AfdRequestBody
{
public string Description { get; set; }
public string[] Urls { get; set; }
}
public class AfdRequestInfo : IRequestInfo
{
public string RequestID { get; set; }
public RequestStatus RequestStatus { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
public class AkamaiPartnerRequest : PartnerRequest
{
public string Network { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,64 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System;
using System.Text.Json.Serialization;
public class AkamaiRequest : CosmosDbEntity, ICdnRequest
{
public AkamaiRequest() { }
public AkamaiRequest(string partnerRequestID, string[] urls)
{
id = Guid.NewGuid().ToString();
PartnerRequestID = partnerRequestID;
Urls = urls;
}
public string PartnerRequestID { get; set; }
public int NumTimesProcessed { get; set; }
/// <summary>
/// PurgeId returned by Akamai
/// </summary>
public string CdnRequestId { get; set; }
/// <summary>
/// Identifier to provide Akamai Technical Support if issues arise.
/// </summary>
public string SupportId { get; set; }
public string Endpoint { get; set; }
public string RequestBody { get; set; }
public string Status { get; set; }
public string[] Urls { get; set; }
}
internal class AkamaiRequestBody
{
[JsonPropertyName("objects")]
public string[] Objects { get; set; }
[JsonPropertyName("hostname")]
public string Hostname { get; set; }
}
public class AkamaiRequestInfo : IRequestInfo
{
public string RequestID { get; set; }
public RequestStatus RequestStatus { get; set; }
public string SupportID { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,161 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
internal static class AfdRequestProcessor
{
private const string RolledOut = "RolledOut";
private static readonly string LoginUrl = Environment.GetEnvironmentVariable("Afd_LoginUrl") ?? "https://fakeUrl";
private static readonly string Resource = Environment.GetEnvironmentVariable("Afd_Resource") ?? "https://fakeResource";
private static readonly string AppId = Environment.GetEnvironmentVariable("Afd_AppId") ?? "fakeAppId";
private static readonly string AppKey = Environment.GetEnvironmentVariable("Afd_Secret") ?? "fakeSecret";
private static readonly ReadOnlyMemory<char> Id = "Id".AsMemory();
private static readonly ReadOnlyMemory<char> Status = "Status".AsMemory();
internal static async Task<AfdRequestInfo> SendPurgeRequest(string afdEndpoint, StringContent purgeRequest, ILogger logger)
{
var azureAuthHandler = new AzureAuthHandler(new HttpClientHandler(), Resource, LoginUrl, AppId, AppKey);
using var httpHandler = new HttpHandler(azureAuthHandler);
return await SendPurgeRequest(afdEndpoint, purgeRequest, httpHandler, logger);
}
internal static async Task<RequestStatus> SendPollRequest(string afdEndpoint, string requestId, ILogger logger)
{
var azureAuthHandler = new AzureAuthHandler(new HttpClientHandler(), Resource, LoginUrl, AppId, AppKey);
using var httpHandler = new HttpHandler(azureAuthHandler);
return await SendPollRequest(afdEndpoint, requestId, httpHandler, logger);
}
internal static async Task<RequestStatus> SendPollRequest(string afdEndpoint, string requestId, IHttpHandler httpHandler, ILogger logger)
{
var requestStatus = RequestStatus.Error;
if (!string.IsNullOrEmpty(afdEndpoint) && !string.IsNullOrEmpty(requestId))
{
try
{
var response = await httpHandler.GetAsync($"{afdEndpoint}/{requestId}");
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(responseContent))
{
requestStatus = GetRequestStatusFromContent(responseContent);
}
else
{
requestStatus = CdnPluginHelper.GetRequestStatusFromResponseCode(response.StatusCode);
}
}
catch (Exception e)
{
logger.LogError($"AfdRequestProcessor: Error while sending poll request: {e.Message}\n {e.StackTrace}");
}
}
return requestStatus;
}
internal static async Task<AfdRequestInfo> SendPurgeRequest(string requestEndPoint, StringContent purgeRequest, IHttpHandler httpHandler, ILogger logger)
{
var requestInfo = new AfdRequestInfo()
{
RequestStatus = RequestStatus.Error
};
if (!string.IsNullOrEmpty(requestEndPoint) && purgeRequest != null)
{
try
{
var response = await httpHandler.PostAsync(requestEndPoint, purgeRequest);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(responseContent) &&
GetRequestID(responseContent, out var requestID))
{
requestInfo.RequestID = requestID;
requestInfo.RequestStatus = RequestStatus.PurgeSubmitted;
}
else
{
logger.LogInformation($"AfdRequestProcessor: Purge Request not successful: {response.StatusCode}, {responseContent}");
requestInfo.RequestStatus = CdnPluginHelper.GetRequestStatusFromResponseCode(response.StatusCode);
}
}
catch (Exception e)
{
logger.LogError($"AfdRequestProcessor: Error while sending purge request: {e.Message}\n {e.StackTrace}");
}
}
return requestInfo;
}
internal static bool GetRequestID(string response, out string requestID)
{
requestID = null;
using JsonDocument document = JsonDocument.Parse(response);
JsonElement root = document.RootElement;
if (root.TryGetProperty(Id.Span, out var idElement) && idElement.ValueKind == JsonValueKind.Number &&
idElement.TryGetInt64(out var ID))
{
requestID = ID.ToString();
return true;
}
return false;
}
internal static RequestStatus GetRequestStatusFromContent(string result)
{
if (GetStringProperty(result, Status, out var status))
{
if (RolledOut.Equals(status, StringComparison.OrdinalIgnoreCase))
{
return RequestStatus.PurgeCompleted;
}
return RequestStatus.PurgeSubmitted;
}
return RequestStatus.Unknown;
}
private static bool GetStringProperty(string response, ReadOnlyMemory<char> propertyName, out string propertyValue)
{
propertyValue = null;
using JsonDocument document = JsonDocument.Parse(response);
JsonElement root = document.RootElement;
if (root.TryGetProperty(propertyName.Span, out var propVal) && propVal.ValueKind == JsonValueKind.String)
{
propertyValue = propVal.ToString();
return true;
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,105 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
internal class AkamaiAuthHandler : DelegatingHandler
{
private const string HashType = "SHA256";
private readonly string clientToken;
private readonly string accessToken;
private readonly string clientSecret;
private readonly KeyedHash hash;
internal List<string> HeadersToInclude { get; private set; }
public AkamaiAuthHandler(
HttpMessageHandler innerHandler,
string clientToken,
string accessToken,
string clientSecret)
: base(innerHandler)
{
this.clientToken = clientToken ?? throw new ArgumentNullException(nameof(clientToken));
this.accessToken = accessToken ?? throw new ArgumentNullException(nameof(accessToken));
this.clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret));
this.hash = KeyedHash.HMACSHA256;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authHeader = await CreateAuthorizationHeader(request);
request.Headers.Add(HeaderNames.Authorization, authHeader);
return await base.SendAsync(request, cancellationToken);
}
public async Task<string> CreateAuthorizationHeader(HttpRequestMessage request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HH:mm:ss+0000");
var requestData = await GetRequestData(request);
return GetAuthorizationHeader(timestamp, requestData);
}
internal string GetAuthorizationData(string timestamp)
{
var nonce = Guid.NewGuid();
return
$"{hash.Name} client_token={clientToken};" +
$"access_token={accessToken};" +
$"timestamp={timestamp};" +
$"nonce={nonce.ToString().ToLowerInvariant()};";
}
internal async Task<string> GetRequestData(HttpRequestMessage request)
{
var bodyStream = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
var requestHash = CdnPluginHelper.ComputeHash(bodyStream.AsSpan(), HashType);
return $"POST\t{request.RequestUri.Scheme}\t" +
$"{request.RequestUri.Host}\t{request.RequestUri.PathAndQuery}\t{string.Empty}\t{requestHash}\t";
}
internal string GetAuthorizationHeader(string timestamp, string requestData)
{
var authData = GetAuthorizationData(timestamp);
var signingKey = CdnPluginHelper.ComputeKeyedHash(timestamp, clientSecret, hash.Algorithm);
var authSignature = CdnPluginHelper.ComputeKeyedHash(requestData + authData, signingKey, hash.Algorithm);
return $"{authData}signature={authSignature}";
}
internal struct KeyedHash
{
public static readonly KeyedHash HMACSHA256 = new KeyedHash()
{
Name = "EG1-HMAC-SHA256",
Algorithm = "HMACSHA256"
};
public string Name { get; private set; }
public string Algorithm { get; private set; }
}
}
}

Просмотреть файл

@ -0,0 +1,87 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
internal static class AkamaiRequestProcessor
{
private static readonly string ClientSecret = Environment.GetEnvironmentVariable("Akamai_ClientSecret") ?? "fakesecret";
private static readonly string AccessToken = Environment.GetEnvironmentVariable("Akamai_AccessToken") ?? "faketoken";
private static readonly string ClientToken = Environment.GetEnvironmentVariable("Akamai_ClientToken") ?? "faketoken";
private static readonly ReadOnlyMemory<char> PurgeId = "purgeId".AsMemory();
private static readonly ReadOnlyMemory<char> SupportId = "supportId".AsMemory();
internal static async Task<AkamaiRequestInfo> SendPurgeRequest(string endpoint, StringContent purgeRequest, ILogger logger)
{
var akamaiAuthHandler = new AkamaiAuthHandler(new HttpClientHandler(), ClientToken, AccessToken, ClientSecret);
using var httpHandler = new HttpHandler(akamaiAuthHandler);
return await SendPurgeRequest(endpoint, purgeRequest, httpHandler, logger);
}
internal static async Task<AkamaiRequestInfo> SendPurgeRequest(string requestEndPoint, StringContent purgeRequest, IHttpHandler httpHandler, ILogger logger)
{
var requestInfo = new AkamaiRequestInfo()
{
RequestStatus = RequestStatus.Error
};
if (!string.IsNullOrEmpty(requestEndPoint) && purgeRequest != null)
{
try
{
var response = await httpHandler.PostAsync(requestEndPoint, purgeRequest);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(responseContent) &&
GetPropertyValue(PurgeId, responseContent, out var requestID) &&
GetPropertyValue(SupportId, responseContent, out var supportID))
{
requestInfo.RequestID = requestID;
requestInfo.RequestStatus = RequestStatus.PurgeCompleted;
requestInfo.SupportID = supportID;
}
else
{
logger.LogInformation($"AkamaiRequestProcessor: Purge Request not successful: {response.StatusCode}, {responseContent}");
requestInfo.RequestStatus = CdnPluginHelper.GetRequestStatusFromResponseCode(response.StatusCode);
}
}
catch (Exception e)
{
logger.LogError($"AkamaiRequestProcessor: Error while sending purge request: {e.Message}\n {e.StackTrace}");
}
}
return requestInfo;
}
internal static bool GetPropertyValue(ReadOnlyMemory<char> propName, string response, out string propValue)
{
propValue = null;
using JsonDocument document = JsonDocument.Parse(response);
JsonElement root = document.RootElement;
if (root.TryGetProperty(propName.Span, out var idElement) && idElement.ValueKind == JsonValueKind.String)
{
propValue = idElement.ToString();
return true;
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Message handler that gets an auth token using client credentials (AppId) based auth from Azure
/// active directory and adds the appropriate header for Authorization to the request
/// </summary>
public class AzureAuthHandler : DelegatingHandler
{
private readonly string loginUrl;
private readonly string resource;
private readonly string appId;
private readonly string appKey; //from KeyVault
public AzureAuthHandler(
HttpMessageHandler innerContent,
string resource,
string loginUrl,
string appId,
string appKey)
: base(innerContent)
{
this.resource = resource ?? throw new ArgumentNullException(nameof(resource));
this.loginUrl = loginUrl ?? throw new ArgumentNullException(nameof(loginUrl));
this.appId = appId ?? throw new ArgumentNullException(nameof(appId));
this.appKey = appKey ?? throw new ArgumentNullException(nameof(appKey));
}
/// <summary>
/// This message handler gets a token from AAD and adds it as an auth header to original request
/// and submits original request
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var authContext = new AuthenticationContext(this.loginUrl);
ClientCredential clientCred = new ClientCredential(this.appId, this.appKey);
var authResult = await authContext.AcquireTokenAsync(this.resource, clientCred);
request.Headers.Authorization = new AuthenticationHeaderValue(
authResult.AccessTokenType,
authResult.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System.Collections.Generic;
public class PartnerRequest: CosmosDbEntity, IPartnerRequest
{
/// <summary>
/// Id of the UserRequest this PartnerRequest was created from
/// </summary>
public string UserRequestID { get; set; }
public string CDN { get; set; }
public string Status { get; set; }
public int NumTotalCdnRequests { get; set; }
public int NumCompletedCdnRequests { get; set; }
public ISet<string> Urls { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,38 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class AfdPartnerRequestTable : CosmosDbEntityClient, IRequestTable<AfdPartnerRequest>
{
public AfdPartnerRequestTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.AfdPartnerCollectionName, EnvironmentConfig.PartnerRequestTablePartitionKey) { }
public AfdPartnerRequestTable(Container container) : base(container) { }
public async Task CreateItem(AfdPartnerRequest partnerRequest)
{
await Create(partnerRequest);
}
public async Task<AfdPartnerRequest> GetItem(string id)
{
return await SelectFirstByIdAsync<AfdPartnerRequest>(id);
}
public async Task UpsertItem(AfdPartnerRequest partnerRequest)
{
await Upsert(partnerRequest);
}
public Task<IEnumerable<AfdPartnerRequest>> GetItems() => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,35 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
internal class AfdRequestTable : CosmosDbEntityClient, IRequestTable<AfdRequest>
{
public AfdRequestTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.AfdCdnCollectionName, EnvironmentConfig.CdnRequestTablePartitionKey) { }
public AfdRequestTable(Container container) : base(container) { }
public async Task CreateItem(AfdRequest cdnRequest)
{
await Create(cdnRequest);
}
public Task<AfdRequest> GetItem(string id) => throw new NotImplementedException();
public async Task UpsertItem(AfdRequest cdnRequest)
{
await base.Upsert(cdnRequest);
}
public Task<IEnumerable<AfdRequest>> GetItems() => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,38 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class AkamaiPartnerRequestTable : CosmosDbEntityClient, IRequestTable<AkamaiPartnerRequest>
{
public AkamaiPartnerRequestTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.AkamaiPartnerCollectionName, EnvironmentConfig.PartnerRequestTablePartitionKey) { }
public AkamaiPartnerRequestTable(Container container) : base(container) { }
public async Task CreateItem(AkamaiPartnerRequest partnerRequest)
{
await Create(partnerRequest);
}
public async Task<AkamaiPartnerRequest> GetItem(string id)
{
return await SelectFirstByIdAsync<AkamaiPartnerRequest>(id);
}
public async Task UpsertItem(AkamaiPartnerRequest partnerRequest)
{
await Upsert(partnerRequest);
}
public Task<IEnumerable<AkamaiPartnerRequest>> GetItems() => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,36 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
internal class AkamaiRequestTable : CosmosDbEntityClient, IRequestTable<AkamaiRequest>
{
public AkamaiRequestTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.AkamaiCdnCollectionName, EnvironmentConfig.CdnRequestTablePartitionKey) { }
public AkamaiRequestTable(Container container) : base(container) { }
public async Task CreateItem(AkamaiRequest cdnRequest)
{
await Create(cdnRequest);
}
public Task<AkamaiRequest> GetItem(string id) => throw new NotImplementedException();
public async Task UpsertItem(AkamaiRequest cdnRequest)
{
await Upsert(cdnRequest);
}
public Task<IEnumerable<AkamaiRequest>> GetItems() => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,60 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System.Threading.Tasks;
public class CdnRequestTableManager : ICdnRequestTableManager<CDN>
{
private readonly IRequestTable<AfdRequest> afdRequestTable;
private readonly IRequestTable<AkamaiRequest> akamaiRequestTable;
public CdnRequestTableManager()
{
afdRequestTable = new AfdRequestTable();
akamaiRequestTable = new AkamaiRequestTable();
}
public CdnRequestTableManager(Container afdRequestContainer, Container akamaiRequestContainer)
{
afdRequestTable = new AfdRequestTable(afdRequestContainer);
akamaiRequestTable = new AkamaiRequestTable(akamaiRequestContainer);
}
public async Task CreateCdnRequest(ICdnRequest request, CDN cdn)
{
if (cdn == CDN.AFD)
{
await afdRequestTable.CreateItem(request as AfdRequest);
}
else if (cdn == CDN.Akamai)
{
await akamaiRequestTable.CreateItem(request as AkamaiRequest);
}
}
public async Task UpdateCdnRequest(ICdnRequest cdnRequest, CDN cdn)
{
if (cdn == CDN.AFD)
{
await afdRequestTable.UpsertItem(cdnRequest as AfdRequest);
}
else if (cdn == CDN.Akamai)
{
await akamaiRequestTable.UpsertItem(cdnRequest as AkamaiRequest);
}
}
public void Dispose()
{
afdRequestTable.Dispose();
akamaiRequestTable.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,73 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System.Threading.Tasks;
public class PartnerRequestTableManager : IPartnerRequestTableManager<CDN>
{
private readonly IRequestTable<AfdPartnerRequest> afdPartnerRequestTable;
private readonly IRequestTable<AkamaiPartnerRequest> akamaiPartnerRequestTable;
public PartnerRequestTableManager()
{
this.afdPartnerRequestTable = new AfdPartnerRequestTable();
this.akamaiPartnerRequestTable = new AkamaiPartnerRequestTable();
}
public PartnerRequestTableManager(Container afdPartnerRequestContainer, Container akamaiPartnerRequestContainer)
{
this.afdPartnerRequestTable = new AfdPartnerRequestTable(afdPartnerRequestContainer);
this.akamaiPartnerRequestTable = new AkamaiPartnerRequestTable(akamaiPartnerRequestContainer);
}
public async Task CreatePartnerRequest(IPartnerRequest partnerRequest, CDN cdn)
{
if (cdn == CDN.AFD)
{
await afdPartnerRequestTable.CreateItem(partnerRequest as AfdPartnerRequest);
}
else if (cdn == CDN.Akamai)
{
await akamaiPartnerRequestTable.CreateItem(partnerRequest as AkamaiPartnerRequest);
}
}
public async Task UpdatePartnerRequest(IPartnerRequest partnerRequest, CDN cdn)
{
if (cdn == CDN.AFD)
{
await afdPartnerRequestTable.UpsertItem(partnerRequest as AfdPartnerRequest);
}
else if (cdn == CDN.Akamai)
{
await akamaiPartnerRequestTable.UpsertItem(partnerRequest as AkamaiPartnerRequest);
}
}
public async Task<IPartnerRequest> GetPartnerRequest(string id, CDN cdn)
{
if (cdn == CDN.AFD)
{
return await afdPartnerRequestTable.GetItem(id);
}
else if (cdn == CDN.Akamai)
{
return await akamaiPartnerRequestTable.GetItem(id);
}
return null;
}
public void Dispose()
{
afdPartnerRequestTable.Dispose();
akamaiPartnerRequestTable.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class UserRequestTable : CosmosDbEntityClient, IRequestTable<UserRequest>
{
public UserRequestTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.UserRequestCosmosContainerId, EnvironmentConfig.UserRequestTablePartitionKey) {}
public UserRequestTable(Container container) : base(container) { }
public async Task CreateItem(UserRequest userRequest)
{
await Create(userRequest);
}
public async Task UpsertItem(UserRequest userRequest)
{
await Upsert(userRequest);
}
public async Task<UserRequest> GetItem(string id)
{
return await SelectFirstByIdAsync<UserRequest>(id);
}
public Task<IEnumerable<UserRequest>> GetItems() => throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,75 @@
NOTICES AND INFORMATION
Do Not Translate or Localize
This software incorporates material from third parties.
Microsoft makes certain open source code available at https://3rdpartysource.microsoft.com,
or you may send a check or money order for US $5.00, including the product name,
the open source component name, platform, and version number, to:
Source Code Compliance Team
Microsoft Corporation
One Microsoft Way
Redmond, WA 98052
USA
Notwithstanding any other terms, you may reverse engineer this software to the extent
required to debug changes to any libraries licensed under the GNU Lesser General Public License.
---------------------------------------------------------
Antlr4 4.8.0 - BSD-3-Clause
[The "BSD 3-clause license"]
Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=====
MIT License for codepointat.js from https://git.io/codepointat
MIT License for fromcodepoint.js from https://git.io/vDW1m
Copyright Mathias Bynens <https://mathiasbynens.be/>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------------------------

Просмотреть файл

@ -0,0 +1,129 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
internal static class CdnPluginHelper
{
internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, IgnoreNullValues = true };
internal static List<string[]> SplitUrlListIntoBatches(ISet<string> urlListToPurge, int batchSize)
{
var listOfBatches = new List<string[]>();
batchSize = (urlListToPurge.Count <= batchSize) ? urlListToPurge.Count : batchSize;
if (urlListToPurge.Count == 0)
{
return listOfBatches;
}
var batch = new string[batchSize];
int i = 0, j = 0;
foreach (var item in urlListToPurge)
{
if (j == batchSize)
{
listOfBatches.Add(batch);
var remainingLength = urlListToPurge.Count - i;
var nextBatchSize = remainingLength < batchSize ? remainingLength : batchSize;
batch = new string[nextBatchSize];
j = 0;
}
batch[j] = item;
i++;
j++;
}
listOfBatches.Add(batch);
return listOfBatches;
}
internal static string ComputeHash(ReadOnlySpan<byte> input, string hashType)
{
return TryComputeHash(input, hashType, out var hash) ? Convert.ToBase64String(hash) : string.Empty;
}
internal static bool IsValidRequest(IPartnerRequest partnerRequest)
{
if (partnerRequest != null && partnerRequest.Urls != null && string.IsNullOrEmpty(partnerRequest.Status) && !string.IsNullOrEmpty(partnerRequest.id))
{
return true;
}
return false;
}
internal static bool TryComputeHash(ReadOnlySpan<byte> input, string hashType, out ReadOnlySpan<byte> output)
{
output = null;
if (input.IsEmpty || string.IsNullOrEmpty(hashType)) { return false; }
using var algorithm = HashAlgorithm.Create(hashType);
var dest = new byte[algorithm.HashSize].AsSpan();
if (algorithm.TryComputeHash(input, dest, out int writtenBytes))
{
output = dest.Slice(0, writtenBytes);
return true;
}
return false;
}
internal static string ComputeKeyedHash(string data, string key, string hashType)
{
return TryComputeKeyedHash(Encoding.UTF8.GetBytes(data), key, hashType, out var keyedHash) ? Convert.ToBase64String(keyedHash) : string.Empty;
}
internal static bool TryComputeKeyedHash(ReadOnlySpan<byte> input, string key, string hashType, out ReadOnlySpan<byte> keyedHash)
{
keyedHash = null;
if (input.IsEmpty || string.IsNullOrEmpty(hashType) || string.IsNullOrEmpty(key)) { return false; }
using var algorithm = HMAC.Create(hashType);
algorithm.Key = Encoding.UTF8.GetBytes(key);
keyedHash = algorithm.ComputeHash(input.ToArray());
var dest = new byte[algorithm.HashSize].AsSpan();
if (algorithm.TryComputeHash(input, dest, out int writtenBytes))
{
keyedHash = dest.Slice(0, writtenBytes);
return true;
}
return false;
}
internal static RequestStatus GetRequestStatusFromResponseCode(HttpStatusCode result)
{
return result switch
{
HttpStatusCode.InternalServerError => RequestStatus.Error,
HttpStatusCode.TooManyRequests => RequestStatus.Throttled,
HttpStatusCode.Unauthorized => RequestStatus.Unauthorized,
_ => RequestStatus.Unknown,
};
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System;
internal static class CdnQueueHelper
{
public readonly static string Throttled = RequestStatus.Throttled.ToString();
public readonly static string Error = RequestStatus.Error.ToString();
public static bool AddCdnRequestToDB(ICdnRequest queueMsg, int maxRetry)
{
// If the request is throttled or has a generic error, it's retried until max retry
// Only update the DB once its exhausted retries to set the final state correctly while minimizing updates
if ((queueMsg.Status.Equals(Throttled, StringComparison.OrdinalIgnoreCase) || queueMsg.Status.Equals(Error, StringComparison.OrdinalIgnoreCase)) && queueMsg.NumTimesProcessed < maxRetry) { return false; }
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using System;
internal static class CdnRequestHelper
{
public static IPartnerRequest CreatePartnerRequest(CDN cdn, Partner partner, UserRequest userRequest, string description, string ticketId)
{
IPartnerRequest partnerRequest;
if (cdn == CDN.Akamai)
{
partnerRequest = new AkamaiPartnerRequest();
}
else if (cdn == CDN.AFD)
{
partnerRequest = new AfdPartnerRequest
{
PartnerID = partner.Name,
TenantID = partner.TenantId,
Description = description + (string.IsNullOrEmpty(ticketId) ? "" : $" ({ticketId})")
};
}
else
{
throw new ArgumentException($"We encountered an unknown CDN: {cdn}");
}
partnerRequest.id = Guid.NewGuid().ToString();
partnerRequest.CDN = cdn.ToString();
partnerRequest.Urls = userRequest.Urls;
partnerRequest.UserRequestID = userRequest.id.ToString();
return partnerRequest;
}
}
public enum CDN
{
AFD = 1,
Akamai = 2
}
}

Просмотреть файл

@ -0,0 +1,51 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using System;
/// <summary>
/// See tests for sample values
/// </summary>
public static class EnvironmentConfig
{
internal const string DatabaseName = "CacheOut";
internal const string CosmosDBConnectionStringName = "CosmosDBConnection";
internal const string BatchQueueConnectionStringName = "CDN_Queue";
internal const string AfdPartnerCollectionName = "AFD_PartnerRequest";
internal const string AfdCdnCollectionName = "AFD_CdnRequest";
internal const string AfdBatchQueueName = "AFDBatchQueue";
internal const string AkamaiPartnerCollectionName = "Akamai_PartnerRequest";
internal const string AkamaiCdnCollectionName = "Akamai_CdnRequest";
internal const string AkamaiBatchQueueName = "AkamaiBatchQueue";
internal const string CosmosDBConnection = "CosmosDBConnection";
public static string CosmosDBConnectionString => Environment.GetEnvironmentVariable(CosmosDBConnection) ?? throw new InvalidOperationException();
public static string CosmosDatabaseId => Environment.GetEnvironmentVariable("CosmosDatabaseId") ?? "CacheOut";
public static string UserRequestCosmosContainerId => Environment.GetEnvironmentVariable("UserRequestCosmosContainerId") ?? "UserRequest";
public static string PartnerCosmosContainerId => Environment.GetEnvironmentVariable("PartnerCosmosContainerId") ?? "Partner";
public static int MaxRetry => (Environment.GetEnvironmentVariable("Max_Retry") != null) ? Convert.ToInt32(Environment.GetEnvironmentVariable("Max_Retry")) : 5;
public static string CdnRequestTablePartitionKey => Environment.GetEnvironmentVariable("CdnRequestTablePartitionKey") ?? "PartnerRequestID";
public static string PartnerRequestTablePartitionKey => Environment.GetEnvironmentVariable("PartnerRequestTablePartitionKey") ?? "UserRequestID";
public static string UserRequestTablePartitionKey => Environment.GetEnvironmentVariable("UserRequestTablePartitionKey") ?? "id";
public static int RequestWaitTime = (Environment.GetEnvironmentVariable("Poll_WaitTime") != null) ? Convert.ToInt32(Environment.GetEnvironmentVariable("Poll_WaitTime")) : 2;
public static int AfdBatchSize = (Environment.GetEnvironmentVariable("Afd_UrlBatchSize")) != null ? Convert.ToInt32(Environment.GetEnvironmentVariable("Afd_UrlBatchSize")) : 200;
public static int AkamaiBatchSize = (Environment.GetEnvironmentVariable("Akamai_UrlBatchSize")) != null ? Convert.ToInt32(Environment.GetEnvironmentVariable("Akamai_UrlBatchSize")) : 200;
}
}

Просмотреть файл

@ -0,0 +1,150 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
[TestClass]
public class AfdPlugin_Test
{
private static readonly string tenantId = "FakeTenant";
private static readonly string partnerId = "FakePartner";
private static readonly string id = Guid.NewGuid().ToString();
private readonly List<ICdnRequest> OutputQueue = new List<ICdnRequest>();
private readonly AfdPlugin plugin = new AfdPlugin(Mock.Of<ILogger>(), 500);
[TestMethod]
public void ProcessPartnerRequest_NullPartnerAndTenantID()
{
string json =
@"{
""CDN"": ""AFD"",
""tenantId"": """",
""partnerId"": """",
""Urls"": [""https://fakeUri?pid=1.1""]}";
Assert.IsFalse(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsFalse(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_HasPartnerAndTenantID()
{
string json =
@$"{{
""CDN"": ""AFD"",
""tenantId"": ""{tenantId}"",
""partnerId"": ""{partnerId}"",
""id"": ""{id}"",
""Urls"": [""https://fakeUri?pid=1.1""]}}";
Assert.IsTrue(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(1, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_NoUrl()
{
string json =
@$"{{
""CDN"": ""AFD"",
""tenantId"": ""{tenantId}"",
""partnerId"": ""{partnerId}"",
""id"": ""{id}""
}}";
Assert.IsFalse(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_MaxUrl()
{
string json =
@$"{{
""CDN"": ""AFD"",
""id"": ""{id}"",
""tenantId"": ""{tenantId}"",
""partnerId"": ""{partnerId}"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.2"",
""https://fakeUri?pid=1.3""]}}";
var plugin2 = new AfdPlugin(Mock.Of<ILogger>(), 2);
Assert.IsTrue(plugin2.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin2.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(2, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_BatchCreated()
{
string json =
@$"{{
""CDN"": ""AFD"",
""Status"": ""BatchCreated"",
""id"": ""{id}"",
""tenantId"": ""{tenantId}"",
""partnerId"": ""{partnerId}"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.2"",
""https://fakeUri?pid=1.3""]}}";
Assert.IsFalse(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_DoesNotAddDuplicate()
{
var desc = "UnitTest";
string json =
@$"{{
""CDN"": ""AFD"",
""tenantId"": ""{tenantId}"",
""partnerId"": ""{partnerId}"",
""id"": ""{id}"",
""Description"": ""{desc}"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1""]}}";
Assert.IsTrue(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(1, OutputQueue.Count);
var req = OutputQueue[0] as AfdRequest;
Assert.AreEqual(tenantId, req.TenantID);
Assert.AreEqual(partnerId, req.PartnerID);
Assert.AreEqual(desc, req.Description);
}
private ICollector<ICdnRequest> CreateCollector()
{
var collector = new Mock<ICollector<ICdnRequest>>();
collector.Setup(c => c.Add(It.IsAny<ICdnRequest>())).Callback<ICdnRequest>((s) => OutputQueue.Add(s));
return collector.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,341 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Text.Json;
[TestClass]
public class AfdQueueProcessor_Test
{
private readonly List<ICdnRequest> OutputDB = new List<ICdnRequest>();
private readonly AfdQueueProcessor processor = new AfdQueueProcessor(Mock.Of<ILogger>());
[TestMethod]
public void ProcessPollRequest_EmptyQueueMessage()
{
var msg = processor.ProcessPollRequest(new AfdRequest(), 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPollRequest_QueueMessage()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100"
};
var msg = processor.ProcessPollRequest(queueMsg, 5).Result;
Assert.IsNotNull(msg);
Assert.AreEqual(1, queueMsg.NumTimesProcessed);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPollRequest_MaxRetry()
{
string json =
@"{
""CDN"": ""AFD"",
""tenantId"": ""1"",
""partnerId"": ""1"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1""]}";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
NumTimesProcessed = 5,
RequestBody = json,
Status = "PurgeSubmitted"
};
var msg = processor.ProcessPollRequest(queueMsg, 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(5, queueMsg.NumTimesProcessed);
Assert.IsTrue(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
Assert.AreEqual(RequestStatus.MaxRetry.ToString(), queueMsg.Status);
}
[TestMethod]
public void ProcessPollRequest_Completed()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
};
var requestInfo = new AfdRequestInfo()
{
RequestStatus = RequestStatus.PurgeCompleted
};
var msg = processor.CompletePollRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
}
[TestMethod]
public void ProcessPollRequest_InProgress()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
};
var requestInfo = new AfdRequestInfo()
{
RequestStatus = RequestStatus.PurgeSubmitted
};
var msg = processor.CompletePollRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(0, pollMsg.NumTimesProcessed);
Assert.AreEqual("100", pollMsg.CdnRequestId);
Assert.AreEqual(RequestStatus.PurgeSubmitted.ToString(), pollMsg.Status);
}
[TestMethod]
public void ProcessPollRequest_ErrorThrottled()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
};
var requestInfo = new AfdRequestInfo()
{
RequestStatus = RequestStatus.Error
};
var msg = processor.CompletePollRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(1, pollMsg.NumTimesProcessed);
Assert.AreEqual("100", pollMsg.CdnRequestId);
requestInfo.RequestStatus = RequestStatus.Throttled;
msg = processor.CompletePollRequest(requestInfo, queueMsg);
pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(2, pollMsg.NumTimesProcessed);
Assert.AreEqual("100", pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_EmptyQueueMessage()
{
var msg = processor.ProcessPurgeRequest(new AfdRequest(), 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPurgeRequest_QueueMessage()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
RequestBody = "testBody",
Urls = new string[]
{
"https://fakeUri?pid=1.1",
}
};
var msg = processor.ProcessPurgeRequest(queueMsg, 5).Result;
Assert.IsNotNull(msg);
Assert.AreEqual(1, queueMsg.NumTimesProcessed);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPurgeRequest_MaxRetry()
{
string json =
@"{
""CDN"": ""AFD"",
""tenantId"": ""1"",
""partnerId"": ""1"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1""]}";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 5,
RequestBody = json,
Urls = new string[]
{
"https://fakeUri?pid=1.1",
},
Status = RequestStatus.PurgeSubmitted.ToString()
};
var msg = processor.ProcessPurgeRequest(queueMsg, 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(5, queueMsg.NumTimesProcessed);
Assert.IsTrue(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
Assert.AreEqual(RequestStatus.MaxRetry.ToString(), queueMsg.Status);
}
[TestMethod]
public void ProcessPurgeRequest_SuccessCreatePollMsg()
{
var requestId = "12345";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1,
Urls = new string[]
{
"https://fakeUri?pid=1.1",
}
};
var requestInfo = new AfdRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.PurgeSubmitted
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(0, pollMsg.NumTimesProcessed);
Assert.AreEqual(requestId, pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_ThrottledIncreaseNumTimesProcessed()
{
var requestId = "12345";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AfdRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Throttled
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(2, pollMsg.NumTimesProcessed);
Assert.AreEqual(null, pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_ErrorIncreaseNumTimesProcessed()
{
var requestId = "12345";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AfdRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Error
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AfdRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(2, pollMsg.NumTimesProcessed);
Assert.AreEqual(null, pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_UnknownUnauthorizedReturnNull()
{
var requestId = "12345";
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AfdRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Unauthorized
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
requestInfo = new AfdRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Unknown
};
msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
}
[TestMethod]
public void AddCdnRequestToDB_ThrottledError()
{
var queueMsg = new AfdRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1,
Status = RequestStatus.Throttled.ToString()
};
Assert.IsFalse(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
queueMsg.Status = RequestStatus.Error.ToString();
Assert.IsFalse(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
}
}
}

Просмотреть файл

@ -0,0 +1,241 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
[TestClass]
public class AfdRequestProcessor_Test
{
private readonly ILogger logger = Mock.Of<ILogger>();
[TestMethod]
public void SendPurgeRequest_ErrorNoEndpoint()
{
var requestInfo = AfdRequestProcessor.SendPurgeRequest(string.Empty, null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_ErrorNoContent()
{
var requestInfo = AfdRequestProcessor.SendPurgeRequest("https://fakeUri", null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_SuccessSubmitted()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var id = "12345";
var requestInfo = AfdRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(id), logger).Result;
Assert.AreEqual(RequestStatus.PurgeSubmitted, requestInfo.RequestStatus);
Assert.AreEqual(id, requestInfo.RequestID);
}
[TestMethod]
public void SendPurgeRequest_UnknownNoRequestID()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var requestInfo = AfdRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(), logger).Result;
Assert.AreEqual(RequestStatus.Unknown, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_Throttled()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var id = "12345";
var requestInfo = AfdRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(id, statusCode: HttpStatusCode.TooManyRequests), logger).Result;
Assert.AreEqual(RequestStatus.Throttled, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPollRequest_ErrorNoEndpoint()
{
var requestInfo = AfdRequestProcessor.SendPollRequest(string.Empty, null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo);
}
[TestMethod]
public void SendPollRequest_ErrorNoContent()
{
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo);
}
[TestMethod]
public void SendPollRequest_SuccessSubmitted()
{
var id = "12345";
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", id, GetHandler(id, "RolledOut"), logger).Result;
Assert.AreEqual(RequestStatus.PurgeCompleted, requestInfo);
}
[TestMethod]
public void SendPollRequest_SuccessNotStarted()
{
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", "12345", GetHandler(status: "NotStarted"), logger).Result;
Assert.AreEqual(RequestStatus.PurgeSubmitted, requestInfo);
}
[TestMethod]
public void SendPollRequest_Unauthorized()
{
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", "12345", GetHandler(statusCode: HttpStatusCode.Unauthorized), logger).Result;
Assert.AreEqual(RequestStatus.Unauthorized, requestInfo);
}
[TestMethod]
public void SendPollRequest_Error()
{
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", "12345", GetHandler(statusCode: HttpStatusCode.InternalServerError), logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo);
}
[TestMethod]
public void SendPollRequest_Throttled()
{
var requestInfo = AfdRequestProcessor.SendPollRequest("https://fakeUri", "12345", GetHandler(statusCode: HttpStatusCode.TooManyRequests), logger).Result;
Assert.AreEqual(RequestStatus.Throttled, requestInfo);
}
[TestMethod]
public void GetRequestStatusFromResponse_CorrectRequestID()
{
var response = @"{
""Id"": 2230090,
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 -01-01T00:00:00"",
""RequestUser"": ""shishar@microsoft.com"",
""CompleteTime"": null,
""Status"": ""NotStarted"",
""PercentComplete"": 0
}";
Assert.IsTrue(AfdRequestProcessor.GetRequestID(response, out var id));
Assert.AreEqual("2230090", id);
var status = AfdRequestProcessor.GetRequestStatusFromContent(response);
Assert.AreEqual(RequestStatus.PurgeSubmitted, status);
}
[TestMethod]
public void GetRequestStatusFromResponse_NoRequestIDAndStatus()
{
var response = @"{
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 -01-01T00:00:00"",
""RequestUser"": ""shishar@microsoft.com"",
""CompleteTime"": null,
""PercentComplete"": 0
}";
Assert.IsFalse(AfdRequestProcessor.GetRequestID(response, out _));
var status = AfdRequestProcessor.GetRequestStatusFromContent(response);
Assert.AreEqual(RequestStatus.Unknown, status);
}
[TestMethod]
public void GetRequestStatusFromResponse_IncorrectFormatRequestIDAndStatus()
{
var response = @"{
""Id"": ""test"",
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 -01-01T00:00:00"",
""RequestUser"": ""shishar@microsoft.com"",
""CompleteTime"": null,
""Status"": 1,
""PercentComplete"": 0
}";
Assert.IsFalse(AfdRequestProcessor.GetRequestID(response, out _));
var status = AfdRequestProcessor.GetRequestStatusFromContent(response);
Assert.AreEqual(RequestStatus.Unknown, status);
}
[TestMethod]
public void GetRequestStatusFromResponse_NoStatus()
{
var response = @"{
""Id"": ""test"",
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 -01-01T00:00:00"",
""RequestUser"": ""shishar@microsoft.com"",
""CompleteTime"": null,
""PercentComplete"": 0
}";
Assert.IsFalse(AfdRequestProcessor.GetRequestID(response, out _));
var status = AfdRequestProcessor.GetRequestStatusFromContent(response);
Assert.AreEqual(RequestStatus.Unknown, status);
}
private static IHttpHandler GetHandler(string requestID = null, string status = "NotStarted", HttpStatusCode statusCode = HttpStatusCode.OK)
{
var responseText = @$"{{
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 - 01 - 01T00: 00:00"",
""RequestUser"": ""shishar @microsoft.com"",
""CompleteTime"": null,
""Status"": ""{status}"",
""PercentComplete"": 0";
if (!string.IsNullOrEmpty(requestID))
{
responseText += @$", ""Id"": {requestID}";
}
responseText += @"}";
var response = new HttpResponseMessage()
{
StatusCode = statusCode,
Content = new StringContent(responseText)
};
var handler = new Mock<IHttpHandler>();
handler.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<StringContent>())).Returns(Task.FromResult(response));
handler.Setup(h => h.GetAsync(It.IsAny<string>())).Returns(Task.FromResult(response));
return handler.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,136 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
[TestClass]
public class AkamaiAuthHandler_Test
{
private readonly string clientToken = "testClientToken";
private readonly string accessToken = "testAccessToken";
private readonly string secret = "secret";
private readonly string host = "testuri";
[TestMethod]
public void GetAuthData_Success()
{
string clientToken = "testClientToken";
string accessToken = "testAccessToken";
string secret = "secret";
var timestamp = new DateTime(1918, 11, 11, 11, 00, 00, DateTimeKind.Utc).ToString("yyyyMMdd'T'HH:mm:ss+0000");
var handler = new AkamaiAuthHandler(new HttpClientHandler(), clientToken, accessToken, secret);
string authData = handler.GetAuthorizationData(timestamp);
var reg = $"EG1-HMAC-SHA256 client_token={clientToken};access_token={accessToken};";
reg += "timestamp=19181111T11:00:00\\+0000;nonce=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12};";
Assert.IsTrue(Regex.IsMatch(authData, reg));
}
[TestMethod]
public void GetRequestData_Success()
{
string clientToken = "testClientToken";
string accessToken = "testAccessToken";
string secret = "secret";
var host = "testuri";
var handler = new AkamaiAuthHandler(new HttpClientHandler(), clientToken, accessToken, secret);
using var req = new HttpRequestMessage(HttpMethod.Post, $"https://{host}/ccu/1")
{
Content = new StringContent("TestContent")
};
var data = $"POST\thttps\t{host}\t/ccu/1\t\tuY/AmsDfO7we5eeTFmBPdGL//fCVwcZ248JRd3NkX+k=\t";
string reqData = handler.GetRequestData(req).Result;
Assert.AreEqual(data, reqData);
}
[TestMethod]
public void GetRequestData_NoContent()
{
string clientToken = "testClientToken";
string accessToken = "testAccessToken";
string secret = "secret";
var host = "testuri";
var handler = new AkamaiAuthHandler(new HttpClientHandler(), clientToken, accessToken, secret);
using var req = new HttpRequestMessage(HttpMethod.Post, $"https://{host}/ccu/1");
var data = $"POST\thttps\t{host}\t/ccu/1\t\t\t";
string reqData = handler.GetRequestData(req).Result;
Assert.AreEqual(data, reqData);
}
[TestMethod]
public void GetRequestData_NoPath()
{
var handler = new AkamaiAuthHandler(new HttpClientHandler(), clientToken, accessToken, secret);
using var req = new HttpRequestMessage(HttpMethod.Post, $"https://{host}")
{
Content = new StringContent("TestContent")
};
var data = $"POST\thttps\t{host}\t/\t\tuY/AmsDfO7we5eeTFmBPdGL//fCVwcZ248JRd3NkX+k=\t";
string reqData = handler.GetRequestData(req).Result;
Assert.AreEqual(data, reqData);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void AkamaiAuthHandler_NullHandler()
{
_ = new AkamaiAuthHandler(null, null, null, null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void AkamaiAuthHandler_Tokens()
{
_ = new AkamaiAuthHandler(new HttpClientHandler(), null, null, null);
}
[TestMethod]
[Ignore]
public void SendRequestToAkamai_Success()
{
string clientToken_ = "real client token";
string accessToken_ = "real access token";
string secret_ = "real secret";
string endpoint = "https://fakeEndpoint";
var urls = @"{""objects"": [""https://fakeUri""]}";
var purgeRequest = new StringContent(urls, Encoding.UTF8, "application/json");
using var httpHandler = new HttpHandler(new AkamaiAuthHandler(new HttpClientHandler(), clientToken_, accessToken_, secret_));
var response = httpHandler.PostAsync(endpoint, purgeRequest).Result;
Assert.IsTrue(response.IsSuccessStatusCode);
}
}
}

Просмотреть файл

@ -0,0 +1,133 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
[TestClass]
public class AkamaiPlugin_Test
{
private readonly List<ICdnRequest> OutputQueue = new List<ICdnRequest>();
private readonly AkamaiPlugin plugin = new AkamaiPlugin(Mock.Of<ILogger>(), 500);
[TestMethod]
public void ProcessPartnerRequest_NoNetwork()
{
string json =
@"{
""CDN"": ""Akamai"",
""id"": ""1"",
""Urls"": [""https://fakeUri?pid=1.1""]}";
Assert.IsTrue(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(1, OutputQueue.Count);
Assert.IsTrue(OutputQueue[0].Endpoint.EndsWith("staging"));
}
[TestMethod]
public void ProcessPartnerRequest_InvalidNetworkValue()
{
string json =
@$"{{
""CDN"": ""Akamai"",
""id"": ""1"",
""Network"": ""Fake"",
""Urls"": [""https://fakeUri?pid=1.1""]}}";
Assert.IsTrue(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(1, OutputQueue.Count);
Assert.IsTrue(OutputQueue[0].Endpoint.EndsWith("staging"));
}
[TestMethod]
public void ProcessPartnerRequest_NoUrl()
{
string json =
@$"{{
""CDN"": ""Akamai"",
""id"": ""1""
}}";
Assert.IsFalse(plugin.ValidPartnerRequest(json, "1", out var _));
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_MaxUrl()
{
string json =
@$"{{
""CDN"": ""Akamai"",
""id"": ""1"",
""Network"": ""Production"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.2"",
""https://fakeUri?pid=1.3""]}}";
var plugin2 = new AkamaiPlugin(Mock.Of<ILogger>(), 2);
Assert.IsTrue(plugin2.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin2.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(2, OutputQueue.Count);
Assert.IsTrue(OutputQueue[0].Endpoint.EndsWith("production"));
}
[TestMethod]
public void ProcessPartnerRequest_BatchCreated()
{
string json =
@$"{{
""CDN"": ""Akamai"",
""id"": ""1"",
""Status"": ""BatchCreated""
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.2"",
""https://fakeUri?pid=1.3""]}}";
Assert.IsFalse(plugin.ValidPartnerRequest(json, "1", out var _));
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessPartnerRequest_DoesNotAddDuplicate()
{
string json =
@$"{{
""CDN"": ""Akamai"",
""id"": ""1"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1""]}}";
Assert.IsTrue(plugin.ValidPartnerRequest(json, "1", out var p));
Assert.IsTrue(plugin.ProcessPartnerRequest(p, queue: CreateCollector()));
Assert.AreEqual(1, OutputQueue.Count);
}
private ICollector<ICdnRequest> CreateCollector()
{
var collector = new Mock<ICollector<ICdnRequest>>();
collector.Setup(c => c.Add(It.IsAny<ICdnRequest>())).Callback<ICdnRequest>((s) => OutputQueue.Add(s));
return collector.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,211 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Text.Json;
[TestClass]
public class AkamaiQueueProcessor_Test
{
private readonly List<ICdnRequest> OutputDB = new List<ICdnRequest>();
private readonly AkamaiQueueProcessor processor = new AkamaiQueueProcessor(Mock.Of<ILogger>());
[TestMethod]
public void ProcessPurgeRequest_EmptyQueueMessage()
{
var msg = processor.ProcessPurgeRequest(new AkamaiRequest(), 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPurgeRequest_QueueMessage()
{
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
CdnRequestId = "100",
RequestBody = "testBody",
Urls = new string[]
{
"https://fakeUri?pid=1.1",
}
};
var msg = processor.ProcessPurgeRequest(queueMsg, 5).Result;
Assert.IsNotNull(msg);
Assert.AreEqual(1, queueMsg.NumTimesProcessed);
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void ProcessPurgeRequest_MaxRetry()
{
string json =
@"{
""CDN"": ""AFD"",
""tenantId"": ""1"",
""partnerId"": ""1"",
""Urls"": [""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1"",
""https://fakeUri?pid=1.1""]}";
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 5,
RequestBody = json,
Urls = new string[]
{
"https://fakeUri?pid=1.1",
},
Status = RequestStatus.PurgeSubmitted.ToString()
};
var msg = processor.ProcessPurgeRequest(queueMsg, 5).Result;
Assert.IsNull(msg);
Assert.AreEqual(5, queueMsg.NumTimesProcessed);
Assert.IsTrue(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
Assert.AreEqual(RequestStatus.MaxRetry.ToString(), queueMsg.Status);
}
[TestMethod]
public void ProcessPurgeRequest_SuccessDontCreatePollMsg()
{
var requestId = "12345";
var supportId = "testID";
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1,
Urls = new string[]
{
"https://fakeUri?pid=1.1",
}
};
var requestInfo = new AkamaiRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.PurgeCompleted,
SupportID = supportId
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
Assert.AreEqual(requestId, queueMsg.CdnRequestId);
Assert.AreEqual(supportId, queueMsg.SupportId);
}
[TestMethod]
public void ProcessPurgeRequest_ThrottledIncreaseNumTimesProcessed()
{
var requestId = "12345";
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AkamaiRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Throttled
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AkamaiRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(2, pollMsg.NumTimesProcessed);
Assert.AreEqual(null, pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_ErrorIncreaseNumTimesProcessed()
{
var requestId = "12345";
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AkamaiRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Error
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
var pollMsg = JsonSerializer.Deserialize<AkamaiRequest>(msg.AsString);
Assert.IsNotNull(msg);
Assert.AreEqual(2, pollMsg.NumTimesProcessed);
Assert.AreEqual(null, pollMsg.CdnRequestId);
}
[TestMethod]
public void ProcessPurgeRequest_UnknownUnauthorizedReturnNull()
{
var requestId = "12345";
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1
};
var requestInfo = new AkamaiRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Unauthorized
};
var msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
requestInfo = new AkamaiRequestInfo()
{
RequestID = requestId,
RequestStatus = RequestStatus.Unknown
};
msg = processor.CompletePurgeRequest(requestInfo, queueMsg);
Assert.IsNull(msg);
}
[TestMethod]
public void AddCdnRequestToDB_ThrottledError()
{
var queueMsg = new AkamaiRequest()
{
Endpoint = "http://testendpoint",
NumTimesProcessed = 1,
Status = RequestStatus.Throttled.ToString()
};
Assert.IsFalse(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
queueMsg.Status = RequestStatus.Error.ToString();
Assert.IsFalse(CdnQueueHelper.AddCdnRequestToDB(queueMsg, 5));
}
}
}

Просмотреть файл

@ -0,0 +1,144 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using CachePurgeLibrary;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
[TestClass]
public class AkamaiRequestProcessor_Test
{
private readonly ILogger logger = Mock.Of<ILogger>();
[TestMethod]
public void SendPurgeRequest_ErrorNoEndpoint()
{
var requestInfo = AkamaiRequestProcessor.SendPurgeRequest(string.Empty, null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_ErrorNoContent()
{
var requestInfo = AkamaiRequestProcessor.SendPurgeRequest("https://fakeUri", null, logger).Result;
Assert.AreEqual(RequestStatus.Error, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_SuccessSubmitted()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var purgeId = "12345";
var supportId = "123";
var requestInfo = AkamaiRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(purgeId, supportId), logger).Result;
Assert.AreEqual(RequestStatus.PurgeCompleted, requestInfo.RequestStatus);
Assert.AreEqual(purgeId, requestInfo.RequestID);
Assert.AreEqual(supportId, requestInfo.SupportID);
}
[TestMethod]
public void SendPurgeRequest_UnknownNoRequestID()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var requestInfo = AkamaiRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(), logger).Result;
Assert.AreEqual(RequestStatus.Unknown, requestInfo.RequestStatus);
}
[TestMethod]
public void SendPurgeRequest_Throttled()
{
var purgeBody = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json");
var id = "12345";
var requestInfo = AkamaiRequestProcessor.SendPurgeRequest("https://fakeUri", purgeBody, GetHandler(id, statusCode: HttpStatusCode.TooManyRequests), logger).Result;
Assert.AreEqual(RequestStatus.Throttled, requestInfo.RequestStatus);
}
[TestMethod]
public void GetRequestStatusFromResponse_CorrectRequestID()
{
var response = @"{
""purgeId"": ""2230091"",
""supportId"": ""2230091""
}";
Assert.IsTrue(AkamaiRequestProcessor.GetPropertyValue("purgeId".AsMemory(), response, out var purgeId));
Assert.AreEqual("2230091", purgeId);
Assert.IsTrue(AkamaiRequestProcessor.GetPropertyValue("supportId".AsMemory(), response, out var supportId));
Assert.AreEqual("2230091", supportId);
}
[TestMethod]
public void GetRequestStatusFromResponse_NoRequestIDAndStatus()
{
var response = @"{
""Description"": ""string"",
""Urls"": [
""https://fakeUri?pid=1.1""
],
""CreateTime"": ""0001 -01-01T00:00:00"",
""RequestUser"": ""shishar@microsoft.com"",
""CompleteTime"": null,
""PercentComplete"": 0
}";
Assert.IsFalse(AkamaiRequestProcessor.GetPropertyValue("PurgeId".AsMemory(), response, out _));
Assert.IsFalse(AkamaiRequestProcessor.GetPropertyValue("supportId".AsMemory(), response, out _));
}
[TestMethod]
public void GetRequestStatusFromResponse_IncorrectFormatRequestIDAndStatus()
{
var response = @"{
""purgeId"": 2230091,
""supportId"": 2230091
}";
Assert.IsFalse(AkamaiRequestProcessor.GetPropertyValue("PurgeId".AsMemory(), response, out _));
Assert.IsFalse(AkamaiRequestProcessor.GetPropertyValue("supportId".AsMemory(), response, out _));
}
private static IHttpHandler GetHandler(string purgeId = null, string supportId = null, HttpStatusCode statusCode = HttpStatusCode.OK)
{
var responseText = "{";
if (!string.IsNullOrEmpty(purgeId))
{
responseText += @$"""purgeId"": ""{purgeId}""";
}
if (!string.IsNullOrEmpty(supportId))
{
responseText += @$", ""supportId"": ""{supportId}""";
}
responseText += "}";
var response = new HttpResponseMessage()
{
StatusCode = statusCode,
Content = new StringContent(responseText)
};
var handler = new Mock<IHttpHandler>();
handler.Setup(h => h.PostAsync(It.IsAny<string>(), It.IsAny<StringContent>())).Returns(Task.FromResult(response));
handler.Setup(h => h.GetAsync(It.IsAny<string>())).Returns(Task.FromResult(response));
return handler.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,68 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary_Test
{
using CachePurgeLibrary;
using Microsoft.Azure.Cosmos;
using Moq;
using System.Collections.Generic;
using System.Threading;
public static class CdnLibraryTestHelper
{
public static Container MockCosmosDbContainer<T>(IDictionary<string, T> containerContents) where T: ICosmosDbEntity
{
var responseMock = new Mock<ItemResponse<T>>();
var containerMock = new Mock<Container>();
containerMock
.Setup(c => c.CreateItemAsync(
It.IsAny<T>(),
It.IsAny<PartitionKey?>(),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>())
)
.Callback<T, PartitionKey?, ItemRequestOptions, CancellationToken>((p, x, y, z) => containerContents.Add(p.id, p))
.ReturnsAsync(responseMock.Object);
containerMock
.Setup(c => c.UpsertItemAsync(
It.IsAny<T>(),
It.IsAny<PartitionKey?>(),
It.IsAny<ItemRequestOptions>(),
It.IsAny<CancellationToken>())
)
.Callback<T, PartitionKey?, ItemRequestOptions, CancellationToken>((p, x, y, z) => containerContents[p.id] = p)
.ReturnsAsync(responseMock.Object);
var feedResponseMock = new Mock<FeedResponse<T>>();
feedResponseMock.Setup(x => x.Count).Returns(() => containerContents.Count);
feedResponseMock.Setup(x => x.GetEnumerator()).Returns(() => containerContents.Values.GetEnumerator());
var feedIteratorMock = new Mock<FeedIterator<T>>();
feedIteratorMock
.Setup(f => f.HasMoreResults)
.Returns(true);
feedIteratorMock
.Setup(f => f.ReadNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(feedResponseMock.Object)
.Callback(() => feedIteratorMock
.Setup(f => f.HasMoreResults)
.Returns(false));
containerMock
.Setup(c => c.GetItemQueryIterator<T>(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<QueryRequestOptions>()))
.Returns(feedIteratorMock.Object);
return containerMock.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CachePurgeLibrary\CachePurgeLibrary.csproj" />
<ProjectReference Include="..\src\CdnLibrary.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,209 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnLibrary
{
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
[TestClass]
public class CdnPluginHelper_Test
{
[TestMethod]
public void SplitRequestIntoBatches_MaxNum()
{
var urls = new HashSet<string>()
{
"http://url1",
"http://url2",
"http://url3",
"http://url4"
};
int maxNum = 3;
var result = CdnPluginHelper.SplitUrlListIntoBatches(urls, maxNum);
Assert.AreEqual(2, result.Count);
int i = 1;
foreach (var r in result)
{
var remainingLength = 4 - i + 1; // Since i is not 0 based
var batchSize = remainingLength < maxNum ? remainingLength : maxNum;
Assert.AreEqual(batchSize, r.Length);
foreach (var url in r)
{
Assert.AreEqual($"http://url{i}", url);
i++;
}
}
}
[TestMethod]
public void SplitRequestIntoBatches_LessThanBatchSize()
{
var urls = new HashSet<string>()
{
"http://url1",
"http://url2",
"http://url3",
"http://url4"
};
int maxNum = 8;
var result = CdnPluginHelper.SplitUrlListIntoBatches(urls, maxNum);
Assert.AreEqual(1, result.Count);
int i = 1;
foreach (var r in result)
{
var remainingLength = 4 - i + 1; // Since i is not 0 based
var batchSize = remainingLength < maxNum ? remainingLength : maxNum;
Assert.AreEqual(batchSize, r.Length);
foreach (var url in r)
{
Assert.AreEqual($"http://url{i}", url);
i++;
}
}
}
[TestMethod]
public void SplitRequestIntoBatches_EqualToBatchSize()
{
var urls = new HashSet<string>()
{
"http://url1",
"http://url2",
"http://url3",
"http://url4"
};
int maxNum = 4;
var result = CdnPluginHelper.SplitUrlListIntoBatches(urls, maxNum);
Assert.AreEqual(1, result.Count);
int i = 1;
foreach (var r in result)
{
var remainingLength = 4 - i + 1; // Since i is not 0 based
var batchSize = remainingLength < maxNum ? remainingLength : maxNum;
Assert.AreEqual(batchSize, r.Length);
foreach (var url in r)
{
Assert.AreEqual($"http://url{i}", url);
i++;
}
}
}
[TestMethod]
public void SplitRequestIntoBatches_NoUrl()
{
var urls = new HashSet<string>();
int maxNum = 3;
var result = CdnPluginHelper.SplitUrlListIntoBatches(urls, maxNum);
Assert.AreEqual(0, result.Count);
}
[TestMethod]
public void SplitRequestIntoBatches_ModMaxNum()
{
var urls = new HashSet<string>()
{
"http://url1",
"http://url2",
"http://url3",
"http://url4"
};
int maxNum = 2;
var result = CdnPluginHelper.SplitUrlListIntoBatches(urls, maxNum);
Assert.AreEqual(maxNum, result.Count);
int i = 1;
foreach (var r in result)
{
Assert.AreEqual(maxNum, r.Length);
foreach (var url in r)
{
Assert.AreEqual($"http://url{i}", url);
i++;
}
}
}
[TestMethod]
public void TryComputeHash_Success()
{
var data = Encoding.UTF8.GetBytes("Test Request Value");
var hashString = CdnPluginHelper.ComputeHash(data, "SHA256");
var algorithm = SHA256.Create();
var hash = algorithm.ComputeHash(data);
Assert.AreEqual(Convert.ToBase64String(hash), hashString);
}
[TestMethod]
public void TryComputeHash_InvalidInput()
{
Assert.IsFalse(CdnPluginHelper.TryComputeHash(null, "SHA256", out _));
Assert.IsFalse(CdnPluginHelper.TryComputeHash(new byte[2] { 1, 2 }, string.Empty, out _));
}
[TestMethod]
public void TryComputeKeyedHash_Success()
{
var data = "Test Request Value";
var key = "testkey";
var hashString = CdnPluginHelper.ComputeKeyedHash(data, key, "HMACSHA256");
var algorithm = new HMACSHA256(Encoding.UTF8.GetBytes(key));
var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(data));
Assert.AreEqual(Convert.ToBase64String(hash), hashString);
}
[TestMethod]
public void TryComputeKeyedHash_InvalidInput()
{
Assert.IsFalse(CdnPluginHelper.TryComputeKeyedHash(null, null, "HMACSHA256", out _));
Assert.IsFalse(CdnPluginHelper.TryComputeKeyedHash(Encoding.UTF8.GetBytes("test"), "test", null, out _));
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnPlugin
{
using CdnLibrary;
using System;
using System.Collections.Generic;
public static class TestEnvironmentConfig
{
public static readonly Dictionary<string, string> EnvironmentVariables = new Dictionary<string, string>
{
[EnvironmentConfig.CosmosDBConnection] = "CosmosDBConnection", // 'CacheOutTestCosmosDbAuthKey' - for 'cacheouttestdb' Cosmos DB
[EnvironmentConfig.CosmosDatabaseId] = "CacheOut", // 'CacheOut' - for 'cacheouttestdb' Cosmos DB
// 'userRequestToPartnerRequest' - for 'cacheouttestdb' Cosmos DB, for testing only
// 'AFD_PartnerRequest' - for 'cacheouttestdb' Cosmos DB, for AFD testing
// todo: 'Akamai_CdnRequest' - for 'cacheouttestdb' Cosmos DB, for Akamai testing
[EnvironmentConfig.PartnerCosmosContainerId] = "partners", // 'partners' - for 'cacheouttestdb' Cosmos DB
[EnvironmentConfig.UserRequestCosmosContainerId] = "userRequests", // 'userRequests' - for 'cacheouttestdb' Cosmos DB
};
public static void SetupTestEnvironment()
{
foreach (var (envKey, envValue) in EnvironmentVariables)
{
Environment.SetEnvironmentVariable(envKey, envValue);
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
<UserSecretsId>e2c7f1fa-5d0c-47e5-8441-1f52ec2a4998</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.15.0" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.7" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.2" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.8" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CdnPlugins_Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CdnLibrary\src\CdnLibrary.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,98 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
/*
* Documentation Links:
* CosmosDB Trigger: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-cosmosdb-v2-trigger?tabs=csharp
* Azure Queue Output: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-output?tabs=csharp
*/
namespace CdnPlugin
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CachePurgeLibrary;
using CdnLibrary;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
public class CdnPluginFunctions
{
private readonly IPartnerRequestTableManager<CDN> partnerRequestTablesManager;
public CdnPluginFunctions(IPartnerRequestTableManager<CDN> partnerRequestTablesManager)
{
this.partnerRequestTablesManager = partnerRequestTablesManager;
}
[FunctionName("AkamaiPlugin")]
public async Task PurgeAkamai(
[CosmosDBTrigger(
databaseName: EnvironmentConfig.DatabaseName,
collectionName: EnvironmentConfig.AkamaiPartnerCollectionName,
ConnectionStringSetting = EnvironmentConfig.CosmosDBConnectionStringName,
LeaseCollectionName = "partnerCollectionLeases",
CreateLeaseCollectionIfNotExists = true)]
IReadOnlyList<Document> partnerRequests,
[Queue(
EnvironmentConfig.AkamaiBatchQueueName),
StorageAccount(EnvironmentConfig.BatchQueueConnectionStringName)]
ICollector<ICdnRequest> queue,
ILogger log)
{
log.LogInformation($"{nameof(PurgeAkamai)} ({nameof(CdnPluginFunctions)}) query: {partnerRequests.Count}");
if (partnerRequests == null || partnerRequests.Count <= 0) { throw new ArgumentNullException(nameof(partnerRequests)); }
var plugin = new AkamaiPlugin(log);
foreach (var r in partnerRequests)
{
if (plugin.ValidPartnerRequest(r.ToString(), r.Id, out var partnerRequest) && plugin.ProcessPartnerRequest(partnerRequest, queue))
{
// If cdnRequest creation was successful, update the PartnerRequest in DB with info
// regarding # of batch requests created
await partnerRequestTablesManager.UpdatePartnerRequest(partnerRequest, CDN.Akamai);
}
}
log.LogInformation($"{nameof(PurgeAkamai)} ({nameof(CdnPluginFunctions)}) finished processing");
}
[FunctionName("AfdPlugin")]
public async Task PurgeAfd(
[CosmosDBTrigger(
databaseName: EnvironmentConfig.DatabaseName,
collectionName: EnvironmentConfig.AfdPartnerCollectionName,
ConnectionStringSetting = EnvironmentConfig.CosmosDBConnectionStringName,
LeaseCollectionName = "partnerCollectionLeases",
CreateLeaseCollectionIfNotExists = true)]
IReadOnlyList<Document> partnerRequests,
[Queue(
EnvironmentConfig.AfdBatchQueueName),
StorageAccount(EnvironmentConfig.BatchQueueConnectionStringName)]
ICollector<ICdnRequest> queue,
ILogger log)
{
log.LogInformation($"{nameof(PurgeAfd)} ({nameof(CdnPluginFunctions)}) query: {partnerRequests.Count}");
if (partnerRequests == null || partnerRequests.Count <= 0) { throw new ArgumentNullException(nameof(partnerRequests)); }
var plugin = new AfdPlugin(log);
foreach (var r in partnerRequests)
{
if (plugin.ValidPartnerRequest(r.ToString(), r.Id, out var partnerRequest) && plugin.ProcessPartnerRequest(partnerRequest, queue))
{
// If cdnRequest creation was successful, update the PartnerRequest in DB with info
// regarding # of batch requests created
await partnerRequestTablesManager.UpdatePartnerRequest(partnerRequest, CDN.AFD);
}
}
log.LogInformation($"{nameof(PurgeAfd)} ({nameof(CdnPluginFunctions)}) finished processing");
}
}
}

Просмотреть файл

@ -0,0 +1,163 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
/*
* Documentation Links:
* CosmosDB Trigger: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-cosmosdb-v2-trigger?tabs=csharp
*/
namespace CdnPlugin
{
using CachePurgeLibrary;
using CdnLibrary;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
public class EventCompletionFunctions
{
private static readonly string PurgeCompleted = RequestStatus.PurgeCompleted.ToString();
private static readonly string BatchCreated = RequestStatus.BatchCreated.ToString();
private static readonly string PurgeSubmitted = RequestStatus.PurgeSubmitted.ToString();
private readonly IRequestTable<UserRequest> userRequestTable;
private readonly IPartnerRequestTableManager<CDN> partnerRequestTableManager;
public EventCompletionFunctions(IRequestTable<UserRequest> userRequestTable, IPartnerRequestTableManager<CDN> partnerRequestTableManager)
{
this.userRequestTable = userRequestTable;
this.partnerRequestTableManager = partnerRequestTableManager;
}
internal async Task CompletePurge(ICdnRequest cdnRequest, CDN cdn, ILogger logger)
{
try
{
if (cdnRequest.Status.Equals(PurgeSubmitted))
{
return;
}
var partnerReq = await partnerRequestTableManager.GetPartnerRequest(cdnRequest.PartnerRequestID, cdn);
if (partnerReq != null && UpdatePartnerRequest(partnerReq, cdnRequest.Status, logger))
{
await partnerRequestTableManager.UpdatePartnerRequest(partnerReq, cdn);
await UpdateUserRequest(partnerReq, logger);
}
}
catch(Exception e)
{
var exceptionMsg = e.InnerException?.Message ?? e.Message;
logger.LogError(exceptionMsg);
}
}
[FunctionName("AfdEventCompletion")]
public async Task CompleteAfd(
[CosmosDBTrigger(
databaseName: EnvironmentConfig.DatabaseName,
collectionName: EnvironmentConfig.AfdCdnCollectionName,
ConnectionStringSetting = EnvironmentConfig.CosmosDBConnectionStringName,
LeaseCollectionName = "cdnCollectionLeases",
CreateLeaseCollectionIfNotExists = true)]
IReadOnlyList<Document> afdRequests,
ILogger logger)
{
logger.LogInformation($"{nameof(CompleteAfd)} ({nameof(EventCompletionFunctions)}) starting: {afdRequests.Count} requests");
if (afdRequests == null || afdRequests.Count == 0) { return; }
foreach (var r in afdRequests)
{
var purgeRequest = JsonSerializer.Deserialize<AfdRequest>(r.ToString(), CdnPluginHelper.JsonSerializerOptions);
await CompletePurge(purgeRequest, CDN.AFD, logger);
}
logger.LogInformation($"{nameof(CompleteAfd)} ({nameof(EventCompletionFunctions)}) finished processing");
}
[FunctionName("AkamaiEventCompletion")]
public async Task CompleteAkamai(
[CosmosDBTrigger(
databaseName: EnvironmentConfig.DatabaseName,
collectionName: EnvironmentConfig.AkamaiCdnCollectionName,
ConnectionStringSetting = EnvironmentConfig.CosmosDBConnectionStringName,
LeaseCollectionName = "cdnCollectionLeases",
CreateLeaseCollectionIfNotExists = true)]
IReadOnlyList<Document> akamaiRequests,
ILogger logger)
{
logger.LogInformation($"{nameof(CompleteAkamai)} ({nameof(EventCompletionFunctions)}) starting: {akamaiRequests.Count} requests");
if (akamaiRequests == null || akamaiRequests.Count == 0) { return; }
foreach (var r in akamaiRequests)
{
var purgeRequest = JsonSerializer.Deserialize<AkamaiRequest>(r.ToString(), CdnPluginHelper.JsonSerializerOptions);
await CompletePurge(purgeRequest, CDN.Akamai, logger);
}
logger.LogInformation($"{nameof(CompleteAkamai)} ({nameof(EventCompletionFunctions)}) finished processing");
}
internal bool UpdatePartnerRequest(IPartnerRequest partnerRequest, string cdnRequestStatus, ILogger logger)
{
try
{
partnerRequest.NumCompletedCdnRequests++;
// If error status set the parentRequest status to the same value
if (!cdnRequestStatus.Equals(PurgeCompleted))
{
partnerRequest.Status = cdnRequestStatus;
}
// ParentRequest status is only overwritten if it's got the default value of created
// to prevent overwriting an error status
if (partnerRequest.NumCompletedCdnRequests >= partnerRequest.NumTotalCdnRequests && partnerRequest.Status.Equals(BatchCreated))
{
partnerRequest.Status = cdnRequestStatus;
}
return true;
}
catch (Exception e)
{
logger.LogError(e.Message);
return false;
}
}
internal async Task UpdateUserRequest(IPartnerRequest partnerRequest, ILogger logger)
{
try
{
// if the partner request is complete, update the user request
if (partnerRequest.NumCompletedCdnRequests >= partnerRequest.NumTotalCdnRequests)
{
var userReq = await userRequestTable.GetItem(partnerRequest.UserRequestID);
if (userReq != null && userReq.NumCompletedPartnerRequests < userReq.NumTotalPartnerRequests)
{
userReq.NumCompletedPartnerRequests++;
await userRequestTable.UpsertItem(userReq);
}
}
}
catch (Exception e)
{
logger.LogError(e.Message);
}
}
}
}

Просмотреть файл

@ -0,0 +1,120 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
/*
* Documentation Links:
* Azure Queue Trigger: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-trigger?tabs=csharp
* Azure Queue Output: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-queue-output?tabs=csharp
*/
namespace CdnPlugin
{
using CachePurgeLibrary;
using CdnLibrary;
using Microsoft.Azure.Storage.Queue;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System.Collections;
using System.Text.Json;
using System.Threading.Tasks;
public class QueueProcessorFunctions
{
private static readonly int MaxRetry = EnvironmentConfig.MaxRetry;
private readonly ICdnRequestTableManager<CDN> cdnRequestTableManager;
public QueueProcessorFunctions(ICdnRequestTableManager<CDN> cdnRequestTableManager)
{
this.cdnRequestTableManager = cdnRequestTableManager;
}
[FunctionName("AfdQueueProcessor")]
public async Task ProcessAfd(
[QueueTrigger(EnvironmentConfig.AfdBatchQueueName, Connection = EnvironmentConfig.BatchQueueConnectionStringName)] CloudQueueMessage queueMessage,
[Queue(EnvironmentConfig.AfdBatchQueueName), StorageAccount(EnvironmentConfig.BatchQueueConnectionStringName)] CloudQueue outputQueue,
ILogger logger)
{
logger.LogInformation($"{nameof(ProcessAfd)} ({nameof(QueueProcessorFunctions)}) start: {queueMessage}");
var message = queueMessage.AsString;
if (string.IsNullOrEmpty(message))
{
logger.LogWarning("AfdQueueProcessor: QueueMessage is empty string");
return;
}
var queueMsg = JsonSerializer.Deserialize<AfdRequest>(message);
if (!ValidCdnRequest(queueMsg))
{
logger.LogError("AfdQueueProcessor: Invalid CdnRequest");
return;
}
var queueProcessor = new AfdQueueProcessor(logger);
CloudQueueMessage msg = !string.IsNullOrEmpty(queueMsg.CdnRequestId) ? await queueProcessor.ProcessPollRequest(queueMsg, MaxRetry) : await queueProcessor.ProcessPurgeRequest(queueMsg, MaxRetry);
if (CdnQueueHelper.AddCdnRequestToDB(queueMsg, MaxRetry))
{
await cdnRequestTableManager.UpdateCdnRequest(queueMsg, CDN.AFD);
}
if (msg != null) { queueProcessor.AddMessageToQueue(outputQueue, msg, queueMsg); }
logger.LogInformation($"{nameof(ProcessAfd)} ({nameof(QueueProcessorFunctions)}) finished processing");
}
[FunctionName("AkamaiQueueProcessor")]
public async Task ProcessAkamai(
[QueueTrigger(EnvironmentConfig.AkamaiBatchQueueName, Connection = EnvironmentConfig.BatchQueueConnectionStringName)] CloudQueueMessage queueMessage,
[Queue(EnvironmentConfig.AkamaiBatchQueueName), StorageAccount(EnvironmentConfig.BatchQueueConnectionStringName)] CloudQueue outputQueue,
ILogger logger)
{
logger.LogInformation($"{nameof(ProcessAkamai)} ({nameof(QueueProcessorFunctions)}) start: {queueMessage}");
var message = queueMessage.AsString;
if (string.IsNullOrEmpty(message))
{
logger.LogWarning("AkamaiQueueProcessor: QueueMessage is empty string");
return;
}
var queueMsg = JsonSerializer.Deserialize<AkamaiRequest>(message);
if (!ValidCdnRequest(queueMsg))
{
logger.LogError("AkamaiQueueProcessor: Invalid CdnRequest");
return;
}
if (string.IsNullOrEmpty(queueMsg.CdnRequestId))
{
var queueProcessor = new AkamaiQueueProcessor(logger);
var msg = await queueProcessor.ProcessPurgeRequest(queueMsg, MaxRetry);
if (CdnQueueHelper.AddCdnRequestToDB(queueMsg, MaxRetry))
{
await cdnRequestTableManager.UpdateCdnRequest(queueMsg, CDN.Akamai);
}
if (msg != null) { queueProcessor.AddMessageToQueue(outputQueue, msg, queueMsg); }
}
logger.LogInformation($"{nameof(ProcessAkamai)} ({nameof(AkamaiQueueProcessor)}) finished processing");
}
private bool ValidCdnRequest(ICdnRequest cdnRequest)
{
if (cdnRequest != null && !string.IsNullOrEmpty(cdnRequest.id) && cdnRequest.Urls != null && cdnRequest.Urls.Length > 0 && cdnRequest.RequestBody != null)
{
return true;
}
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
using CachePurgeLibrary;
using CdnLibrary;
using CdnPlugin;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(Startup))]
namespace CdnPlugin
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<ICdnRequestTableManager<CDN>>((s) => { return new CdnRequestTableManager(); });
builder.Services.AddSingleton<IRequestTable<UserRequest>>((s) => { return new UserRequestTable(); });
builder.Services.AddSingleton<IPartnerRequestTableManager<CDN>>((s) => { return new PartnerRequestTableManager(); });
}
}
}

Просмотреть файл

@ -0,0 +1,75 @@
NOTICES AND INFORMATION
Do Not Translate or Localize
This software incorporates material from third parties.
Microsoft makes certain open source code available at https://3rdpartysource.microsoft.com,
or you may send a check or money order for US $5.00, including the product name,
the open source component name, platform, and version number, to:
Source Code Compliance Team
Microsoft Corporation
One Microsoft Way
Redmond, WA 98052
USA
Notwithstanding any other terms, you may reverse engineer this software to the extent
required to debug changes to any libraries licensed under the GNU Lesser General Public License.
---------------------------------------------------------
Antlr4 4.8.0 - BSD-3-Clause
[The "BSD 3-clause license"]
Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=====
MIT License for codepointat.js from https://git.io/codepointat
MIT License for fromcodepoint.js from https://git.io/vDW1m
Copyright Mathias Bynens <https://mathiasbynens.be/>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------------------------------------------------------

20
CdnPlugins/src/host.json Normal file
Просмотреть файл

@ -0,0 +1,20 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"samplingExcludedTypes": "Request",
"isEnabled": true
}
},
"fileLoggingMode": "always",
"console": { "isEnabled": "false" },
"logLevel": {
"default": "Debug"
}
},
"queues": {
"maxPollingInterval": 60000
}
}

Просмотреть файл

@ -0,0 +1,153 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnPlugin
{
using CachePurgeLibrary;
using CdnLibrary;
using CdnLibrary_Test;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
[TestClass]
public class CdnPlugin_Test
{
private readonly IDictionary<string, AfdPartnerRequest> afdPartnerRequest = new Dictionary<string, AfdPartnerRequest>();
private readonly IDictionary<string, AkamaiPartnerRequest> akamaiPartnerRequest = new Dictionary<string, AkamaiPartnerRequest>();
private readonly List<ICdnRequest> OutputDB = new List<ICdnRequest>();
private static readonly string tenantId = "FakeTenant";
private static readonly string partnerId = "FakePartner";
private static readonly string[] urls = new string[] { "https://fakeUrls" };
CdnPluginFunctions cdnPluginFunctions;
[TestInitialize]
public void Setup()
{
var afdPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(afdPartnerRequest);
var akamaiPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(akamaiPartnerRequest);
var partnerRequestTable = new PartnerRequestTableManager(afdPartnerRequestContainer, akamaiPartnerRequestContainer);
cdnPluginFunctions = new CdnPluginFunctions(partnerRequestTable);
}
[TestMethod]
[ExpectedException(typeof(AggregateException))]
public void PurgeAfd_Fail_Empty()
{
IReadOnlyList<Document> doc = new List<Document>();
cdnPluginFunctions.PurgeAfd(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
[ExpectedException(typeof(AggregateException))]
public void PurgeAkamai_Fail_Empty()
{
IReadOnlyList<Document> doc = new List<Document>();
cdnPluginFunctions.PurgeAkamai(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void PurgeAfd_Success()
{
var id = Guid.NewGuid().ToString();
var partnerDoc = new Document();
partnerDoc.SetPropertyValue("CDN", "AFD");
partnerDoc.SetPropertyValue("tenantId", tenantId);
partnerDoc.SetPropertyValue("partnerId", partnerId);
partnerDoc.SetPropertyValue("Urls", urls);
partnerDoc.SetPropertyValue("id", id);
IReadOnlyList<Document> doc = new List<Document>()
{
partnerDoc
};
cdnPluginFunctions.PurgeAfd(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(1, OutputDB.Count);
Assert.AreEqual(id, OutputDB[0].PartnerRequestID);
}
[TestMethod]
public void PurgeAkamai_Success()
{
var id = Guid.NewGuid().ToString();
var partnerDoc = new Document();
partnerDoc.SetPropertyValue("CDN", "Akamai");
partnerDoc.SetPropertyValue("tenantId", tenantId);
partnerDoc.SetPropertyValue("partnerId", partnerId);
partnerDoc.SetPropertyValue("Urls", urls);
partnerDoc.SetPropertyValue("id", id);
IReadOnlyList<Document> doc = new List<Document>()
{
partnerDoc
};
cdnPluginFunctions.PurgeAkamai(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(1, OutputDB.Count);
Assert.AreEqual(id, OutputDB[0].PartnerRequestID);
}
[TestMethod]
public void PurgeAfd_Fail_NoId()
{
var partnerDoc = new Document();
partnerDoc.SetPropertyValue("CDN", "Afd");
partnerDoc.SetPropertyValue("tenantId", tenantId);
partnerDoc.SetPropertyValue("partnerId", partnerId);
partnerDoc.SetPropertyValue("Urls", urls);
IReadOnlyList<Document> doc = new List<Document>()
{
partnerDoc
};
cdnPluginFunctions.PurgeAfd(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputDB.Count);
}
[TestMethod]
public void PurgeAkamai_Fail_NoId()
{
var partnerDoc = new Document();
partnerDoc.SetPropertyValue("CDN", "Akamai");
partnerDoc.SetPropertyValue("tenantId", tenantId);
partnerDoc.SetPropertyValue("partnerId", partnerId);
partnerDoc.SetPropertyValue("Urls", urls);
IReadOnlyList<Document> doc = new List<Document>()
{
partnerDoc
};
cdnPluginFunctions.PurgeAkamai(doc, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputDB.Count);
}
private ICollector<ICdnRequest> CreateCollector()
{
var collector = new Mock<ICollector<ICdnRequest>>();
collector.Setup(c => c.Add(It.IsAny<ICdnRequest>())).Callback<ICdnRequest>((s) => OutputDB.Add(s));
return collector.Object;
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CdnLibrary\src\CdnLibrary.csproj" />
<ProjectReference Include="..\..\CdnLibrary\tests\CdnLibrary_Test.csproj" />
<ProjectReference Include="..\src\CdnPlugins.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,109 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnPlugin
{
using CachePurgeLibrary;
using CdnLibrary;
using CdnLibrary_Test;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
[TestClass]
public class EventCompletion_Test
{
private readonly ILogger logger = Mock.Of<ILogger>();
private static readonly string PurgeCompleted = RequestStatus.PurgeCompleted.ToString();
private readonly IDictionary<string, AfdPartnerRequest> afdPartnerRequest = new Dictionary<string, AfdPartnerRequest>();
private readonly IDictionary<string, AkamaiPartnerRequest> akamaiPartnerRequest = new Dictionary<string, AkamaiPartnerRequest>();
EventCompletionFunctions completionFunctions;
[TestInitialize]
public void Setup()
{
var afdPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(afdPartnerRequest);
var akamaiPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(akamaiPartnerRequest);
var partnerRequestTable = new PartnerRequestTableManager(afdPartnerRequestContainer, akamaiPartnerRequestContainer);
completionFunctions = new EventCompletionFunctions(null, partnerRequestTable);
}
[TestMethod]
public void UpdatePartnerRequest_Success()
{
var partnerRequest = new PartnerRequest()
{
NumCompletedCdnRequests = 0,
NumTotalCdnRequests = 1,
Status = "BatchCreated"
};
Assert.IsTrue(completionFunctions.UpdatePartnerRequest(partnerRequest, PurgeCompleted, logger));
Assert.AreEqual(1, partnerRequest.NumCompletedCdnRequests);
Assert.AreEqual(PurgeCompleted, partnerRequest.Status);
}
[TestMethod]
public void UpdatePartnerRequest_ErrorNoStatus()
{
var partnerRequest = new PartnerRequest()
{
NumCompletedCdnRequests = 0,
NumTotalCdnRequests = 1
};
Assert.IsFalse(completionFunctions.UpdatePartnerRequest(partnerRequest, PurgeCompleted, logger));
}
[TestMethod]
public void UpdatePartnerRequest_ErrorStatusPreserved()
{
var partnerRequest = new PartnerRequest()
{
NumCompletedCdnRequests = 0,
NumTotalCdnRequests = 1,
Status = "Error"
};
Assert.IsTrue(completionFunctions.UpdatePartnerRequest(partnerRequest, "Error", logger));
Assert.AreEqual(1, partnerRequest.NumCompletedCdnRequests);
Assert.AreEqual("Error", partnerRequest.Status);
}
[TestMethod]
public void UpdatePartnerRequest_NotComplete()
{
var partnerRequest = new PartnerRequest()
{
NumCompletedCdnRequests = 0,
NumTotalCdnRequests = 2,
Status = "BatchCreated"
};
Assert.IsTrue(completionFunctions.UpdatePartnerRequest(partnerRequest, PurgeCompleted, logger));
Assert.AreEqual(1, partnerRequest.NumCompletedCdnRequests);
Assert.AreEqual("BatchCreated", partnerRequest.Status);
}
[TestMethod]
public void UpdatePartnerRequest_CdnRequestError()
{
var partnerRequest = new PartnerRequest()
{
NumCompletedCdnRequests = 0,
NumTotalCdnRequests = 2,
Status = "Error"
};
Assert.IsTrue(completionFunctions.UpdatePartnerRequest(partnerRequest, "Error", logger));
Assert.AreEqual(1, partnerRequest.NumCompletedCdnRequests);
Assert.AreEqual("Error", partnerRequest.Status);
}
}
}

Просмотреть файл

@ -0,0 +1,133 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace CdnPlugin
{
using CdnLibrary;
using CdnLibrary_Test;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Queue;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
using System.Text.Json;
[TestClass]
public class QueueProcessor_Test
{
private readonly IDictionary<string, AfdRequest> afdRequest = new Dictionary<string, AfdRequest>();
private readonly IDictionary<string, AkamaiRequest> akamaiRequest = new Dictionary<string, AkamaiRequest>();
private readonly List<CloudQueueMessage> OutputQueue = new List<CloudQueueMessage>();
private static readonly string[] urls = new string[] { "https://fakeUrls" };
private QueueProcessorFunctions queueFunctions;
[TestInitialize]
public void Setup()
{
var afdRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(afdRequest);
var akamaiRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(akamaiRequest);
var cdnRequestTableManager = new CdnRequestTableManager(afdRequestContainer, akamaiRequestContainer);
queueFunctions = new QueueProcessorFunctions(cdnRequestTableManager);
}
[TestMethod]
public void ProcessAfd_EmptyRequest()
{
var request = new AfdRequest();
var cloudQueueMsg= new CloudQueueMessage(JsonSerializer.Serialize(request));
queueFunctions.ProcessAfd(cloudQueueMsg, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessAfd_Success()
{
var afdRequestBody = new AfdRequestBody()
{
Urls = urls,
Description = "Test"
};
var request = new AfdRequest()
{
id = "1",
Urls = urls,
RequestBody = JsonSerializer.Serialize(afdRequestBody, CdnPluginHelper.JsonSerializerOptions),
Endpoint = "testendpoint"
};
var cloudQueueMsg = new CloudQueueMessage(JsonSerializer.Serialize(request));
queueFunctions.ProcessAfd(cloudQueueMsg, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(1, OutputQueue.Count);
}
[TestMethod]
public void ProcessAkamai_Fail()
{
var request = new AkamaiRequest();
var cloudQueueMsg = new CloudQueueMessage(JsonSerializer.Serialize(request));
queueFunctions.ProcessAkamai(cloudQueueMsg, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(0, OutputQueue.Count);
}
[TestMethod]
public void ProcessAkamai_Success()
{
var afdRequestBody = new AkamaiRequestBody()
{
Hostname = "testhostname",
Objects=urls
};
var request = new AkamaiRequest()
{
id = "1",
Urls = urls,
RequestBody = JsonSerializer.Serialize(afdRequestBody, CdnPluginHelper.JsonSerializerOptions),
Endpoint = "testendpoint"
};
var cloudQueueMsg = new CloudQueueMessage(JsonSerializer.Serialize(request));
queueFunctions.ProcessAfd(cloudQueueMsg, CreateCollector(), Mock.Of<ILogger>()).Wait();
Assert.AreEqual(1, OutputQueue.Count);
}
private CloudQueue CreateCollector()
{
var collector = new Mock<TestCloudQueue>();
collector.Setup(c => c.AddMessage(
It.IsAny<CloudQueueMessage>(),
It.IsAny<TimeSpan?>(),
It.IsAny<TimeSpan?>(),
It.IsAny<QueueRequestOptions>(),
It.IsAny<OperationContext>())).Callback<CloudQueueMessage, TimeSpan?, TimeSpan?, QueueRequestOptions, OperationContext>((s, a, b, c, d) => OutputQueue.Add(s));
return collector.Object;
}
}
public class TestCloudQueue : CloudQueue
{
public TestCloudQueue() : base(new Uri("http://fakeUri")) { }
}
}

Двоичные данные
InitialDesign.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 36 KiB

23
LICENSE.txt Normal file
Просмотреть файл

@ -0,0 +1,23 @@
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,111 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using CachePurgeLibrary;
using CdnLibrary;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
public class CacheFunctions
{
// todo: handle exceptions and logging across all Azure Functions
private readonly IRequestTable<Partner> partnerTable;
private readonly IRequestTable<UserRequest> userRequestTable;
private readonly IPartnerRequestTableManager<CDN> partnerRequestTable;
public CacheFunctions(IRequestTable<Partner> partnerTable,
IRequestTable<UserRequest> userRequestTable,
IPartnerRequestTableManager<CDN> partnerRequestTable)
{
this.partnerTable = partnerTable;
this.partnerRequestTable = partnerRequestTable;
this.userRequestTable = userRequestTable;
}
[FunctionName("CreateCachePurgeRequestByHostname")]
public async Task<IActionResult> CreateCachePurgeRequestByHostname(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "{partnerId:guid}/CachePurgeByHostname")]
HttpRequest req,
string partnerId,
ILogger log)
{
log.LogInformation($"{nameof(CreateCachePurgeRequestByHostname)}");
try
{
if (partnerId == null)
{
return new StringResult("Invalid Partner Id");
}
var bodyContent = await new StreamReader(req.Body).ReadToEndAsync();
var purgeRequest = JsonSerializer.Deserialize<PurgeRequest>(bodyContent);
var urls = new HashSet<string>(purgeRequest.Urls);
var description = purgeRequest.Description;
var ticketId = purgeRequest.TicketId;
var hostname = purgeRequest.Hostname;
log.LogInformation($"{nameof(CreateCachePurgeRequestByHostname)}: purging {urls.Count} urls for partner {partnerId}");
var partner = await partnerTable.GetItem(partnerId);
var userRequest = new UserRequest(partner.id, description, ticketId, hostname, urls);
await userRequestTable.CreateItem(userRequest);
var userRequestId = userRequest.id;
foreach (var partnerCdnConfiguration in partner.CdnConfigurations)
{
var cdnWithCredentials = partnerCdnConfiguration.CdnWithCredentials;
foreach (var cdnWithCredential in cdnWithCredentials)
{
var cdn = Enum.Parse<CDN>(cdnWithCredential.Key);
var partnerRequest = CdnRequestHelper.CreatePartnerRequest(cdn, partner, userRequest, description, ticketId);
await partnerRequestTable.CreatePartnerRequest(partnerRequest, cdn);
userRequest.NumTotalPartnerRequests++;
}
}
await userRequestTable.UpsertItem(userRequest);
return new StringResult(userRequestId);
}
catch (Exception e)
{
return new ExceptionResult(e);
}
}
[FunctionName("CachePurgeRequestByHostnameStatus")]
public async Task<IActionResult> CachePurgeRequestByHostnameStatus(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "{partnerId}/CachePurgeStatus/{userRequestId}")]
HttpRequest req,
string partnerId,
string userRequestId,
ILogger log)
{
log.LogInformation($"{nameof(CachePurgeRequestByHostnameStatus)}: {userRequestId} (partnerId={partnerId})");
try
{
var userRequest = await userRequestTable.GetItem(userRequestId);
return new UserRequestStatusResult(userRequest);
}
catch (Exception e)
{
log.LogInformation($"{nameof(CachePurgeRequestByHostnameStatus)}: got exception {e.Message}; {e.StackTrace}");
return new ExceptionResult(e);
}
}
}
}

Просмотреть файл

@ -0,0 +1,97 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using CachePurgeLibrary;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
public class PartnerFunctions
{
private const string PartnerIdParameter = "partnerId";
private readonly IRequestTable<Partner> partnerTable;
public PartnerFunctions(IRequestTable<Partner> partnerTable)
{
this.partnerTable = partnerTable;
}
[FunctionName("GetPartner")]
public async Task<IActionResult> GetPartner(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "partners/{partnerId:guid}")]
HttpRequest req,
ILogger log)
{
try
{
if (req.Query.TryGetValue(PartnerIdParameter, out var id))
{
var partner = await partnerTable.GetItem(id);
return new PartnerResult(partner);
}
return new StringResult("Please pass in partnerId query parameter");
}
catch (Exception e)
{
return new ExceptionResult(e);
}
}
[FunctionName("CreatePartner")]
public async Task<IActionResult> CreatePartner(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "partners")]
HttpRequest req,
ILogger log)
{
try
{
var requestContent = await new StreamReader(req.Body).ReadToEndAsync();
var createPartnerRequest = JsonSerializer.Deserialize<PartnerConfigRequest>(requestContent);
log.LogInformation($"{nameof(CreatePartner)}: {createPartnerRequest}");
var tenant = createPartnerRequest.Tenant;
var name = createPartnerRequest.Name;
var contactEmail = createPartnerRequest.ContactEmail;
var notifyContactEmail = createPartnerRequest.NotifyContactEmail;
var cdnConfiguration = createPartnerRequest.CdnConfiguration;
var partner = new Partner(tenant, name, contactEmail, notifyContactEmail, new[] { cdnConfiguration });
await partnerTable.CreateItem(partner);
return new StringResult(partner.id);
}
catch (Exception e)
{
return new ExceptionResult(e);
}
}
[FunctionName("ListPartners")]
public async Task<IActionResult> ListPartners(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "partners")]
HttpRequest req,
ILogger log)
{
try
{
var partners = await partnerTable.GetItems();
return new EnumerableResult<PartnerResult>(partners.Select(p => new PartnerResult(p)).ToList());
}
catch (Exception e)
{
return new ExceptionResult(e);
}
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using MultiCdnApi;
using CachePurgeLibrary;
using CdnLibrary;
[assembly: FunctionsStartup(typeof(Startup))]
namespace MultiCdnApi
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IRequestTable<Partner>>((s) => { return new PartnerTable(); });
builder.Services.AddSingleton<IRequestTable<UserRequest>>((s) => { return new UserRequestTable(); });
builder.Services.AddSingleton<IPartnerRequestTableManager<CDN>>((s) => { return new PartnerRequestTableManager(); });
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.15.0" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CdnLibrary\src\CdnLibrary.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,32 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using CachePurgeLibrary;
public class PartnerConfigRequest
{
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string Tenant { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string Name { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string ContactEmail { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string NotifyContactEmail { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public CdnConfiguration CdnConfiguration { get; set; }
public override string ToString()
{
return $"{nameof(Tenant)}: {Tenant}, " +
$"{nameof(Name)}: {Name}, " +
$"{nameof(ContactEmail)}: {ContactEmail}, " +
$"{nameof(NotifyContactEmail)}: {NotifyContactEmail}, " +
$"{nameof(CdnConfiguration)}: {CdnConfiguration}";
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System.Collections.Generic;
internal class PurgeRequest
{
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public IEnumerable<string> Urls { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string Description { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string TicketId { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global - used in JSON serialization/deserialization
public string Hostname { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,15 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
public class EnumerableResult<T>: JsonResult where T : JsonResult
{
public EnumerableResult(IEnumerable<T> values) : base(values) {}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System;
using Microsoft.AspNetCore.Mvc;
public class ExceptionResult: JsonResult
{
public ExceptionResult(Exception exception) : base(exception) {}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System.Collections.Generic;
using CachePurgeLibrary;
using Microsoft.AspNetCore.Mvc;
public class PartnerResult : JsonResult
{
public PartnerResult(Partner partner) : base(new object())
{
var cdnConfigurationResults = new List<CdnConfigurationValue>();
var partnerCdnConfigurations = partner.CdnConfigurations;
foreach (var cdnConfiguration in partnerCdnConfigurations)
{
var credentials = new Dictionary<string, string>();
foreach (var cdnConfigurationCredentialKey in cdnConfiguration.CdnWithCredentials.Keys)
{
credentials[cdnConfigurationCredentialKey] = string.Empty;
}
cdnConfigurationResults.Add(new CdnConfigurationValue
{
Hostname = cdnConfiguration.Hostname,
CdnCredentials = credentials
});
}
Value = new PartnerValue
{
Id = partner.id,
TenantId = partner.TenantId,
Name = partner.Name,
ContactEmail = partner.ContactEmail,
NotifyContactEmail = partner.NotifyContactEmail,
CdnConfigurations = cdnConfigurationResults
};
}
}
public class PartnerValue
{
public string Id { get; set; }
public string TenantId { get; set; }
public string Name { get; set; }
public string ContactEmail { get; set; }
public string NotifyContactEmail { get; set; }
public List<CdnConfigurationValue> CdnConfigurations { get; set; }
}
public class CdnConfigurationValue
{
public string Hostname { get; set; }
public IDictionary<string, string> CdnCredentials { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using Microsoft.AspNetCore.Mvc;
public class StringResult: JsonResult
{
public StringResult(string str) : base(str) {}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using CachePurgeLibrary;
using Microsoft.AspNetCore.Mvc;
public class UserRequestStatusResult : JsonResult
{
public UserRequestStatusResult(UserRequest userRequest) : base(new object())
{
Value = new UserRequestStatusValue
{
Id = userRequest.id,
NumCompletedPartnerRequests = userRequest.NumCompletedPartnerRequests,
NumTotalPartnerRequests = userRequest.NumTotalPartnerRequests
};
}
}
public class UserRequestStatusValue
{
public string Id { get; set; }
public int NumCompletedPartnerRequests { get; set; }
public int NumTotalPartnerRequests { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using CachePurgeLibrary;
using CdnLibrary;
using Microsoft.Azure.Cosmos;
using System.Collections.Generic;
using System.Threading.Tasks;
public class PartnerTable : CosmosDbEntityClient, IRequestTable<Partner>
{
private static readonly string ContainerId = EnvironmentConfig.PartnerCosmosContainerId;
public PartnerTable() : base(EnvironmentConfig.CosmosDBConnectionString, EnvironmentConfig.CosmosDatabaseId,
EnvironmentConfig.PartnerCosmosContainerId, "id") { }
public PartnerTable(Container container) : base(container) { }
public async Task<IEnumerable<Partner>> GetItems()
{
if (Container == null)
{
await CreateContainer();
}
using var queryIterator = Container.GetItemQueryIterator<Partner>(
$"SELECT * FROM {ContainerId} c");
var partners = new List<Partner>();
while (queryIterator.HasMoreResults)
{
var feedResponse = await queryIterator.ReadNextAsync();
foreach (var partner in feedResponse)
{
partners.Add(partner);
}
}
return partners;
}
public async Task CreateItem(Partner request)
{
await Create(request);
}
public async Task UpsertItem(Partner request)
{
await Upsert(request);
}
public async Task<Partner> GetItem(string id)
{
return await SelectFirstByIdAsync<Partner>(id);
}
}
}

11
MultiCdnApi/src/host.json Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"samplingExcludedTypes": "Request",
"isEnabled": true
}
}
}
}

Просмотреть файл

@ -0,0 +1,165 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CachePurgeLibrary;
using CdnLibrary;
using CdnLibrary_Test;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
[TestClass]
public class CacheFunctions_Test
{
private CacheFunctions cacheFunctions;
private IPartnerRequestTableManager<CDN> partnerRequestTable;
private PartnerTable partnerTable;
private IRequestTable<UserRequest> userRequestTable;
private string testPartnerId;
private const string TestDescription = "Test description";
private const string TestTicketId = "Test ticket id";
private const string TestHostname = "https://test.hostname.com";
private const string TestPartnerId = "FakePartner";
private const string TestTenantId = "FakeTenant";
private readonly IDictionary<string, UserRequest> userRequestDict = new Dictionary<string, UserRequest>();
private readonly IDictionary<string, AfdPartnerRequest> afdPartnerRequest = new Dictionary<string, AfdPartnerRequest>();
private readonly IDictionary<string, AkamaiPartnerRequest> akamaiPartnerRequest = new Dictionary<string, AkamaiPartnerRequest>();
[TestInitialize]
public void Setup()
{
partnerTable = new PartnerTable(CdnLibraryTestHelper.MockCosmosDbContainer(new Dictionary<string, Partner>()));
userRequestTable = new UserRequestTable(CdnLibraryTestHelper.MockCosmosDbContainer(userRequestDict));
var afdPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(afdPartnerRequest);
var akamaiPartnerRequestContainer = CdnLibraryTestHelper.MockCosmosDbContainer(akamaiPartnerRequest);
partnerRequestTable = new PartnerRequestTableManager(afdPartnerRequestContainer, akamaiPartnerRequestContainer);
cacheFunctions = new CacheFunctions(partnerTable, userRequestTable, partnerRequestTable);
const string testTenantName = TestTenantId;
const string testPartnerName = TestPartnerId;
const string testContactEmail = "testDri@n/a.com";
const string testNotifyContactEmail = "testNotify@n/a.com";
const string rawCdnConfiguration = "{\"Hostname\": \"\", \"CdnWithCredentials\": {\"AFD\":\"\", \"Akamai\":\"\"}}";
var partner = new Partner(testTenantName, testPartnerName, testContactEmail, testNotifyContactEmail, new[] { new CdnConfiguration(rawCdnConfiguration) });
partnerTable.CreateItem(partner).Wait();
testPartnerId = partner.id;
}
[TestMethod]
public async Task CreateCachePurgeRequestByHostname()
{
var userRequestId = await CallPurgeFunctionWithDefaultParameters();
var savedUserRequest = await userRequestTable.GetItem(userRequestId);
var partnerRequest = await partnerRequestTable.GetPartnerRequest(savedUserRequest.id, CDN.AFD);
AssertIsTestRequest(partnerRequest);
}
[TestMethod]
public async Task CreateCachePurgeRequestByHostname_Fail()
{
var defaultHttpRequest = new DefaultHttpRequest(new DefaultHttpContext())
{
Body = new MemoryStream(Encoding.UTF8.GetBytes(TestHostname))
};
var result = await cacheFunctions.CreateCachePurgeRequestByHostname(
defaultHttpRequest,
null,
Mock.Of<ILogger>());
Assert.IsTrue(result is JsonResult);
}
[TestMethod]
public async Task CreateCachePurgeRequestByHostname_CosmosDbSerialization()
{
_ = await CallPurgeFunctionWithDefaultParameters();
var partnerRequest = afdPartnerRequest.First();
AssertIsTestRequest(partnerRequest.Value);
}
[TestMethod]
public async Task TestCachePurgeStatus()
{
var userRequestId = await CallPurgeFunctionWithDefaultParameters();
var userRequestStatusResult = await CallPurgeStatus(userRequestId);
Assert.AreEqual(typeof(UserRequestStatusValue), userRequestStatusResult.Value.GetType());
var userRequestStatusValue = (UserRequestStatusValue) userRequestStatusResult.Value;
Assert.AreEqual(userRequestId, userRequestStatusValue.Id);
Assert.AreEqual(2, userRequestStatusValue.NumTotalPartnerRequests); // we have 2 plugins
Assert.AreEqual(0, userRequestStatusValue.NumCompletedPartnerRequests); // 0 because it is not initialized in plugins yet
}
private static void AssertIsTestRequest(IPartnerRequest partnerRequest)
{
var afdPartnerRequest = partnerRequest as AfdPartnerRequest;
Assert.IsNotNull(afdPartnerRequest);
Assert.AreEqual($"{TestDescription} ({TestTicketId})", afdPartnerRequest.Description);
Assert.AreEqual(1, partnerRequest.Urls.Count);
Assert.IsTrue(partnerRequest.Urls.Contains(TestHostname));
Assert.AreEqual(CDN.AFD.ToString(), partnerRequest.CDN);
Assert.AreEqual(TestTenantId, afdPartnerRequest.TenantID);
Assert.AreEqual(TestPartnerId, afdPartnerRequest.PartnerID);
Assert.AreEqual(partnerRequest.UserRequestID, partnerRequest.UserRequestID);
}
private async Task<string> CallPurgeFunctionWithDefaultParameters()
{
var defaultHttpRequest = new DefaultHttpRequest(new DefaultHttpContext())
{
Body = new MemoryStream(Encoding.UTF8.GetBytes("{" +
$@"""Description"": ""{TestDescription}""," +
$@"""TicketId"": ""{TestTicketId}""," +
$@"""Hostname"": ""{TestHostname}""," +
$@"""Urls"": [""{TestHostname}""]" +
"}"))
};
var result = await cacheFunctions.CreateCachePurgeRequestByHostname(
defaultHttpRequest,
testPartnerId,
Mock.Of<ILogger>());
Assert.AreEqual(typeof(StringResult), result.GetType());
Assert.IsTrue(((StringResult) result).Value is string);
return (string) ((StringResult) result).Value;
}
private async Task<UserRequestStatusResult> CallPurgeStatus(string userRequestId)
{
var defaultHttpRequest = new DefaultHttpRequest(new DefaultHttpContext());
var statusResponse = await cacheFunctions.CachePurgeRequestByHostnameStatus(
defaultHttpRequest,
testPartnerId,
userRequestId,
Mock.Of<ILogger>());
Assert.AreEqual(typeof(UserRequestStatusResult), statusResponse.GetType());
var userRequestStatusResult = (UserRequestStatusResult) statusResponse;
return userRequestStatusResult;
}
[TestCleanup]
public void Teardown()
{
partnerRequestTable.Dispose();
partnerTable.Dispose();
userRequestTable.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,170 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using CachePurgeLibrary;
using CdnLibrary;
using CdnLibrary_Test;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
[TestClass]
public class PartnersFunctions_Test
{
private PartnerFunctions partnerFunctions;
private IRequestTable<Partner> partnerTable;
private const string TenantId = "FakeTenant";
private const string Name = "FakePartner";
private const string DriContact = "driContact@example.test";
private const string NotifyContact = "notifyContact@example.test";
private const string TestHostname = "test_hostname";
private readonly Dictionary<string, Partner> partners = new Dictionary<string, Partner>();
[TestInitialize]
public void Setup()
{
partnerTable = new PartnerTable(CdnLibraryTestHelper.MockCosmosDbContainer(partners));
partnerFunctions = new PartnerFunctions(partnerTable);
}
[TestMethod]
public void TestCreatePartner_Success()
{
var testPartnerResponse = CreateTestPartner();
Assert.AreEqual(typeof(StringResult), testPartnerResponse.GetType());
var partnerGuid = ((StringResult) testPartnerResponse).Value.ToString();
Assert.IsTrue(partners.Count > 0);
var partner = partnerTable.GetItem(partnerGuid).Result;
TestPartner(partner);
}
[TestMethod]
public void TestCreatePartner_Fail()
{
var createPartnerResponse = partnerFunctions.CreatePartner(new DefaultHttpRequest(new DefaultHttpContext())
{
Query = new QueryCollection(new Dictionary<string, StringValues>
{
["Test"] = "Bad"
}
)}, null).Result;
Assert.AreEqual(typeof(ExceptionResult), createPartnerResponse.GetType());
}
private static void TestPartner(Partner partner)
{
Assert.AreEqual(TenantId, partner.TenantId);
Assert.AreEqual(Name, partner.Name);
Assert.AreEqual(DriContact, partner.ContactEmail);
Assert.AreEqual(NotifyContact, partner.NotifyContactEmail);
var partnerCdnConfigurations = partner.CdnConfigurations.ToList();
Assert.AreEqual(1, partnerCdnConfigurations.Count);
Assert.AreEqual(TestHostname, partnerCdnConfigurations[0].Hostname);
var cdnWithCredentials = partnerCdnConfigurations[0].CdnWithCredentials;
Assert.AreEqual(2, cdnWithCredentials.Count);
Assert.AreEqual("", cdnWithCredentials[CDN.AFD.ToString()]);
Assert.AreEqual("", cdnWithCredentials[CDN.Akamai.ToString()]);
}
private static void TestPartnerSerialization(JsonResult partner)
{
Assert.AreEqual(typeof(PartnerValue), partner.Value.GetType());
var partnerValue = (PartnerValue) partner.Value;
Assert.AreEqual(TenantId, partnerValue.TenantId);
Assert.AreEqual(Name, partnerValue.Name);
Assert.AreEqual(DriContact, partnerValue.ContactEmail);
Assert.AreEqual(NotifyContact, partnerValue.NotifyContactEmail);
var partnerCdnConfigurations = partnerValue.CdnConfigurations.ToList();
Assert.AreEqual(1, partnerCdnConfigurations.Count);
Assert.AreEqual(TestHostname, partnerCdnConfigurations[0].Hostname);
var cdnWithCredentials = partnerCdnConfigurations[0].CdnCredentials;
Assert.AreEqual(2, cdnWithCredentials.Count);
Assert.AreEqual("", cdnWithCredentials[CDN.AFD.ToString()]);
Assert.AreEqual("", cdnWithCredentials[CDN.Akamai.ToString()]);
}
[TestMethod]
public void TestGetPartner_Success()
{
var testPartnerResponse = CreateTestPartner();
Assert.AreEqual(typeof(StringResult), testPartnerResponse.GetType());
var partnerId = ((StringResult) testPartnerResponse).Value.ToString();
var partnerResult = partnerFunctions.GetPartner(new DefaultHttpRequest(new DefaultHttpContext())
{
Query = new QueryCollection(new Dictionary<string, StringValues>
{
["partnerId"] = partnerId
})
}, null).Result;
Assert.AreEqual(typeof(PartnerResult), partnerResult.GetType());
var partner = (PartnerResult) partnerResult;
TestPartnerSerialization(partner);
}
[TestMethod]
public void TestGetPartner_Fail()
{
var partnerResult = partnerFunctions.GetPartner(new DefaultHttpRequest(new DefaultHttpContext())
{
Query = new QueryCollection()
}, null).Result;
Assert.AreEqual(typeof(StringResult), partnerResult.GetType());
}
[TestMethod]
public void TestListPartners()
{
this.partners.Clear();
CreateTestPartner();
var partnersResponse =
partnerFunctions.ListPartners(new DefaultHttpRequest(new DefaultHttpContext()), null).Result;
Assert.AreEqual(typeof(EnumerableResult<PartnerResult>), partnersResponse.GetType());
var partnersValue = ((EnumerableResult<PartnerResult>) partnersResponse).Value;
var retrievedPartners = partnersValue as IEnumerable<PartnerResult>;
Assert.IsNotNull(retrievedPartners);
var retrievedPartnersList = retrievedPartners.ToList();
Assert.AreEqual(1, retrievedPartnersList.Count);
TestPartnerSerialization(retrievedPartnersList.First());
}
private IActionResult CreateTestPartner()
{
var createPartnerResponse = partnerFunctions.CreatePartner(new DefaultHttpRequest(new DefaultHttpContext())
{
Body = new MemoryStream(Encoding.UTF8.GetBytes("{" +
$@"""Tenant"": ""{TenantId}""," +
$@"""Name"": ""{Name}""," +
$@"""ContactEmail"": ""{DriContact}""," +
$@"""NotifyContactEmail"": ""{NotifyContact}""," +
$@"""CdnConfiguration"": {{""Hostname"": ""{TestHostname}"", ""CdnWithCredentials"": {{""AFD"":"""", ""Akamai"":""""}}}}" +
"}"))
}, Mock.Of<ILogger>());
return createPartnerResponse.Result;
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
/* -----------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
* ----------------------------------------------------------------------- */
namespace MultiCdnApi
{
using System;
using System.Linq;
using CachePurgeLibrary;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
public class FunctionsStartup_Test
{
private IFunctionsHostBuilder functionsHostBuilder;
[SetUp]
public void Setup()
{
var multiCdnFunctionsStartup = new Startup();
functionsHostBuilder = Mock.Of<IFunctionsHostBuilder>(b => b.Services == new ServiceCollection());
multiCdnFunctionsStartup.Configure(functionsHostBuilder);
}
[Test]
public void TestSetupPartnerTable()
{
var partnerTableService = FindServiceByType(functionsHostBuilder, typeof(IRequestTable<Partner>));
Assert.IsNotNull(partnerTableService);
}
[Test]
public void TestSetupUserRequestTable()
{
var userRequestTable = FindServiceByType(functionsHostBuilder, typeof(IRequestTable<UserRequest>));
Assert.IsNotNull(userRequestTable);
}
private static ServiceDescriptor FindServiceByType(IFunctionsHostBuilder functionsHostBuilder, Type type)
{
return functionsHostBuilder.Services.First(t => t.ServiceType == type);
}
}
}

Просмотреть файл

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\CdnLibrary\src\CdnLibrary.csproj" />
<ProjectReference Include="..\..\CdnLibrary\tests\CdnLibrary_Test.csproj" />
<ProjectReference Include="..\src\MultiCdnApi.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,33 +1,32 @@
# Project
> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
# CachePurge: Cache Invalidation and Purge for multiple CDNs
CachePurge is a centralized service that allows the user to invalidate and purge cache from multiple CDNs easily. CacheOut is implemented in C# using the dotnet core framework
As the maintainer of this project, please make a few updates:
## Introduction
CachePurge uses a combination of [Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/), [CosmosDB](https://docs.microsoft.com/en-us/azure/cosmos-db/) and [Azure Queues](https://docs.microsoft.com/en-us/azure/storage/queues/storage-queues-introduction) to create a workflow that begins upon receiving a purge request from the user.
- Improving this README.MD file to provide a great experience
- Updating SUPPORT.MD with content about this project's support experience
- Understanding the security reporting process in SECURITY.MD
- Remove this section from the README
The design is as follows:
![Cache Purge Initial Design](InitialDesign.png)
## Contributing
# Getting Started
#### Local Setup
- Refer to this doc for prerequisites: [Code and Test Azure functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-local)
- Clone this repo to your local drive
- Once your local environment is setup, edit [EnvironmentConfig.cs](CdnLibrary/src/Utils/EnvironmentConfig.cs) with your config values
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
#### Making changes
The project structure is divided into the following:
a) **CachePurgeLibrary**:
- non-CDN specific code that is utilized throughout the solution
- no changes needed if you're not making significant structural changes to the project
b) **CdnLibrary**:
- CDN library code used in CdnPlugins and MultiCdnApi
- will need to edit this project heavily to add your own custom CDN specific processing logic
c) **CdnPlugins**:
- contains CdnPlugin functions and EventCompletion function
- change as needed based on CDNs used
d) **MultiCdnApi**:
- contains user facing API
- change as needed based on CDNs and user input
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.
To make changes to the CDNs for e.g. to add or remove a CDN, you will need to add your custom CDN logic to [CdnLibrary](CdnLibrary/src/CdnLibrary.csproj). Propagate the changes down to [CdnPlugins](CdnPlugins/src/CdnPlugins.csproj) and [MultiCdnApi](MultiCdnApi/src/MultiCdnApi.csproj).

Просмотреть файл

@ -1,13 +1,3 @@
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
# Support
## How to file issues and get help