Files
2024-02-15 17:01:50 +00:00

833 lines
31 KiB
Python

from datetime import datetime, timedelta, timezone
from functools import cached_property
from django.urls import reverse
from rest_framework.exceptions import ErrorDetail
from sentry.api.serializers.rest_framework.release import ReleaseWithVersionSerializer
from sentry.constants import BAD_RELEASE_CHARS, MAX_VERSION_LENGTH
from sentry.models.commitauthor import CommitAuthor
from sentry.models.commitfilechange import CommitFileChange
from sentry.models.environment import Environment
from sentry.models.orgauthtoken import OrgAuthToken
from sentry.models.release import Release, ReleaseProject
from sentry.models.releasecommit import ReleaseCommit
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
from sentry.models.repository import Repository
from sentry.silo import SiloMode
from sentry.testutils.cases import APITestCase, ReleaseCommitPatchTest, TestCase
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
from sentry.testutils.skips import requires_snuba
from sentry.utils.security.orgauthtoken_token import generate_token, hash_token
pytestmark = [requires_snuba]
@region_silo_test
class ProjectReleaseListTest(APITestCase):
def test_simple(self):
self.login_as(user=self.user)
team = self.create_team()
project1 = self.create_project(teams=[team], name="foo")
project2 = self.create_project(teams=[team], name="bar")
release1 = Release.objects.create(
organization_id=project1.organization_id,
version="1",
date_added=datetime(2013, 8, 13, 3, 8, 24, 880386),
)
release1.add_project(project1)
ReleaseProject.objects.filter(project=project1, release=release1).update(new_groups=5)
release2 = Release.objects.create(
organization_id=project1.organization_id,
version="2",
date_added=datetime(2013, 8, 14, 3, 8, 24, 880386),
)
release2.add_project(project1)
release3 = Release.objects.create(
organization_id=project1.organization_id,
version="3",
date_added=datetime(2013, 8, 12, 3, 8, 24, 880386),
date_released=datetime(2013, 8, 15, 3, 8, 24, 880386),
user_agent="my_agent",
)
release3.add_project(project1)
release4 = Release.objects.create(organization_id=project2.organization_id, version="4")
release4.add_project(project2)
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project1.organization.slug, "project_slug": project1.slug},
)
response = self.client.get(url, format="json")
assert response.status_code == 200, response.content
assert len(response.data) == 3
assert response.data[0]["version"] == release3.version
assert response.data[0]["userAgent"] == "my_agent"
assert response.data[1]["version"] == release2.version
assert response.data[2]["version"] == release1.version
assert response.data[2]["newGroups"] == 5
def test_query_filter(self):
self.login_as(user=self.user)
team = self.create_team()
project = self.create_project(teams=[team], name="foo")
release = Release.objects.create(
organization_id=project.organization_id,
version="foobar",
date_added=datetime(2013, 8, 13, 3, 8, 24, 880386),
)
release.add_project(project)
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.get(url + "?query=foo", format="json")
assert response.status_code == 200, response.content
assert len(response.data) == 1
assert response.data[0]["version"] == release.version
response = self.client.get(url + "?query=baz", format="json")
assert response.status_code == 200, response.content
assert len(response.data) == 0
release = Release.objects.create(
organization_id=project.organization_id,
version="foo.bar-1.0.0",
date_added=datetime(2013, 8, 14, 3, 8, 24, 880386),
)
release.add_project(project)
response = self.client.get(url + "?query=1", format="json")
assert response.status_code == 200, response.content
assert len(response.data) == 1
@region_silo_test
class ProjectReleaseListEnvironmentsTest(APITestCase):
def setUp(self):
self.login_as(user=self.user)
self.datetime = datetime(2013, 8, 13, 3, 8, 24, tzinfo=timezone.utc)
team = self.create_team()
project1 = self.create_project(teams=[team], name="foo")
project2 = self.create_project(teams=[team], name="bar")
env1 = self.make_environment("prod", project1)
env2 = self.make_environment("staging", project2)
env3 = self.make_environment("test", project1)
release1 = Release.objects.create(
organization_id=project1.organization_id, version="1", date_added=self.datetime
)
release1.add_project(project1)
ReleaseProjectEnvironment.objects.create(
release_id=release1.id,
project_id=project1.id,
environment_id=env1.id,
first_seen=self.datetime,
last_seen=self.datetime,
new_issues_count=1,
)
release2 = Release.objects.create(
organization_id=project2.organization_id, version="2", date_added=self.datetime
)
release2.add_project(project2)
ReleaseProjectEnvironment.objects.create(
release_id=release2.id,
project_id=project2.id,
environment_id=env2.id,
first_seen=self.datetime,
last_seen=self.datetime + timedelta(seconds=60),
new_issues_count=6,
)
release3 = Release.objects.create(
organization_id=project1.organization_id,
version="3",
date_added=self.datetime,
date_released=self.datetime,
)
release3.add_project(project1)
ReleaseProjectEnvironment.objects.create(
release_id=release3.id,
project_id=project1.id,
environment_id=env3.id,
first_seen=self.datetime,
last_seen=self.datetime + timedelta(days=20),
new_issues_count=2,
)
release4 = Release.objects.create(organization_id=project2.organization_id, version="4")
release4.add_project(project2)
self.project1 = project1
self.project2 = project2
self.release1 = release1
self.release2 = release2
self.release3 = release3
self.release4 = release4
self.env1 = env1
self.env2 = env2
self.env3 = env3
def make_environment(self, name, project):
env = Environment.objects.create(organization_id=project.organization_id, name=name)
env.add_project(project)
return env
def assert_releases(self, response, releases):
assert response.status_code == 200, response.content
assert len(response.data) == len(releases)
response_versions = sorted(r["version"] for r in response.data)
releases_versions = sorted(r.version for r in releases)
assert response_versions == releases_versions
def assert_release_details(self, release, new_issues_count, first_seen, last_seen):
assert release["newGroups"] == new_issues_count
assert release["firstEvent"] == first_seen
assert release["lastEvent"] == last_seen
def test_environments_filter(self):
url = reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project1.organization.slug,
"project_slug": self.project1.slug,
},
)
response = self.client.get(url + "?environment=" + self.env1.name, format="json")
self.assert_releases(response, [self.release1])
response = self.client.get(url + "?environment=" + self.env2.name, format="json")
self.assert_releases(response, [])
response = self.client.get(url + "?environment=" + self.env3.name, format="json")
self.assert_releases(response, [self.release3])
url = reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project2.organization.slug,
"project_slug": self.project2.slug,
},
)
response = self.client.get(url + "?environment=" + self.env2.name, format="json")
self.assert_releases(response, [self.release2])
def test_all_environments(self):
url = reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project1.organization.slug,
"project_slug": self.project1.slug,
},
)
response = self.client.get(url, format="json")
self.assert_releases(response, [self.release1, self.release3])
def test_invalid_environment(self):
url = reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project1.organization.slug,
"project_slug": self.project1.slug,
},
)
response = self.client.get(url + "?environment=" + "invalid_environment", format="json")
self.assert_releases(response, [])
def test_new_issues_last_seen_first_seen(self):
def sort_releases_by_version(releases):
return sorted(releases, key=lambda release: release["version"])
url = reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project1.organization.slug,
"project_slug": self.project1.slug,
},
)
ReleaseProjectEnvironment.objects.create(
release_id=self.release1.id,
project_id=self.project1.id,
environment_id=self.env3.id,
first_seen=self.datetime + timedelta(seconds=120),
last_seen=self.datetime + timedelta(seconds=700),
new_issues_count=7,
)
# TODO(LB): This is testing all environments but it will not work
# given what I did with the release serializer
# it will instead rely on tagstore. Not sure how to fix this.
# response = self.client.get(url, format='json')
# self.assert_releases(response, [self.release1, self.release3])
# releases = sort_releases_by_version(response.data)
# self.assert_release_details(
# release=releases[0],
# new_issues_count=8,
# first_seen=self.datetime,
# last_seen=self.datetime + timedelta(seconds=700),
# )
# self.assert_release_details(
# release=releases[1],
# new_issues_count=2,
# first_seen=self.datetime,
# last_seen=self.datetime + timedelta(days=20),
# )
response = self.client.get(url + "?environment=" + self.env1.name, format="json")
self.assert_releases(response, [self.release1])
releases = sort_releases_by_version(response.data)
self.assert_release_details(
release=releases[0],
new_issues_count=1,
first_seen=self.datetime,
last_seen=self.datetime,
)
response = self.client.get(url + "?environment=" + self.env3.name, format="json")
self.assert_releases(response, [self.release1, self.release3])
releases = sort_releases_by_version(response.data)
self.assert_release_details(
release=releases[0],
new_issues_count=7,
first_seen=self.datetime + timedelta(seconds=120),
last_seen=self.datetime + timedelta(seconds=700),
)
self.assert_release_details(
release=releases[1],
new_issues_count=2,
first_seen=self.datetime,
last_seen=self.datetime + timedelta(days=20),
)
@region_silo_test
class ProjectReleaseCreateTest(APITestCase):
def test_minimal(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(
url,
data={"version": "1.2.1"},
HTTP_USER_AGENT="sentry-cli/2.77.4",
)
assert response.status_code == 201, response.content
assert response.data["version"]
release = Release.objects.get(
version=response.data["version"],
user_agent="sentry-cli/2.77.4",
)
assert not release.owner_id
assert release.organization == project.organization
assert release.projects.first() == project
def test_ios_release(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(url, data={"version": "1.2.1 (123)"})
assert response.status_code == 201, response.content
assert response.data["version"]
release = Release.objects.get(version=response.data["version"])
assert not release.owner_id
assert release.organization == project.organization
assert release.projects.first() == project
def test_duplicate(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
release = Release.objects.create(version="1.2.1", organization_id=project.organization_id)
release.add_project(project)
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(url, data={"version": "1.2.1"})
assert response.status_code == 208, response.content
def test_duplicate_across_org(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
release = Release.objects.create(version="1.2.1", organization_id=project.organization_id)
release.add_project(project)
project2 = self.create_project(name="bar", organization=project.organization)
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project2.organization.slug, "project_slug": project2.slug},
)
response = self.client.post(url, data={"version": "1.2.1"})
# since project2 was added, should be 201
assert response.status_code == 201, response.content
assert (
Release.objects.filter(version="1.2.1", organization_id=project.organization_id).count()
== 1
)
assert ReleaseProject.objects.get(release=release, project=project)
assert ReleaseProject.objects.get(release=release, project=project2)
def test_version_whitespace(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(url, data={"version": "1.2.3\n"})
assert response.status_code == 400, response.content
response = self.client.post(url, data={"version": "\n1.2.3"})
assert response.status_code == 400, response.content
response = self.client.post(url, data={"version": "1.\n2.3"})
assert response.status_code == 400, response.content
response = self.client.post(url, data={"version": "1.2.3\f"})
assert response.status_code == 400, response.content
response = self.client.post(url, data={"version": "1.2.3\t"})
assert response.status_code == 400, response.content
response = self.client.post(url, data={"version": "1.2.3"})
assert response.status_code == 201, response.content
assert response.data["version"] == "1.2.3"
release = Release.objects.get(
organization_id=project.organization_id, version=response.data["version"]
)
assert not release.owner_id
def test_features(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(url, data={"version": "1.2.1", "owner": self.user.email})
assert response.status_code == 201, response.content
assert response.data["version"]
release = Release.objects.get(
organization_id=project.organization_id, version=response.data["version"]
)
assert release.owner_id == self.user.id
def test_commits(self):
self.login_as(user=self.user)
project = self.create_project(name="foo")
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": project.organization.slug, "project_slug": project.slug},
)
response = self.client.post(
url, data={"version": "1.2.1", "commits": [{"id": "a" * 40}, {"id": "b" * 40}]}
)
assert response.status_code == 201, (response.status_code, response.content)
assert response.data["version"]
release = Release.objects.get(
organization_id=project.organization_id, version=response.data["version"]
)
rc_list = list(
ReleaseCommit.objects.filter(release=release)
.select_related("commit", "commit__author")
.order_by("order")
)
assert len(rc_list) == 2
for rc in rc_list:
assert rc.organization_id
def test_org_auth_token(self):
org = self.create_organization()
org2 = self.create_organization()
team1 = self.create_team(organization=org)
project1 = self.create_project(teams=[team1], organization=org)
release1 = Release.objects.create(
organization_id=org.id, version="1", date_added=datetime(2013, 8, 13, 3, 8, 24, 880386)
)
release1.add_project(project1)
url = reverse(
"sentry-api-0-project-releases",
kwargs={"organization_slug": org.slug, "project_slug": project1.slug},
)
# test right org, wrong permissions level
with assume_test_silo_mode(SiloMode.CONTROL):
bad_token_str = generate_token(org.slug, "")
OrgAuthToken.objects.create(
organization_id=org.id,
name="token 1",
token_hashed=hash_token(bad_token_str),
token_last_characters="ABCD",
scope_list=[],
date_last_used=None,
)
response = self.client.post(
url,
data={"version": "1.2.1"},
HTTP_AUTHORIZATION=f"Bearer {bad_token_str}",
)
assert response.status_code == 403
# test wrong org, right permissions level
with assume_test_silo_mode(SiloMode.CONTROL):
wrong_org_token_str = generate_token(org2.slug, "")
OrgAuthToken.objects.create(
organization_id=org2.id,
name="token 1",
token_hashed=hash_token(wrong_org_token_str),
token_last_characters="ABCD",
scope_list=["org:ci"],
date_last_used=None,
)
response = self.client.post(
url,
data={"version": "1.2.1"},
HTTP_AUTHORIZATION=f"Bearer {wrong_org_token_str}",
)
assert response.status_code == 403
# test right org, right permissions level
with assume_test_silo_mode(SiloMode.CONTROL):
good_token_str = generate_token(org.slug, "")
OrgAuthToken.objects.create(
organization_id=org.id,
name="token 1",
token_hashed=hash_token(good_token_str),
token_last_characters="ABCD",
scope_list=["org:ci"],
date_last_used=None,
)
with outbox_runner():
response = self.client.post(
url,
data={"version": "1.2.1"},
HTTP_AUTHORIZATION=f"Bearer {good_token_str}",
)
assert response.status_code == 201, response.content
# Make sure org token usage was updated
with assume_test_silo_mode(SiloMode.CONTROL):
org_token = OrgAuthToken.objects.get(token_hashed=hash_token(good_token_str))
assert org_token.date_last_used is not None
assert org_token.project_last_used_id == project1.id
@region_silo_test
class ProjectReleaseCreateCommitPatch(ReleaseCommitPatchTest):
@cached_property
def url(self):
return reverse(
"sentry-api-0-project-releases",
kwargs={
"organization_slug": self.project.organization.slug,
"project_slug": self.project.slug,
},
)
def test_commits_with_patch_set(self):
response = self.client.post(
self.url,
data={
"version": "2d1ab93fe4bb42db80890f01f8358fc9f8fbff3b",
"projects": [self.project.slug],
"commits": [
{
"patch_set": [
{"path": "hello.py", "type": "M"},
{"path": "templates/hola.html", "type": "D"},
],
"repository": "laurynsentry/helloworld",
"author_email": "[email protected]",
"timestamp": "2018-11-29T18:50:28+03:00",
"author_name": "Lauryn Brown",
"message": "made changes to hello.",
"id": "2d1ab93fe4bb42db80890f01f8358fc9f8fbff3b",
},
{
"patch_set": [
{"path": "templates/hello.html", "type": "M"},
{"path": "templates/goodbye.html", "type": "A"},
],
"repository": "laurynsentry/helloworld",
"author_email": "[email protected]",
"timestamp": "2018-11-30T22:51:14+03:00",
"author_name": "Lauryn Brown",
"message": "Changed release",
"id": "be2fe070f6d1b8a572b67defc87af2582f9b0d78",
},
],
},
)
assert response.status_code == 201, (response.status_code, response.content)
assert response.data["version"]
release = Release.objects.get(organization_id=self.org.id, version=response.data["version"])
repo = Repository.objects.get(organization_id=self.org.id, name="laurynsentry/helloworld")
assert repo.provider is None
rc_list = list(
ReleaseCommit.objects.filter(release=release).select_related("commit", "commit__author")
)
assert len(rc_list) == 2
for rc in rc_list:
assert rc.organization_id
author = CommitAuthor.objects.get(
organization_id=self.org.id, email="[email protected]"
)
assert author.name == "Lauryn Brown"
commits = [rc.commit for rc in rc_list]
commits.sort(key=lambda c: c.date_added)
self.assert_commit(
commit=commits[0],
repo_id=repo.id,
key="2d1ab93fe4bb42db80890f01f8358fc9f8fbff3b",
author_id=author.id,
message="made changes to hello.",
)
self.assert_commit(
commit=commits[1],
repo_id=repo.id,
key="be2fe070f6d1b8a572b67defc87af2582f9b0d78",
author_id=author.id,
message="Changed release",
)
file_changes = CommitFileChange.objects.filter(organization_id=self.org.id).order_by(
"filename"
)
self.assert_file_change(file_changes[0], "M", "hello.py", commits[0].id)
self.assert_file_change(file_changes[1], "A", "templates/goodbye.html", commits[1].id)
self.assert_file_change(file_changes[2], "M", "templates/hello.html", commits[1].id)
self.assert_file_change(file_changes[3], "D", "templates/hola.html", commits[0].id)
def test_invalid_patch_type(self):
response = self.client.post(
self.url,
data={
"version": "2d1ab93fe4bb42db80890f01f8358fc9f8fbff3b",
"projects": [self.project.slug],
"commits": [
{
"patch_set": [
{"path": "hello.py", "type": "Z"},
{"path": "templates/hola.html", "type": "D"},
],
"repository": "laurynsentry/helloworld",
"author_email": "[email protected]",
"timestamp": "2018-11-29T18:50:28+03:00",
"author_name": "Lauryn Brown",
"message": "made changes to hello.",
"id": "2d1ab93fe4bb42db80890f01f8358fc9f8fbff3b",
}
],
},
)
assert response.status_code == 400
assert response.json() == {
"commits": {
"patch_set": {
"type": [
"Commit patch_set type Z is not supported.",
]
}
}
}
@region_silo_test
class ReleaseSerializerTest(TestCase):
def setUp(self):
super().setUp()
self.version = "1234567890"
self.repo_name = "repo/name"
self.repo2_name = "repo2/name"
self.commits = [{"id": "a" * 40}, {"id": "b" * 40}]
self.ref = "master"
self.url = "https://example.com"
self.dateReleased = "1000-10-10T06:06"
def test_simple(self):
serializer = ReleaseWithVersionSerializer(
data={
"version": self.version,
"owner": self.user.username,
"ref": self.ref,
"url": self.url,
"dateReleased": self.dateReleased,
"commits": self.commits,
},
context={"organization": self.organization},
)
assert serializer.is_valid()
assert sorted(serializer.fields.keys()) == sorted(
["version", "owner", "ref", "url", "dateReleased", "commits", "status"]
)
result = serializer.validated_data
assert result["version"] == self.version
assert result["owner"]
assert result["owner"].id == self.user.id
assert result["owner"].username == self.user.username
assert result["ref"] == self.ref
assert result["url"] == self.url
assert result["dateReleased"] == datetime(1000, 10, 10, 6, 6, tzinfo=timezone.utc)
assert result["commits"] == self.commits
def test_fields_not_required(self):
serializer = ReleaseWithVersionSerializer(
data={"version": self.version},
context={"organization": self.organization},
)
assert serializer.is_valid()
def test_do_not_allow_null_commits(self):
serializer = ReleaseWithVersionSerializer(
data={"version": self.version, "commits": None},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_ref_limited_by_max_version_length(self):
serializer = ReleaseWithVersionSerializer(
data={"version": self.version, "ref": "a" * MAX_VERSION_LENGTH},
context={"organization": self.organization},
)
assert serializer.is_valid()
serializer = ReleaseWithVersionSerializer(
data={"version": self.version, "ref": "a" * (MAX_VERSION_LENGTH + 1)},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_version_limited_by_max_version_length(self):
serializer = ReleaseWithVersionSerializer(
data={"version": "a" * MAX_VERSION_LENGTH},
context={"organization": self.organization},
)
assert serializer.is_valid()
serializer = ReleaseWithVersionSerializer(
data={"version": "a" * (MAX_VERSION_LENGTH + 1)},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_version_does_not_allow_whitespace(self):
for char in BAD_RELEASE_CHARS:
serializer = ReleaseWithVersionSerializer(
data={"version": char},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_version_does_not_allow_current_dir_path(self):
serializer = ReleaseWithVersionSerializer(
data={"version": "."},
context={"organization": self.organization},
)
assert not serializer.is_valid()
serializer = ReleaseWithVersionSerializer(
data={"version": ".."},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_version_does_not_allow_null_or_empty_value(self):
serializer = ReleaseWithVersionSerializer(
data={"version": None},
context={"organization": self.organization},
)
assert not serializer.is_valid()
serializer = ReleaseWithVersionSerializer(
data={"version": ""},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_version_cannot_be_latest(self):
serializer = ReleaseWithVersionSerializer(
data={"version": "Latest"},
context={"organization": self.organization},
)
assert not serializer.is_valid()
def test_owner_must_have_org_access(self):
serializer = ReleaseWithVersionSerializer(
data={
"version": self.version,
"owner": self.create_user().username,
"ref": self.ref,
"url": self.url,
"dateReleased": self.dateReleased,
"commits": self.commits,
},
context={"organization": self.organization},
)
assert not serializer.is_valid()
assert serializer.errors == {
"owner": [ErrorDetail("User does not have access to this organization", "invalid")]
}