Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Copyright (C) 2026, Aurora Innovation AB

This program is free software, distributed under the terms of
the GNU General Public License Version 2.

StasisBroadcast duplicate-claim (409) test.

Two ARI apps ('app-one' and 'app-two') are both registered. Both receive
a CallBroadcast event. The first claim attempt succeeds (HTTP 204); the
second must be rejected with HTTP 409 Conflict. The winning app is
verified via StasisStart.
"""

import logging

LOGGER = logging.getLogger(__name__)


class _State(object):
success_app = None
conflict_app = None


STATE = _State()


def on_broadcast(ari, event, test_object):
"""Attempt to claim the channel; record 204 success or 409 conflict."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("CallBroadcast for app '%s', channel %s", application, channel_id)

# Allow non-2xx so we can inspect the status code rather than catching
# an HTTPError raised by the default ari.post() error handling.
ari.set_allow_errors(True)
resp = ari.post('events', 'claim',
channelId=channel_id, application=application)
status = resp.status_code

LOGGER.info("Claim by '%s': HTTP %d", application, status)

if status == 204:
if STATE.success_app is not None:
LOGGER.error("More than one claim succeeded (first: '%s', now: '%s')",
STATE.success_app, application)
test_object.set_passed(False)
return False
STATE.success_app = application
elif status == 409:
if STATE.conflict_app is not None:
LOGGER.error("More than one claim got 409 (first: '%s', now: '%s')",
STATE.conflict_app, application)
test_object.set_passed(False)
return False
STATE.conflict_app = application
else:
LOGGER.error("Unexpected HTTP %d from claim by '%s'", status, application)
test_object.set_passed(False)
return False

return True


def on_stasis_start(ari, event, test_object):
"""Verify the winner app received StasisStart and clean up."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("StasisStart for channel %s in app '%s'", channel_id, application)

if STATE.success_app is not None and application != STATE.success_app:
LOGGER.error("StasisStart arrived in '%s' but winning app was '%s'",
application, STATE.success_app)
test_object.set_passed(False)
return False

ari.delete('channels', channel_id)
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[default]

exten => s,1,NoOp()
same => n,Answer()
same => n,StasisBroadcast(5000)
same => n,Hangup()
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
testinfo:
summary: 'StasisBroadcast returns HTTP 409 when channel is already claimed.'
description: |
Verifies that when two ARI applications both receive a CallBroadcast
event and both attempt to claim the same channel, the first claim
succeeds (HTTP 204) and the second claim is rejected (HTTP 409 Conflict).

Two apps ('app-one' and 'app-two') are registered on the same
WebSocket. Both receive CallBroadcast. The first to be processed
claims the channel; the second receives a 409. The winner app is
confirmed via StasisStart.

test-modules:
add-test-to-search-path: True
test-object:
config-section: test-object-config
typename: ari.AriTestObject
modules:
-
config-section: ari-config
typename: ari.WebSocketEventModule
-
config-section: ari-test-stopper
typename: pluggable_modules.EventActionModule

test-object-config:
apps: app-one,app-two
stop-on-end: False
reactor-timeout: 15

ari-test-stopper:
-
ari-events:
match:
type: StasisEnd
stop_test:

ari-config:
events:
-
conditions:
match:
type: CallBroadcast
count: 2
callback:
module: claim_already_taken
method: on_broadcast
-
conditions:
match:
type: StasisStart
count: 1
callback:
module: claim_already_taken
method: on_stasis_start

properties:
dependencies:
- python : autobahn.websocket
- python : requests
- python : twisted
- python : starpy
- asterisk : res_stasis_broadcast
- asterisk : app_stasis_broadcast
- asterisk : res_ari_events
- asterisk : res_ari_channels
- asterisk : app_echo
tags:
- ARI
61 changes: 61 additions & 0 deletions tests/rest_api/stasis_broadcast/claim_filter/claim_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Copyright (C) 2026, Aurora Innovation AB

This program is free software, distributed under the terms of
the GNU General Public License Version 2.

StasisBroadcast app_filter test.

Two ARI apps are registered: 'ivr-main' and 'support'. The dialplan
invokes StasisBroadcast with app_filter=^ivr-.*, so only 'ivr-main'
should receive the CallBroadcast event.

The count: 1 constraint on CallBroadcast in the YAML acts as the filter
verification — a second delivery (to 'support') would increment the count
to 2 and cause the test to fail at teardown.
"""

import logging

LOGGER = logging.getLogger(__name__)


def on_broadcast(ari, event, test_object):
"""Claim the channel on behalf of 'ivr-main', asserting it is the recipient."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("CallBroadcast for app '%s', channel %s", application, channel_id)

if application != 'ivr-main':
LOGGER.error("Expected CallBroadcast for 'ivr-main'; received for '%s'",
application)
test_object.set_passed(False)
return False

ari.set_allow_errors(True)
resp = ari.post('events', 'claim',
channelId=channel_id, application='ivr-main')

if resp.status_code != 204:
LOGGER.error("Expected HTTP 204 from claim, got %d: %s",
resp.status_code, resp.text)
test_object.set_passed(False)
return False

LOGGER.info("Channel %s claimed by 'ivr-main'", channel_id)
return True


def on_stasis_start(ari, event, test_object):
"""Verify the channel entered the correct Stasis app and clean up."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("StasisStart for channel %s in app '%s'", channel_id, application)

if application != 'ivr-main':
LOGGER.error("Expected StasisStart in 'ivr-main', got '%s'", application)
test_object.set_passed(False)
return False

ari.delete('channels', channel_id)
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[default]

exten => s,1,NoOp()
same => n,Answer()
same => n,StasisBroadcast(5000,^ivr-.*)
same => n,Hangup()
73 changes: 73 additions & 0 deletions tests/rest_api/stasis_broadcast/claim_filter/test-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
testinfo:
summary: 'StasisBroadcast app_filter restricts which apps receive CallBroadcast.'
description: |
Verifies that the app_filter argument (a POSIX extended regex) limits
delivery of CallBroadcast events to matching ARI applications only.

Two apps are registered: 'ivr-main' (matches '^ivr-.*') and 'support'
(does not match). Only 'ivr-main' should receive CallBroadcast. The
count: 1 assertion on CallBroadcast acts as the filter correctness
check — if 'support' also received the event the count would be 2 and
the test would fail.

'ivr-main' then claims the channel and confirms via StasisStart.

test-modules:
add-test-to-search-path: True
test-object:
config-section: test-object-config
typename: ari.AriTestObject
modules:
-
config-section: ari-config
typename: ari.WebSocketEventModule
-
config-section: ari-test-stopper
typename: pluggable_modules.EventActionModule

test-object-config:
apps: ivr-main,support
stop-on-end: False
reactor-timeout: 15

ari-test-stopper:
-
ari-events:
match:
type: StasisEnd
application: ivr-main
stop_test:

ari-config:
events:
-
conditions:
match:
type: CallBroadcast
count: 1
callback:
module: claim_filter
method: on_broadcast
-
conditions:
match:
type: StasisStart
application: ivr-main
count: 1
callback:
module: claim_filter
method: on_stasis_start

properties:
dependencies:
- python : autobahn.websocket
- python : requests
- python : twisted
- python : starpy
- asterisk : res_stasis_broadcast
- asterisk : app_stasis_broadcast
- asterisk : res_ari_events
- asterisk : res_ari_channels
- asterisk : app_echo
tags:
- ARI
54 changes: 54 additions & 0 deletions tests/rest_api/stasis_broadcast/claim_success/claim_success.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Copyright (C) 2026, Aurora Innovation AB

This program is free software, distributed under the terms of
the GNU General Public License Version 2.

Nominal StasisBroadcast claim test.

A channel enters StasisBroadcast via dialplan. The 'testsuite' ARI
application receives a CallBroadcast event, claims the channel via
POST /events/claim, and expects the channel to appear in StasisStart.
"""

import logging

LOGGER = logging.getLogger(__name__)


def on_broadcast(ari, event, test_object):
"""Handle the CallBroadcast event and claim the channel."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("CallBroadcast for app '%s', channel %s", application, channel_id)

# Allow non-2xx responses so we can inspect the status code ourselves
# (the ARI helper raises by default, and 204 triggers a spurious log
# entry due to integer vs. float division in Python 3).
ari.set_allow_errors(True)
resp = ari.post('events', 'claim',
channelId=channel_id, application='testsuite')

if resp.status_code != 204:
LOGGER.error("Expected HTTP 204 from claim, got %d: %s",
resp.status_code, resp.text)
test_object.set_passed(False)
return False

LOGGER.info("Channel %s claimed successfully", channel_id)
return True


def on_stasis_start(ari, event, test_object):
"""Handle StasisStart for the claimed channel and clean up."""
channel_id = event['channel']['id']
application = event.get('application', '')
LOGGER.info("StasisStart for channel %s in app '%s'", channel_id, application)

if application != 'testsuite':
LOGGER.error("Expected StasisStart in 'testsuite', got '%s'", application)
test_object.set_passed(False)
return False

ari.delete('channels', channel_id)
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[default]

exten => s,1,NoOp()
same => n,Answer()
same => n,StasisBroadcast(5000)
same => n,Hangup()
Loading