# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc import math import os from datetime import datetime from flask import Flask from solstone.convey.utils import ( created, format_date, format_date_short, format_month_day, relative_time, respond_collection, safe_day_path, safe_journal_path, time_since, ) from solstone.think.utils import day_path def _app_context(): return Flask(__name__).app_context() def test_format_date(): assert "2024" not in format_date("20240102") assert format_date("bad") == "bad" def test_format_date_short(monkeypatch): from datetime import datetime # Mock today as Nov 29, 2025 class MockDatetime(datetime): @classmethod def now(cls): return datetime(2025, 11, 29, 12, 0, 0) monkeypatch.setattr("solstone.convey.utils.datetime", MockDatetime) # Test relative dates assert format_date_short("20251129") == "Today" assert format_date_short("20251128") == "Yesterday" assert format_date_short("20251130") == "Tomorrow" # Test within past 6 days - should return day name assert format_date_short("20251127") == "Thursday" assert format_date_short("20251124") == "Monday" # Test older date same year - short format without year result = format_date_short("20250815") assert "Aug" in result assert "15" in result assert "'" not in result # No year suffix # Test date >6 months ago in different year - should have year suffix result = format_date_short("20240301") assert "Mar" in result assert "'24" in result # Test invalid date - should return input unchanged assert format_date_short("bad") == "bad" def test_format_month_day(): two_digit = datetime(2026, 6, 22, 12, 0, 0).timestamp() * 1000 assert format_month_day(two_digit) == "jun 22" one_digit = datetime(2026, 6, 2, 12, 0, 0).timestamp() * 1000 assert format_month_day(one_digit) == "jun 2" def test_time_since(monkeypatch): monkeypatch.setattr("time.time", lambda: 120) assert time_since(60) == "1 minute ago" def test_relative_time(): cases = [ (-1, "0 seconds"), (math.inf, "0 seconds"), (0, "0 seconds"), (1, "1 second"), (59, "59 seconds"), (60, "1 minute"), (119, "1 minute"), (120, "2 minutes"), (3599, "59 minutes"), (3600, "1 hour"), (7199, "1 hour"), (7200, "2 hours"), (86399, "23 hours"), (86400, "1 day"), (604799, "6 days"), (604800, "1 week"), (1209600, "2 weeks"), (2419199, "3 weeks"), (2419200, "1 month"), (5183999, "1 month"), (5184000, "2 months"), (31536000, "12 months"), ] for seconds, expected in cases: assert relative_time(seconds) == expected def test_list_day_folders(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) from solstone.think.utils import day_dirs day_path("20240101") day_path("20240103") days = sorted(day_dirs().keys()) assert days == ["20240101", "20240103"] def test_respond_collection_default_total(): with _app_context(): response, status = respond_collection([{"id": 1}, {"id": 2}]) assert status == 200 body = response.get_json() assert body == {"items": [{"id": 1}, {"id": 2}], "total": 2} assert "next_cursor" not in body def test_respond_collection_explicit_total_omits_cursor(): with _app_context(): response, status = respond_collection([{"id": 1}], total=57) assert status == 200 body = response.get_json() assert body == {"items": [{"id": 1}], "total": 57} assert "next_cursor" not in body def test_respond_collection_with_cursor(): with _app_context(): response, status = respond_collection([], total=99, cursor="20240102") assert status == 200 assert response.get_json() == { "items": [], "total": 99, "next_cursor": "20240102", } def test_created_returns_201_without_location(): with _app_context(): response, status = created({"id": "abc", "name": "thing"}) assert status == 201 assert response.get_json() == {"id": "abc", "name": "thing"} assert "Location" not in response.headers def test_created_sets_location_header(): with _app_context(): response, status = created({"id": "abc"}, location="/app/things/abc") assert status == 201 assert response.headers["Location"] == "/app/things/abc" assert response.get_json() == {"id": "abc"} def _assert_invalid_path_error(error): assert error is not None response, status = error assert status == 400 assert response.get_json() == { "error": "I couldn't use that path.", "reason_code": "invalid_path", "detail": "", } def _assert_invalid_path_error_403(error): assert error is not None response, status = error assert status == 403 assert response.get_json() == { "error": "I couldn't use that path.", "reason_code": "invalid_path", "detail": "", } def test_safe_journal_path_accepts_contained_path(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) path, error = safe_journal_path("facets/work/facet.json") assert error is None assert path == tmp_path / "facets" / "work" / "facet.json" assert path.is_absolute() def test_safe_journal_path_rejects_invalid_relpaths(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) with _app_context(): for relpath in ("..", "../escape", "/etc/passwd", "a\\b", ""): path, error = safe_journal_path(relpath) assert path is None _assert_invalid_path_error(error) def test_safe_journal_path_rejects_symlink_escape(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) outside = tmp_path.parent / f"{tmp_path.name}_outside" outside.mkdir() os.symlink(outside, tmp_path / "out") with _app_context(): path, error = safe_journal_path("out/secret.txt") assert path is None _assert_invalid_path_error(error) def test_safe_day_path_accepts_contained_path(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) day = "20240101" day_dir = day_path(day) path, error = safe_day_path(day, "test/143022_300/mic_audio.flac") assert error is None assert path == day_dir / "test" / "143022_300" / "mic_audio.flac" assert path.is_absolute() def test_safe_day_path_rejects_invalid_relpaths(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) with _app_context(): for relpath in ("..", "../escape", "/etc/passwd", "a\\b", ""): path, error = safe_day_path("20240101", relpath) assert path is None _assert_invalid_path_error_403(error) def test_safe_day_path_rejects_symlink_escape(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) day_dir = day_path("20240101") outside = tmp_path.parent / f"{tmp_path.name}_outside" outside.mkdir() os.symlink(outside, day_dir / "out") with _app_context(): path, error = safe_day_path("20240101", "out/secret.txt") assert path is None _assert_invalid_path_error_403(error) def test_safe_day_path_keeps_date_like_first_segment_contained(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) day_dir = day_path("20240101") path, error = safe_day_path("20240101", "20260101/foo.flac") assert error is None assert path is not None assert path.is_relative_to(day_dir)