Auto-flag questions with media at write time, not just via one-time sweep

The flag-media-questions CLI command only caught questions that
existed in the DB when it ran once. Any question created, imported,
or edited afterward could add media without ever being flagged for
review, since nothing gated new writes going forward. CreateAsync,
UpdateAsync, and UpsertManyByExternalIdAsync now all run new/updated
questions through ApplyMediaReviewGate, which flags any question that
has media and isn't already flagged.
This commit is contained in:
hz
2026-07-23 03:29:57 -05:00
parent 59ffaf4376
commit c62eff8846
2 changed files with 67 additions and 0 deletions
@@ -0,0 +1,49 @@
using QuestionsApi.Models;
using QuestionsApi.Services;
using Xunit;
namespace QuestionsApi.Tests;
public sealed class QuestionStoreTests
{
[Fact]
public void ApplyMediaReviewGate_FlagsUnflaggedQuestionWithMedia()
{
var question = new Question { Media = ["asset-1"] };
QuestionStore.ApplyMediaReviewGate(question);
Assert.NotNull(question.FlaggedForReviewUtc);
Assert.Equal("system", question.FlaggedByUserId);
Assert.False(string.IsNullOrWhiteSpace(question.FlagReason));
}
[Fact]
public void ApplyMediaReviewGate_LeavesMediaFreeQuestionsAlone()
{
var question = new Question { Media = [] };
QuestionStore.ApplyMediaReviewGate(question);
Assert.Null(question.FlaggedForReviewUtc);
}
[Fact]
public void ApplyMediaReviewGate_DoesNotOverwriteAnExistingFlag()
{
var flaggedAt = DateTime.UtcNow.AddDays(-1);
var question = new Question
{
Media = ["asset-1"],
FlaggedForReviewUtc = flaggedAt,
FlagReason = "Reported by student",
FlaggedByUserId = "user-42"
};
QuestionStore.ApplyMediaReviewGate(question);
Assert.Equal(flaggedAt, question.FlaggedForReviewUtc);
Assert.Equal("Reported by student", question.FlagReason);
Assert.Equal("user-42", question.FlaggedByUserId);
}
}
@@ -6,6 +6,9 @@ namespace QuestionsApi.Services;
public class QuestionStore
{
private const string MediaReviewReason = "Contains media - pending review";
private const string MediaReviewFlaggedBy = "system";
private readonly IMongoCollection<Question> _col;
public QuestionStore(MongoDatabaseProvider mongo)
@@ -105,6 +108,7 @@ public class QuestionStore
var question = FromRequest(req);
question.CreatedUtc = DateTime.UtcNow;
question.UpdatedUtc = DateTime.UtcNow;
ApplyMediaReviewGate(question);
try
{
@@ -131,6 +135,7 @@ public class QuestionStore
question.FlaggedForReviewUtc = existing.FlaggedForReviewUtc;
question.FlagReason = existing.FlagReason;
question.FlaggedByUserId = existing.FlaggedByUserId;
ApplyMediaReviewGate(question);
var result = await _col.ReplaceOneAsync(q => q.Id == id, question);
return result.MatchedCount == 0 ? null : question;
}
@@ -158,6 +163,7 @@ public class QuestionStore
question.Id = ObjectId.GenerateNewId().ToString();
question.CreatedUtc = DateTime.UtcNow;
}
ApplyMediaReviewGate(question);
await _col.ReplaceOneAsync(filter, question, new ReplaceOptions { IsUpsert = true });
count++;
@@ -246,6 +252,18 @@ public class QuestionStore
return (int)result.MatchedCount;
}
// Questions with media must be reviewed before reaching students - gate any question that
// gains media and isn't already flagged, so newly created/imported/edited questions can't
// slip past the media-review requirement the way a one-time sweep alone would miss.
internal static void ApplyMediaReviewGate(Question question)
{
if (question.Media.Count == 0 || question.FlaggedForReviewUtc is not null)
return;
question.FlaggedForReviewUtc = DateTime.UtcNow;
question.FlagReason = MediaReviewReason;
question.FlaggedByUserId = MediaReviewFlaggedBy;
}
private static Question FromRequest(UpsertQuestionRequest req) => new()
{
ExternalId = req.ExternalId.Trim(),