Skip to content

Commit

Permalink
Maintenance/ivw prep fixes (#98)
Browse files Browse the repository at this point in the history
* Refactor TwitterClient; Make Tweet Delay configurable; Change Tweet template on Dev

* Fix integration tests.

* Use FluentAssertions and use AutoFixture for data generation

* Create repository instance only once and not in each test.

* Use AutoFixture for all test data generation.

* Create a separate AppConfig file for the integration tests.
  • Loading branch information
Zifah authored Aug 26, 2024
1 parent 709ebb2 commit 219ad16
Show file tree
Hide file tree
Showing 24 changed files with 524 additions and 707 deletions.
4 changes: 4 additions & 0 deletions Api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
},
"MySQL": {
"ConnectionString": "Server=localhost;User ID=root;Password=SweetFresh06;Database=dictionary"
},
"Twitter": {
"TweetTemplate": "{name}: \"{meaning}\" {link}",
"TweetIntervalSeconds": 5
}
}
6 changes: 6 additions & 0 deletions Api/appsettings.Test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Twitter": {
"TweetTemplate": "{name}: \"{meaning}\" {link}",
"TweetIntervalSeconds": 0.1
}
}
3 changes: 2 additions & 1 deletion Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"ConsumerKey": "your-consumer-key",
"ConsumerSecret": "your-consumer-secret",
"NameUrlPrefix": "https://www.yorubaname.com/entries",
"TweetTemplate": "New name entry: {name}, {meaning}. More here: {link}"
"TweetTemplate": "New name entry: {name}, {meaning}. More here: {link}",
"TweetIntervalSeconds": 180
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Core.StringObjectConverters;
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Website.Utilities
namespace Core.StringObjectConverters
{
public static class JsonSerializerOptionsProvider
{
Expand Down
5 changes: 3 additions & 2 deletions Infrastructure/Configuration/TwitterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ public record TwitterConfig(
string AccessToken,
string AccessTokenSecret,
string NameUrlPrefix,
string TweetTemplate)
string TweetTemplate,
decimal TweetIntervalSeconds)
{
public TwitterConfig() : this("", "", "", "", "", "") { }
public TwitterConfig() : this("", "", "", "", "", "", default) { }
}
}
3 changes: 3 additions & 0 deletions Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Infrastructure.Configuration;
using Infrastructure.Twitter;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Tweetinvi;
Expand Down Expand Up @@ -26,6 +27,8 @@ public static IServiceCollection AddTwitterClient(this IServiceCollection servic
);
});

services.AddSingleton<ITwitterClientV2, TwitterClientV2>();

return services;
}
}
Expand Down
12 changes: 6 additions & 6 deletions Infrastructure/Services/NamePostingService.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
using Application.Domain;
using Application.Events;
using Infrastructure.Configuration;
using Infrastructure.Twitter;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using Tweetinvi;

namespace Infrastructure.Services
{
public class NamePostingService(
ConcurrentQueue<PostPublishedNameCommand> nameQueue,
ITwitterClient twitterApiClient,
ITwitterClientV2 twitterApiClient,
ILogger<NamePostingService> logger,
NameEntryService nameEntryService,
IOptions<TwitterConfig> twitterConfig) : BackgroundService
{
private const string TweetComposeFailure = "Failed to build tweet for name: {name}. It was not found in the database.";
private readonly ConcurrentQueue<PostPublishedNameCommand> _nameQueue = nameQueue;
private readonly TweetsV2Poster _twitterApiClient = new (twitterApiClient);
private readonly ITwitterClientV2 _twitterApiClient = twitterApiClient;
private readonly ILogger<NamePostingService> _logger = logger;
private readonly NameEntryService _nameEntryService = nameEntryService;
private readonly TwitterConfig _twitterConfig = twitterConfig.Value;
private const int TweetIntervalMs = 3 * 60 * 1000; // 3 minutes

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tweetIntervalMs = (int)(_twitterConfig.TweetIntervalSeconds * 1000);
while (!stoppingToken.IsCancellationRequested)
{
if (!_nameQueue.TryDequeue(out var indexedName))
{
await Task.Delay(TweetIntervalMs, stoppingToken);
await Task.Delay(tweetIntervalMs, stoppingToken);
continue;
}

Expand All @@ -56,7 +56,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
_nameQueue.Enqueue(indexedName);
}

await Task.Delay(TweetIntervalMs, stoppingToken);
await Task.Delay(tweetIntervalMs, stoppingToken);
}
}

Expand Down
87 changes: 0 additions & 87 deletions Infrastructure/TweetsV2Poster.cs

This file was deleted.

7 changes: 7 additions & 0 deletions Infrastructure/Twitter/ITwitterClientV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Infrastructure.Twitter
{
public interface ITwitterClientV2
{
Task<TweetV2PostResponse> PostTweet(string text);
}
}
17 changes: 17 additions & 0 deletions Infrastructure/Twitter/TweetV2PostData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace Infrastructure.Twitter
{
public record TweetV2PostData
{
[JsonProperty("id")]
public string Id { get; init; }
[JsonProperty("text")]
public string Text { get; init; }

public TweetV2PostData(string id, string text)
{
Id = id;
Text = text;
}
}
}
17 changes: 17 additions & 0 deletions Infrastructure/Twitter/TweetV2PostResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace Infrastructure.Twitter
{
public record TweetV2PostResponse
{
[JsonProperty("data")]
public TweetV2PostData Data { get; init; }

public string? Id => Data?.Id;
public string? Text => Data?.Text;

public TweetV2PostResponse(TweetV2PostData data)
{
Data = data;
}
}
}
52 changes: 52 additions & 0 deletions Infrastructure/Twitter/TwitterClientV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Text;
using Tweetinvi;
using Newtonsoft.Json;
namespace Infrastructure.Twitter
{
public class TwitterClientV2 : ITwitterClientV2
{
private readonly ITwitterClient _twitterV1Client;

public TwitterClientV2(ITwitterClient twitterClient)
{
_twitterV1Client = twitterClient;
}

public async Task<TweetV2PostResponse> PostTweet(string text)
{
var result = await _twitterV1Client.Execute.AdvanceRequestAsync(
(request) =>
{
var jsonBody = _twitterV1Client.Json.Serialize(new TweetV2PostRequest
{
Text = text
});

var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

request.Query.Url = "https://api.twitter.com/2/tweets";
request.Query.HttpMethod = Tweetinvi.Models.HttpMethod.POST;
request.Query.HttpContent = content;
}
);

if (!result.Response.IsSuccessStatusCode)
{
throw new Exception($"Error when posting tweet:{Environment.NewLine}{result.Content}");
}

return _twitterV1Client.Json.Deserialize<TweetV2PostResponse>(result.Content);
}

/// <summary>
/// There are a lot more fields according to:
/// https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets
/// but these are the ones we care about for our use case.
/// </summary>
private class TweetV2PostRequest
{
[JsonProperty("text")]
public string Text { get; set; } = string.Empty;
}
}
}
33 changes: 26 additions & 7 deletions Test/BootStrappedApiFactory.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Api;
using Core.Repositories;
using Core.StringObjectConverters;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using Infrastructure.MongoDB.Repositories;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Xunit;
namespace Test;

public abstract class BootStrappedApiFactory : WebApplicationFactory <IApiMarker>, IAsyncLifetime
public class BootStrappedApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const int HostPort = 27018;
private const int ContainerPort = 27017;
private const string MongoDbDatabaseName = "yoruba_names_dictionary_test_DB";
private const string MongoDbPassword = "password";
private const string MongoDbUsername = "admin";

public HttpClient HttpClient { get; private set; } = default!;


public JsonSerializerOptions JsonSerializerOptions { get; init; }

public BootStrappedApiFactory()
{
JsonSerializerOptions = JsonSerializerOptionsProvider.GetJsonSerializerOptionsWithCustomConverters();
}

private readonly IContainer _testDbContainer =
new ContainerBuilder()
.WithImage("mongo:latest")
.WithEnvironment( new Dictionary<string, string>
.WithEnvironment(new Dictionary<string, string>
{
{"MONGO_INITDB_ROOT_USERNAME", MongoDbUsername},
{"MONGO_INITDB_ROOT_PASSWORD", MongoDbPassword},
Expand All @@ -35,14 +46,22 @@ public abstract class BootStrappedApiFactory : WebApplicationFactory <IApiMarker
.WithPortBinding(HostPort, ContainerPort)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(ContainerPort))
.Build();

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.Sources.Clear();
config.AddJsonFile("appSettings.Test.json", optional: false, reloadOnChange: true);
});

builder.UseEnvironment("Test");

builder.ConfigureTestServices(x =>
{
x.AddSingleton<IMongoClient, MongoClient>(s => new MongoClient( $"mongodb://{MongoDbUsername}:{MongoDbPassword}@localhost:{HostPort}"));
x.AddScoped(s => s.GetRequiredService<IMongoClient>().GetDatabase(MongoDbDatabaseName));
x.AddScoped<INameEntryRepository, NameEntryRepository>();
x.AddSingleton<IMongoClient, MongoClient>(s => new MongoClient($"mongodb://{MongoDbUsername}:{MongoDbPassword}@localhost:{HostPort}"));
x.AddSingleton(s => s.GetRequiredService<IMongoClient>().GetDatabase(MongoDbDatabaseName));
x.AddSingleton<INameEntryRepository, NameEntryRepository>();
});
}

Expand Down
Loading

0 comments on commit 219ad16

Please sign in to comment.