236 lines
7.7 KiB
Python
236 lines
7.7 KiB
Python
import json
|
|
import uuid
|
|
from decimal import Decimal
|
|
from typing import override
|
|
|
|
from django.core.cache import cache
|
|
from django.test import Client, TestCase
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from apps.events.tests.helpers import make_event_type, make_exposure_type
|
|
from apps.experiments.services import (
|
|
experiment_approve,
|
|
experiment_start,
|
|
experiment_submit_for_review,
|
|
)
|
|
from apps.experiments.tests.helpers import add_two_variants, make_experiment
|
|
from apps.metrics.services import (
|
|
experiment_metric_add,
|
|
metric_definition_create,
|
|
)
|
|
from apps.reviews.services import review_settings_update
|
|
from apps.reviews.tests.helpers import make_approver, make_experimenter
|
|
from apps.users.tests.helpers import auth_header
|
|
|
|
|
|
class APIContractFlowTest(TestCase):
|
|
@override
|
|
def setUp(self) -> None:
|
|
cache.clear()
|
|
self.client = Client()
|
|
|
|
review_settings_update(
|
|
default_min_approvals=1,
|
|
allow_any_approver=True,
|
|
)
|
|
|
|
owner = make_experimenter("_api")
|
|
self.auth = auth_header(owner)
|
|
approver = make_approver("_api")
|
|
|
|
self.experiment = make_experiment(
|
|
owner=owner,
|
|
suffix="_api",
|
|
traffic_allocation=Decimal("100.00"),
|
|
)
|
|
add_two_variants(self.experiment)
|
|
|
|
self.metric = metric_definition_create(
|
|
key="ctr_api",
|
|
name="CTR",
|
|
metric_type="ratio",
|
|
direction="higher_is_better",
|
|
calculation_rule={
|
|
"numerator_event": "api_click",
|
|
"denominator_event": "api_exposure",
|
|
},
|
|
)
|
|
experiment_metric_add(
|
|
experiment=self.experiment,
|
|
metric=self.metric,
|
|
is_primary=True,
|
|
)
|
|
|
|
make_exposure_type(name="api_exposure")
|
|
make_event_type(
|
|
name="api_click",
|
|
display_name="Click",
|
|
requires_exposure=True,
|
|
)
|
|
|
|
self.experiment = experiment_submit_for_review(
|
|
experiment=self.experiment, user=owner
|
|
)
|
|
self.experiment = experiment_approve(
|
|
experiment=self.experiment, approver=approver
|
|
)
|
|
self.experiment = experiment_start(
|
|
experiment=self.experiment, user=owner
|
|
)
|
|
|
|
def test_decide_to_events_to_report_via_http(self) -> None:
|
|
decide_resp = self.client.post(
|
|
reverse("api-1:decide"),
|
|
data=json.dumps(
|
|
{
|
|
"subject_id": "api_user_1",
|
|
"flags": [self.experiment.flag.key],
|
|
"subject_attributes": {},
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(decide_resp.status_code, 200)
|
|
decide_data = decide_resp.json()
|
|
|
|
self.assertEqual(len(decide_data["decisions"]), 1)
|
|
decision = decide_data["decisions"][0]
|
|
self.assertEqual(decision["reason"], "experiment_assigned")
|
|
self.assertIsNotNone(decision["decision_id"])
|
|
self.assertIsNotNone(decision["variant_id"])
|
|
|
|
now = timezone.now().isoformat()
|
|
events_resp = self.client.post(
|
|
reverse("api-1:ingest_events"),
|
|
data=json.dumps(
|
|
{
|
|
"events": [
|
|
{
|
|
"event_id": "api_exp_1",
|
|
"event_type": "api_exposure",
|
|
"decision_id": decision["decision_id"],
|
|
"subject_id": "api_user_1",
|
|
"timestamp": now,
|
|
"properties": {},
|
|
},
|
|
{
|
|
"event_id": "api_click_1",
|
|
"event_type": "api_click",
|
|
"decision_id": decision["decision_id"],
|
|
"subject_id": "api_user_1",
|
|
"timestamp": now,
|
|
"properties": {},
|
|
},
|
|
]
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(events_resp.status_code, 200)
|
|
events_data = events_resp.json()
|
|
self.assertEqual(events_data["accepted"], 2)
|
|
self.assertEqual(events_data["rejected"], 0)
|
|
self.assertEqual(events_data["duplicates"], 0)
|
|
|
|
report_resp = self.client.get(
|
|
reverse(
|
|
"api-1:get_experiment_report",
|
|
args=[self.experiment.pk],
|
|
),
|
|
HTTP_AUTHORIZATION=self.auth,
|
|
)
|
|
self.assertEqual(report_resp.status_code, 200)
|
|
report = report_resp.json()
|
|
|
|
self.assertEqual(report["experiment_id"], str(self.experiment.pk))
|
|
total_exposures = sum(v["exposures"] for v in report["variants"])
|
|
self.assertEqual(total_exposures, 1)
|
|
self.assertEqual(len(report["variants"]), 2)
|
|
|
|
for variant in report["variants"]:
|
|
if variant["exposures"] > 0:
|
|
self.assertEqual(len(variant["metrics"]), 1)
|
|
self.assertEqual(
|
|
variant["metrics"][0]["metric_key"], "ctr_api"
|
|
)
|
|
|
|
def test_decide_returns_all_requested_flags(self) -> None:
|
|
resp = self.client.post(
|
|
reverse("api-1:decide"),
|
|
data=json.dumps(
|
|
{
|
|
"subject_id": "api_user_2",
|
|
"flags": [
|
|
self.experiment.flag.key,
|
|
"nonexistent_flag",
|
|
],
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
data = resp.json()
|
|
|
|
self.assertEqual(len(data["decisions"]), 2)
|
|
reasons = {d["flag"]: d["reason"] for d in data["decisions"]}
|
|
self.assertEqual(
|
|
reasons[self.experiment.flag.key], "experiment_assigned"
|
|
)
|
|
self.assertEqual(reasons["nonexistent_flag"], "flag_not_found")
|
|
|
|
def test_events_dedup_via_http(self) -> None:
|
|
cache.clear()
|
|
decide_resp = self.client.post(
|
|
reverse("api-1:decide"),
|
|
data=json.dumps(
|
|
{
|
|
"subject_id": "api_user_3",
|
|
"flags": [self.experiment.flag.key],
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
)
|
|
decision = decide_resp.json()["decisions"][0]
|
|
now = timezone.now().isoformat()
|
|
|
|
event_payload = json.dumps(
|
|
{
|
|
"events": [
|
|
{
|
|
"event_id": "api_ddup_1",
|
|
"event_type": "api_exposure",
|
|
"decision_id": decision["decision_id"],
|
|
"subject_id": "api_user_3",
|
|
"timestamp": now,
|
|
"properties": {},
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
r1 = self.client.post(
|
|
reverse("api-1:ingest_events"),
|
|
data=event_payload,
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(r1.json()["accepted"], 1)
|
|
|
|
r2 = self.client.post(
|
|
reverse("api-1:ingest_events"),
|
|
data=event_payload,
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(r2.json()["duplicates"], 1)
|
|
self.assertEqual(r2.json()["accepted"], 0)
|
|
|
|
def test_report_404_for_unknown_experiment(self) -> None:
|
|
resp = self.client.get(
|
|
reverse(
|
|
"api-1:get_experiment_report",
|
|
args=[uuid.uuid4()],
|
|
),
|
|
HTTP_AUTHORIZATION=self.auth,
|
|
)
|
|
self.assertEqual(resp.status_code, 404)
|