chzzk #1

Merged
TheLovinator merged 8 commits from chzzk into master 2026-04-01 04:09:20 +02:00
14 changed files with 650 additions and 9 deletions
Showing only changes of commit 677aedf42b - Show all commits

Add Chzzk campaign and reward models, import command, and schemas
Some checks failed
Deploy to Server / deploy (push) Failing after 19s

Joakim Hellsén 2026-03-31 21:57:12 +02:00
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk

View file

View file

View file

@ -0,0 +1,137 @@
from typing import TYPE_CHECKING
from typing import Any
if TYPE_CHECKING:
import argparse
from chzzk.schemas import ChzzkCampaignV1
from chzzk.schemas import ChzzkCampaignV2
from typing import TYPE_CHECKING
import requests
from django.core.management.base import BaseCommand
from django.utils import timezone
from chzzk.models import ChzzkCampaign
from chzzk.models import ChzzkReward
from chzzk.schemas import ChzzkApiResponseV1
from chzzk.schemas import ChzzkApiResponseV2
if TYPE_CHECKING:
import argparse
CHZZK_API_URLS: list[tuple[str, str]] = [
("v1", "https://api.chzzk.naver.com/service/v1/drops/campaigns/{campaign_no}"),
("v2", "https://api.chzzk.naver.com/service/v2/drops/campaigns/{campaign_no}"),
]
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0"
)
class Command(BaseCommand):
"""Django management command to scrape Chzzk drops campaigns from both v1 and v2 APIs and store them in the database."""
help = "Scrape Chzzk drops campaigns from both v1 and v2 APIs and store them."
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
"""Add command-line arguments for the management command."""
parser.add_argument("campaign_no", type=int, help="Campaign number to fetch")
def handle(self, **options) -> None:
"""Main handler for the management command. Fetches campaign data from both API versions, validates, and stores them."""
campaign_no: int = int(options["campaign_no"])
for api_version, url_template in CHZZK_API_URLS:
url: str = url_template.format(campaign_no=campaign_no)
resp: requests.Response = requests.get(
url,
timeout=2,
headers={
"Accept": "application/json",
"User-Agent": USER_AGENT,
},
)
resp.raise_for_status()
data: dict[str, Any] = resp.json()
campaign_data: ChzzkCampaignV1 | ChzzkCampaignV2
if api_version == "v1":
campaign_data = ChzzkApiResponseV1.model_validate(data).content
elif api_version == "v2":
campaign_data = ChzzkApiResponseV2.model_validate(data).content
else:
msg: str = f"Unknown API version: {api_version}"
self.stdout.write(self.style.ERROR(msg))
continue
# Save campaign
campaign_obj, created = ChzzkCampaign.objects.update_or_create(
campaign_no=campaign_data.campaign_no,
source_api=api_version,
defaults={
"title": campaign_data.title,
"image_url": campaign_data.image_url,
"description": campaign_data.description,
"category_type": campaign_data.category_type,
"category_id": campaign_data.category_id,
"category_value": campaign_data.category_value,
"pc_link_url": campaign_data.pc_link_url,
"mobile_link_url": campaign_data.mobile_link_url,
"service_id": campaign_data.service_id,
"state": campaign_data.state,
"start_date": campaign_data.start_date,
"end_date": campaign_data.end_date,
"has_ios_based_reward": campaign_data.has_ios_based_reward,
"drops_campaign_not_started": campaign_data.drops_campaign_not_started,
"campaign_reward_type": getattr(
campaign_data,
"campaign_reward_type",
"",
),
"reward_type": getattr(campaign_data, "reward_type", ""),
"account_link_url": campaign_data.account_link_url,
"scraped_at": timezone.now(),
"scrape_status": "success",
"raw_json": data,
},
)
if created:
self.stdout.write(
self.style.SUCCESS(
f"Created campaign {campaign_no} from {api_version}",
),
)
for reward in campaign_data.reward_list:
reward_, created = ChzzkReward.objects.update_or_create(
campaign=campaign_obj,
reward_no=reward.reward_no,
defaults={
"image_url": reward.image_url,
"title": reward.title,
"reward_type": reward.reward_type,
"campaign_reward_type": getattr(
reward,
"campaign_reward_type",
"",
),
"condition_type": reward.condition_type,
"condition_for_minutes": reward.condition_for_minutes,
"ios_based_reward": reward.ios_based_reward,
"code_remaining_count": reward.code_remaining_count,
},
)
if created:
self.stdout.write(
self.style.SUCCESS(
f" Created reward {reward_.reward_no} for campaign {campaign_no}",
),
)
self.stdout.write(
self.style.SUCCESS(
f"Imported campaign {campaign_no} from {api_version}",
),
)

View file

@ -0,0 +1,100 @@
# Generated by Django 6.0.3 on 2026-03-31 19:33
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
"""Initial migration for ChzzkCampaign and ChzzkReward models."""
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="ChzzkCampaign",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("campaign_no", models.BigIntegerField(unique=True)),
("title", models.CharField(max_length=255)),
("image_url", models.URLField()),
("description", models.TextField()),
("category_type", models.CharField(max_length=64)),
("category_id", models.CharField(max_length=128)),
("category_value", models.CharField(max_length=128)),
("pc_link_url", models.URLField()),
("mobile_link_url", models.URLField()),
("service_id", models.CharField(max_length=128)),
("state", models.CharField(max_length=64)),
("start_date", models.DateTimeField()),
("end_date", models.DateTimeField()),
("has_ios_based_reward", models.BooleanField()),
("drops_campaign_not_started", models.BooleanField()),
(
"campaign_reward_type",
models.CharField(blank=True, default="", max_length=64),
),
(
"reward_type",
models.CharField(blank=True, default="", max_length=64),
),
("account_link_url", models.URLField()),
("scraped_at", models.DateTimeField(default=django.utils.timezone.now)),
("source_api", models.CharField(max_length=16)),
("scrape_status", models.CharField(default="success", max_length=32)),
("raw_json", models.JSONField()),
],
options={
"ordering": ["-start_date"],
"unique_together": {("campaign_no", "source_api")},
},
),
migrations.CreateModel(
name="ChzzkReward",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("reward_no", models.BigIntegerField()),
("image_url", models.URLField()),
("title", models.CharField(max_length=255)),
("reward_type", models.CharField(max_length=64)),
(
"campaign_reward_type",
models.CharField(blank=True, default="", max_length=64),
),
("condition_type", models.CharField(max_length=64)),
("condition_for_minutes", models.IntegerField()),
("ios_based_reward", models.BooleanField()),
("code_remaining_count", models.IntegerField()),
(
"campaign",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="rewards",
to="chzzk.chzzkcampaign",
),
),
],
options={
"unique_together": {("campaign", "reward_no")},
},
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 6.0.3 on 2026-03-31 19:53
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
"""Alter campaign_no field in ChzzkCampaign to remove unique constraint."""
dependencies = [
("chzzk", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="chzzkcampaign",
name="campaign_no",
field=models.BigIntegerField(),
),
]

View file

@ -0,0 +1,63 @@
from django.db import models
from django.utils import timezone
class ChzzkCampaign(models.Model):
"""Chzzk campaign, including scraping metadata."""
campaign_no = models.BigIntegerField()
title = models.CharField(max_length=255)
image_url = models.URLField()
description = models.TextField()
category_type = models.CharField(max_length=64)
category_id = models.CharField(max_length=128)
category_value = models.CharField(max_length=128)
pc_link_url = models.URLField()
mobile_link_url = models.URLField()
service_id = models.CharField(max_length=128)
state = models.CharField(max_length=64)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
has_ios_based_reward = models.BooleanField()
drops_campaign_not_started = models.BooleanField()
campaign_reward_type = models.CharField(max_length=64, blank=True, default="")
reward_type = models.CharField(max_length=64, blank=True, default="")
account_link_url = models.URLField()
# Scraping metadata
scraped_at = models.DateTimeField(default=timezone.now)
source_api = models.CharField(max_length=16) # 'v1' or 'v2'
scrape_status = models.CharField(max_length=32, default="success")
raw_json = models.JSONField()
class Meta:
unique_together = ("campaign_no", "source_api")
ordering = ["-start_date"]
def __str__(self) -> str:
return f"{self.title} (#{self.campaign_no})"
class ChzzkReward(models.Model):
"""Chzzk reward belonging to a campaign."""
campaign = models.ForeignKey(
ChzzkCampaign,
related_name="rewards",
on_delete=models.CASCADE,
)
reward_no = models.BigIntegerField()
image_url = models.URLField()
title = models.CharField(max_length=255)
reward_type = models.CharField(max_length=64)
campaign_reward_type = models.CharField(max_length=64, blank=True, default="")
condition_type = models.CharField(max_length=64)
condition_for_minutes = models.IntegerField()
ios_based_reward = models.BooleanField()
code_remaining_count = models.IntegerField()
class Meta:
unique_together = ("campaign", "reward_no")
def __str__(self) -> str:
return f"{self.title} (#{self.reward_no})"

107
chzzk/schemas.py Normal file
View file

@ -0,0 +1,107 @@
from typing import Any
from pydantic import BaseModel
from pydantic import Field
class ChzzkRewardV1(BaseModel):
"""Pydantic schema for Chzzk v1 reward object."""
title: str
reward_no: int = Field(..., alias="rewardNo")
image_url: str = Field(..., alias="imageUrl")
reward_type: str = Field(..., alias="rewardType")
condition_type: str = Field(..., alias="conditionType")
condition_for_minutes: int = Field(..., alias="conditionForMinutes")
ios_based_reward: bool = Field(..., alias="iosBasedReward")
code_remaining_count: int = Field(..., alias="codeRemainingCount")
# Only in v1 API
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
model_config = {"extra": "forbid"}
class ChzzkRewardV2(BaseModel):
"""Pydantic schema for Chzzk v2 reward object."""
title: str
reward_no: int = Field(..., alias="rewardNo")
image_url: str = Field(..., alias="imageUrl")
reward_type: str = Field(..., alias="rewardType")
condition_type: str = Field(..., alias="conditionType")
condition_for_minutes: int = Field(..., alias="conditionForMinutes")
ios_based_reward: bool = Field(..., alias="iosBasedReward")
code_remaining_count: int = Field(..., alias="codeRemainingCount")
model_config = {"extra": "forbid"}
class ChzzkCampaignV1(BaseModel):
"""Pydantic schema for Chzzk v1 campaign object."""
title: str
state: str
description: str
campaign_no: int = Field(..., alias="campaignNo")
image_url: str = Field(..., alias="imageUrl")
category_type: str = Field(..., alias="categoryType")
category_id: str = Field(..., alias="categoryId")
category_value: str = Field(..., alias="categoryValue")
pc_link_url: str = Field(..., alias="pcLinkUrl")
mobile_link_url: str = Field(..., alias="mobileLinkUrl")
service_id: str = Field(..., alias="serviceId")
start_date: str = Field(..., alias="startDate")
end_date: str = Field(..., alias="endDate")
reward_list: list[ChzzkRewardV1] = Field(..., alias="rewardList")
has_ios_based_reward: bool = Field(..., alias="hasIosBasedReward")
drops_campaign_not_started: bool = Field(..., alias="dropsCampaignNotStarted")
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
account_link_url: str = Field(..., alias="accountLinkUrl")
model_config = {"extra": "forbid"}
class ChzzkCampaignV2(BaseModel):
"""Pydantic schema for Chzzk v2 campaign object."""
title: str
state: str
campaign_no: int = Field(..., alias="campaignNo")
image_url: str = Field(..., alias="imageUrl")
description: str
category_type: str = Field(..., alias="categoryType")
category_id: str = Field(..., alias="categoryId")
category_value: str = Field(..., alias="categoryValue")
pc_link_url: str = Field(..., alias="pcLinkUrl")
mobile_link_url: str = Field(..., alias="mobileLinkUrl")
service_id: str = Field(..., alias="serviceId")
start_date: str = Field(..., alias="startDate")
end_date: str = Field(..., alias="endDate")
reward_list: list[ChzzkRewardV2] = Field(..., alias="rewardList")
has_ios_based_reward: bool = Field(..., alias="hasIosBasedReward")
drops_campaign_not_started: bool = Field(..., alias="dropsCampaignNotStarted")
reward_type: str | None = Field(None, alias="rewardType")
account_link_url: str = Field(..., alias="accountLinkUrl")
model_config = {"extra": "forbid"}
class ChzzkApiResponseV1(BaseModel):
"""Pydantic schema for Chzzk v1 API response."""
code: int
message: Any | None
content: ChzzkCampaignV1
model_config = {"extra": "forbid"}
class ChzzkApiResponseV2(BaseModel):
"""Pydantic schema for Chzzk v2 API response."""
code: int
message: Any | None
content: ChzzkCampaignV2
model_config = {"extra": "forbid"}

View file

@ -0,0 +1,40 @@
import json
from pathlib import Path
import pydantic
import pytest
from chzzk.schemas import ChzzkApiResponseV1
from chzzk.schemas import ChzzkApiResponseV2
TESTS_DIR = Path(__file__).parent
@pytest.mark.parametrize(
("fname", "data_model"),
[
("v1_905.json", ChzzkApiResponseV1),
("v2_905.json", ChzzkApiResponseV2),
],
)
def test_chzzk_schema_strict(fname: str, data_model: type) -> None:
"""Test that the schema strictly validates the given JSON file against the provided Pydantic model."""
with Path(TESTS_DIR / fname).open(encoding="utf-8") as f:
data = json.load(f)
# Should not raise
data_model.model_validate(data)
@pytest.mark.parametrize(
("fname", "data_model"),
[
("v1_905.json", ChzzkApiResponseV2),
("v2_905.json", ChzzkApiResponseV1),
],
)
def test_chzzk_schema_cross_fail(fname: str, data_model: type) -> None:
"""Test that the schema fails when validating the wrong JSON file/model combination."""
with Path(TESTS_DIR / fname).open(encoding="utf-8") as f:
data = json.load(f)
with pytest.raises(pydantic.ValidationError):
data_model.model_validate(data)

58
chzzk/tests/v1_905.json generated Normal file
View file

@ -0,0 +1,58 @@
{
"code": 200,
"message": null,
"content": {
"campaignNo": 905,
"title": "붉은사막 드롭스 #2",
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMzcg/MDAxNzc0MzU4Mzk3MjM0.JQAXkEYe2ntJ5gvzr5U5egn78DalY24mi1hodGyPYcog.tko3iBzm7dOqDKdjZAZX2ozWrj-tKjVcN3v3ieQaNzQg.JPEG/KR_260319_sony_masterimage_2160x2160.jpg",
"description": "치지직에서 붉은사막 방송을 시청하고, 특별한 보상을 획득해 보세요.",
"categoryType": "GAME",
"categoryId": "CrimsonDesert",
"categoryValue": "붉은사막",
"pcLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
"mobileLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
"serviceId": "Pearl_Abyss_Event",
"state": "PROMOTED",
"startDate": "2026-03-26 09:00:00",
"endDate": "2026-04-02 08:59:00",
"rewardList": [
{
"rewardNo": 2521,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTIg/MDAxNzc0MzU4NTgxMTY5.WFRtLuWw0qciEmMlnm79culfGW1lj-KwvXTaZLkbzMIg.PxoTwZ6v_i1N7Vkyj--OxmeuEJsRvQDWgjT1sOB6kfog.PNG/Week-2-60-min.png",
"title": "푸른 정찰대 등자",
"rewardType": "TIME_BASED",
"campaignRewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 60,
"iosBasedReward": false,
"codeRemainingCount": 0
},
{
"rewardNo": 2522,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTYw/MDAxNzc0MzU4NjEwODkz.6SMLoYofJ6A2a9Nd1IlKn_CC3JLyR8ecxkt8cO-rzcAg.kQSP_H08VoS1qXGiB_3BBMYT_0IT07YPz09n0lNmD1cg.PNG/Week-2-120-min.png",
"title": "푸른 정찰대 마면",
"rewardType": "TIME_BASED",
"campaignRewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 120,
"iosBasedReward": false,
"codeRemainingCount": 0
},
{
"rewardNo": 2523,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMjU3/MDAxNzc0MzU4NjM5MDgw.7u6E8ybLp7x9MNESI3od1Sf3AE_6AobSMFfb18MhsNog.M5ZvA1SSjaOObB2TB-P9Rw2rtMcV8zXipL0i7adILdwg.PNG/Week-2-180-min.png",
"title": "푸른 정찰대 안장",
"rewardType": "TIME_BASED",
"campaignRewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 180,
"iosBasedReward": false,
"codeRemainingCount": 0
}
],
"hasIosBasedReward": false,
"dropsCampaignNotStarted": false,
"campaignRewardType": "IN_APP",
"accountLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops"
}
}

55
chzzk/tests/v2_905.json generated Normal file
View file

@ -0,0 +1,55 @@
{
"code": 200,
"message": null,
"content": {
"campaignNo": 905,
"title": "붉은사막 드롭스 #2",
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMzcg/MDAxNzc0MzU4Mzk3MjM0.JQAXkEYe2ntJ5gvzr5U5egn78DalY24mi1hodGyPYcog.tko3iBzm7dOqDKdjZAZX2ozWrj-tKjVcN3v3ieQaNzQg.JPEG/KR_260319_sony_masterimage_2160x2160.jpg",
"description": "치지직에서 붉은사막 방송을 시청하고, 특별한 보상을 획득해 보세요.",
"categoryType": "GAME",
"categoryId": "CrimsonDesert",
"categoryValue": "붉은사막",
"pcLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
"mobileLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
"serviceId": "Pearl_Abyss_Event",
"state": "PROMOTED",
"startDate": "2026-03-26 09:00:00",
"endDate": "2026-04-02 08:59:00",
"rewardList": [
{
"rewardNo": 2521,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTIg/MDAxNzc0MzU4NTgxMTY5.WFRtLuWw0qciEmMlnm79culfGW1lj-KwvXTaZLkbzMIg.PxoTwZ6v_i1N7Vkyj--OxmeuEJsRvQDWgjT1sOB6kfog.PNG/Week-2-60-min.png",
"title": "푸른 정찰대 등자",
"rewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 60,
"iosBasedReward": false,
"codeRemainingCount": 0
},
{
"rewardNo": 2522,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTYw/MDAxNzc0MzU4NjEwODkz.6SMLoYofJ6A2a9Nd1IlKn_CC3JLyR8ecxkt8cO-rzcAg.kQSP_H08VoS1qXGiB_3BBMYT_0IT07YPz09n0lNmD1cg.PNG/Week-2-120-min.png",
"title": "푸른 정찰대 마면",
"rewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 120,
"iosBasedReward": false,
"codeRemainingCount": 0
},
{
"rewardNo": 2523,
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMjU3/MDAxNzc0MzU4NjM5MDgw.7u6E8ybLp7x9MNESI3od1Sf3AE_6AobSMFfb18MhsNog.M5ZvA1SSjaOObB2TB-P9Rw2rtMcV8zXipL0i7adILdwg.PNG/Week-2-180-min.png",
"title": "푸른 정찰대 안장",
"rewardType": "IN_APP",
"conditionType": "TIME_BASED",
"conditionForMinutes": 180,
"iosBasedReward": false,
"codeRemainingCount": 0
}
],
"hasIosBasedReward": false,
"dropsCampaignNotStarted": false,
"rewardType": "IN_APP",
"accountLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops"
}
}

View file

@ -140,10 +140,11 @@ INSTALLED_APPS: list[str] = [
"django.contrib.staticfiles",
"django.contrib.postgres",
# Internal apps
"twitch.apps.TwitchConfig",
"kick.apps.KickConfig",
"youtube.apps.YoutubeConfig",
"chzzk.apps.ChzzkConfig",
"core.apps.CoreConfig",
"kick.apps.KickConfig",
"twitch.apps.TwitchConfig",
"youtube.apps.YoutubeConfig",
# Third-party apps
"django_celery_results",
"django_celery_beat",
@ -171,10 +172,35 @@ TEMPLATES: list[dict[str, Any]] = [
},
]
DATABASES: dict[str, dict[str, Any]] = (
{"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
if TESTING
else {
def configure_databases(*, testing: bool, base_dir: Path) -> dict[str, dict[str, Any]]:
"""Configure Django databases based on environment variables and testing mode.
Args:
testing (bool): Whether the application is running in testing mode.
base_dir (Path): The base directory of the project, used for SQLite file location.
Returns:
dict[str, dict[str, Any]]: The DATABASES setting for Django.
"""
use_sqlite: bool = env_bool("USE_SQLITE", default=False)
if testing:
return {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
},
}
if use_sqlite:
return {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": str(base_dir / "db.sqlite3"),
},
}
# Default: PostgreSQL
return {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("POSTGRES_DB", "ttvdrops"),
@ -187,6 +213,11 @@ DATABASES: dict[str, dict[str, Any]] = (
"OPTIONS": {"connect_timeout": env_int("DB_CONNECT_TIMEOUT", 10)},
},
}
DATABASES: dict[str, dict[str, Any]] = configure_databases(
testing=TESTING,
base_dir=BASE_DIR,
)
if not TESTING:

View file

@ -3,6 +3,7 @@ import os
import sys
from contextlib import contextmanager
from typing import TYPE_CHECKING
from typing import Any
import pytest
from django.contrib.sessions.models import Session
@ -249,14 +250,14 @@ def test_email_settings_from_env(
assert reloaded.SERVER_EMAIL == "me@example.com"
def test_database_settings_when_not_testing(
def test_database_settings_when_use_sqlite_enabled(
monkeypatch: pytest.MonkeyPatch,
reload_settings_module: Callable[..., ModuleType],
) -> None:
"""When not running tests, DATABASES should use the Postgres configuration."""
# Ensure the module believes it's not running tests
"""When USE_SQLITE=1 and not testing, DATABASES should use SQLite file."""
monkeypatch.setattr("sys.argv", ["manage.py", "runserver"])
monkeypatch.delenv("PYTEST_VERSION", raising=False)
monkeypatch.setenv("USE_SQLITE", "1")
reloaded: ModuleType = reload_settings_module(
TESTING=None,
@ -271,7 +272,36 @@ def test_database_settings_when_not_testing(
)
assert reloaded.TESTING is False
db_cfg = reloaded.DATABASES["default"]
db_cfg: dict[str, Any] = reloaded.DATABASES["default"]
assert db_cfg["ENGINE"] == "django.db.backends.sqlite3"
# Should use a file, not in-memory
assert db_cfg["NAME"].endswith("db.sqlite3")
def test_database_settings_when_not_testing(
monkeypatch: pytest.MonkeyPatch,
reload_settings_module: Callable[..., ModuleType],
) -> None:
"""When not running tests, DATABASES should use the Postgres configuration."""
# Ensure the module believes it's not running tests and USE_SQLITE is unset
monkeypatch.setattr("sys.argv", ["manage.py", "runserver"])
monkeypatch.delenv("PYTEST_VERSION", raising=False)
monkeypatch.setenv("USE_SQLITE", "0")
reloaded: ModuleType = reload_settings_module(
TESTING=None,
PYTEST_VERSION=None,
POSTGRES_DB="prod_db",
POSTGRES_USER="prod_user",
POSTGRES_PASSWORD="secret",
POSTGRES_HOST="db.host",
POSTGRES_PORT="5433",
CONN_MAX_AGE="120",
CONN_HEALTH_CHECKS="0",
)
assert reloaded.TESTING is False
db_cfg: dict[str, Any] = reloaded.DATABASES["default"]
assert db_cfg["ENGINE"] == "django.db.backends.postgresql"
assert db_cfg["NAME"] == "prod_db"
assert db_cfg["USER"] == "prod_user"

View file

@ -620,7 +620,7 @@ class ExampleJsonImportTests(TestCase):
command = Command()
repo_root: Path = Path(__file__).resolve().parents[2]
example_path: Path = repo_root / "example.json"
example_path: Path = repo_root / "twitch" / "tests" / "example.json"
responses = json.loads(example_path.read_text(encoding="utf-8"))