Skip to content
Closed
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
28 changes: 27 additions & 1 deletion django-backend/soroscan/health.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
"""
Health check endpoints for Kubernetes liveness/readiness probes.
"""
import time

from django.core.cache import cache
from django.db import connection
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response


PROCESS_START_TIME = time.monotonic()


def get_uptime_seconds() -> int:
"""Return how long the current Django process has been running."""
return max(1, int(time.monotonic() - PROCESS_START_TIME))


def format_uptime(seconds: int) -> str:
"""Format uptime as human-readable days, hours, minutes, and seconds."""
days, remainder = divmod(seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{days}d {hours:02}:{minutes:02}:{seconds:02}"


@api_view(["GET"])
@permission_classes([AllowAny])
def health_view(request):
"""Liveness probe - app is running."""
return Response({"status": "ok"})
uptime_seconds = get_uptime_seconds()

return Response(
{
"status": "ok",
"uptime_seconds": uptime_seconds,
"uptime": format_uptime(uptime_seconds),
}
)


@api_view(["GET"])
Expand Down
38 changes: 36 additions & 2 deletions django-backend/soroscan/ingest/tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework import status
from rest_framework.test import APIClient

from soroscan import health


@pytest.fixture
Expand All @@ -14,14 +15,47 @@ def api_client():

@pytest.mark.django_db
class TestHealthView:
def test_health_returns_ok(self, api_client):
def test_health_returns_ok_with_uptime(self, api_client):
url = reverse("health")
response = api_client.get(url)

assert response.status_code == status.HTTP_200_OK
assert response.data == {"status": "ok"}
assert response.data["status"] == "ok"
assert "uptime_seconds" in response.data
assert "uptime" in response.data
assert isinstance(response.data["uptime_seconds"], int)
assert response.data["uptime_seconds"] > 0
assert isinstance(response.data["uptime"], str)
assert response["X-SoroScan-Version"] == settings.SOFTWARE_VERSION

def test_health_uptime_is_accurate(self, api_client, monkeypatch):
monkeypatch.setattr(health, "PROCESS_START_TIME", 100.0)
monkeypatch.setattr(health.time, "monotonic", lambda: 165.0)

url = reverse("health")
response = api_client.get(url)

assert response.status_code == status.HTTP_200_OK
assert response.data["uptime_seconds"] == 65
assert response.data["uptime"] == "0d 00:01:05"

def test_health_uptime_counter_increases_across_requests(self, api_client, monkeypatch):
times = iter([110.0, 125.0])

monkeypatch.setattr(health, "PROCESS_START_TIME", 100.0)
monkeypatch.setattr(health.time, "monotonic", lambda: next(times))

url = reverse("health")

first_response = api_client.get(url)
second_response = api_client.get(url)

assert first_response.status_code == status.HTTP_200_OK
assert second_response.status_code == status.HTTP_200_OK
assert first_response.data["uptime_seconds"] == 10
assert second_response.data["uptime_seconds"] == 25
assert second_response.data["uptime_seconds"] > first_response.data["uptime_seconds"]


@pytest.mark.django_db
class TestReadinessView:
Expand Down
85 changes: 85 additions & 0 deletions soroscan-frontend/__tests__/event-table-responsive.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { fireEvent, render, screen } from "@testing-library/react";

import { EventTable } from "@/app/dashboard/components/EventTable";
import type { EventRecord } from "@/components/ingest/types";

const mockEvents: EventRecord[] = [
{
id: "event-1",
contractId: "contract-1234567890abcdef",
contractName: "Test Contract",
eventType: "transfer",
ledger: 12345,
eventIndex: 1,
timestamp: "2026-05-29T10:00:00.000Z",
txHash: "tx-1234567890abcdef",
payload: { amount: "100" },
payloadHash: "payload-123",
schemaVersion: "1.0",
validationStatus: "valid",
},
];

describe("EventTable responsive card grid", () => {
it("renders the mobile card grid container", () => {
render(
<EventTable
events={mockEvents}
loading={false}
onEventClick={jest.fn()}
/>,
);

expect(screen.getByTestId("events-card-grid")).toBeInTheDocument();
});

it("shows event information inside the mobile cards", () => {
render(
<EventTable
events={mockEvents}
loading={false}
onEventClick={jest.fn()}
/>,
);

expect(screen.getAllByText("transfer").length).toBeGreaterThan(0);

expect(screen.getAllByText("12345").length).toBeGreaterThan(0);

expect(screen.getAllByText(/contract\.\.\.abcdef/i).length).toBeGreaterThan(
0,
);

expect(screen.getAllByText(/tx-12345\.\.\.abcdef/i).length).toBeGreaterThan(
0,
);
});

it("calls onEventClick when card detail button is clicked", () => {
const onEventClick = jest.fn();

render(
<EventTable
events={mockEvents}
loading={false}
onEventClick={onEventClick}
/>,
);

fireEvent.click(screen.getAllByText(/View/i)[1]);

expect(onEventClick).toHaveBeenCalledWith(mockEvents[0]);
});

it("shows loading state", () => {
render(<EventTable events={[]} loading={true} onEventClick={jest.fn()} />);

expect(screen.getByText("Loading events...")).toBeInTheDocument();
});

it("shows empty state when no events are available", () => {
render(<EventTable events={[]} loading={false} onEventClick={jest.fn()} />);

expect(screen.getByText(/No events found/i)).toBeInTheDocument();
});
});
Loading
Loading