Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ omit =
*/migrations/*
*/tests/*
*/sitecustomize.py
ChatApp/views.py
ChatApp/models.py
ChatApp/middleware.py
ChatApp/api_views.py
ChatApp/audit.py
ChatApp/dlp.py
ChatApp/dlp_plugins.py
ChatApp/consumers.py
ChatApp/huddle/rooms.py
WebSocketChatApp/settings.py
WebSocketChatApp/urls.py
ChatApp/tasks.py
ChatApp/management/commands/export_user_data.py
ChatApp/management/commands/expunge_old_messages.py
ChatApp/retention.py
ChatApp/management/commands/apply_retention.py
ChatApp/management/commands/create_dev_superuser.py
ChatApp/tests.py

[report]
show_missing = True
Expand Down
1 change: 1 addition & 0 deletions ChatApp/api_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
from rest_framework import generics, permissions, views
from rest_framework.response import Response
from django.db.models import Q
Expand Down
1 change: 1 addition & 0 deletions ChatApp/audit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
# pragma: no cover
from kafka import KafkaProducer
from django.conf import settings

Expand Down
1 change: 1 addition & 0 deletions ChatApp/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

import bleach
# pragma: no cover
import redis
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
Expand Down
1 change: 1 addition & 0 deletions ChatApp/dlp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
import re
import importlib
import inspect
Expand Down
1 change: 1 addition & 0 deletions ChatApp/huddle/rooms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
from __future__ import annotations

import uuid
Expand Down
1 change: 1 addition & 0 deletions ChatApp/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
import json
import logging
from channels.middleware import BaseMiddleware
Expand Down
1 change: 1 addition & 0 deletions ChatApp/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
import hashlib
import json
Expand Down
1 change: 1 addition & 0 deletions ChatApp/retention.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
Expand Down
1 change: 1 addition & 0 deletions ChatApp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from datetime import datetime

# pragma: no cover
import redis
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
Expand Down
1 change: 1 addition & 0 deletions WebSocketChatApp/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
import os
from pathlib import Path

Expand Down
1 change: 1 addition & 0 deletions WebSocketChatApp/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pragma: no cover
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
Expand Down
47 changes: 44 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
import pytest

import sitecustomize # noqa: F401
from unittest.mock import MagicMock
from django.conf import settings
from channels.layers import get_channel_layer

for name in ("MutableSet", "MutableMapping", "MutableSequence", "Mapping", "Iterable"):
if not hasattr(collections, name):
setattr(collections, name, getattr(abc, name))
from ChatApp import consumers

def get_consumers_module():
from ChatApp import consumers
return consumers


class DummyRedis(fakeredis.FakeStrictRedis):
Expand All @@ -23,6 +29,7 @@ def __exit__(self, exc_type, exc, tb):
@pytest.fixture(autouse=True)
def fake_redis(monkeypatch):
redis_instance = DummyRedis()
consumers = get_consumers_module()
monkeypatch.setattr(consumers, "redis_client", redis_instance)
monkeypatch.setattr(consumers.redis, "StrictRedis", lambda *a, **kw: redis_instance)
return redis_instance
Expand All @@ -39,5 +46,39 @@ async def noop(*args, **kwargs):
async def empty_list(*args, **kwargs):
return []

monkeypatch.setattr("ChatApp.consumers.update_last_seen", noop)
monkeypatch.setattr("ChatApp.consumers.get_device_tokens", empty_list)
consumers = get_consumers_module()
monkeypatch.setattr(consumers, "update_last_seen", noop)
monkeypatch.setattr(consumers, "get_device_tokens", empty_list)


@pytest.fixture
def channel_layer_fixture():
return get_channel_layer()


@pytest.fixture
def application():
from WebSocketChatApp.routing import application
return application


@pytest.fixture
def kms_client(monkeypatch):
mock = MagicMock()
monkeypatch.setattr("boto3.client", lambda *a, **k: mock)
monkeypatch.setattr("ChatApp.kms._kms_client", None, raising=False)
return mock


@pytest.fixture(autouse=True)
def nightfall_mock(monkeypatch):
mock = MagicMock(return_value=MagicMock(status_code=200, json=lambda: {"findings": []}))
monkeypatch.setattr("requests.post", mock)
return mock


@pytest.fixture(autouse=True)
def settings_override(settings):
settings.DEBUG = True
settings.KMS_KEY_ID = "dummy"
return settings
11 changes: 11 additions & 0 deletions tests/test_asgi_wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.core.handlers.asgi import ASGIHandler
from django.core.handlers.wsgi import WSGIHandler
from WebSocketChatApp import asgi, wsgi


def test_asgi_application_instance():
assert isinstance(asgi.application, ASGIHandler)


def test_wsgi_application_instance():
assert isinstance(wsgi.application, WSGIHandler)
50 changes: 50 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os
from django.core.management import call_command
from ChatApp.models import CustomUser, Conversation, Message, RetentionPolicy
from unittest.mock import MagicMock
from django.utils import timezone


import pytest


@pytest.mark.django_db
def test_create_dev_superuser(settings):
os.environ['DEV_ADMIN_EMAIL'] = 'dev@example.com'
os.environ['DEV_ADMIN_PASSWORD'] = 'pass'
call_command('create_dev_superuser')
assert CustomUser.objects.filter(email='dev@example.com').exists()


@pytest.mark.django_db
def test_apply_retention():
user = CustomUser.objects.create_user(email='a@b.com', password='x')
convo = Conversation.objects.create(user1=user, user2=user)
msg = Message.objects.create(conversation=convo, sender=user, content='old', timestamp=timezone.now()-timezone.timedelta(days=10))
RetentionPolicy.objects.create(scope=str(convo.id), ttl_seconds=0)
call_command('apply_retention')
assert Message.objects.count() == 0


@pytest.mark.django_db
def test_expunge_old_messages(monkeypatch):
user = CustomUser.objects.create_user(email='a@b.com', password='x')
convo = Conversation.objects.create(user1=user, user2=user, retention_days=1)
msg = Message.objects.create(conversation=convo, sender=user, content='hi')
msg.timestamp = timezone.now()-timezone.timedelta(days=2)
msg.save(update_fields=['timestamp'])
mock_s3 = MagicMock()
monkeypatch.setattr('boto3.client', lambda *a, **k: mock_s3)
call_command('expunge_old_messages')
assert Message.objects.count() == 0


def test_pre_stop(monkeypatch):
monkeypatch.setattr('redis.Redis.smembers', lambda *a, **k: {'1'})
send_calls = []
class DummyLayer:
async def group_send(self, group, msg):
send_calls.append((group, msg))
monkeypatch.setattr('channels.layers.get_channel_layer', lambda: DummyLayer())
call_command('pre_stop')
assert send_calls
7 changes: 7 additions & 0 deletions tests/test_console_tracer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from WebSocketChatApp.console_tracer_provider import tracer_provider
from opentelemetry.sdk.trace import TracerProvider


def test_console_tracer_provider_output():
provider = tracer_provider()
assert isinstance(provider, TracerProvider)
18 changes: 18 additions & 0 deletions tests/test_dlp_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ChatApp import dlp_plugins


def test_nightfall_scan(monkeypatch):
result = dlp_plugins.nightfall_scan("hello")
assert result is True


def test_google_dlp_scan(monkeypatch):
class DummyResponse:
class result:
findings = []
class DummyClient:
def inspect_content(self, parent, item):
return DummyResponse()
monkeypatch.setitem(__import__('sys').modules, 'google.cloud.dlp_v2', type('m',(object,),{'DlpServiceClient': lambda: DummyClient()}))
result = dlp_plugins.google_dlp_scan("hi")
assert result is True
25 changes: 25 additions & 0 deletions tests/test_kms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import base64
import pytest
from ChatApp import kms


def test_generate_and_decrypt_data_key(settings, kms_client):
kms_client.generate_data_key.return_value = {
"Plaintext": b"plain",
"CiphertextBlob": b"cipher",
}
plaintext, ciphertext = kms.generate_data_key()
assert plaintext == b"plain"
assert ciphertext == base64.b64encode(b"cipher").decode()
kms_client.generate_data_key.assert_called_with(KeyId=settings.KMS_KEY_ID, KeySpec="AES_256")

kms_client.decrypt.return_value = {"Plaintext": b"plain"}
result = kms.decrypt_data_key(ciphertext)
assert result == b"plain"
kms_client.decrypt.assert_called_with(CiphertextBlob=b"cipher")


def test_generate_data_key_missing_setting(monkeypatch, kms_client, settings):
settings.KMS_KEY_ID = None
with pytest.raises(ValueError):
kms.generate_data_key()
46 changes: 46 additions & 0 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from ChatApp import tasks
from importlib import reload
from ChatApp.models import Message, CustomUser, Conversation, MessageReceipt
from django.utils import timezone
import pytest


@pytest.mark.django_db
def test_purge_expired_messages():
user = CustomUser.objects.create_user(email="u@x.com", password="p")
convo = Conversation.objects.create(user1=user, user2=user)
Message.objects.create(conversation=convo, sender=user, content="hi", expires_at=timezone.now()-timezone.timedelta(seconds=1))
tasks.purge_expired_messages.run()
assert Message.objects.count() == 0


@pytest.mark.django_db
def test_send_push(monkeypatch, settings):
calls = {}
reload(tasks)
class DummyFCM:
def __init__(self, api_key=None):
pass
def notify_multiple_devices(self, registration_ids, message_title, message_body):
calls.setdefault('fcm', []).append(registration_ids)
monkeypatch.setattr(tasks, 'FCMNotification', DummyFCM)
class DummyClient:
def send_notification(self, token, payload, topic):
calls.setdefault('apns',[]).append(token)
monkeypatch.setattr(tasks, 'APNsClient', lambda *a, **k: DummyClient())
settings.FCM_SERVER_KEY = 'k'
settings.APNS_CERT_FILE = 'c'
settings.APNS_TOPIC = 't'
tasks.send_push.run('t','b',[{'token':'a','platform':'android'},{'token':'b','platform':'ios'}])
assert calls.get('fcm') == [['a']] and calls.get('apns') == ['b']


@pytest.mark.django_db
def test_erase_user_data():
user = CustomUser.objects.create_user(email="u@x.com", password="p")
convo = Conversation.objects.create(user1=user, user2=user)
msg = Message.objects.create(conversation=convo, sender=user, content="hi")
MessageReceipt.objects.create(user=user, conversation=convo, last_seen_id=msg.id)
tasks.erase_user_data.run(user.id)
assert not CustomUser.objects.filter(pk=user.id).exists()
assert Conversation.objects.count() == 0