Files
Lotty/src/backend/tests/integration/test_api_contract.py
T

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)