using MongoDB.Bson; using QuestionsApi.Services; using Xunit; namespace QuestionsApi.Tests; public sealed class ContentPromotionTests { [Fact] public void CollectMediaIdsDeduplicatesValidObjectIds() { var id = ObjectId.GenerateNewId(); var questions = new[] { new BsonDocument("Media", new BsonArray { id.ToString(), "not-an-object-id" }), new BsonDocument("Media", new BsonArray { id.ToString() }) }; var result = ContentPromotionCommand.CollectMediaIds(questions); Assert.Equal(new[] { id }, result); } [Fact] public void ContentHashDoesNotDependOnInputOrder() { var first = new BsonDocument { ["ExternalId"] = "a", ["Stem"] = "one" }; var second = new BsonDocument { ["ExternalId"] = "b", ["Stem"] = "two" }; Assert.Equal( ContentPromotionCommand.HashDocuments([first, second]), ContentPromotionCommand.HashDocuments([second, first])); } [Fact] public void ReadyQuestionEligibilityRequiresPublishedAndUnflagged() { Assert.True(QuestionPromotionCommand.IsEligible(new BsonDocument { ["IsPublished"] = true, ["FlaggedForReviewUtc"] = BsonNull.Value })); Assert.True(QuestionPromotionCommand.IsEligible(new BsonDocument("IsPublished", true))); Assert.False(QuestionPromotionCommand.IsEligible(new BsonDocument { ["IsPublished"] = true, ["FlaggedForReviewUtc"] = DateTime.UtcNow })); Assert.False(QuestionPromotionCommand.IsEligible(new BsonDocument("IsPublished", false))); } [Fact] public void ProductionPromotionFlagsMediaForSeparateApproval() { var updated = new DateTime(2026, 7, 30, 12, 0, 0, DateTimeKind.Utc); var source = new BsonDocument { ["ExternalId"] = "media-question", ["IsPublished"] = true, ["FlaggedForReviewUtc"] = BsonNull.Value, ["UpdatedUtc"] = updated, ["Media"] = new BsonArray { ObjectId.GenerateNewId().ToString() } }; var prepared = QuestionPromotionCommand.PrepareForProduction(source); Assert.Equal(updated, prepared["FlaggedForReviewUtc"].ToUniversalTime()); Assert.Equal(QuestionPromotionCommand.ProductionMediaReviewReason, prepared["FlagReason"].AsString); Assert.Equal(QuestionPromotionCommand.ProductionMediaReviewActor, prepared["FlaggedByUserId"].AsString); Assert.True(source["FlaggedForReviewUtc"].IsBsonNull); } [Fact] public void ProductionPromotionLeavesMediaFreeQuestionApprovalStateAlone() { var source = new BsonDocument { ["ExternalId"] = "text-question", ["IsPublished"] = true, ["FlaggedForReviewUtc"] = BsonNull.Value, ["Media"] = new BsonArray() }; var prepared = QuestionPromotionCommand.PrepareForProduction(source); Assert.True(prepared["FlaggedForReviewUtc"].IsBsonNull); Assert.False(prepared.Contains("FlagReason")); } [Fact] public void QuestionMediaIdsSupportStoredApiUrlsAndRawIds() { var first = ObjectId.GenerateNewId(); var second = ObjectId.GenerateNewId(); var questions = new[] { new BsonDocument { ["ExternalId"] = "one", ["Media"] = new BsonArray { $"/api/questions/media/{first}", second.ToString() } }, new BsonDocument { ["ExternalId"] = "two", ["Media"] = new BsonArray { $"https://example.test/api/questions/media/{first}" } } }; var result = QuestionPromotionCommand.CollectMediaIds(questions); Assert.Equal(new HashSet { first, second }, result); } [Fact] public void InvalidLocalQuestionMediaReferenceFailsValidation() { var question = new BsonDocument { ["ExternalId"] = "bad-media", ["Media"] = new BsonArray { "/api/questions/media/not-an-id" } }; var exception = Assert.Throws( () => QuestionPromotionCommand.CollectMediaIds([question])); Assert.Contains("bad-media", exception.Message); } [Fact] public void QuestionComparisonClassifiesInsertUpdateAndUnchanged() { var source = new[] { Question("insert", "new"), Question("update", "new"), Question("same", "same") }; var target = new[] { Question("update", "old"), new BsonDocument { ["Stem"] = "same", ["ExternalId"] = "same", ["_id"] = ObjectId.GenerateNewId() } }; var result = QuestionPromotionCommand.ClassifyQuestions(source, target); Assert.Equal(1, result.Inserted); Assert.Equal(1, result.Updated); Assert.Equal(1, result.Unchanged); } [Fact] public void QuestionHashIgnoresMongoIdsFieldOrderAndInputOrder() { var first = Question("a", "one"); first["_id"] = ObjectId.GenerateNewId(); var second = Question("b", "two"); var equivalentFirst = new BsonDocument { ["Stem"] = "one", ["_id"] = ObjectId.GenerateNewId(), ["ExternalId"] = "a" }; Assert.Equal( QuestionPromotionCommand.HashQuestions([first, second]), QuestionPromotionCommand.HashQuestions([second, equivalentFirst])); } [Fact] public async Task ReadyQuestionApplyRequiresExplicitProductionConfirmation() { using var output = new StringWriter(); var result = await QuestionPromotionCommand.RunAsync( [ "--source-connection", "mongodb://source.invalid", "--source-database", "uat", "--target-connection", "mongodb://target.invalid", "--target-database", "prd", "--apply" ], output); Assert.Equal(1, result); Assert.Contains("--confirm PROMOTE_TO_PRD is required", output.ToString()); } private static BsonDocument Question(string externalId, string stem) => new() { ["ExternalId"] = externalId, ["Stem"] = stem }; }