Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,14 @@ jobs:
- name: Tests
if: ${{ !cancelled() && steps.deps.conclusion == 'success' }}
run: |
coverage run runtests.py --parallel
WIFI_MESH=1 coverage run runtests.py --parallel --exclude-tag=selenium_tests
SAMPLE_APP=1 coverage run ./runtests.py --parallel --exclude-tag=selenium_tests
coverage run runtests.py --tag=no_parallel
coverage run runtests.py --parallel --exclude-tag=no_parallel
WIFI_MESH=1 coverage run runtests.py --parallel \
--exclude-tag=no_parallel \
--exclude-tag=selenium_tests
SAMPLE_APP=1 coverage run ./runtests.py --parallel \
--exclude-tag=no_parallel \
--exclude-tag=selenium_tests
coverage combine
coverage xml
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
const data=JSON.parse(e.data);
if(data.type==="broadcast_topology"){
const topology=JSON.parse(data.topology);
window.graph.utils.JSONDataUpdate.call(window.graph, topology);
graph.utils.JSONDataUpdate.call(graph, topology);
}
}

Expand Down
192 changes: 192 additions & 0 deletions openwisp_network_topology/tests/test_realtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import json
from copy import copy
from time import sleep

import pytest
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
from channels.testing import ChannelsLiveServerTestCase, WebsocketCommunicator
from django.conf import settings
from django.test import tag
from django.urls import reverse
from django.utils.module_loading import import_string
from selenium.webdriver.common.by import By
from swapper import load_model

from openwisp_users.tests.utils import TestOrganizationMixin
from openwisp_utils.tests import SeleniumTestMixin

from .utils import CreateGraphObjectsMixin, LoadMixin

Link = load_model("topology", "Link")
Node = load_model("topology", "Node")
Topology = load_model("topology", "Topology")


@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
@tag("selenium_tests")
@tag("no_parallel")
class TestRealTime(
TestOrganizationMixin,
CreateGraphObjectsMixin,
LoadMixin,
SeleniumTestMixin,
ChannelsLiveServerTestCase,
):
app_label = "topology"
prefix = f"admin:{app_label}"
node_model = Node
link_model = Link
topology_model = Topology
application = import_string(getattr(settings, "ASGI_APPLICATION"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? This is imported when the class is loaded. This may happen before ready for all app config is executed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't imported. I tried it.

class CreateGraphObjectsMixin(object):
def _create_topology(self, **kwargs):
options = dict(
label="TestNetwork",
parser="netdiff.OlsrParser",
strategy="fetch",
url="http://127.0.0.1:9090",
protocol="OLSR",
version="0.8",
metric="ETX",
created="2017-07-10T20:02:52.483Z",
modified="2015-07-14T20:02:52.483Z",
)
options.update(kwargs)
t = self.topology_model(**options)
t.full_clean()
t.save()
return t

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought you were talking about models since GitHub highlighted those too. Yeah, it seems like we could have used it as well. I used this as a reference after seeing it in another test:

application = import_string(getattr(settings, "ASGI_APPLICATION"))


def setUp(self):
org = self._create_org()
self.admin = self._create_admin(
username=self.admin_username, password=self.admin_password
)
self.admin_client = self.client
self.admin_client.force_login(self.admin)

self.topology = self._create_topology(organization=org)
self.node1 = self._create_node(
label="node1",
addresses=["192.168.0.1"],
topology=self.topology,
organization=org,
)
self.node2 = self._create_node(
label="node2",
addresses=["192.168.0.2"],
topology=self.topology,
organization=org,
)
self.link = self._create_link(
source=self.node1,
target=self.node2,
topology=self.topology,
status="up",
)

def _snooze(self):
"""Allows a bit of time for the UI to update, reduces flakyness"""
sleep(0.25)

async def _get_communicator(self, admin_client, topology_id):
session_id = admin_client.cookies["sessionid"].value
communicator = WebsocketCommunicator(
self.application,
path=f"ws/network-topology/topology/{topology_id}/",
headers=[
(
b"cookie",
f"sessionid={session_id}".encode("ascii"),
)
],
)
return communicator

async def test_real_time_link_status_update(self):
# preparation
self.link.status = "down"
await database_sync_to_async(self.link.save)()
communicator = await self._get_communicator(self.admin_client, self.topology.pk)
connected, _ = await communicator.connect()
assert connected is True
path = reverse(f"{self.prefix}_topology_change", args=[self.topology.pk])
self.login()
self.open(path)
self.find_element(By.CSS_SELECTOR, "input.visualizelink").click()
# changing the status of a link will change it in the browser graph too
self.link.status = "up"
await database_sync_to_async(self.link.save)()
message = await communicator.receive_json_from()
assert (
json.loads(message["topology"])["links"][0]["properties"]["status"] == "up"
)
self._snooze()
self.assertEqual(
self.web_driver.execute_script("return graph.data;")["links"][0][
"properties"
]["status"],
"up",
)
# test status changing to down
self.link.status = "down"
await sync_to_async(self.link.save)()
message = await communicator.receive_json_from()
assert (
json.loads(message["topology"])["links"][0]["properties"]["status"]
== "down"
)
self._snooze()
self.assertEqual(
self.web_driver.execute_script("return graph.data;")["links"][0][
"properties"
]["status"],
"down",
)
await communicator.disconnect()

async def test_node_status_update(self):
# preparation
communicator = await self._get_communicator(self.admin_client, self.topology.pk)
connected, _ = await communicator.connect()
assert connected is True
path = reverse(f"{self.prefix}_topology_change", args=[self.topology.pk])
self.login()
self.open(path)
self.find_element(By.CSS_SELECTOR, "input.visualizelink").click()
# saving a new node will add it to the UI
new_node = copy(self.node1)
new_node.pk = None
await database_sync_to_async(new_node.save)()
message = await communicator.receive_json_from()
self.assertEqual(len(json.loads(message["topology"])["nodes"]), 3)
self._snooze()
self.assertEqual(
len(self.web_driver.execute_script("return graph.data;")["nodes"]),
3,
)
# deleting the node from the DB will remove it from the UI
await database_sync_to_async(new_node.delete)()
message = await communicator.receive_json_from()
self.assertEqual(len(json.loads(message["topology"])["nodes"]), 2)
self._snooze()
self.assertEqual(
len(self.web_driver.execute_script("return graph.data;")["nodes"]),
2,
)
await communicator.disconnect()

async def test_node_link_update(self):
# preparation
communicator = await self._get_communicator(self.admin_client, self.topology.pk)
connected, _ = await communicator.connect()
assert connected is True
path = reverse(f"{self.prefix}_topology_change", args=[self.topology.pk])
self.login()
self.open(path)
self.find_element(By.CSS_SELECTOR, "input.visualizelink").click()
# deleting the link from the DB will remove it from the UI
await database_sync_to_async(self.link.delete)()
message = await communicator.receive_json_from()
self.assertEqual(len(json.loads(message["topology"])["links"]), 0)
self._snooze()
self.assertEqual(
len(self.web_driver.execute_script("return graph.data;")["links"]),
0,
)
# adding a link will add it to the UI
new_link = copy(self.link)
new_link.pk = None
await database_sync_to_async(new_link.save)()
message = await communicator.receive_json_from()
self.assertEqual(len(json.loads(message["topology"])["links"]), 1)
self._snooze()
self.assertEqual(
len(self.web_driver.execute_script("return graph.data;")["links"]),
1,
)
await communicator.disconnect()
12 changes: 11 additions & 1 deletion tests/openwisp2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
}
}

if TESTING and "--exclude-tag=no_parallel" not in sys.argv:
DATABASES["default"]["TEST"] = {
"NAME": os.path.join(BASE_DIR, "openwisp_network_topology_tests.db"),
}

SECRET_KEY = "@q4z-^s=mv59#o=uutv4*m=h@)ik4%zp1)-k^_(!_7*x_&+ze$"

Expand Down Expand Up @@ -93,7 +97,13 @@

ASGI_APPLICATION = "openwisp2.asgi.application"

CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
# Needed to test UI updates via websockets
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": ["redis://localhost/9"]},
}
}
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"

LANGUAGE_CODE = "en-gb"
Expand Down
Loading