def test_get_project_config(default_project, insta_snapshot, full, has_ops_breakdown): # We could use the default_project fixture here, but we would like to avoid 1) hitting the db 2) creating a mock default_project.update_option("sentry:relay_pii_config", PII_CONFIG) default_project.organization.update_option("sentry:relay_pii_config", PII_CONFIG) keys = ProjectKey.objects.filter(project=default_project) with Feature( {"organizations:performance-ops-breakdown": has_ops_breakdown}): cfg = get_project_config(default_project, full_config=full, project_keys=keys) cfg = cfg.to_dict() # Remove keys that change everytime cfg.pop("lastChange") cfg.pop("lastFetch") cfg.pop("rev") # public keys change every time assert cfg.pop("projectId") == default_project.id assert len(cfg.pop("publicKeys")) == len(keys) assert cfg.pop("organizationId") == default_project.organization.id insta_snapshot(cfg)
def test_cannot_add_error_created_hook_without_flag(self): self.login_as(user=self.user) with Feature({"organizations:integrations-event-hooks": False}): app = self.create_sentry_app(name="SampleApp", organization=self.org) url = reverse("sentry-api-0-sentry-app-details", args=[app.slug]) response = self.client.put(url, data={"events": ("error",)}, format="json") assert response.status_code == 403
def test_redacted_symbol_source_secrets(self, create_audit_entry): with Feature( {"organizations:symbol-sources": True, "organizations:custom-symbol-sources": True} ): config = { "id": "honk", "name": "honk source", "layout": { "type": "native", }, "filetypes": ["pe"], "type": "http", "url": "http://honk.beep", "username": "******", "password": "******", } self.get_valid_response( self.org_slug, self.proj_slug, symbolSources=json.dumps([config]) ) assert self.project.get_option("sentry:symbol_sources") == json.dumps([config]) # redact password redacted_source = config.copy() redacted_source["password"] = {"hidden-secret": True} # check that audit entry was created with redacted password assert create_audit_entry.called call = faux.faux(create_audit_entry) assert call.kwarg_equals("data", {"sentry:symbol_sources": [redacted_source]}) self.get_valid_response( self.org_slug, self.proj_slug, symbolSources=json.dumps([redacted_source]) ) # on save the magic object should be replaced with the previously set password assert self.project.get_option("sentry:symbol_sources") == json.dumps([config])
def test_approve_requires_invite_members_feature(self, mock_invite_email): self.login_as(user=self.user) with Feature({"organizations:invite-members": False}): resp = self.get_response(self.org.slug, self.invite_request.id, approve=1) assert resp.status_code == 400 assert mock_invite_email.call_count == 0
def test_auth_setup(self, auth_log): self.auth_provider.delete() self.login_as(self.user) data = {"init": True, "provider": self.provider_name} with Feature(["organizations:sso-basic", "organizations:sso-saml2"]): setup = self.client.post(self.setup_path, data) assert setup.status_code == 302 redirect = urlparse(setup.get("Location", "")) assert redirect.path == "/sso_url" auth = self.accept_auth(follow=True) messages = list(map(lambda m: str(m), auth.context["messages"])) assert len(messages) == 2 assert messages[ 0] == "You have successfully linked your account to your SSO provider." assert messages[1].startswith( "SSO has been configured for your organization") # require 2FA disabled when saml is enabled org = Organization.objects.get(id=self.org.id) assert not org.flags.require_2fa.is_set event = AuditLogEntry.objects.get(target_object=org.id, event=AuditLogEntryEvent.ORG_EDIT, actor=self.user) assert "require_2fa to False when enabling SSO" in event.get_note() auth_log.info.assert_called_once_with( "Require 2fa disabled during sso setup", extra={"organization_id": self.org.id})
def test_redacted_symbol_source_secrets_unknown_secret(self, create_audit_entry): with Feature( {"organizations:symbol-sources": True, "organizations:custom-symbol-sources": True} ): config = { "id": "honk", "name": "honk source", "layout": { "type": "native", }, "filetypes": ["pe"], "type": "http", "url": "http://honk.beep", "username": "******", "password": "******", } self.get_valid_response( self.org_slug, self.proj_slug, symbolSources=json.dumps([config]) ) assert self.project.get_option("sentry:symbol_sources") == json.dumps([config]) # prepare new call, this secret is not known new_source = config.copy() new_source["password"] = {"hidden-secret": True} new_source["id"] = "oops" response = self.get_response( self.org_slug, self.proj_slug, symbolSources=json.dumps([new_source]) ) assert response.status_code == 400 assert json.loads(response.content) == { "symbolSources": ["Sources contain unknown hidden secret"] }
def test_auth_setup(self, auth_log): self.auth_provider.delete() self.login_as(self.user) data = {'init': True, 'provider': self.provider_name} with Feature(['organizations:sso-basic', 'organizations:sso-saml2']): setup = self.client.post(self.setup_path, data) assert setup.status_code == 302 redirect = urlparse(setup.get('Location', '')) assert redirect.path == '/sso_url' auth = self.accept_auth(follow=True) messages = map(lambda m: six.text_type(m), auth.context['messages']) assert len(messages) == 2 assert messages[ 0] == 'You have successfully linked your account to your SSO provider.' assert messages[1].startswith( 'SSO has been configured for your organization') # require 2FA disabled when saml is enabled org = Organization.objects.get(id=self.org.id) assert not org.flags.require_2fa.is_set event = AuditLogEntry.objects.get(target_object=org.id, event=AuditLogEntryEvent.ORG_EDIT, actor=self.user) assert 'require_2fa to False when enabling SAML SSO' in event.get_note( ) auth_log.info.assert_called_once_with( 'Require 2fa disabled during saml sso setup', extra={'organization_id': self.org.id})
def test_project_config_satisfaction_thresholds( default_project, insta_snapshot, has_project_transaction_threshold_overrides, has_project_transaction_threshold, ): if has_project_transaction_threshold: default_project.projecttransactionthreshold_set.create( organization=default_project.organization, threshold=500, metric=TransactionMetric.LCP.value, ) if has_project_transaction_threshold_overrides: default_project.projecttransactionthresholdoverride_set.create( organization=default_project.organization, transaction="foo", threshold=400, metric=TransactionMetric.DURATION.value, ) default_project.projecttransactionthresholdoverride_set.create( organization=default_project.organization, transaction="bar", threshold=600, metric=TransactionMetric.LCP.value, ) with Feature( { "organizations:transaction-metrics-extraction": True, } ): cfg = get_project_config(default_project, full_config=True) cfg = cfg.to_dict() insta_snapshot(cfg["config"]["transactionMetrics"]["satisfactionThresholds"])
def test_setting_dynamic_sampling_rules(self): """ Test that we can set sampling rules """ with Feature({"organizations:filters-and-sampling": True}): resp = self.client.put(self.path, data={"dynamicSampling": _dyn_sampling_data()}) assert resp.status_code == 200, resp.content assert self.project.get_option("sentry:dynamic_sampling") == _dyn_sampling_data()
def test_error_missing_feature(client, default_project): group = Group.objects.create(project=default_project) with Feature({"organizations:grouping-tree-ui": False}): response = client.get(f"/api/0/issues/{group.id}/grouping/levels/", format="json") assert response.status_code == 403 assert response.data["detail"]["code"] == "missing_feature"
def test_sources_no_feature(default_project): features = {"organizations:symbol-sources": False, "organizations:custom-symbol-sources": False} with Feature(features): sources = get_sources_for_project(default_project) assert len(sources) == 1 assert sources[0]["type"] == "sentry" assert sources[0]["id"] == "sentry:project"
def test_cannot_create_with_error_created_hook_without_flag(self): with Feature({"organizations:integrations-event-hooks": False}): response = self.get_error_response(**self.get_data( events=("error", )), status_code=403) assert response.data == { "non_field_errors": [ "Your organization does not have access to the 'error' resource subscription." ] }
def test_setting_dynamic_sampling_rules_roundtrip(self): """ Tests that we get the same dynamic sampling rules that previously set """ with Feature({"organizations:filters-and-sampling": True}): resp = self.client.put(self.path, data={"dynamicSampling": _dyn_sampling_data()}) assert resp.status_code == 200, resp.content response = self.client.get(self.path) assert response.status_code == 200 assert response.data["dynamicSampling"] == _dyn_sampling_data()
def test_exposes_features(call_endpoint, task_runner): with Feature({"organizations:metrics-extraction": True}): with task_runner(): result, status_code = call_endpoint(full_config=True) assert status_code < 400 for config in result["configs"].values(): config = config["config"] assert "features" in config assert config["features"] == ["organizations:metrics-extraction"]
def test_sources_builtin_disabled(default_project): features = {"organizations:symbol-sources": False, "organizations:custom-symbol-sources": False} default_project.update_option("sentry:builtin_symbol_sources", ["microsoft"]) with Feature(features): sources = get_sources_for_project(default_project) source_ids = map(lambda s: s["id"], sources) assert source_ids == ["sentry:project"]
def test_project_config_with_span_attributes(default_project, insta_snapshot): # The span attributes config is not set with the flag turnd off cfg = get_project_config(default_project, full_config=True) cfg = cfg.to_dict() assert "spanAttributes" not in cfg["config"] with Feature("projects:performance-suspect-spans-ingestion"): cfg = get_project_config(default_project, full_config=True) cfg = cfg.to_dict() insta_snapshot(cfg["config"]["spanAttributes"])
def test_respects_feature_flag(self): self.login_as(user=self.owner_user) user = self.create_user("*****@*****.**") with Feature({"organizations:invite-members": False}): resp = self.client.post( self.url, {"email": user.email, "role": "member", "teams": [self.team.slug]} ) assert resp.status_code == 403
def test_setting_dynamic_sampling_rules_roundtrip(self): """ Tests that we get the same dynamic sampling rules that previously set """ data = _dyn_sampling_data() with Feature({"organizations:filters-and-sampling": True}): self.get_valid_response(self.org_slug, self.proj_slug, dynamicSampling=data) response = self.get_valid_response(self.org_slug, self.proj_slug, method="get") saved_config = _remove_ids_from_dynamic_rules(response.data["dynamicSampling"]) original_data = _remove_ids_from_dynamic_rules(data) assert saved_config == original_data
def test_sources_builtin(default_project): features = {"organizations:symbol-sources": True, "organizations:custom-symbol-sources": False} default_project.update_option("sentry:builtin_symbol_sources", ["microsoft"]) with Feature(features): sources = get_sources_for_project(default_project) # XXX: The order matters here! Project is always first, then builtin sources source_ids = map(lambda s: s["id"], sources) assert source_ids == ["sentry:project", "sentry:microsoft"]
def test_sources_custom_disabled(default_project): features = {"organizations:symbol-sources": True, "organizations:custom-symbol-sources": False} default_project.update_option("sentry:builtin_symbol_sources", []) default_project.update_option("sentry:symbol_sources", CUSTOM_SOURCE_CONFIG) with Feature(features): sources = get_sources_for_project(default_project) source_ids = map(lambda s: s["id"], sources) assert source_ids == ["sentry:project"]
def test_cannot_create_with_error_created_hook_without_flag(self): self.login_as(user=self.user) with Feature({"organizations:integrations-event-hooks": False}): kwargs = {"events": ("error",)} response = self._post(**kwargs) assert response.status_code == 403, response.content assert ( response.content == '{"non_field_errors":["Your organization does not have access to the \'error\' resource subscription."]}' )
def test_respects_feature_flag(self): user = self.create_user("*****@*****.**") with Feature({"organizations:invite-members": False}): data = { "email": user.email, "role": "member", "teams": [self.team.slug] } self.get_error_response(self.organization.slug, **data, status_code=403)
def test_sources_builtin_unknown(default_project): features = { "organizations:symbol-sources": True, "organizations:custom-symbol-sources": False } default_project.update_option("sentry:builtin_symbol_sources", ["invalid"]) with Feature(features): sources = get_sources_for_project(default_project) source_ids = list(map(lambda s: s["id"], sources)) assert source_ids == ["sentry:project"]
def test_sources_custom(default_project): features = {"organizations:symbol-sources": True, "organizations:custom-symbol-sources": True} # Remove builtin sources explicitly to avoid defaults default_project.update_option("sentry:builtin_symbol_sources", []) default_project.update_option("sentry:symbol_sources", CUSTOM_SOURCE_CONFIG) with Feature(features): sources = get_sources_for_project(default_project) # XXX: The order matters here! Project is always first, then custom sources source_ids = map(lambda s: s["id"], sources) assert source_ids == ["sentry:project", "custom"]
def test_relays_dyamic_sampling(client, call_endpoint, default_project, dyn_sampling_data): """ Tests that dynamic sampling configuration set in project details are retrieved in relay configs """ default_project.update_option("sentry:dynamic_sampling", dyn_sampling_data()) with Feature({"organizations:filters-and-sampling": True}): result, status_code = call_endpoint(full_config=False) assert status_code < 400 dynamic_sampling = safe.get_path( result, "configs", str(default_project.id), "config", "dynamicSampling" ) assert dynamic_sampling == dyn_sampling_data()
def test_respects_feature_flag(self): self.login_as(user=self.owner_user) user = self.create_user('*****@*****.**') with Feature({'organizations:invite-members': False}): resp = self.client.post(self.url, { 'email': user.email, 'role': 'member', 'teams': [ self.team.slug, ] }) assert resp.status_code == 403
def test_project_config_with_breakdown(default_project, insta_snapshot, transaction_metrics): with Feature( { "organizations:performance-ops-breakdown": True, "organizations:transaction-metrics-extraction": transaction_metrics == "with_metrics", } ): cfg = get_project_config(default_project, full_config=True) cfg = cfg.to_dict() insta_snapshot( { "breakdownsV2": cfg["config"]["breakdownsV2"], "transactionMetrics": cfg["config"].get("transactionMetrics"), } )
def test_project_config_uses_filters_and_sampling_feature( default_project, dyn_sampling_data, has_dyn_sampling, full_config): """ Tests that dynamic sampling information is retrieved for both "full config" and "restricted config" but only when the organization has "organizations:filter-and-sampling" feature enabled. """ default_project.update_option("sentry:dynamic_sampling", dyn_sampling_data()) with Feature({"organizations:filters-and-sampling": has_dyn_sampling}): cfg = get_project_config(default_project, full_config=full_config) cfg = cfg.to_dict() dynamic_sampling = get_path(cfg, "config", "dynamicSampling") if has_dyn_sampling: assert dynamic_sampling == dyn_sampling_data() else: assert dynamic_sampling is None
def test_project_config_uses_filter_features(default_project, has_custom_filters): error_messages = ["some_error"] releases = ["1.2.3", "4.5.6"] default_project.update_option("sentry:error_messages", error_messages) default_project.update_option("sentry:releases", releases) with Feature({"projects:custom-inbound-filters": has_custom_filters}): cfg = get_project_config(default_project, full_config=True) cfg = cfg.to_dict() cfg_error_messages = get_path(cfg, "config", "filterSettings", "errorMessages") cfg_releases = get_path(cfg, "config", "filterSettings", "releases") if has_custom_filters: assert {"patterns": error_messages} == cfg_error_messages assert {"releases": releases} == cfg_releases else: assert cfg_releases is None assert cfg_error_messages is None
def test_setting_dynamic_sampling_rules(self): """ Test that we can set sampling rules """ with Feature({"organizations:filters-and-sampling": True}): self.get_valid_response( self.org_slug, self.proj_slug, dynamicSampling=_dyn_sampling_data() ) original_config = _dyn_sampling_data() saved_config = self.project.get_option("sentry:dynamic_sampling") # test that we have unique ids ids = set() for rule in saved_config["rules"]: rid = rule["id"] assert rid not in ids ids.add(rid) next_id = saved_config["next_id"] assert next_id not in ids # short of ids and next_id the saved config should be the same as the original one _remove_ids_from_dynamic_rules(saved_config) _remove_ids_from_dynamic_rules(original_config) assert original_config == saved_config