Recently Written · git

subplz-web

git clone https://github.com/equwal/subplz-web

Log | Files | Refs


tests/test_free_credits.py (8773 bytes)

1 """Free credits for conversions on the server.
2 
3 The rules under test:
4 
5   anonymous  one free credit, while the server takes conversions. A buyer
6              whose email came from Stripe is in this group until the email
7              is verified.
8   verified   an account that signed in with an emailed link gets two free
9              credits in all (a free credit that a device used before counts
10              toward the two), one more each day, and one more for every two
11              books that the server converted for it
12   daily      the daily credit does not carry over to the next day
13   order      a server job spends the daily credit, then a free credit, then
14              a bought one
15   refund     a failed job gives its credit back to the same kind
16   merge      a second device that signs in adds no free credits
17 """
18 
19 from __future__ import annotations
20 
21 import time
22 from datetime import timedelta
23 from urllib.parse import parse_qs, urlparse
24 
25 import pytest
26 
27 from backend import billing, runner
28 from backend.db import JobStatus
29 from backend.settings import Settings, settings
30 
31 from .conftest import (
32     account_id, checkout_event, get_account_row, get_job_row, make_job,
33     post_webhook,
34 )
35 
36 BOOK = {
37     "audio_filename": "free.m4b", "audio_bytes": 4321,
38     "text_filename": "free.epub", "language": "en",
39 }
40 SRT = "1\n00:00:00,000 --> 00:00:01,000\nhello\n\n"
41 
42 
43 @pytest.fixture
44 def cloud(monkeypatch):
45     """The server takes conversions, and sign-in links come back in the response."""
46     monkeypatch.setattr(settings, "cloud_enabled", True)
47     monkeypatch.setattr(settings, "dev_login_links", True)
48 
49 
50 def unique(name: str) -> str:
51     """An email for one test only: an email joins accounts across tests."""
52     return f"{name}-{time.time_ns()}@example.com"
53 
54 
55 def sign_in(client, email: str) -> None:
56     link = client.post("/api/auth/request", json={"email": email}).json()["dev_link"]
57     token = parse_qs(urlparse(link).query)["login"][0]
58     assert client.post("/api/auth/verify", json={"token": token}).status_code == 200
59 
60 
61 def buy(client, plan: str, email: str) -> None:
62     event = checkout_event(f"cs_{time.time_ns()}", account_id(client), plan, email=email)
63     assert post_webhook(client, event).status_code == 200
64 
65 
66 def start(client, job_id: str):
67     return client.post(f"/api/jobs/{job_id}/start", json={"language": "en"})
68 
69 
70 def account(client) -> dict:
71     return client.get("/api/account").json()
72 
73 
74 def days_later(monkeypatch, days: int) -> None:
75     later = billing._today() + timedelta(days=days)
76     monkeypatch.setattr(billing, "_today", lambda: later)
77 
78 
79 def convert_in_browser(client, n: int) -> None:
80     job = client.post("/api/local/jobs", json={**BOOK, "audio_bytes": 100 + n}).json()
81     r = client.post(f"/api/local/jobs/{job['id']}/finish",
82                     json={"srt": SRT, "filename": "free.srt"})
83     assert r.status_code == 200
84 
85 
86 def test_defaults():
87     fields = Settings.model_fields
88     assert fields["free_credits_anonymous"].default == 1
89     assert fields["free_credits_verified"].default == 2
90     assert fields["daily_free_credit"].default is True
91     assert fields["books_per_bonus_credit"].default == 2
92 
93 
94 def test_a_new_visitor_has_one_free_credit(client, cloud):
95     a = account(client)
96     assert a["signed_in"] is False and a["email_verified"] is False
97     assert a["credits"] == 1 and a["free_credits"] == 1
98     assert a["cloud_allowed"] is True
99     assert a["free_credits_with_account"] == 2 and a["daily_free_credit"] is True
100 
101 
102 def test_the_free_credit_pays_for_one_server_job(client, cloud):
103     job_id = make_job(client)
104     assert start(client, job_id).status_code == 200
105     job = get_job_row(job_id)
106     assert job.free_credit_spent == 1 and job.credit_spent == 0
107 
108     a = account(client)
109     assert a["credits"] == 0 and a["cloud_allowed"] is False
110     assert start(client, make_job(client)).status_code == 402
111 
112 
113 def test_a_verified_email_gets_two_free_credits_in_all_and_one_a_day(client, cloud):
114     assert start(client, make_job(client)).status_code == 200  # the anonymous credit
115     assert account(client)["free_credits_with_account"] == 1
116 
117     sign_in(client, unique("verify"))
118     a = account(client)
119     assert a["signed_in"] is True and a["email_verified"] is True
120     assert a["free_credits"] == 2  # one of the two, and the daily credit
121     assert a["free_credits_with_account"] == 0
122 
123 
124 def test_a_buyer_gets_account_credits_only_after_verifying_the_email(client, cloud):
125     email = unique("buyer")
126     buy(client, "pack10", email)
127     a = account(client)
128     assert a["signed_in"] is True and a["email_verified"] is False
129     assert a["credits"] == 11 and a["free_credits"] == 1  # 10 bought, the anonymous one
130     assert a["free_credits_with_account"] == 2
131 
132     sign_in(client, email)  # the same address: this verifies the account
133     a = account(client)
134     assert a["email_verified"] is True
135     assert a["free_credits"] == 3 and a["credits"] == 13
136 
137 
138 def test_the_daily_credit_does_not_carry_over(client, cloud, monkeypatch):
139     sign_in(client, unique("daily"))
140     assert account(client)["free_credits"] == 3  # two, and today's
141 
142     days_later(monkeypatch, 1)
143     assert account(client)["free_credits"] == 3  # not 4: yesterday's credit is gone
144     for _ in range(3):
145         assert start(client, make_job(client)).status_code == 200
146     assert start(client, make_job(client)).status_code == 402
147 
148     days_later(monkeypatch, 1)
149     assert account(client)["free_credits"] == 1  # the credit of the new day
150     job_id = make_job(client)
151     assert start(client, job_id).status_code == 200
152     assert get_job_row(job_id).daily_credit_on == billing._today()
153 
154 
155 def test_every_two_books_converted_on_the_server_earn_a_free_credit(client, cloud):
156     sign_in(client, unique("bonus"))
157     before = account(client)["free_credits"]
158 
159     make_job(client, JobStatus.succeeded)
160     assert account(client)["free_credits"] == before
161     make_job(client, JobStatus.succeeded)
162     assert account(client)["free_credits"] == before + 1
163 
164     # Books converted in the browser do not count: the server cannot check them.
165     convert_in_browser(client, 1)
166     convert_in_browser(client, 2)
167     assert account(client)["free_credits"] == before + 1
168 
169 
170 def test_free_credits_are_spent_before_bought_ones(client, cloud):
171     buy(client, "single", unique("order"))  # a retired plan: one credit
172     assert account(client)["credits"] == 2  # one bought, the anonymous one
173 
174     job_id = make_job(client)
175     assert start(client, job_id).status_code == 200
176     assert get_job_row(job_id).free_credit_spent == 1
177     row = get_account_row(account_id(client))
178     assert row.purchased_credits == 1 and row.free_credits_used == 1
179 
180 
181 def test_a_failed_job_gives_the_free_credit_back(client, cloud):
182     job_id = make_job(client)
183     assert start(client, job_id).status_code == 200
184     runner.run_job(job_id)  # fails: nothing was staged
185 
186     assert account(client)["free_credits"] == 1
187     assert get_job_row(job_id).free_credit_spent == 0
188     assert get_account_row(account_id(client)).purchased_credits == 0
189 
190 
191 def test_a_failed_job_gives_the_daily_credit_back(client, cloud):
192     sign_in(client, unique("refund"))
193     job_id = make_job(client)
194     assert start(client, job_id).status_code == 200
195     assert get_job_row(job_id).daily_credit_on == billing._today()
196 
197     runner.run_job(job_id)  # fails: nothing was staged
198     assert get_job_row(job_id).daily_credit_on is None
199     assert account(client)["free_credits"] == 3
200 
201 
202 def test_a_second_device_adds_no_free_credits(client, second_client, cloud):
203     email = unique("merge")
204     sign_in(client, email)
205     assert account(client)["free_credits"] == 3
206 
207     # The second device spends its anonymous credit, then signs in as the same person.
208     assert start(second_client, make_job(second_client)).status_code == 200
209     sign_in(second_client, email)
210     assert account(second_client)["free_credits"] == 2
211     assert account(client)["free_credits"] == 2
212 
213     # A new cookie that signs in again brings no free credit along either.
214     client.post("/api/auth/signout")
215     assert account(client)["free_credits"] == 1
216     sign_in(client, email)
217     assert account(client)["free_credits"] == 2
218 
219 
220 def test_no_free_credits_while_the_server_takes_no_conversions(client):
221     # conftest leaves SUBPLZ_WEB_CLOUD_ENABLED off.
222     a = account(client)
223     assert a["credits"] == 0 and a["free_credits"] == 0
224     assert a["free_credits_with_account"] == 0 and a["daily_free_credit"] is False
225     assert start(client, make_job(client)).status_code == 402
226 
227 
228 def test_browser_jobs_leave_the_free_credit_alone(client, cloud):
229     r = client.post("/api/local/jobs", json=BOOK)
230     assert r.status_code == 200 and r.json()["tier"] == "free"
231     assert account(client)["free_credits"] == 1