mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-07 02:11:22 +02:00
Add background-hash-mark addition for region routing
Per https://buymeacoffee.com/ripplebiz/region-filtering: > After some discussions, and that there is some confusion around #channels and #regions, it's been decided to drop the requirement to have the '#' prefix. So, region names will just be plain alphanumeric (and '-'), with no # prefix. > For backwards compatibility, the names will internally have a '#' prepended, but for all client GUI's and command lines, you generally won't see mention of '#' prefixes. The next firmware release (v1.12.0) and subsequent Ripple firmware and Liam's app will have modified UI to remove the '#' requirement. So, silently add, but don't duplicate, for users who have already added hashmarks.
This commit is contained in:
+2
-1
@@ -262,10 +262,11 @@ class RadioManager:
|
|||||||
|
|
||||||
# Apply flood scope from settings (best-effort; older firmware
|
# Apply flood scope from settings (best-effort; older firmware
|
||||||
# may not support set_flood_scope)
|
# may not support set_flood_scope)
|
||||||
|
from app.region_scope import normalize_region_scope
|
||||||
from app.repository import AppSettingsRepository
|
from app.repository import AppSettingsRepository
|
||||||
|
|
||||||
app_settings = await AppSettingsRepository.get()
|
app_settings = await AppSettingsRepository.get()
|
||||||
scope = app_settings.flood_scope
|
scope = normalize_region_scope(app_settings.flood_scope)
|
||||||
try:
|
try:
|
||||||
await mc.commands.set_flood_scope(scope if scope else "")
|
await mc.commands.set_flood_scope(scope if scope else "")
|
||||||
logger.info("Applied flood_scope=%r", scope or "(disabled)")
|
logger.info("Applied flood_scope=%r", scope or "(disabled)")
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Helpers for normalizing MeshCore flood-scope / region names."""
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_region_scope(scope: str | None) -> str:
|
||||||
|
"""Normalize a user-facing region scope into MeshCore's internal form.
|
||||||
|
|
||||||
|
Region names are now user-facing plain strings like ``Esperance``.
|
||||||
|
Internally, MeshCore still expects hashtag-style names like ``#Esperance``.
|
||||||
|
|
||||||
|
Backward compatibility:
|
||||||
|
- blank/None stays disabled (`""`)
|
||||||
|
- existing leading ``#`` is preserved
|
||||||
|
"""
|
||||||
|
|
||||||
|
stripped = (scope or "").strip()
|
||||||
|
if not stripped:
|
||||||
|
return ""
|
||||||
|
if stripped.startswith("#"):
|
||||||
|
return stripped
|
||||||
|
return f"#{stripped}"
|
||||||
@@ -9,6 +9,7 @@ from app.dependencies import require_connected
|
|||||||
from app.models import Channel, ChannelDetail, ChannelMessageCounts, ChannelTopSender
|
from app.models import Channel, ChannelDetail, ChannelMessageCounts, ChannelTopSender
|
||||||
from app.radio import radio_manager
|
from app.radio import radio_manager
|
||||||
from app.radio_sync import upsert_channel_from_radio_slot
|
from app.radio_sync import upsert_channel_from_radio_slot
|
||||||
|
from app.region_scope import normalize_region_scope
|
||||||
from app.repository import ChannelRepository, MessageRepository
|
from app.repository import ChannelRepository, MessageRepository
|
||||||
from app.websocket import broadcast_event
|
from app.websocket import broadcast_event
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ async def set_channel_flood_scope_override(
|
|||||||
if not channel:
|
if not channel:
|
||||||
raise HTTPException(status_code=404, detail="Channel not found")
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
|
||||||
override = request.flood_scope_override.strip() or None
|
override = normalize_region_scope(request.flood_scope_override) or None
|
||||||
updated = await ChannelRepository.update_flood_scope_override(channel.key, override)
|
updated = await ChannelRepository.update_flood_scope_override(channel.key, override)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail="Failed to update flood-scope override")
|
raise HTTPException(status_code=500, detail="Failed to update flood-scope override")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.models import (
|
|||||||
SendDirectMessageRequest,
|
SendDirectMessageRequest,
|
||||||
)
|
)
|
||||||
from app.radio import radio_manager
|
from app.radio import radio_manager
|
||||||
|
from app.region_scope import normalize_region_scope
|
||||||
from app.repository import AmbiguousPublicKeyPrefixError, AppSettingsRepository, MessageRepository
|
from app.repository import AmbiguousPublicKeyPrefixError, AppSettingsRepository, MessageRepository
|
||||||
from app.websocket import broadcast_error, broadcast_event
|
from app.websocket import broadcast_error, broadcast_event
|
||||||
|
|
||||||
@@ -31,12 +32,12 @@ async def _send_channel_message_with_effective_scope(
|
|||||||
action_label: str,
|
action_label: str,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Send a channel message, temporarily overriding flood scope when configured."""
|
"""Send a channel message, temporarily overriding flood scope when configured."""
|
||||||
override_scope = (channel.flood_scope_override or "").strip()
|
override_scope = normalize_region_scope(channel.flood_scope_override)
|
||||||
baseline_scope = ""
|
baseline_scope = ""
|
||||||
|
|
||||||
if override_scope:
|
if override_scope:
|
||||||
settings = await AppSettingsRepository.get()
|
settings = await AppSettingsRepository.get()
|
||||||
baseline_scope = settings.flood_scope
|
baseline_scope = normalize_region_scope(settings.flood_scope)
|
||||||
|
|
||||||
if override_scope and override_scope != baseline_scope:
|
if override_scope and override_scope != baseline_scope:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.models import AppSettings
|
from app.models import AppSettings
|
||||||
|
from app.region_scope import normalize_region_scope
|
||||||
from app.repository import AppSettingsRepository
|
from app.repository import AppSettingsRepository
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -123,8 +124,7 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
|
|||||||
# Flood scope
|
# Flood scope
|
||||||
flood_scope_changed = False
|
flood_scope_changed = False
|
||||||
if update.flood_scope is not None:
|
if update.flood_scope is not None:
|
||||||
stripped = update.flood_scope.strip()
|
kwargs["flood_scope"] = normalize_region_scope(update.flood_scope)
|
||||||
kwargs["flood_scope"] = stripped
|
|
||||||
flood_scope_changed = True
|
flood_scope_changed = True
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from './ui/sonner';
|
import { toast } from './ui/sonner';
|
||||||
import { isFavorite } from '../utils/favorites';
|
import { isFavorite } from '../utils/favorites';
|
||||||
import { handleKeyboardActivate } from '../utils/a11y';
|
import { handleKeyboardActivate } from '../utils/a11y';
|
||||||
|
import { stripRegionScopePrefix } from '../utils/regionScope';
|
||||||
import { ContactAvatar } from './ContactAvatar';
|
import { ContactAvatar } from './ContactAvatar';
|
||||||
import { ContactStatusInfo } from './ContactStatusInfo';
|
import { ContactStatusInfo } from './ContactStatusInfo';
|
||||||
import type { Channel, Contact, Conversation, Favorite, RadioConfig } from '../types';
|
import type { Channel, Contact, Conversation, Favorite, RadioConfig } from '../types';
|
||||||
@@ -54,8 +55,8 @@ export function ChatHeader({
|
|||||||
const handleEditFloodScopeOverride = () => {
|
const handleEditFloodScopeOverride = () => {
|
||||||
if (conversation.type !== 'channel' || !onSetChannelFloodScopeOverride) return;
|
if (conversation.type !== 'channel' || !onSetChannelFloodScopeOverride) return;
|
||||||
const nextValue = window.prompt(
|
const nextValue = window.prompt(
|
||||||
'Enter regional override flood scope for this room. This temporarily changes the radio flood scope before send and restores it after, which significantly slows room sends. Leave blank to clear.\n\nNote: some radio clients (including the official ones) implicitly add a "#" character to the beginning of a region specifier. That needs to be manually added here, so if you use the official app with region "Anytown", you should enter it here as "#Anytown".',
|
'Enter regional override flood scope for this room. This temporarily changes the radio flood scope before send and restores it after, which significantly slows room sends. Leave blank to clear.',
|
||||||
activeChannel?.flood_scope_override ?? ''
|
stripRegionScopePrefix(activeChannel?.flood_scope_override)
|
||||||
);
|
);
|
||||||
if (nextValue === null) return;
|
if (nextValue === null) return;
|
||||||
onSetChannelFloodScopeOverride(conversation.id, nextValue);
|
onSetChannelFloodScopeOverride(conversation.id, nextValue);
|
||||||
@@ -140,7 +141,7 @@ export function ChatHeader({
|
|||||||
)}
|
)}
|
||||||
{conversation.type === 'channel' && activeChannel?.flood_scope_override && (
|
{conversation.type === 'channel' && activeChannel?.flood_scope_override && (
|
||||||
<span className="basis-full sm:basis-auto text-[11px] text-amber-700 dark:text-amber-300 truncate">
|
<span className="basis-full sm:basis-auto text-[11px] text-amber-700 dark:text-amber-300 truncate">
|
||||||
Regional override active: {activeChannel.flood_scope_override}
|
Regional override active: {stripRegionScopePrefix(activeChannel.flood_scope_override)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{conversation.type === 'contact' &&
|
{conversation.type === 'contact' &&
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from '../ui/button';
|
|||||||
import { Separator } from '../ui/separator';
|
import { Separator } from '../ui/separator';
|
||||||
import { toast } from '../ui/sonner';
|
import { toast } from '../ui/sonner';
|
||||||
import { RADIO_PRESETS } from '../../utils/radioPresets';
|
import { RADIO_PRESETS } from '../../utils/radioPresets';
|
||||||
|
import { stripRegionScopePrefix } from '../../utils/regionScope';
|
||||||
import type {
|
import type {
|
||||||
AppSettings,
|
AppSettings,
|
||||||
AppSettingsUpdate,
|
AppSettingsUpdate,
|
||||||
@@ -83,7 +84,7 @@ export function SettingsRadioSection({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAdvertIntervalHours(String(Math.round(appSettings.advert_interval / 3600)));
|
setAdvertIntervalHours(String(Math.round(appSettings.advert_interval / 3600)));
|
||||||
setFloodScope(appSettings.flood_scope);
|
setFloodScope(stripRegionScopePrefix(appSettings.flood_scope));
|
||||||
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
||||||
}, [appSettings]);
|
}, [appSettings]);
|
||||||
|
|
||||||
@@ -256,7 +257,7 @@ export function SettingsRadioSection({
|
|||||||
if (newAdvertInterval !== appSettings.advert_interval) {
|
if (newAdvertInterval !== appSettings.advert_interval) {
|
||||||
update.advert_interval = newAdvertInterval;
|
update.advert_interval = newAdvertInterval;
|
||||||
}
|
}
|
||||||
if (floodScope !== appSettings.flood_scope) {
|
if (floodScope !== stripRegionScopePrefix(appSettings.flood_scope)) {
|
||||||
update.flood_scope = floodScope;
|
update.flood_scope = floodScope;
|
||||||
}
|
}
|
||||||
const newMaxRadioContacts = parseInt(maxRadioContacts, 10);
|
const newMaxRadioContacts = parseInt(maxRadioContacts, 10);
|
||||||
@@ -554,10 +555,10 @@ export function SettingsRadioSection({
|
|||||||
id="flood-scope"
|
id="flood-scope"
|
||||||
value={floodScope}
|
value={floodScope}
|
||||||
onChange={(e) => setFloodScope(e.target.value)}
|
onChange={(e) => setFloodScope(e.target.value)}
|
||||||
placeholder="#MyRegion"
|
placeholder="MyRegion"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Tag outgoing flood messages with a region name (e.g. #MyRegion). Repeaters configured for
|
Tag outgoing flood messages with a region name (e.g. MyRegion). Repeaters configured for
|
||||||
that region can forward the traffic, while repeaters configured to deny other regions may
|
that region can forward the traffic, while repeaters configured to deny other regions may
|
||||||
drop it. Leave empty to disable.
|
drop it. Leave empty to disable.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ describe('ChatHeader key visibility', () => {
|
|||||||
|
|
||||||
render(<ChatHeader {...baseProps} conversation={conversation} channels={[channel]} />);
|
render(<ChatHeader {...baseProps} conversation={conversation} channels={[channel]} />);
|
||||||
|
|
||||||
expect(screen.getByText('Regional override active: #Esperance')).toBeInTheDocument();
|
expect(screen.getByText('Regional override active: Esperance')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prompts for regional override when globe button is clicked', () => {
|
it('prompts for regional override when globe button is clicked', () => {
|
||||||
@@ -125,7 +125,7 @@ describe('ChatHeader key visibility', () => {
|
|||||||
const channel = makeChannel(key, '#flightless', true);
|
const channel = makeChannel(key, '#flightless', true);
|
||||||
const conversation: Conversation = { type: 'channel', id: key, name: '#flightless' };
|
const conversation: Conversation = { type: 'channel', id: key, name: '#flightless' };
|
||||||
const onSetChannelFloodScopeOverride = vi.fn();
|
const onSetChannelFloodScopeOverride = vi.fn();
|
||||||
const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue('#Esperance');
|
const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue('Esperance');
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
@@ -139,7 +139,7 @@ describe('ChatHeader key visibility', () => {
|
|||||||
fireEvent.click(screen.getByTitle('Set regional override'));
|
fireEvent.click(screen.getByTitle('Set regional override'));
|
||||||
|
|
||||||
expect(promptSpy).toHaveBeenCalled();
|
expect(promptSpy).toHaveBeenCalled();
|
||||||
expect(onSetChannelFloodScopeOverride).toHaveBeenCalledWith(key, '#Esperance');
|
expect(onSetChannelFloodScopeOverride).toHaveBeenCalledWith(key, 'Esperance');
|
||||||
promptSpy.mockRestore();
|
promptSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function stripRegionScopePrefix(scope: string | null | undefined): string {
|
||||||
|
if (!scope) return '';
|
||||||
|
return scope.startsWith('#') ? scope.slice(1) : scope;
|
||||||
|
}
|
||||||
@@ -270,7 +270,7 @@ class TestChannelFloodScopeOverride:
|
|||||||
with patch("app.routers.channels.broadcast_event") as mock_broadcast:
|
with patch("app.routers.channels.broadcast_event") as mock_broadcast:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/api/channels/{key}/flood-scope-override",
|
f"/api/channels/{key}/flood-scope-override",
|
||||||
json={"flood_scope_override": "#Esperance"},
|
json={"flood_scope_override": "Esperance"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -283,6 +283,20 @@ class TestChannelFloodScopeOverride:
|
|||||||
mock_broadcast.assert_called_once()
|
mock_broadcast.assert_called_once()
|
||||||
assert mock_broadcast.call_args.args[0] == "channel"
|
assert mock_broadcast.call_args.args[0] == "channel"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_existing_hash_is_not_doubled(self, test_db, client):
|
||||||
|
key = "CC" * 16
|
||||||
|
await ChannelRepository.upsert(key=key, name="#flightless", is_hashtag=True)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/channels/{key}/flood-scope-override",
|
||||||
|
json={"flood_scope_override": "#Esperance"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["flood_scope_override"] == "#Esperance"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_blank_override_clears_channel_flood_scope_override(self, test_db, client):
|
async def test_blank_override_clears_channel_flood_scope_override(self, test_db, client):
|
||||||
key = "BB" * 16
|
key = "BB" * 16
|
||||||
|
|||||||
@@ -655,6 +655,40 @@ class TestPostConnectSetupOrdering:
|
|||||||
|
|
||||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_plain_flood_scope_is_normalized_during_setup(self):
|
||||||
|
"""Legacy/plain stored flood_scope is normalized before applying to radio."""
|
||||||
|
from app.models import AppSettings
|
||||||
|
from app.radio import RadioManager
|
||||||
|
|
||||||
|
rm = RadioManager()
|
||||||
|
mock_mc = MagicMock()
|
||||||
|
mock_mc.start_auto_message_fetching = AsyncMock()
|
||||||
|
mock_mc.commands.set_flood_scope = AsyncMock()
|
||||||
|
rm._meshcore = mock_mc
|
||||||
|
|
||||||
|
mock_settings = AppSettings(flood_scope="TestRegion")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("app.event_handlers.register_event_handlers"),
|
||||||
|
patch("app.keystore.export_and_store_private_key", new_callable=AsyncMock),
|
||||||
|
patch("app.radio_sync.sync_radio_time", new_callable=AsyncMock),
|
||||||
|
patch(
|
||||||
|
"app.repository.AppSettingsRepository.get",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=mock_settings,
|
||||||
|
),
|
||||||
|
patch("app.radio_sync.sync_and_offload_all", new_callable=AsyncMock, return_value={}),
|
||||||
|
patch("app.radio_sync.start_periodic_sync"),
|
||||||
|
patch("app.radio_sync.send_advertisement", new_callable=AsyncMock, return_value=False),
|
||||||
|
patch("app.radio_sync.start_periodic_advert"),
|
||||||
|
patch("app.radio_sync.drain_pending_messages", new_callable=AsyncMock, return_value=0),
|
||||||
|
patch("app.radio_sync.start_message_polling"),
|
||||||
|
):
|
||||||
|
await rm.post_connect_setup()
|
||||||
|
|
||||||
|
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_flood_scope_empty_resets_during_setup(self):
|
async def test_flood_scope_empty_resets_during_setup(self):
|
||||||
"""Empty flood_scope calls set_flood_scope("") during post_connect_setup."""
|
"""Empty flood_scope calls set_flood_scope("") during post_connect_setup."""
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ class TestOutgoingChannelBroadcast:
|
|||||||
mc = _make_mc(name="MyNode")
|
mc = _make_mc(name="MyNode")
|
||||||
chan_key = "de" * 16
|
chan_key = "de" * 16
|
||||||
await ChannelRepository.upsert(key=chan_key, name="#flightless")
|
await ChannelRepository.upsert(key=chan_key, name="#flightless")
|
||||||
await ChannelRepository.update_flood_scope_override(chan_key, "#Esperance")
|
await ChannelRepository.update_flood_scope_override(chan_key, "Esperance")
|
||||||
await AppSettingsRepository.update(flood_scope="#Baseline")
|
await AppSettingsRepository.update(flood_scope="Baseline")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("app.routers.messages.require_connected", return_value=mc),
|
patch("app.routers.messages.require_connected", return_value=mc),
|
||||||
@@ -303,8 +303,8 @@ class TestOutgoingChannelBroadcast:
|
|||||||
mc = _make_mc(name="MyNode")
|
mc = _make_mc(name="MyNode")
|
||||||
chan_key = "df" * 16
|
chan_key = "df" * 16
|
||||||
await ChannelRepository.upsert(key=chan_key, name="#matching")
|
await ChannelRepository.upsert(key=chan_key, name="#matching")
|
||||||
await ChannelRepository.update_flood_scope_override(chan_key, "#Esperance")
|
await ChannelRepository.update_flood_scope_override(chan_key, "Esperance")
|
||||||
await AppSettingsRepository.update(flood_scope="#Esperance")
|
await AppSettingsRepository.update(flood_scope="Esperance")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("app.routers.messages.require_connected", return_value=mc),
|
patch("app.routers.messages.require_connected", return_value=mc),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class TestUpdateSettings:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_flood_scope_round_trip(self, test_db):
|
async def test_flood_scope_round_trip(self, test_db):
|
||||||
"""Flood scope should be saved and retrieved correctly."""
|
"""Flood scope should be saved and retrieved correctly."""
|
||||||
result = await update_settings(AppSettingsUpdate(flood_scope="#MyRegion"))
|
result = await update_settings(AppSettingsUpdate(flood_scope="MyRegion"))
|
||||||
assert result.flood_scope == "#MyRegion"
|
assert result.flood_scope == "#MyRegion"
|
||||||
|
|
||||||
fresh = await AppSettingsRepository.get()
|
fresh = await AppSettingsRepository.get()
|
||||||
@@ -70,7 +70,13 @@ class TestUpdateSettings:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_flood_scope_whitespace_stripped(self, test_db):
|
async def test_flood_scope_whitespace_stripped(self, test_db):
|
||||||
"""Flood scope should be stripped of whitespace."""
|
"""Flood scope should be stripped of whitespace."""
|
||||||
result = await update_settings(AppSettingsUpdate(flood_scope=" #MyRegion "))
|
result = await update_settings(AppSettingsUpdate(flood_scope=" MyRegion "))
|
||||||
|
assert result.flood_scope == "#MyRegion"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_flood_scope_existing_hash_is_not_doubled(self, test_db):
|
||||||
|
"""Existing leading hash should be preserved for backward compatibility."""
|
||||||
|
result = await update_settings(AppSettingsUpdate(flood_scope="#MyRegion"))
|
||||||
assert result.flood_scope == "#MyRegion"
|
assert result.flood_scope == "#MyRegion"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -92,7 +98,7 @@ class TestUpdateSettings:
|
|||||||
mock_rm.radio_operation = mock_radio_op
|
mock_rm.radio_operation = mock_radio_op
|
||||||
|
|
||||||
with patch("app.radio.radio_manager", mock_rm):
|
with patch("app.radio.radio_manager", mock_rm):
|
||||||
await update_settings(AppSettingsUpdate(flood_scope="#TestRegion"))
|
await update_settings(AppSettingsUpdate(flood_scope="TestRegion"))
|
||||||
|
|
||||||
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
mock_mc.commands.set_flood_scope.assert_awaited_once_with("#TestRegion")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user