Skip to content

Commit

Permalink
Support a JSON environment config key
Browse files Browse the repository at this point in the history
  • Loading branch information
instantiator committed Feb 8, 2025
1 parent 417d3b7 commit 5cb6947
Show file tree
Hide file tree
Showing 18 changed files with 191 additions and 34 deletions.
4 changes: 3 additions & 1 deletion Presence.Posting.Lib.Tests/ATConnectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Text;
using FishyFlip.Lexicon.App.Bsky.Embed;
using FishyFlip.Lexicon.App.Bsky.Feed;
using Presence.Posting.Lib.Config;
using Presence.Posting.Lib.Connections;
using Presence.Posting.Lib.Connections.AT;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;
using Presence.SocialFormat.Lib.Post;

Expand Down Expand Up @@ -126,7 +128,7 @@ public async Task ATConnection_Posts_WithLinkAndTagFacets()
[TestCategory("Unit")]
public async Task ATConnection_IdentifiesFacets()
{
var connection = new ATConnection(new ATAccount("TEST1", new Dictionary<NetworkCredentialType, string>()));
var connection = new ATConnection(new ATAccount("TEST1", new Dictionary<NetworkCredentialType, string?>()));
var post = new CommonPost(0, ATThreadComposer.AT_POST_RENDER_RULES)
{
Message =
Expand Down
5 changes: 3 additions & 2 deletions Presence.Posting.Lib.Tests/ConsoleConnectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Presence.Posting.Lib.Connections;
using Presence.Posting.Lib.Connections.Console;
using Presence.Posting.Lib.Constants;

namespace Presence.Posting.Lib.Tests;

Expand All @@ -10,15 +11,15 @@ public class ConsoleConnectionTests
[TestMethod]
public async Task ConsoleConnection_Connects_WithSingleCredential()
{
var connection = new ConsoleConnection(new ConsoleAccount("TEST", new Dictionary<NetworkCredentialType, string>() { { NetworkCredentialType.PrintPrefix, "TEST> " } }));
var connection = new ConsoleConnection(new ConsoleAccount("TEST", new Dictionary<NetworkCredentialType, string?>() { { NetworkCredentialType.PrintPrefix, "TEST> " } }));
var ok = await connection.ConnectAsync();
Assert.IsTrue(ok);
}

[TestMethod]
public async Task ConnectionFactory_Creates_ConsoleConnection()
{
var connection = ConnectionFactory.CreateConsole("TEST", new Dictionary<NetworkCredentialType, string>() { { NetworkCredentialType.PrintPrefix, "TEST> " } });
var connection = ConnectionFactory.CreateConsole("TEST", new Dictionary<NetworkCredentialType, string?>() { { NetworkCredentialType.PrintPrefix, "TEST> " } });
Assert.IsNotNull(connection);
await connection.ConnectAsync();
Assert.IsTrue(connection.Connected);
Expand Down
3 changes: 2 additions & 1 deletion Presence.Posting.Lib.Tests/CredentialTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Presence.Posting.Lib.Connections;
using Presence.Posting.Lib.Constants;

namespace Presence.Posting.Lib.Tests;

Expand All @@ -9,7 +10,7 @@ public class CredentialTests
[TestMethod]
public void ATAccount_Validates()
{
var account = new ATAccount("TEST", new Dictionary<NetworkCredentialType, string>
var account = new ATAccount("TEST", new Dictionary<NetworkCredentialType, string?>
{
});
var (valid, errors) = account.Validate();
Expand Down
119 changes: 119 additions & 0 deletions Presence.Posting.Lib.Tests/EnvironmentConfigReaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Collections;
using System.Text.Json;
using Presence.Posting.Lib.Config;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Tests;

[TestClass]
public class EnvironmentConfigReaderTests
{

[TestMethod]
[TestCategory("Unit")]
public void EnvironmentConfigReader_ExtractsIgnoringCase_MultipleAccounts()
{
var environments = new List<IDictionary>()
{
new Dictionary<string,string>()
{
{ ConfigKeys.ACCOUNTS_ENV_KEY, "TEST0,TEST1" },
{ "TEST0_CONSOLE_PRINTPREFIX", "printprefix:test0" },
{ "TEST1_AT_ACCOUNTNAME", "account:test1" },
{ "TEST1_AT_APPPASSWORD", "password:test1" }
},
new Dictionary<string,string>()
{
{ ConfigKeys.ACCOUNTS_ENV_KEY, "TEST0,TEST1" },
{ "TEST0_console_printprefix", "printprefix:test0" },
{ "TEST1_at_accountname", "account:test1" },
{ "TEST1_at_apppassword", "password:test1" }
}
};

foreach (var env in environments)
{
var reader = new EnvironmentConfigReader(env);
Assert.AreEqual(2, reader.Count);
Assert.IsTrue(reader.ContainsKey("TEST0"));
Assert.IsTrue(reader.ContainsKey("TEST1"));
Assert.AreEqual("printprefix:test0", reader["TEST0"][SocialNetwork.Console][NetworkCredentialType.PrintPrefix]);
Assert.AreEqual("account:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AccountName]);
Assert.AreEqual("password:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AppPassword]);
}
}

[TestMethod]
[TestCategory("Unit")]
public void EnvironmentConfigReader_ExtractsOnly_AccountsInTheAccountsVariable()
{
var env = new Dictionary<string, string>()
{
{ ConfigKeys.ACCOUNTS_ENV_KEY, "TEST0,TEST1" },
{ "TEST0_CONSOLE_PRINTPREFIX", "printprefix:test0" },
{ "TEST1_AT_ACCOUNTNAME", "account:test1" },
{ "TEST1_AT_APPPASSWORD", "password:test1" },
{ "TEST2_AT_ACCOUNTNAME", "account:test2" },
{ "TEST2_AT_APPPASSWORD", "password:test2" },
};
var reader = new EnvironmentConfigReader(env);
Assert.AreEqual(2, reader.Count);
Assert.IsTrue(reader.ContainsKey("TEST0"));
Assert.IsTrue(reader.ContainsKey("TEST1"));
Assert.IsFalse(reader.ContainsKey("TEST2"));
}

[TestMethod]
[TestCategory("Unit")]
public void EnvironmentConfigReader_CanExtract_JsonConfiguration()
{
var env = new Dictionary<string, string>()
{
{ ConfigKeys.JSON_DATA_ENV_KEY, @"
{
""PRESENCE_ACCOUNTS"": ""TEST0,TEST1"",
""TEST0_CONSOLE_PRINTPREFIX"": ""printprefix:test0"",
""TEST1_AT_ACCOUNTNAME"": ""account:test1"",
""TEST1_AT_APPPASSWORD"": ""password:test1""
}" }
};
var reader = new EnvironmentConfigReader(env);
Assert.AreEqual(2, reader.Count, JsonSerializer.Serialize(reader));
Assert.IsTrue(reader.ContainsKey("TEST0"));
Assert.IsTrue(reader.ContainsKey("TEST1"));
Assert.AreEqual("printprefix:test0", reader["TEST0"][SocialNetwork.Console][NetworkCredentialType.PrintPrefix]);
Assert.AreEqual("account:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AccountName]);
Assert.AreEqual("password:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AppPassword]);
}

[TestMethod]
[TestCategory("Unit")]
public void EnvironmentConfigReader_JsonConfiguration_Supersedes_OtherKeys()
{
var env = new Dictionary<string, string>()
{
{
ConfigKeys.JSON_DATA_ENV_KEY, @"
{
""PRESENCE_ACCOUNTS"": ""TEST0,TEST1"",
""TEST0_CONSOLE_PRINTPREFIX"": ""json:printprefix:test0"",
""TEST1_AT_ACCOUNTNAME"": ""json:account:test1"",
""TEST1_AT_APPPASSWORD"": ""json:password:test1""
}"
},
{ "PRESENCE_ACCOUNTS", "TEST0" },
{ "TEST0_CONSOLE_PRINTPREFIX", "ignored:printprefix:test0" },
{ "TEST1_AT_ACCOUNTNAME", "ignored:account:test1" },
{ "TEST1_AT_APPPASSWORD", "ignored:password:test1" }
};
var reader = new EnvironmentConfigReader(env);
Assert.AreEqual(2, reader.Count, JsonSerializer.Serialize(reader));
Assert.IsTrue(reader.ContainsKey("TEST0"));
Assert.IsTrue(reader.ContainsKey("TEST1"));
Assert.AreEqual("json:printprefix:test0", reader["TEST0"][SocialNetwork.Console][NetworkCredentialType.PrintPrefix]);
Assert.AreEqual("json:account:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AccountName]);
Assert.AreEqual("json:password:test1", reader["TEST1"][SocialNetwork.AT][NetworkCredentialType.AppPassword]);
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using Presence.Posting.Lib.Connections;
using Presence.Posting.Lib.Config;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Tests;

[TestClass]
[TestCategory("Integration")]
public class ConfigTests
public class IntegrationConfigTests
{
[TestMethod]
[TestCategory("Integration")]
public void ConfigPresent()
{
var env = Environment.GetEnvironmentVariables();
Expand All @@ -26,5 +27,4 @@ public void ConfigPresent()
Assert.IsTrue(environment["TEST1"][SocialNetwork.AT].ContainsKey(NetworkCredentialType.AccountName));
Assert.IsTrue(environment["TEST1"][SocialNetwork.AT].ContainsKey(NetworkCredentialType.AppPassword));
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
using System.Collections;
using Presence.Posting.Lib.Constants;
using Presence.Posting.Lib.Helpers;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Connections;
namespace Presence.Posting.Lib.Config;

public class EnvironmentConfigReader : Dictionary<string, Dictionary<SocialNetwork, Dictionary<NetworkCredentialType, string>>>
public class EnvironmentConfigReader : Dictionary<string, Dictionary<SocialNetwork, Dictionary<NetworkCredentialType, string?>>>
{
public const string ACCOUNTS_ENV_KEY = "PRESENCE_ACCOUNTS";

public EnvironmentConfigReader(IDictionary env)
{
// extract account prefixes
var strings = env.ToStringDictionary();
var prefixes = strings[ACCOUNTS_ENV_KEY].Split(',');
// If the JSON data key is present use that in preference to other environment variables.
// Otherwise, convert the the environment dictionary to a simple string dictionary.
var strings = (env.Contains(ConfigKeys.JSON_DATA_ENV_KEY)
? JsonConfigReader.ReadJsonConfig(env[ConfigKeys.JSON_DATA_ENV_KEY] as string)
: null) ?? env.ToStringDictionary();

// extract prefixes
var prefixes = strings.ContainsKey(ConfigKeys.ACCOUNTS_ENV_KEY) && !string.IsNullOrWhiteSpace(strings[ConfigKeys.ACCOUNTS_ENV_KEY])
? strings[ConfigKeys.ACCOUNTS_ENV_KEY]!.Split(',')
: throw new ArgumentException($"Please provide a comma separated list of account prefixes in config key: {ConfigKeys.ACCOUNTS_ENV_KEY}");

// extract credentials per network per prefix
var credentials = prefixes
Expand All @@ -37,9 +43,9 @@ public EnvironmentConfigReader(IDictionary env)
}
}

private Dictionary<NetworkCredentialType,string> ExtractCredentials(string prefix, SocialNetwork network, IDictionary<string,string> env)
private Dictionary<NetworkCredentialType,string?> ExtractCredentials(string prefix, SocialNetwork network, IDictionary<string,string?> env)
=> env
.ToDictionary(kv => kv.Key.Trim(), kv => kv.Value.Trim())
.ToDictionary(kv => kv.Key.Trim(), kv => kv.Value?.Trim())
.Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.ToDictionary(kv => kv.Key.Substring(prefix.Length).Trim('_'), kv => kv.Value)
.Where(kv => kv.Key.StartsWith(network.ToString(), StringComparison.OrdinalIgnoreCase))
Expand Down
13 changes: 13 additions & 0 deletions Presence.Posting.Lib/Config/JsonConfigReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json;

namespace Presence.Posting.Lib.Config;

public class JsonConfigReader
{
public static IDictionary<string, string?>? ReadJsonConfig(string? json) =>
string.IsNullOrWhiteSpace(json)
? null
: JsonSerializer.Deserialize<Dictionary<string, string?>>(json);
// ?.Where(kv => !string.IsNullOrWhiteSpace(kv.Value))
// .ToDictionary(kv => kv.Key, kv => kv.Value!);
}
3 changes: 2 additions & 1 deletion Presence.Posting.Lib/Connections/AT/ATAccount.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Connections;

public class ATAccount : AbstractNetworkAccount
{
public ATAccount(string prefix, IDictionary<NetworkCredentialType, string> credentials) : base(prefix, credentials)
public ATAccount(string prefix, IDictionary<NetworkCredentialType, string?> credentials) : base(prefix, credentials)
{
}

Expand Down
5 changes: 3 additions & 2 deletions Presence.Posting.Lib/Connections/AT/ATConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using FishyFlip.Lexicon.Com.Atproto.Repo;
using FishyFlip.Models;
using FishyFlip.Tools;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Helpers;
using Presence.SocialFormat.Lib.Post;

Expand Down Expand Up @@ -56,8 +57,8 @@ protected override async Task<bool> ConnectImplementationAsync(INetworkAccount c

await RateLimitAsync();
var (session, error) = await Protocol.AuthenticateWithPasswordResultAsync(
credentials![NetworkCredentialType.AccountName],
credentials![NetworkCredentialType.AppPassword]);
credentials![NetworkCredentialType.AccountName]!,
credentials![NetworkCredentialType.AppPassword]!);

if (error != null)
{
Expand Down
9 changes: 5 additions & 4 deletions Presence.Posting.Lib/Connections/AbstractNetworkAccount.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Connections;

public abstract class AbstractNetworkAccount : Dictionary<NetworkCredentialType, string>, INetworkAccount
public abstract class AbstractNetworkAccount : Dictionary<NetworkCredentialType, string?>, INetworkAccount
{
protected AbstractNetworkAccount(string accountPrefix, IDictionary<NetworkCredentialType, string>? credentials = null)
protected AbstractNetworkAccount(string accountPrefix, IDictionary<NetworkCredentialType, string?>? credentials = null)
{
AccountPrefix = accountPrefix;
SetCredentials(credentials ?? new Dictionary<NetworkCredentialType, string>());
SetCredentials(credentials ?? new Dictionary<NetworkCredentialType, string?>());
}

public string AccountPrefix { get; init; }
Expand All @@ -25,7 +26,7 @@ protected AbstractNetworkAccount(string accountPrefix, IDictionary<NetworkCreden
return (errors.Count() == 0, errors);
}

private void SetCredentials(IDictionary<NetworkCredentialType, string> credentials)
private void SetCredentials(IDictionary<NetworkCredentialType, string?> credentials)
{
foreach (var (key, value) in credentials)
{
Expand Down
8 changes: 5 additions & 3 deletions Presence.Posting.Lib/Connections/ConnectionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections;
using Presence.Posting.Lib.Config;
using Presence.Posting.Lib.Connections.AT;
using Presence.Posting.Lib.Connections.Console;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Connections;
Expand All @@ -13,7 +15,7 @@ public static IEnumerable<INetworkConnection> CreateConnections(IDictionary env)
return environment.Keys.SelectMany((prefix) => environment[prefix].Keys.Select((network) => CreateConnection(prefix, network, environment[prefix][network])));
}

public static INetworkConnection CreateConnection(string prefix, SocialNetwork network, IDictionary<NetworkCredentialType, string> credentials)
public static INetworkConnection CreateConnection(string prefix, SocialNetwork network, IDictionary<NetworkCredentialType, string?> credentials)
{
return network switch
{
Expand All @@ -23,9 +25,9 @@ public static INetworkConnection CreateConnection(string prefix, SocialNetwork n
};
}

public static ConsoleConnection CreateConsole(string prefix, IDictionary<NetworkCredentialType, string> credentials)
public static ConsoleConnection CreateConsole(string prefix, IDictionary<NetworkCredentialType, string?> credentials)
=> new ConsoleConnection(new ConsoleAccount(prefix, credentials));

public static ATConnection CreateAT(string prefix, IDictionary<NetworkCredentialType, string> credentials)
public static ATConnection CreateAT(string prefix, IDictionary<NetworkCredentialType, string?> credentials)
=> new ATConnection(new ATAccount(prefix, credentials));
}
3 changes: 2 additions & 1 deletion Presence.Posting.Lib/Connections/Console/ConsoleAccount.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Presence.Posting.Lib.Connections;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib;

public class ConsoleAccount : AbstractNetworkAccount
{
public ConsoleAccount(string accountPrefix, IDictionary<NetworkCredentialType, string>? credentials = null) : base(accountPrefix, credentials)
public ConsoleAccount(string accountPrefix, IDictionary<NetworkCredentialType, string?>? credentials = null) : base(accountPrefix, credentials)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Post;

namespace Presence.Posting.Lib.Connections.Console;
Expand Down
3 changes: 2 additions & 1 deletion Presence.Posting.Lib/Connections/INetworkAccount.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using Presence.Posting.Lib.Constants;
using Presence.SocialFormat.Lib.Networks;

namespace Presence.Posting.Lib.Connections;

public interface INetworkAccount : IDictionary<NetworkCredentialType, string>
public interface INetworkAccount : IDictionary<NetworkCredentialType, string?>
{
public SocialNetwork SocialNetwork { get; }
public string AccountPrefix { get; }
Expand Down
8 changes: 8 additions & 0 deletions Presence.Posting.Lib/Constants/ConfigKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Presence.Posting.Lib.Constants;

public static class ConfigKeys
{
public const string JSON_DATA_ENV_KEY = "PRESENCE_CONFIG_JSON";
public const string ACCOUNTS_ENV_KEY = "PRESENCE_ACCOUNTS";

}
Loading

0 comments on commit 5cb6947

Please sign in to comment.