defmodule OpenAgents.NotificationsTest do
use OpenAgents.DataCase, async: true
import OpenAgents.AccountsFixtures
import OpenAgents.IssuesFixtures
alias OpenAgents.Issues
alias OpenAgents.Notifications
alias OpenAgents.Notifications.Mentions
alias OpenAgents.Repositories
describe "mention extraction" do
test "reads a login out of prose" do
assert Mentions.extract("thanks @octocat, shipping now") == ["octocat"]
end
test "downcases and deduplicates" do
assert Mentions.extract("@Octocat and @octocat and @OCTOCAT") == ["octocat"]
end
test "ignores a login inside an inline code span" do
assert Mentions.extract("run `git config user.name @octocat` first") == []
end
test "ignores a login inside a fenced block" do
body = """
Here is the transcript:
```
ssh @octocat@example.com
```
and nothing else
"""
assert Mentions.extract(body) == []
end
test "ignores an email address and a path" do
assert Mentions.extract("mail me at someone@octocat or see docs/@octocat") == []
end
test "accepts inner hyphens but not a trailing one" do
assert Mentions.extract("@open-agents-inc") == ["open-agents-inc"]
assert Mentions.extract("@octocat- done") == ["octocat"]
end
test "returns an empty list for a blank body" do
assert Mentions.extract(nil) == []
assert Mentions.extract("") == []
end
end
describe "subscriptions" do
setup do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
issue = issue_fixture(repository, %{title: "a title"})
%{author: author, repository: repository, issue: issue}
end
test "an author follows the issue they opened", %{repository: repository} do
opener = repository_user_fixture("opener-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, opener, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "opened by me"}, opener)
assert Notifications.subscribed?(issue, opener)
end
test "a commenter follows the issue they commented on", %{
issue: issue,
repository: repository
} do
commenter = repository_user_fixture("commenter-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, commenter, "contributor")
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "looking"}, commenter)
assert Notifications.subscribed?(issue, commenter)
end
test "unsubscribing survives commenting again", %{issue: issue, repository: repository} do
muter = repository_user_fixture("muter-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, muter, "contributor")
{:ok, _subscription} = Notifications.unsubscribe(issue, muter)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "one more"}, muter)
refute Notifications.subscribed?(issue, muter)
end
test "an explicit subscribe overrides an earlier mute", %{issue: issue, author: author} do
{:ok, _muted} = Notifications.unsubscribe(issue, author)
{:ok, _resubscribed} = Notifications.subscribe(issue, author, "manual")
assert Notifications.subscribed?(issue, author)
end
end
describe "comment fan-out" do
setup do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
%{author: author, watcher: watcher, repository: repository, issue: issue}
end
test "a subscriber hears about a new comment", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "a first look"}, watcher)
assert [notification] = Notifications.list_notifications(author)
assert notification.kind == "issue_comment"
assert notification.issue_id == issue.id
assert notification.actor_login == watcher.github_login
assert is_nil(notification.read_at)
end
test "the commenter is never notified about their own comment", %{
watcher: watcher,
issue: issue
} do
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "talking to myself"}, watcher)
assert Notifications.list_notifications(watcher) == []
end
test "a mention notifies somebody who never touched the issue", %{
repository: repository,
author: author,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "viewer")
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{stranger.github_login} take a look"}, author)
assert [notification] = Notifications.list_notifications(stranger)
assert notification.kind == "mention"
end
test "a mention also starts a subscription", %{
repository: repository,
author: author,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "viewer")
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{stranger.github_login} ping"}, author)
assert Notifications.subscribed?(issue, stranger)
end
test "a subscriber who is also mentioned gets exactly one record, as a mention", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{author.github_login} over to you"}, watcher)
assert [notification] = Notifications.list_notifications(author)
assert notification.kind == "mention"
end
test "a mention in the issue body notifies on open", %{
repository: repository,
author: author
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "viewer")
{:ok, issue} =
Issues.create_issue(
repository,
%{title: "needs review", body: "@#{stranger.github_login} thoughts?"},
author
)
assert [notification] = Notifications.list_notifications(stranger)
assert notification.kind == "mention"
assert notification.issue_id == issue.id
assert is_nil(notification.comment_id)
end
test "a mention of a login that is not an account notifies nobody", %{
author: author,
issue: issue
} do
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@nobody-has-this-login here"}, author)
assert Notifications.list_notifications(author) == []
end
end
describe "authorization" do
test "a private repository never notifies a reader who lost membership" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
reader = repository_user_fixture("reader-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author, %{visibility: "private"})
{:ok, _member} = Repositories.add_member(repository, reader, "viewer")
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, author)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "an update"}, reader)
{:ok, _second} = Issues.create_comment(issue, %{"body" => "another update"}, author)
assert [_notification] = Notifications.list_notifications(reader)
:ok = Repositories.remove_member(repository, author, reader.id)
assert Notifications.list_notifications(reader) == []
assert Notifications.unread_count(reader) == 0
end
test "a mention in a private repository never reaches a non-member" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
outsider = repository_user_fixture("outsider-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author, %{visibility: "private"})
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, author)
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{outsider.github_login} look"}, author)
assert Notifications.list_notifications(outsider) == []
assert Notifications.unread_count(outsider) == 0
end
test "a mention in a public repository reaches a non-member" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
outsider = repository_user_fixture("outsider-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author, %{visibility: "public"})
{:ok, issue} = Issues.create_issue(repository, %{title: "open to all"}, author)
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{outsider.github_login} look"}, author)
assert [notification] = Notifications.list_notifications(outsider)
assert notification.kind == "mention"
end
test "one account cannot mark another account's notification read" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
intruder = repository_user_fixture("intruder-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "hello"}, watcher)
assert [notification] = Notifications.list_notifications(author)
assert {:ok, 0} = Notifications.mark_read(intruder, notification.id)
assert Notifications.unread_count(author) == 1
end
test "a malformed identifier marks nothing read" do
user = repository_user_fixture("user-#{System.unique_integer([:positive])}")
assert {:ok, 0} = Notifications.mark_read(user, "not-a-uuid")
end
end
describe "idempotency" do
test "replaying the same fan-out writes one record" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
{:ok, comment} = Issues.create_comment(issue, %{"body" => "hello"}, watcher)
assert [_one] = Notifications.list_notifications(author)
Notifications.comment_created(issue, comment, watcher)
Notifications.comment_created(issue, comment, watcher)
assert [_still_one] = Notifications.list_notifications(author)
assert Notifications.unread_count(author) == 1
end
test "a replay after the record was read does not resurrect it as unread" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
{:ok, comment} = Issues.create_comment(issue, %{"body" => "hello"}, watcher)
{:ok, 1} = Notifications.mark_all_read(author)
assert Notifications.unread_count(author) == 0
Notifications.comment_created(issue, comment, watcher)
assert Notifications.unread_count(author) == 0
end
test "two different comments produce two records" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
{:ok, _first} = Issues.create_comment(issue, %{"body" => "one"}, watcher)
{:ok, _second} = Issues.create_comment(issue, %{"body" => "two"}, watcher)
assert Notifications.unread_count(author) == 2
end
end
describe "preferences" do
setup do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
%{author: author, watcher: watcher, repository: repository, issue: issue}
end
test "every category but label changes is on for an account that never chose", %{
author: author
} do
preferences = Notifications.preferences(author)
assert preferences.mentions_enabled
assert preferences.issue_comments_enabled
assert preferences.assignments_enabled
assert preferences.issue_activity_enabled
refute preferences.label_changes_enabled
refute Notifications.preferences_recorded?(author)
end
test "turning issue comments off stops subscriber delivery", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _preferences} =
Notifications.update_preferences(author, %{issue_comments_enabled: false})
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "an update"}, watcher)
assert Notifications.list_notifications(author) == []
end
test "turning issue comments off leaves mentions working", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _preferences} =
Notifications.update_preferences(author, %{issue_comments_enabled: false})
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{author.github_login} still here"}, watcher)
assert [notification] = Notifications.list_notifications(author)
assert notification.kind == "mention"
end
test "turning mentions off stops mention delivery", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _preferences} = Notifications.update_preferences(author, %{mentions_enabled: false})
{:ok, _muted} = Notifications.unsubscribe(issue, author)
{:ok, _comment} =
Issues.create_comment(issue, %{"body" => "@#{author.github_login} hello"}, watcher)
assert Notifications.list_notifications(author) == []
end
test "a second update rewrites the same row", %{author: author} do
{:ok, _first} = Notifications.update_preferences(author, %{mentions_enabled: false})
{:ok, second} = Notifications.update_preferences(author, %{mentions_enabled: true})
assert second.mentions_enabled
assert Notifications.preferences_recorded?(author)
assert Notifications.preferences(author).mentions_enabled
end
end
describe "reading" do
setup do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
%{author: author, watcher: watcher, issue: issue}
end
test "marking one read leaves the rest unread", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _first} = Issues.create_comment(issue, %{"body" => "one"}, watcher)
{:ok, _second} = Issues.create_comment(issue, %{"body" => "two"}, watcher)
[newest | _rest] = Notifications.list_notifications(author)
assert {:ok, 1} = Notifications.mark_read(author, newest.id)
assert Notifications.unread_count(author) == 1
end
test "marking the same record read twice counts once", %{
author: author,
watcher: watcher,
issue: issue
} do
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "one"}, watcher)
[notification] = Notifications.list_notifications(author)
assert {:ok, 1} = Notifications.mark_read(author, notification.id)
assert {:ok, 0} = Notifications.mark_read(author, notification.id)
end
test "an anonymous reader has an empty inbox" do
assert Notifications.list_notifications(nil) == []
assert Notifications.unread_count(nil) == 0
end
end
describe "issue event fan-out" do
setup do
actor = repository_user_fixture("actor-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(actor)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, watcher)
%{actor: actor, watcher: watcher, repository: repository, issue: issue}
end
test "closing an issue reaches a subscriber", %{
actor: actor,
watcher: watcher,
issue: issue
} do
{:ok, _closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
assert [notification] = Notifications.list_notifications(watcher)
assert notification.kind == "state_changed"
assert notification.issue_id == issue.id
assert notification.actor_login == actor.github_login
assert is_nil(notification.comment_id)
end
test "reopening reaches a subscriber as a second record", %{
actor: actor,
watcher: watcher,
issue: issue
} do
{:ok, closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
{:ok, _reopened} = Issues.update_issue(closed, %{"state" => "open"}, actor)
assert [_reopen, _close] = Notifications.list_notifications(watcher)
assert Notifications.unread_count(watcher) == 2
end
test "editing the title announces nothing", %{actor: actor, watcher: watcher, issue: issue} do
{:ok, _renamed} = Issues.update_issue(issue, %{"title" => "a better title"}, actor)
assert Notifications.list_notifications(watcher) == []
end
test "labelling reaches a subscriber who asked for label changes", %{
actor: actor,
watcher: watcher,
issue: issue
} do
{:ok, _preferences} =
Notifications.update_preferences(watcher, %{label_changes_enabled: true})
{:ok, labelled} = Issues.add_labels(issue, ["bug"], actor)
assert [notification] = Notifications.list_notifications(watcher)
assert notification.kind == "labeled"
{:ok, _unlabelled} = Issues.remove_label(labelled, "bug", actor)
assert kinds(watcher) == ~w(labeled unlabeled)
end
test "labelling reaches nobody by default", %{actor: actor, watcher: watcher, issue: issue} do
{:ok, _labelled} = Issues.add_labels(issue, ["bug"], actor)
assert Notifications.list_notifications(watcher) == []
end
test "assignment reaches the assignee who never followed the issue", %{
actor: actor,
repository: repository,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "contributor")
refute Notifications.subscribed?(issue, stranger)
{:ok, _assigned} = Issues.add_assignees(issue, [stranger.github_login], actor)
assert [notification] = Notifications.list_notifications(stranger)
assert notification.kind == "assigned"
assert notification.actor_login == actor.github_login
end
test "assignment starts the assignee following the issue", %{
actor: actor,
repository: repository,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "contributor")
{:ok, assigned} = Issues.add_assignees(issue, [stranger.github_login], actor)
assert Notifications.subscribed?(assigned, stranger)
{:ok, _comment} = Issues.create_comment(assigned, %{"body" => "over to you"}, actor)
assert kinds(stranger) == ~w(assigned issue_comment)
end
test "unassignment reaches the account taken off", %{
actor: actor,
repository: repository,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "contributor")
{:ok, assigned} = Issues.add_assignees(issue, [stranger.github_login], actor)
{:ok, _removed} = Issues.remove_assignees(assigned, [stranger.github_login], actor)
assert kinds(stranger) == ~w(assigned unassigned)
end
test "assigning yourself announces nothing to yourself", %{
watcher: watcher,
issue: issue
} do
{:ok, _assigned} = Issues.add_assignees(issue, [watcher.github_login], watcher)
assert Notifications.list_notifications(watcher) == []
end
test "turning assignments off stops delivery to the assignee", %{
actor: actor,
repository: repository,
issue: issue
} do
stranger = repository_user_fixture("stranger-#{System.unique_integer([:positive])}")
{:ok, _member} = Repositories.add_member(repository, stranger, "contributor")
{:ok, _preferences} =
Notifications.update_preferences(stranger, %{assignments_enabled: false})
{:ok, _assigned} = Issues.add_assignees(issue, [stranger.github_login], actor)
assert Notifications.list_notifications(stranger) == []
end
test "turning issue activity off stops state changes but leaves comments", %{
actor: actor,
watcher: watcher,
issue: issue
} do
{:ok, _preferences} =
Notifications.update_preferences(watcher, %{issue_activity_enabled: false})
{:ok, closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
{:ok, _comment} = Issues.create_comment(closed, %{"body" => "done"}, actor)
assert [%{kind: "issue_comment"}] = Notifications.list_notifications(watcher)
end
test "an unattributed close still reaches the subscriber", %{
watcher: watcher,
issue: issue
} do
{:ok, _closed} = Issues.update_issue(issue, %{"state" => "closed"})
assert [notification] = Notifications.list_notifications(watcher)
assert notification.kind == "state_changed"
assert is_nil(notification.actor_login)
end
end
describe "issue event authorization" do
test "a private repository never notifies a subscriber who is no longer a member" do
owner = repository_user_fixture("owner-#{System.unique_integer([:positive])}")
former = repository_user_fixture("former-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(owner, %{visibility: "private"})
{:ok, _member} = Repositories.add_member(repository, former, "contributor")
# Following the issue while a member, then losing membership. The
# subscription row survives, so nothing but fan-out's own predicate
# stands between this account and a record about a private issue.
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, former)
:ok = Repositories.remove_member(repository, owner, former.id)
assert Notifications.subscribed?(issue, former)
{:ok, _closed} = Issues.update_issue(issue, %{"state" => "closed"}, owner)
assert Notifications.list_notifications(former) == []
assert Notifications.unread_count(former) == 0
end
test "assigning an account that cannot write the repository is refused before fan-out" do
owner = repository_user_fixture("owner-#{System.unique_integer([:positive])}")
outsider = repository_user_fixture("outsider-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(owner, %{visibility: "private"})
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, owner)
# `OpenAgents.Issues.UnknownReference` rather than `Ecto.NoResultsError`:
# a login this repository cannot assign is a rejected field, and the API
# answers `422` naming it rather than the `404` a repository the caller
# cannot see answers with. The refusal is what matters here, and it still
# happens before anything is announced.
assert_raise OpenAgents.Issues.UnknownReference, fn ->
Issues.update_issue(
issue,
%{"assignees" => [%{"login" => outsider.github_login}]},
owner
)
end
assert Notifications.list_notifications(outsider) == []
end
test "an event notification disappears when the recipient's membership ends" do
owner = repository_user_fixture("owner-#{System.unique_integer([:positive])}")
reader = repository_user_fixture("reader-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(owner, %{visibility: "private"})
{:ok, _member} = Repositories.add_member(repository, reader, "viewer")
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, reader)
{:ok, _closed} = Issues.update_issue(issue, %{"state" => "closed"}, owner)
assert [%{kind: "state_changed"}] = Notifications.list_notifications(reader)
:ok = Repositories.remove_member(repository, owner, reader.id)
assert Notifications.list_notifications(reader) == []
assert Notifications.unread_count(reader) == 0
end
end
describe "issue event idempotency" do
test "replaying the same derivation writes one record" do
actor = repository_user_fixture("actor-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(actor)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, watcher)
{:ok, closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
assert Notifications.unread_count(watcher) == 1
Notifications.issue_updated(issue, closed, actor)
Notifications.issue_updated(issue, closed, actor)
assert Notifications.unread_count(watcher) == 1
end
test "a replay after the record was read does not resurrect it as unread" do
actor = repository_user_fixture("actor-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(actor)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, watcher)
{:ok, closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
{:ok, 1} = Notifications.mark_all_read(watcher)
Notifications.issue_updated(issue, closed, actor)
assert Notifications.unread_count(watcher) == 0
end
test "closing an already closed issue derives nothing" do
actor = repository_user_fixture("actor-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(actor)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, watcher)
{:ok, closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
{:ok, _again} = Issues.update_issue(closed, %{"state" => "closed"}, actor)
assert Notifications.unread_count(watcher) == 1
end
end
describe "the unread count" do
test "counts past the page the inbox loads" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a busy thread"}, author)
over_a_page = Notifications.per_page() + 5
Enum.each(1..over_a_page, fn n ->
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "note #{n}"}, watcher)
end)
# The count is an aggregate over the same authorized query the inbox
# composes, not the size of what the inbox loaded. `length/1` over
# `list_notifications/1` would stop at one page and report 50 for ever
# after, which is the mistake this repository already removed from the
# homepage once.
assert length(Notifications.list_notifications(author)) == Notifications.per_page()
assert Notifications.unread_count(author) == over_a_page
end
test "an unreadable repository is not counted" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
reader = repository_user_fixture("reader-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author, %{visibility: "private"})
{:ok, _member} = Repositories.add_member(repository, reader, "viewer")
{:ok, issue} = Issues.create_issue(repository, %{title: "internal"}, reader)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "an update"}, author)
assert Notifications.unread_count(reader) == 1
:ok = Repositories.remove_member(repository, author, reader.id)
assert Notifications.unread_count(reader) == 0
end
end
describe "unread announcements" do
test "a delivered notification announces the recipient's new count" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
:ok = Notifications.subscribe_unread(author)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "hello"}, watcher)
author_id = author.id
assert_receive {:unread_notifications_changed, ^author_id}
end
test "a derived issue event announces the subscriber's new count" do
actor = repository_user_fixture("actor-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(actor)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, watcher)
:ok = Notifications.subscribe_unread(watcher)
{:ok, _closed} = Issues.update_issue(issue, %{"state" => "closed"}, actor)
watcher_id = watcher.id
assert_receive {:unread_notifications_changed, ^watcher_id}
end
test "marking read announces the reader's new count" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
watcher = repository_user_fixture("watcher-#{System.unique_integer([:positive])}")
repository = repository_with_member_fixture(author)
{:ok, _member} = Repositories.add_member(repository, watcher, "contributor")
{:ok, issue} = Issues.create_issue(repository, %{title: "a title"}, author)
{:ok, _comment} = Issues.create_comment(issue, %{"body" => "hello"}, watcher)
:ok = Notifications.subscribe_unread(author)
[notification] = Notifications.list_notifications(author)
{:ok, 1} = Notifications.mark_read(author, notification.id)
author_id = author.id
assert_receive {:unread_notifications_changed, ^author_id}
end
test "marking nothing read announces nothing" do
author = repository_user_fixture("author-#{System.unique_integer([:positive])}")
:ok = Notifications.subscribe_unread(author)
{:ok, 0} = Notifications.mark_all_read(author)
refute_receive {:unread_notifications_changed, _user_id}, 50
end
end
# Two events landing in the same second share an `inserted_at`, and the
# tiebreak is a random UUID, so asserting on delivery order would assert on
# nothing. Compare the set of kinds instead.
defp kinds(user) do
user |> Notifications.list_notifications() |> Enum.map(& &1.kind) |> Enum.sort()
end
end