Skip to content

Commit adaf0e8

Browse files
committed
Add basic integration test for the API
We just start PHP server running selfoss and a web server with feeds, and then try to add the feed to selfoss, fetch the contents and then view the fetched items. But this should be good enough smoke test.
1 parent 0e7d97d commit adaf0e8

File tree

10 files changed

+307
-0
lines changed

10 files changed

+307
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ node_modules
1515
.env
1616
vendor/
1717
.php_cs.cache
18+
__pycache__

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ cache:
3232

3333
before_install:
3434
- nvm install 12
35+
- pyenv install 3.6.3
36+
- pyenv global 3.6.3
37+
- pip3 install requests bcrypt
3538
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi
3639
- composer self-update
3740

@@ -44,6 +47,7 @@ script:
4447
- npm run lint:server
4548
- if [ "$CS_FIXER" = true ]; then npm run cs:server; fi
4649
- npm run test:server
50+
- npm run test:integration
4751

4852
before_deploy:
4953
- git config --global user.email 'Travis CI'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"lint:client": "npm run --prefix assets/ lint",
2828
"lint:server": "composer run-script lint",
2929
"test:server": "composer run-script test",
30+
"test:integration": "python3 tests/integration/run.py",
3031
"postinstall": "npm run install-dependencies"
3132
}
3233
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import sys
2+
import threading
3+
from http.server import BaseHTTPRequestHandler, HTTPServer
4+
from .feeds.fibonacci import numbers_feed
5+
6+
7+
FIBONACCI_FEED_LENGTH = 20
8+
9+
10+
class DataServer(BaseHTTPRequestHandler):
11+
'''
12+
A web server returning various feeds for selfoss to fetch.
13+
'''
14+
def do_GET(self):
15+
self.send_response(200)
16+
self.send_header('Content-type', 'application/rss+xml')
17+
self.end_headers()
18+
# Currently, it will only send a feed listing first few fibonacci numbers.
19+
self.wfile.write(numbers_feed(FIBONACCI_FEED_LENGTH).encode('utf-8'))
20+
21+
22+
class DataServerThread(threading.Thread):
23+
'''
24+
A thread that starts and stops `DataServer`.
25+
'''
26+
def __init__(self, host_name='localhost', port=8000):
27+
super().__init__()
28+
self.host_name = host_name
29+
self.port = port
30+
31+
def run(self):
32+
with HTTPServer((self.host_name, self.port), DataServer) as self.web_server:
33+
print(f'selfoss server started http://{self.host_name}:{self.port}', file=sys.stderr)
34+
35+
self.web_server.serve_forever()
36+
37+
print('selfoss server stopped.', file=sys.stderr)
38+
39+
def stop(self):
40+
self.web_server.shutdown()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import datetime
2+
from xml.dom import getDOMImplementation
3+
4+
5+
def numbers(n: int):
6+
'''
7+
Generates first *n* fibonacci numbers.
8+
'''
9+
numbers = []
10+
a = 1
11+
b = 2
12+
for i in range(n):
13+
numbers.append((i, a))
14+
a, b = b, a + b
15+
yield i, a
16+
17+
18+
def numbers_feed(n: int) -> str:
19+
'''
20+
Generates a RSS feed containing first *n* fibonacci numbers.
21+
'''
22+
doc = getDOMImplementation().createDocument(None, 'rss', None)
23+
root = doc.documentElement
24+
root.setAttribute('version', '2.0')
25+
26+
channel = doc.createElement('channel')
27+
root.appendChild(channel)
28+
29+
title = doc.createElement('title')
30+
channel.appendChild(title)
31+
title.appendChild(doc.createTextNode(f'{n} numbers'))
32+
33+
od = datetime.datetime.now() - datetime.timedelta(minutes=n)
34+
35+
for (k, a) in reversed(list(numbers(n))):
36+
item = doc.createElement('item')
37+
channel.appendChild(item)
38+
39+
item_title = doc.createElement('title')
40+
item.appendChild(item_title)
41+
item_title.appendChild(doc.createTextNode(f'{a}'))
42+
43+
item_guid = doc.createElement('guid')
44+
item.appendChild(item_guid)
45+
item_guid.setAttribute('isPermaLink', 'true')
46+
item_guid.appendChild(doc.createTextNode(f'https://en.wikipedia.org/wiki/{a}'))
47+
48+
d = od + datetime.timedelta(minutes=k)
49+
item_pubdate = doc.createElement('pubDate')
50+
item.appendChild(item_pubdate)
51+
item_pubdate.appendChild(doc.createTextNode(d.strftime('%a, %d %b %Y %H:%M:%S %z')))
52+
53+
return doc.toprettyxml(indent=' '*4)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import time
2+
import unittest
3+
from pathlib import Path
4+
from .data_server import DataServerThread
5+
from .selfoss_server import SelfossServerThread
6+
7+
8+
class SelfossIntegration(unittest.TestCase):
9+
'''
10+
Base class for selfoss integration tests.
11+
It starts selfoss server and a server providing test feeds.
12+
'''
13+
def setUp(self):
14+
current_dir = Path(__file__).parent.absolute()
15+
16+
self.data_host_name = 'localhost'
17+
self.data_port = 8080
18+
self.selfoss_host_name = 'localhost'
19+
self.selfoss_port = 8081
20+
self.selfoss_username = 'admin'
21+
self.selfoss_password = 'hunter2'
22+
23+
self.selfoss_root = current_dir.parent.parent.parent
24+
25+
self.selfoss_thread = SelfossServerThread(
26+
selfoss_root=self.selfoss_root,
27+
password=self.selfoss_password,
28+
username=self.selfoss_username,
29+
host_name=self.selfoss_host_name,
30+
port=self.selfoss_port,
31+
)
32+
self.selfoss_thread.start()
33+
34+
self.data_server_thread = DataServerThread(
35+
host_name=self.data_host_name,
36+
port=self.data_port,
37+
)
38+
self.data_server_thread.start()
39+
40+
# Wait for the servers to become properly initialized.
41+
time.sleep(2)
42+
43+
def tearDown(self):
44+
self.selfoss_thread.stop()
45+
self.data_server_thread.stop()
46+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import requests
2+
3+
4+
class SelfossApi:
5+
def __init__(self, base_uri: str):
6+
self.base_uri = base_uri
7+
# We still use cookies for authentication so let’s persist them across requests.
8+
self.session = requests.Session()
9+
10+
def login(self, username, password):
11+
r = self.session.post(
12+
f'{self.base_uri}/login',
13+
data={
14+
'username': username,
15+
'password': password,
16+
},
17+
)
18+
r.raise_for_status()
19+
20+
return r.json()
21+
22+
def logout(self):
23+
r = self.session.get(
24+
f'{self.base_uri}/logout',
25+
)
26+
r.raise_for_status()
27+
28+
return r.json()
29+
30+
def get_items(self):
31+
r = self.session.get(
32+
f'{self.base_uri}/items',
33+
)
34+
r.raise_for_status()
35+
36+
return r.json()
37+
38+
def add_source(self, spout: str, **params):
39+
r = self.session.post(
40+
f'{self.base_uri}/source',
41+
data={
42+
**params,
43+
'spout': spout,
44+
},
45+
)
46+
r.raise_for_status()
47+
48+
return r.json()
49+
50+
def refresh_all(self):
51+
r = self.session.get(
52+
f'{self.base_uri}/update',
53+
)
54+
r.raise_for_status()
55+
56+
return r.text
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import bcrypt
2+
import os
3+
import subprocess
4+
import tempfile
5+
import threading
6+
from pathlib import Path
7+
8+
9+
class SelfossServerThread(threading.Thread):
10+
'''
11+
A thread that starts and stops PHP’s built-in web server running selfoss.
12+
'''
13+
def __init__(self, selfoss_root: Path, username: str, password: str, host_name: str, port: int):
14+
super().__init__()
15+
self.selfoss_root = selfoss_root
16+
self.username = username
17+
self.password = password
18+
self.host_name = host_name
19+
self.port = port
20+
21+
def run(self):
22+
with tempfile.TemporaryDirectory() as temp_dir:
23+
# Set up data directories.
24+
temp_dir = Path(temp_dir)
25+
data_dir = temp_dir / 'data'
26+
(data_dir / 'sqlite').mkdir(parents=True)
27+
(data_dir / 'thumbnails').mkdir(parents=True)
28+
(data_dir / 'favicons').mkdir(parents=True)
29+
30+
# Configure selfoss using environment variables for convenience.
31+
test_env = {
32+
**os.environ,
33+
'SELFOSS_DATADIR': data_dir,
34+
'SELFOSS_LOGGER_DESTINATION': 'error_log',
35+
'SELFOSS_USERNAME': self.username,
36+
'SELFOSS_PASSWORD': bcrypt.hashpw(self.password.encode('utf-8'), bcrypt.gensalt()),
37+
'SELFOSS_DB_TYPE': 'sqlite',
38+
'SELFOSS_PUBLIC': '1',
39+
'SELFOSS_LOGGER_LEVEL': 'DEBUG',
40+
}
41+
42+
current_dir = Path(__file__).parent.absolute()
43+
44+
php_command = [
45+
'php',
46+
# We need to enable reading environment variables.
47+
'-d', 'variables_order=EGPCS',
48+
'-S', f'{self.host_name}:{self.port}',
49+
'-c', current_dir / 'php.ini',
50+
self.selfoss_root / 'run.php',
51+
]
52+
53+
# Create the subprocess.
54+
self.proc = subprocess.Popen(
55+
php_command,
56+
env=test_env,
57+
cwd=self.selfoss_root,
58+
)
59+
60+
# Wait for it to finish.
61+
self.proc.communicate()
62+
63+
def stop(self):
64+
self.proc.kill()

tests/integration/php.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
display_errors = on
2+
display_startup_errors = on
3+
error_reporting = E_ALL
4+
log_errors = on

tests/integration/run.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import requests
2+
import unittest
3+
from helpers.data_server import FIBONACCI_FEED_LENGTH
4+
from helpers.integration import SelfossIntegration
5+
from helpers.selfoss_api import SelfossApi
6+
7+
8+
class BasicWorkflowTest(SelfossIntegration):
9+
def test_basic_workflow(self):
10+
selfoss_base_uri = f'http://{self.selfoss_host_name}:{self.selfoss_port}'
11+
selfoss_api = SelfossApi(selfoss_base_uri)
12+
13+
items = selfoss_api.get_items()
14+
assert len(items) == 0, 'New selfoss instance should have no items.'
15+
16+
fibonacci_feed_uri = f'http://{self.data_host_name}:{self.data_port}/fibonacci.xml'
17+
18+
try:
19+
add_feed = selfoss_api.add_source('spouts\\rss\\feed', url=fibonacci_feed_uri)
20+
assert 'Adding source is privileged operation and should fail without login.'
21+
except requests.exceptions.HTTPError as e:
22+
assert e.response.status_code == 403, 'Adding source should require authentication.'
23+
24+
login = selfoss_api.login(self.selfoss_username, self.selfoss_password)
25+
assert login['success'], f'Authentication should succeed but it failed with {login["error"]}.'
26+
27+
add_feed = selfoss_api.add_source('spouts\\rss\\feed', url=fibonacci_feed_uri)
28+
assert add_feed['success'], 'Adding source should succeed.'
29+
assert add_feed['title'] == '20 numbers', 'Source should auto-detect feed title.'
30+
31+
refresh = selfoss_api.refresh_all()
32+
assert refresh == 'finished', 'Refreshing sources should succeed.'
33+
34+
items = selfoss_api.get_items()
35+
assert len(items) == FIBONACCI_FEED_LENGTH, 'After updating sources, there should be all items from the sources.'
36+
37+
if __name__ == '__main__':
38+
unittest.main()

0 commit comments

Comments
 (0)