🐸 Frogue(Fed)

~ololduck/Frogue

Raw

//! Comments on tickets and patch proposals: the rules a comment lives by,
//! and the store that keeps its rows.
//!
//! The rules sit here rather than in the handlers because everything that
//! speaks about a comment has to agree on them: what a comment body must be,
//! and who may change one.

pub mod store;
pub use store::CommentAuthor;

/// How long a comment's content may be, which is the width of the column it
/// is kept in: a longer one is refused here rather than by SQLite in words of
/// its own.
pub const MAX_CONTENT_LEN: usize = 65_536;

/// The content a comment may be saved with: the text with the space around it
/// taken off, or the reason it cannot be one.
///
/// The content is the one thing a comment cannot do without - it is what the
/// thread is for - so it is asked for in words of ours rather than left to a
/// `NOT NULL` to say no to.
pub fn checked_content(content: &str) -> Result<&str, String> {
    let content = content.trim();

    if content.is_empty() {
        return Err("a comment needs content".to_string());
    }
    if content.chars().count() > MAX_CONTENT_LEN {
        return Err(format!(
            "a comment may be at most {MAX_CONTENT_LEN} characters long"
        ));
    }

    Ok(content)
}

/// Whether `viewer` may edit or delete a comment written by `author` in a
/// repository owned by `owner`. If the comment was created by a remote actor
/// (author is None), only the repository owner may change it.
///
/// The person who wrote the comment, and the person whose repository it is,
/// and nobody else.
pub fn may_change(author: Option<i64>, owner: i64, viewer: i64) -> bool {
    author.is_some_and(|a| viewer == a) || viewer == owner
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_comment_content_is_the_text_with_the_space_around_it_gone() {
        assert_eq!(checked_content("  Great point \n"), Ok("Great point"));
    }

    #[test]
    fn a_comment_content_of_nothing_is_refused_in_words_of_ours() {
        assert_eq!(
            checked_content(""),
            Err("a comment needs content".to_string())
        );
        assert_eq!(
            checked_content("   "),
            Err("a comment needs content".to_string())
        );
        assert_eq!(
            checked_content("\n\t"),
            Err("a comment needs content".to_string())
        );
    }

    #[test]
    fn a_comment_content_longer_than_the_column_holds_says_how_long_is_too_long() {
        let long = "x".repeat(MAX_CONTENT_LEN + 1);

        assert_eq!(
            checked_content(&long).unwrap_err(),
            "a comment may be at most 65536 characters long"
        );
        // The width is counted in characters rather than bytes, so a comment
        // of accents fills the column the same way the column counts it.
        assert!(checked_content(&"é".repeat(MAX_CONTENT_LEN)).is_ok());
    }

    #[test]
    fn a_comment_is_changed_by_the_one_who_wrote_it_or_the_owner_of_the_repository() {
        assert!(may_change(Some(3), 7, 3), "the author may change it");
        assert!(may_change(Some(3), 7, 7), "the owner may change it");
        assert!(!may_change(Some(3), 7, 9), "somebody else may not");
        // Remote-created comments (author is None) can only be changed by the owner
        assert!(
            may_change(None, 7, 7),
            "the owner may change remote-created comments"
        );
        assert!(
            !may_change(None, 7, 3),
            "non-owners may not change remote-created comments"
        );
    }
}