tangled
alpha
login
or
join now
zenfyr.dev
/
xpost
2
fork
atom
social media crossposting tool. 3rd time's the charm
mastodon
misskey
crossposting
bluesky
2
fork
atom
overview
issues
1
pulls
pipelines
start adding tests
zenfyr.dev
5 months ago
eda25493
749c26db
verified
This commit was signed with the committer's
known signature
.
zenfyr.dev
SSH Key Fingerprint:
SHA256:TtcIcnTnoAB5mqHofsaOxIgiMzfVBxej1AXT7DQdrTE=
0/1
run-tests.yml
failed
45s
+329
6 changed files
expand all
collapse all
unified
split
.tangled
workflows
run-tests.yml
pyproject.toml
tests
util
html_test.py
markdown_test.py
util_test.py
uv.lock
+14
.tangled/workflows/run-tests.yml
reviewed
···
1
1
+
when:
2
2
+
- event: ["push", "manual"]
3
3
+
branch: ["next"]
4
4
+
5
5
+
engine: nixery
6
6
+
7
7
+
dependencies:
8
8
+
nixpkgs:
9
9
+
- uv
10
10
+
11
11
+
steps:
12
12
+
- name: run tests
13
13
+
command: |
14
14
+
uv run pytest -vv
+8
pyproject.toml
reviewed
···
11
11
"requests>=2.32.5",
12
12
"websockets>=15.0.1",
13
13
]
14
14
+
15
15
+
[dependency-groups]
16
16
+
dev = [
17
17
+
"pytest>=8.4.2",
18
18
+
]
19
19
+
20
20
+
[tool.pytest.ini_options]
21
21
+
pythonpath = ["."]
+16
tests/util/html_test.py
reviewed
···
1
1
+
import html
2
2
+
from util.html import HTMLToFragmentsParser
3
3
+
import cross.fragments as f
4
4
+
import pytest
5
5
+
6
6
+
@pytest.fixture()
7
7
+
def parser():
8
8
+
return HTMLToFragmentsParser()
9
9
+
10
10
+
def test_html(parser: HTMLToFragmentsParser):
11
11
+
input = "<p><del>excuse</del> <em>me</em>, <strong>test</strong> post</p><blockquote><p>very testy <a href=\"https://google.com\" target=\"_blank\" rel=\"nofollow noopener\">post</a></p></blockquote><pre><code>cat << food<br></code></pre>"
12
12
+
parser.feed(input)
13
13
+
text, fragments = parser.get_result()
14
14
+
15
15
+
# TODO
16
16
+
#assert text == "~~excuse~~ *me*, **test** post\n\n> very testy post\n\n```\ncat << food\n```\n"
+161
tests/util/markdown_test.py
reviewed
···
1
1
+
from util.markdown import MarkdownParser
2
2
+
import cross.fragments as f
3
3
+
import pytest
4
4
+
5
5
+
EMOJI = "🤬🤬"
6
6
+
7
7
+
8
8
+
@pytest.fixture()
9
9
+
def parser():
10
10
+
return MarkdownParser()
11
11
+
12
12
+
13
13
+
def test_empty(parser: MarkdownParser):
14
14
+
text, frgs = parser.parse("")
15
15
+
assert text == ""
16
16
+
assert frgs == []
17
17
+
18
18
+
19
19
+
def test_no_formatting(parser: MarkdownParser):
20
20
+
text, frgs = parser.parse("text no formatting!")
21
21
+
assert text == "text no formatting!"
22
22
+
assert frgs == []
23
23
+
24
24
+
25
25
+
def test_link(parser: MarkdownParser):
26
26
+
text, frgs = parser.parse("https://google.com")
27
27
+
assert text == "https://google.com"
28
28
+
assert len(frgs) == 1
29
29
+
30
30
+
frg = frgs[0]
31
31
+
assert isinstance(frg, f.LinkFragment)
32
32
+
assert frg.start == 0 and frg.end == 18
33
33
+
assert frg.url == "https://google.com"
34
34
+
35
35
+
36
36
+
def test_link_emojis(parser: MarkdownParser):
37
37
+
input = f"{EMOJI} https://google.com"
38
38
+
text, frgs = parser.parse(input)
39
39
+
assert text == input
40
40
+
assert len(frgs) == 1
41
41
+
42
42
+
frg = frgs[0]
43
43
+
assert isinstance(frg, f.LinkFragment)
44
44
+
assert frg.start == 9 and frg.end == 27
45
45
+
assert frg.url == "https://google.com"
46
46
+
47
47
+
48
48
+
def test_label_link(parser: MarkdownParser):
49
49
+
text, frgs = parser.parse("[hello](https://google.com)")
50
50
+
assert text == "hello"
51
51
+
assert len(frgs) == 1
52
52
+
53
53
+
frg = frgs[0]
54
54
+
assert isinstance(frg, f.LinkFragment)
55
55
+
assert frg.start == 0 and frg.end == 5
56
56
+
assert frg.url == "https://google.com"
57
57
+
58
58
+
59
59
+
def test_label_link_emojis(parser: MarkdownParser):
60
60
+
input = f"[{EMOJI}]( https://google.com)"
61
61
+
text, frgs = parser.parse(input)
62
62
+
assert text == EMOJI
63
63
+
assert len(frgs) == 1
64
64
+
65
65
+
frg = frgs[0]
66
66
+
assert isinstance(frg, f.LinkFragment)
67
67
+
assert frg.start == 0 and frg.end == 8
68
68
+
assert frg.url == "https://google.com"
69
69
+
70
70
+
71
71
+
def test_tag(parser: MarkdownParser):
72
72
+
input = "#testing"
73
73
+
text, frgs = parser.parse(input)
74
74
+
assert text == input
75
75
+
assert len(frgs) == 1
76
76
+
77
77
+
frg = frgs[0]
78
78
+
assert isinstance(frg, f.TagFragment)
79
79
+
assert frg.start == 0 and frg.end == 8
80
80
+
assert frg.tag == "testing"
81
81
+
82
82
+
def test_tag_emojis(parser: MarkdownParser):
83
83
+
input = f"{EMOJI} #testing"
84
84
+
text, frgs = parser.parse(input)
85
85
+
assert text == input
86
86
+
assert len(frgs) == 1
87
87
+
88
88
+
frg = frgs[0]
89
89
+
assert isinstance(frg, f.TagFragment)
90
90
+
assert frg.start == 9 and frg.end == 17
91
91
+
assert frg.tag == "testing"
92
92
+
93
93
+
def test_mention(parser: MarkdownParser):
94
94
+
input = "@zen@merping.synth.download"
95
95
+
text, frgs = parser.parse(input)
96
96
+
assert text == input
97
97
+
assert len(frgs) == 1
98
98
+
99
99
+
frg = frgs[0]
100
100
+
assert isinstance(frg, f.MentionFragment)
101
101
+
assert frg.start == 0 and frg.end == 27
102
102
+
assert frg.uri == "zen@merping.synth.download"
103
103
+
104
104
+
def test_mention_emojis(parser: MarkdownParser):
105
105
+
input = f"{EMOJI} @zen@merping.synth.download"
106
106
+
text, frgs = parser.parse(input)
107
107
+
assert text == input
108
108
+
assert len(frgs) == 1
109
109
+
110
110
+
frg = frgs[0]
111
111
+
assert isinstance(frg, f.MentionFragment)
112
112
+
assert frg.start == 9 and frg.end == 36
113
113
+
assert frg.uri == "zen@merping.synth.download"
114
114
+
115
115
+
def test_mixed(parser: MarkdownParser):
116
116
+
input = "#testing_tag @zen@merping.synth.download [hello](https://zenfyr.dev/) hii! https://example.com"
117
117
+
text, frgs = parser.parse(input)
118
118
+
119
119
+
expected_text = "#testing_tag @zen@merping.synth.download hello hii! https://example.com"
120
120
+
assert text == expected_text
121
121
+
assert len(frgs) == 4
122
122
+
123
123
+
assert isinstance(frgs[0], f.TagFragment)
124
124
+
assert frgs[0].start == 0 and frgs[0].end == 12
125
125
+
assert frgs[0].tag == "testing_tag"
126
126
+
127
127
+
assert isinstance(frgs[1], f.MentionFragment)
128
128
+
assert frgs[1].start == 13 and frgs[1].end == 40
129
129
+
assert frgs[1].uri == "zen@merping.synth.download"
130
130
+
131
131
+
assert isinstance(frgs[2], f.LinkFragment)
132
132
+
assert frgs[2].start == 41 and frgs[2].end == 46
133
133
+
assert frgs[2].url == "https://zenfyr.dev/"
134
134
+
135
135
+
assert isinstance(frgs[3], f.LinkFragment)
136
136
+
assert frgs[3].start == 52 and frgs[3].end == 71
137
137
+
assert frgs[3].url == "https://example.com"
138
138
+
139
139
+
def test_mixed_html(parser: MarkdownParser):
140
140
+
input = f"<p>#testing_tag @zen@merping.synth.download</p> {EMOJI} <a href=\"https://zenfyr.dev/\"><b>hello</b></a> hii! https://example.com"
141
141
+
text, frgs = parser.parse(input)
142
142
+
143
143
+
expected_text = f"#testing_tag @zen@merping.synth.download\n\n {EMOJI} **hello** hii! https://example.com"
144
144
+
assert text == expected_text
145
145
+
assert len(frgs) == 4
146
146
+
147
147
+
assert isinstance(frgs[0], f.TagFragment)
148
148
+
assert frgs[0].start == 0 and frgs[0].end == 12
149
149
+
assert frgs[0].tag == "testing_tag"
150
150
+
151
151
+
assert isinstance(frgs[1], f.MentionFragment)
152
152
+
assert frgs[1].start == 13 and frgs[1].end == 40
153
153
+
assert frgs[1].uri == "zen@merping.synth.download"
154
154
+
155
155
+
assert isinstance(frgs[2], f.LinkFragment)
156
156
+
assert frgs[2].start == 52 and frgs[2].end == 61
157
157
+
assert frgs[2].url == "https://zenfyr.dev/"
158
158
+
159
159
+
assert isinstance(frgs[3], f.LinkFragment)
160
160
+
assert frgs[3].start == 67 and frgs[3].end == 86
161
161
+
assert frgs[3].url == "https://example.com"
+61
tests/util/util_test.py
reviewed
···
1
1
+
import util.util as u
2
2
+
from unittest.mock import patch
3
3
+
import pytest
4
4
+
5
5
+
6
6
+
def test_normalize_service_url_http():
7
7
+
assert u.normalize_service_url("http://example.com") == "http://example.com"
8
8
+
assert u.normalize_service_url("http://example.com/") == "http://example.com"
9
9
+
10
10
+
11
11
+
def test_normalize_service_url_invalid_schemes():
12
12
+
with pytest.raises(ValueError, match="Invalid service url"):
13
13
+
_ = u.normalize_service_url("ftp://example.com")
14
14
+
with pytest.raises(ValueError, match="Invalid service url"):
15
15
+
_ = u.normalize_service_url("example.com")
16
16
+
with pytest.raises(ValueError, match="Invalid service url"):
17
17
+
_ = u.normalize_service_url("//example.com")
18
18
+
19
19
+
20
20
+
def test_read_env_missing_env_var():
21
21
+
data = {"token": "env:MISSING_VAR", "keep": "value"}
22
22
+
with patch.dict("os.environ", {}, clear=True):
23
23
+
u.read_env(data)
24
24
+
assert data == {"keep": "value"}
25
25
+
assert "token" not in data
26
26
+
27
27
+
28
28
+
def test_read_env_no_env_prefix():
29
29
+
data = {"token": "literal_value", "number": 123}
30
30
+
u.read_env(data)
31
31
+
assert data == {"token": "literal_value", "number": 123}
32
32
+
33
33
+
34
34
+
def test_read_env_deeply_nested():
35
35
+
data = {"level1": {"level2": {"token": "env:DEEP_TOKEN"}}}
36
36
+
with patch.dict("os.environ", {"DEEP_TOKEN": "deep_secret"}):
37
37
+
u.read_env(data)
38
38
+
assert data["level1"]["level2"]["token"] == "deep_secret"
39
39
+
40
40
+
41
41
+
def test_read_env_mixed_types():
42
42
+
data = {
43
43
+
"string": "env:TOKEN",
44
44
+
"number": 42,
45
45
+
"list": [1, 2, 3],
46
46
+
"none": None,
47
47
+
"bool": True,
48
48
+
}
49
49
+
with patch.dict("os.environ", {"TOKEN": "secret"}):
50
50
+
u.read_env(data)
51
51
+
assert data["string"] == "secret"
52
52
+
assert data["number"] == 42
53
53
+
assert data["list"] == [1, 2, 3]
54
54
+
assert data["none"] is None
55
55
+
assert data["bool"] is True
56
56
+
57
57
+
58
58
+
def test_read_env_empty_dict():
59
59
+
data = {}
60
60
+
u.read_env(data)
61
61
+
assert data == {}
+69
uv.lock
reviewed
···
69
69
]
70
70
71
71
[[package]]
72
72
+
name = "colorama"
73
73
+
version = "0.4.6"
74
74
+
source = { registry = "https://pypi.org/simple" }
75
75
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
76
76
+
wheels = [
77
77
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
78
78
+
]
79
79
+
80
80
+
[[package]]
72
81
name = "dnspython"
73
82
version = "2.8.0"
74
83
source = { registry = "https://pypi.org/simple" }
···
93
102
]
94
103
95
104
[[package]]
105
105
+
name = "iniconfig"
106
106
+
version = "2.3.0"
107
107
+
source = { registry = "https://pypi.org/simple" }
108
108
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
109
109
+
wheels = [
110
110
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
111
111
+
]
112
112
+
113
113
+
[[package]]
114
114
+
name = "packaging"
115
115
+
version = "25.0"
116
116
+
source = { registry = "https://pypi.org/simple" }
117
117
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
118
118
+
wheels = [
119
119
+
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
120
120
+
]
121
121
+
122
122
+
[[package]]
123
123
+
name = "pluggy"
124
124
+
version = "1.6.0"
125
125
+
source = { registry = "https://pypi.org/simple" }
126
126
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
127
127
+
wheels = [
128
128
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
129
129
+
]
130
130
+
131
131
+
[[package]]
132
132
+
name = "pygments"
133
133
+
version = "2.19.2"
134
134
+
source = { registry = "https://pypi.org/simple" }
135
135
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
136
136
+
wheels = [
137
137
+
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
138
138
+
]
139
139
+
140
140
+
[[package]]
141
141
+
name = "pytest"
142
142
+
version = "8.4.2"
143
143
+
source = { registry = "https://pypi.org/simple" }
144
144
+
dependencies = [
145
145
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
146
146
+
{ name = "iniconfig" },
147
147
+
{ name = "packaging" },
148
148
+
{ name = "pluggy" },
149
149
+
{ name = "pygments" },
150
150
+
]
151
151
+
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
152
152
+
wheels = [
153
153
+
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
154
154
+
]
155
155
+
156
156
+
[[package]]
96
157
name = "python-magic"
97
158
version = "0.4.27"
98
159
source = { registry = "https://pypi.org/simple" }
···
168
229
{ name = "websockets" },
169
230
]
170
231
232
232
+
[package.dev-dependencies]
233
233
+
dev = [
234
234
+
{ name = "pytest" },
235
235
+
]
236
236
+
171
237
[package.metadata]
172
238
requires-dist = [
173
239
{ name = "dnspython", specifier = ">=2.8.0" },
···
176
242
{ name = "requests", specifier = ">=2.32.5" },
177
243
{ name = "websockets", specifier = ">=15.0.1" },
178
244
]
245
245
+
246
246
+
[package.metadata.requires-dev]
247
247
+
dev = [{ name = "pytest", specifier = ">=8.4.2" }]