Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.

Commit 26de3e8

Browse files
authored
Merge pull request #373 from arpit73/master
Publishing Public Suffix List as a DAFSA binary
2 parents cb65c4e + 4cca805 commit 26de3e8

File tree

3 files changed

+317
-0
lines changed

3 files changed

+317
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ venv/
44
__pycache__/
55
docs/build/
66
requests_cache1.sqlite
7+
.vscode/

commands/publish_dafsa.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
import json
3+
import tempfile
4+
import subprocess
5+
6+
import requests
7+
from kinto_http import Client, KintoException
8+
9+
10+
PSL_FILENAME = "public_suffix_list.dat"
11+
12+
COMMIT_HASH_URL = (
13+
f"https://api.github.com/repos/publicsuffix/list/commits?path={PSL_FILENAME}"
14+
)
15+
LIST_URL = f"https://raw.githubusercontent.com/publicsuffix/list/master/{PSL_FILENAME}"
16+
17+
MAKE_DAFSA_PY = "https://raw.githubusercontent.com/arpit73/temp_dafsa_testing_repo/master/publishing/make_dafsa.py" # noqa
18+
PREPARE_TLDS_PY = "https://raw.githubusercontent.com/arpit73/temp_dafsa_testing_repo/master/publishing/prepare_tlds.py" # noqa
19+
20+
BUCKET_ID = os.getenv("BUCKET_ID", "main-workspace")
21+
COLLECTION_ID = "public-suffix-list"
22+
RECORD_ID = "tld-dafsa"
23+
24+
25+
def get_latest_hash(url):
26+
response = requests.get(url)
27+
response.raise_for_status()
28+
return response.json()[0]["sha"]
29+
30+
31+
def download_resources(directory, *urls):
32+
for url in urls:
33+
file_name = os.path.basename(url)
34+
file_location = os.path.join(directory, file_name)
35+
response = requests.get(url, stream=True)
36+
response.raise_for_status()
37+
38+
with open(file_location, "wb") as f:
39+
for chunk in response.iter_content(chunk_size=1024):
40+
f.write(chunk)
41+
42+
43+
def get_stored_hash(client):
44+
record = {}
45+
try:
46+
record = client.get_record(id=RECORD_ID)
47+
except KintoException as e:
48+
if e.response is None or e.response.status_code == 404:
49+
raise
50+
return record.get("data", {}).get("commit-hash")
51+
52+
53+
def prepare_dafsa(directory):
54+
download_resources(directory, LIST_URL, MAKE_DAFSA_PY, PREPARE_TLDS_PY)
55+
"""
56+
prepare_tlds.py is called with the three arguments the location of
57+
the downloaded public suffix list, the name of the output file and
58+
the '--bin' flag to create a binary file
59+
"""
60+
output_binary_name = "dafsa.bin"
61+
output_binary_path = os.path.join(directory, output_binary_name)
62+
prepare_tlds_py_path = os.path.join(directory, "prepare_tlds.py")
63+
raw_psl_path = os.path.join(directory, PSL_FILENAME)
64+
# Make the DAFSA
65+
command = (
66+
f"python3 {prepare_tlds_py_path} {raw_psl_path} --bin > {output_binary_path}"
67+
)
68+
run = subprocess.Popen(
69+
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
70+
)
71+
run.wait()
72+
if run.returncode != 0:
73+
raise Exception("DAFSA Build Failed !!!")
74+
75+
return output_binary_path
76+
77+
78+
def remote_settings_publish(client, latest_hash, binary_path):
79+
# Upload the attachment
80+
binary_name = os.path.basename(binary_path)
81+
mimetype = "application/octet-stream"
82+
filecontent = open(binary_path, "rb").read()
83+
record_uri = client.get_endpoint("record", id=RECORD_ID)
84+
attachment_uri = f"{record_uri}/attachment"
85+
multipart = [("attachment", (binary_name, filecontent, mimetype))]
86+
commit_hash = json.dumps({"commit-hash": latest_hash})
87+
client.session.request(
88+
method="post", data=commit_hash, endpoint=attachment_uri, files=multipart
89+
)
90+
# Requesting the new record for review
91+
client.patch_collection(data={"status": "to-review"})
92+
93+
94+
def publish_dafsa(event, context):
95+
server = event.get("server") or os.getenv("SERVER")
96+
auth = event.get("auth") or os.getenv("AUTH")
97+
# Auth format assumed to be "Username:Password"
98+
if auth:
99+
auth = tuple(auth.split(":", 1))
100+
101+
client = Client(
102+
server_url=server, auth=auth, bucket=BUCKET_ID, collection=COLLECTION_ID
103+
)
104+
105+
latest_hash = get_latest_hash(COMMIT_HASH_URL)
106+
stored_hash = get_stored_hash(client)
107+
108+
if stored_hash != latest_hash:
109+
with tempfile.TemporaryDirectory() as tmp:
110+
output_binary_path = prepare_dafsa(tmp)
111+
remote_settings_publish(client, latest_hash, output_binary_path)

tests/test_publish_dafsa.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import os
2+
import tempfile
3+
import unittest
4+
from unittest import mock
5+
6+
import requests
7+
import responses
8+
from kinto_http import Client, KintoException
9+
10+
11+
from commands.publish_dafsa import (
12+
get_latest_hash,
13+
download_resources,
14+
get_stored_hash,
15+
prepare_dafsa,
16+
remote_settings_publish,
17+
publish_dafsa,
18+
PREPARE_TLDS_PY,
19+
MAKE_DAFSA_PY,
20+
LIST_URL,
21+
BUCKET_ID,
22+
COLLECTION_ID,
23+
RECORD_ID,
24+
COMMIT_HASH_URL,
25+
)
26+
27+
28+
class TestsGetLatestHash(unittest.TestCase):
29+
def test_get_latest_hash_returns_sha1_hash(self):
30+
size_latest_hash = len(get_latest_hash(COMMIT_HASH_URL))
31+
self.assertEqual(size_latest_hash, 40)
32+
33+
@responses.activate
34+
def test_HTTPError_raised_when_404(self):
35+
responses.add(
36+
responses.GET, COMMIT_HASH_URL, json={"error": "not found"}, status=404
37+
)
38+
with self.assertRaises(requests.exceptions.HTTPError) as e:
39+
get_latest_hash(COMMIT_HASH_URL)
40+
self.assertEqual(e.status_code, 404)
41+
42+
43+
class TestDownloadResources(unittest.TestCase):
44+
def test_all_files_downloaded_with_correct_names(self):
45+
with tempfile.TemporaryDirectory() as tmp:
46+
download_resources(tmp, PREPARE_TLDS_PY, MAKE_DAFSA_PY, LIST_URL)
47+
self.assertEqual(
48+
sorted(os.listdir(tmp)),
49+
sorted(["public_suffix_list.dat", "prepare_tlds.py", "make_dafsa.py"]),
50+
)
51+
52+
@responses.activate
53+
def test_HTTPError_raised_when_404(self):
54+
with tempfile.TemporaryDirectory() as tmp:
55+
responses.add(
56+
responses.GET, PREPARE_TLDS_PY, json={"error": "not found"}, status=404
57+
)
58+
with self.assertRaises(requests.exceptions.HTTPError) as e:
59+
download_resources(tmp, PREPARE_TLDS_PY)
60+
self.assertEqual(e.status_code, 404)
61+
62+
63+
class TestGetStoredHash(unittest.TestCase):
64+
def setUp(self):
65+
server = "https://fake-server.net/v1"
66+
auth = ("arpit73", "pAsSwErD")
67+
self.client = Client(
68+
server_url=server, auth=auth, bucket=BUCKET_ID, collection=COLLECTION_ID
69+
)
70+
self.record_uri = server + self.client.get_endpoint(
71+
"record", id=RECORD_ID, bucket=BUCKET_ID, collection=COLLECTION_ID
72+
)
73+
74+
@responses.activate
75+
def test_stored_hash_fetched_successfully(self):
76+
responses.add(
77+
responses.GET,
78+
self.record_uri,
79+
json={"data": {"commit-hash": "fake-commit-hash"}},
80+
)
81+
stored_hash = get_stored_hash(self.client)
82+
self.assertEqual(stored_hash, "fake-commit-hash")
83+
84+
@responses.activate
85+
def test_KintoException_raised_when_stored_hash_fetching_failed(self):
86+
responses.add(
87+
responses.GET, self.record_uri, json={"error": "not found"}, status=404
88+
)
89+
with self.assertRaises(KintoException) as e:
90+
get_stored_hash(self.client)
91+
self.assertEqual(e.status_code, 404)
92+
93+
94+
class TestPrepareDafsa(unittest.TestCase):
95+
def test_file_is_created_in_output_folder(self):
96+
with tempfile.TemporaryDirectory() as tmp:
97+
output_binary_path = prepare_dafsa(tmp)
98+
self.assertIn(os.path.basename(output_binary_path), os.listdir(tmp))
99+
self.assertGreater(os.path.getsize(output_binary_path), 0)
100+
101+
def test_exception_is_raised_when_process_returns_non_zero(self):
102+
with tempfile.TemporaryDirectory() as tmp:
103+
with mock.patch("subprocess.Popen") as mocked:
104+
mocked.return_value.returncode = 1
105+
with self.assertRaises(Exception) as e:
106+
prepare_dafsa(tmp)
107+
self.assertIn("DAFSA Build Failed", str(e.exception))
108+
109+
110+
class TestRemoteSettingsPublish(unittest.TestCase):
111+
def setUp(self):
112+
server = "https://fake-server.net/v1"
113+
auth = ("arpit73", "pAsSwErD")
114+
self.client = Client(
115+
server_url=server, auth=auth, bucket=BUCKET_ID, collection=COLLECTION_ID
116+
)
117+
record_uri = server + self.client.get_endpoint(
118+
"record", id=RECORD_ID, bucket=BUCKET_ID, collection=COLLECTION_ID
119+
)
120+
self.collection_uri = server + self.client.get_endpoint(
121+
"collection", bucket=BUCKET_ID, collection=COLLECTION_ID
122+
)
123+
self.attachment_uri = f"{record_uri}/attachment"
124+
125+
@responses.activate
126+
def test_record_was_posted(self):
127+
responses.add(
128+
responses.POST,
129+
self.attachment_uri,
130+
json={"data": {"commit-hash": "fake-commit-hash"}},
131+
)
132+
responses.add(
133+
responses.PATCH, self.collection_uri, json={"data": {"status": "to-review"}}
134+
)
135+
136+
with tempfile.TemporaryDirectory() as tmp:
137+
dafsa_filename = f"{tmp}/dafsa.bin"
138+
with open(dafsa_filename, "wb") as f:
139+
f.write(b"some binary data")
140+
remote_settings_publish(self.client, "fake-commit-hash", dafsa_filename)
141+
142+
self.assertEqual(len(responses.calls), 2)
143+
144+
self.assertEqual(responses.calls[0].request.url, self.attachment_uri)
145+
self.assertEqual(responses.calls[0].request.method, "POST")
146+
147+
self.assertEqual(responses.calls[1].request.url, self.collection_uri)
148+
self.assertEqual(responses.calls[1].request.method, "PATCH")
149+
150+
151+
class TestPublishDafsa(unittest.TestCase):
152+
def setUp(self):
153+
self.event = {
154+
"server": "https://fake-server.net/v1",
155+
"auth": "arpit73:pAsSwErD",
156+
}
157+
client = Client(
158+
server_url=self.event.get("server"),
159+
auth=("arpit73", "pAsSwErD"),
160+
bucket=BUCKET_ID,
161+
collection=COLLECTION_ID,
162+
)
163+
self.record_uri = self.event.get("server") + client.get_endpoint(
164+
"record", id=RECORD_ID, bucket=BUCKET_ID, collection=COLLECTION_ID
165+
)
166+
167+
mocked = mock.patch("commands.publish_dafsa.prepare_dafsa")
168+
self.addCleanup(mocked.stop)
169+
self.mocked_prepare = mocked.start()
170+
171+
mocked = mock.patch("commands.publish_dafsa.remote_settings_publish")
172+
self.addCleanup(mocked.stop)
173+
self.mocked_publish = mocked.start()
174+
175+
@responses.activate
176+
def test_prepare_and_publish_are_not_called_when_hashes_matches(self):
177+
responses.add(
178+
responses.GET, COMMIT_HASH_URL, json=[{"sha": "fake-commit-hash"}]
179+
)
180+
responses.add(
181+
responses.GET,
182+
self.record_uri,
183+
json={"data": {"commit-hash": "fake-commit-hash"}},
184+
)
185+
186+
publish_dafsa(self.event, context=None)
187+
188+
self.assertFalse(self.mocked_prepare.called)
189+
self.assertFalse(self.mocked_publish.called)
190+
191+
@responses.activate
192+
def test_prepare_and_publish_are_called_when_hashes_do_not_match(self):
193+
responses.add(
194+
responses.GET, COMMIT_HASH_URL, json=[{"sha": "fake-commit-hash"}]
195+
)
196+
responses.add(
197+
responses.GET,
198+
self.record_uri,
199+
json={"data": {"commit-hash": "different-fake-commit-hash"}},
200+
)
201+
202+
publish_dafsa(self.event, context=None)
203+
204+
self.assertTrue(self.mocked_prepare.called)
205+
self.assertTrue(self.mocked_publish.called)

0 commit comments

Comments
 (0)