Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# Changelog
## [v0.4.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.4.0)
- Feat
- **MFA Support**: Added Multi-Factor Authentication (MFA) support for login operations
- Added `mfaSecret` parameter to `Login` and `LoginAsync` methods for TOTP generation
- Automatic TOTP token generation from Base32-encoded MFA secrets using Otp.NET library
- Comprehensive test coverage for MFA functionality including unit and integration tests
- Supports both explicit token and MFA secret-based authentication flows

## [v0.3.2](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.3.2)
- Fix
- Added Test cases for the Release
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,123 @@ public void Test007_Should_Return_Loggedin_User_With_Organizations_detail()
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test008_Should_Fail_Login_With_Invalid_MfaSecret()
{
ContentstackClient client = new ContentstackClient();
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
string invalidMfaSecret = "INVALID_BASE32_SECRET!@#";

try
{
ContentstackResponse contentstackResponse = client.Login(credentials, null, invalidMfaSecret);
Assert.Fail("Expected exception for invalid MFA secret");
}
catch (ArgumentException)
{
// Expected exception for invalid Base32 encoding
Assert.IsTrue(true);
}
catch (Exception e)
{
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
}
}

[TestMethod]
[DoNotParallelize]
public void Test009_Should_Generate_TOTP_Token_With_Valid_MfaSecret()
{
ContentstackClient client = new ContentstackClient();
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
string validMfaSecret = "JBSWY3DPEHPK3PXP"; // Valid Base32 test secret

try
{
// This should fail due to invalid credentials, but should succeed in generating TOTP
ContentstackResponse contentstackResponse = client.Login(credentials, null, validMfaSecret);
}
catch (ContentstackErrorException errorException)
{
// Expected to fail due to invalid credentials, but we verify it processed the MFA secret
// The error should be about credentials, not about MFA secret format
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
Assert.IsTrue(errorException.Message.Contains("email or password") ||
errorException.Message.Contains("credentials") ||
errorException.Message.Contains("authentication"));
}
catch (ArgumentException)
{
Assert.Fail("Should not throw ArgumentException for valid MFA secret");
}
catch (Exception e)
{
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
}
}

[TestMethod]
[DoNotParallelize]
public async System.Threading.Tasks.Task Test010_Should_Generate_TOTP_Token_With_Valid_MfaSecret_Async()
{
ContentstackClient client = new ContentstackClient();
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
string validMfaSecret = "JBSWY3DPEHPK3PXP"; // Valid Base32 test secret

try
{
// This should fail due to invalid credentials, but should succeed in generating TOTP
ContentstackResponse contentstackResponse = await client.LoginAsync(credentials, null, validMfaSecret);
}
catch (ContentstackErrorException errorException)
{
// Expected to fail due to invalid credentials, but we verify it processed the MFA secret
// The error should be about credentials, not about MFA secret format
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
Assert.IsTrue(errorException.Message.Contains("email or password") ||
errorException.Message.Contains("credentials") ||
errorException.Message.Contains("authentication"));
}
catch (ArgumentException)
{
Assert.Fail("Should not throw ArgumentException for valid MFA secret");
}
catch (Exception e)
{
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
}
}

[TestMethod]
[DoNotParallelize]
public void Test011_Should_Prefer_Explicit_Token_Over_MfaSecret()
{
ContentstackClient client = new ContentstackClient();
NetworkCredential credentials = new NetworkCredential("test_user", "test_password");
string validMfaSecret = "JBSWY3DPEHPK3PXP";
string explicitToken = "123456";

try
{
// This should fail due to invalid credentials, but should use explicit token
ContentstackResponse contentstackResponse = client.Login(credentials, explicitToken, validMfaSecret);
}
catch (ContentstackErrorException errorException)
{
// Expected to fail due to invalid credentials
// The important thing is that it didn't throw an exception about MFA secret processing
Assert.AreEqual(HttpStatusCode.UnprocessableEntity, errorException.StatusCode);
}
catch (ArgumentException)
{
Assert.Fail("Should not throw ArgumentException when explicit token is provided");
}
catch (Exception e)
{
Assert.Fail($"Unexpected exception type: {e.GetType().Name} - {e.Message}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
using System;
using System.Threading.Tasks;
using Contentstack.Management.Core.Models;
using Contentstack.Management.Core.Tests.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Contentstack.Management.Core.Tests.IntegrationTest
{
[TestClass]
public class Contentstack016_VariantsTest
{
private Stack _stack;

[TestInitialize]
public void Initialize()
{
StackResponse response = StackResponse.getStack(Contentstack.Client.serializer);
_stack = Contentstack.Client.Stack(response.Stack.APIKey);
}

[TestMethod]
[DoNotParallelize]
public async Task Test001_Should_Create_Variants()
{
try
{
VariantsModel variantsModel = new VariantsModel();
ContentstackResponse response = await _stack.Variants().CreateAsync(variantsModel);
Assert.AreEqual(System.Net.HttpStatusCode.Created, response.StatusCode);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test002_Should_Fetch_Variants_By_Uid()
{
try
{
// First create a variant to ensure we have something to fetch
VariantsModel variantsModel = new VariantsModel();
ContentstackResponse createResponse = await _stack.Variants().CreateAsync(variantsModel);
Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse.StatusCode);

// Extract UID from created variant
var createdVariant = createResponse.OpenJObjectResponse();
string variantUid = createdVariant["variant"]["uid"].ToString();

// Test fetching by UID
ContentstackResponse fetchResponse = await _stack.Variants(variantUid).FetchAsync();
Assert.IsTrue(fetchResponse.StatusCode == System.Net.HttpStatusCode.OK ||
fetchResponse.StatusCode == System.Net.HttpStatusCode.NotFound);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test003_Should_FetchByUid_Multiple_Variants()
{
try
{
// Create multiple variants first
VariantsModel variantsModel1 = new VariantsModel();
VariantsModel variantsModel2 = new VariantsModel();

ContentstackResponse createResponse1 = await _stack.Variants().CreateAsync(variantsModel1);
ContentstackResponse createResponse2 = await _stack.Variants().CreateAsync(variantsModel2);

Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse1.StatusCode);
Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse2.StatusCode);

// Extract UIDs from created variants
var createdVariant1 = createResponse1.OpenJObjectResponse();
var createdVariant2 = createResponse2.OpenJObjectResponse();
string variantUid1 = createdVariant1["variant"]["uid"].ToString();
string variantUid2 = createdVariant2["variant"]["uid"].ToString();

// Test fetching multiple variants by UIDs
string[] uids = { variantUid1, variantUid2 };
ContentstackResponse fetchResponse = await _stack.Variants().FetchByUidAsync(uids);

Assert.IsTrue(fetchResponse.StatusCode == System.Net.HttpStatusCode.OK ||
fetchResponse.StatusCode == System.Net.HttpStatusCode.NotFound);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test004_Should_FetchByUid_Single_Variant()
{
try
{
// Create a variant first
VariantsModel variantsModel = new VariantsModel();
ContentstackResponse createResponse = await _stack.Variants().CreateAsync(variantsModel);
Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse.StatusCode);

// Extract UID from created variant
var createdVariant = createResponse.OpenJObjectResponse();
string variantUid = createdVariant["variant"]["uid"].ToString();

// Test fetching single variant using FetchByUid
string[] uids = { variantUid };
ContentstackResponse fetchResponse = await _stack.Variants().FetchByUidAsync(uids);

Assert.IsTrue(fetchResponse.StatusCode == System.Net.HttpStatusCode.OK ||
fetchResponse.StatusCode == System.Net.HttpStatusCode.NotFound);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test005_Should_Handle_FetchByUid_With_Nonexistent_Uids()
{
try
{
// Test fetching with non-existent UIDs
string[] nonExistentUids = { "nonexistent_uid_1", "nonexistent_uid_2" };
ContentstackResponse fetchResponse = await _stack.Variants().FetchByUidAsync(nonExistentUids);

// Should return 404 or empty result, not crash
Assert.IsTrue(fetchResponse.StatusCode == System.Net.HttpStatusCode.NotFound ||
fetchResponse.StatusCode == System.Net.HttpStatusCode.OK);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test006_Should_FetchByUid_Sync_Method()
{
try
{
// Create a variant first
VariantsModel variantsModel = new VariantsModel();
ContentstackResponse createResponse = _stack.Variants().Create(variantsModel);
Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse.StatusCode);

// Extract UID from created variant
var createdVariant = createResponse.OpenJObjectResponse();
string variantUid = createdVariant["variant"]["uid"].ToString();

// Test synchronous FetchByUid
string[] uids = { variantUid };
ContentstackResponse fetchResponse = _stack.Variants().FetchByUid(uids);

Assert.IsTrue(fetchResponse.StatusCode == System.Net.HttpStatusCode.OK ||
fetchResponse.StatusCode == System.Net.HttpStatusCode.NotFound);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test007_Should_Delete_Variant()
{
try
{
// Create a variant first
VariantsModel variantsModel = new VariantsModel();
ContentstackResponse createResponse = await _stack.Variants().CreateAsync(variantsModel);
Assert.AreEqual(System.Net.HttpStatusCode.Created, createResponse.StatusCode);

// Extract UID from created variant
var createdVariant = createResponse.OpenJObjectResponse();
string variantUid = createdVariant["variant"]["uid"].ToString();

// Test deleting the variant
ContentstackResponse deleteResponse = await _stack.Variants(variantUid).DeleteAsync();
Assert.IsTrue(deleteResponse.StatusCode == System.Net.HttpStatusCode.OK ||
deleteResponse.StatusCode == System.Net.HttpStatusCode.NoContent ||
deleteResponse.StatusCode == System.Net.HttpStatusCode.NotFound);
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test008_Should_Validate_FetchByUid_Parameters()
{
try
{
// Test with null UIDs
Assert.ThrowsException<ArgumentException>(() => _stack.Variants().FetchByUid(null));

// Test with empty UIDs array
string[] emptyUids = new string[0];
Assert.ThrowsException<ArgumentException>(() => _stack.Variants().FetchByUid(emptyUids));
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test009_Should_Validate_FetchByUidAsync_Parameters()
{
try
{
// Test with null UIDs
await Assert.ThrowsExceptionAsync<ArgumentException>(() => _stack.Variants().FetchByUidAsync(null));

// Test with empty UIDs array
string[] emptyUids = new string[0];
await Assert.ThrowsExceptionAsync<ArgumentException>(() => _stack.Variants().FetchByUidAsync(emptyUids));
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test010_Should_Validate_Instance_With_Uid_Cannot_Use_FetchByUid()
{
try
{
// Test that an instance with UID cannot call FetchByUid
string instanceUid = "some_uid";
string[] uids = { "uid1", "uid2" };

Assert.ThrowsException<InvalidOperationException>(() =>
_stack.Variants(instanceUid).FetchByUid(uids));
}
catch (Exception e)
{
Assert.Fail(e.Message);
}
}
}
}
Loading
Loading