Added a lot of unit tests
This commit is contained in:
parent
0938fb98b7
commit
16d4a0724c
32 changed files with 4268 additions and 497 deletions
0
tests/unit_tests/__init__.py
Normal file
0
tests/unit_tests/__init__.py
Normal file
284
tests/unit_tests/conftest.py
Normal file
284
tests/unit_tests/conftest.py
Normal file
|
@ -0,0 +1,284 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pendulum
|
||||
import pytest
|
||||
from connectors import GestionSportsConnector
|
||||
from models import (
|
||||
BookingFilter,
|
||||
BookingOpening,
|
||||
BookingPlatform,
|
||||
Club,
|
||||
Court,
|
||||
Sport,
|
||||
TotalBookings,
|
||||
Url,
|
||||
User,
|
||||
)
|
||||
|
||||
TEST_FOLDER = Path(__file__).parent.parent
|
||||
DATA_FOLDER = TEST_FOLDER / "data"
|
||||
RESPONSES_FOLDER = DATA_FOLDER / "responses"
|
||||
|
||||
court11 = Court(id="1", name="Court 1", number=1, isIndoor=True)
|
||||
court12 = Court(id="2", name="Court 2", number=2, isIndoor=False)
|
||||
court13 = Court(id="3", name="Court 3", number=3, isIndoor=True)
|
||||
court14 = Court(id="4", name="Court 4", number=4, isIndoor=True)
|
||||
|
||||
sport1 = Sport(
|
||||
name="Sport1",
|
||||
id=8,
|
||||
duration=99,
|
||||
price=54,
|
||||
players=3,
|
||||
courts=[court11, court12, court13, court14],
|
||||
)
|
||||
|
||||
court21 = Court(id="1", name="Court 1", number=1, isIndoor=False)
|
||||
court22 = Court(id="2", name="Court 2", number=2, isIndoor=True)
|
||||
court23 = Court(id="3", name="Court 3", number=3, isIndoor=True)
|
||||
court24 = Court(id="4", name="Court 4", number=4, isIndoor=True)
|
||||
|
||||
sport2 = Sport(
|
||||
name="Sport 2",
|
||||
id=10,
|
||||
duration=44,
|
||||
price=23,
|
||||
players=1,
|
||||
courts=[court21, court22, court23, court24],
|
||||
)
|
||||
|
||||
landing_url = Url(
|
||||
name="landing-page",
|
||||
path="landing.html",
|
||||
)
|
||||
|
||||
login_url = Url(
|
||||
name="login",
|
||||
path="login.html",
|
||||
payloadTemplate="gestion-sports/login-payload.txt",
|
||||
)
|
||||
|
||||
booking_url = Url(
|
||||
name="booking",
|
||||
path="booking.html",
|
||||
payloadTemplate="gestion-sports/booking-payload.txt",
|
||||
)
|
||||
|
||||
user_bookings_url = Url(
|
||||
name="user-bookings",
|
||||
path="user_bookings.html",
|
||||
payloadTemplate="gestion-sports/user-bookings-payload.txt",
|
||||
)
|
||||
|
||||
cancellation_url = Url(
|
||||
name="cancellation",
|
||||
path="cancel.html",
|
||||
payloadTemplate="gestion-sports/booking-cancellation-payload.txt",
|
||||
)
|
||||
|
||||
booking_opening = BookingOpening(daysBefore=10, time="03:27")
|
||||
|
||||
total_bookings = TotalBookings(peakHours=3, offPeakHours="unlimited")
|
||||
|
||||
booking_platform = BookingPlatform(
|
||||
id="gestion-sports",
|
||||
clubId=21,
|
||||
url="https://ptf1.com",
|
||||
hoursBeforeCancellation=7,
|
||||
bookingOpening=booking_opening,
|
||||
totalBookings=total_bookings,
|
||||
sports=[sport1, sport2],
|
||||
urls={
|
||||
"landing-page": landing_url,
|
||||
"login": login_url,
|
||||
"booking": booking_url,
|
||||
"user-bookings": user_bookings_url,
|
||||
"cancellation": cancellation_url,
|
||||
},
|
||||
)
|
||||
|
||||
club = Club(
|
||||
id="super_club",
|
||||
name="Super Club",
|
||||
url="https://superclub.com",
|
||||
bookingPlatform=booking_platform,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def a_club() -> Club:
|
||||
return club
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connector() -> GestionSportsConnector:
|
||||
return GestionSportsConnector(club)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
return User(login="padel.testing@jouf.fr", password="ridicule")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def booking_filter() -> BookingFilter:
|
||||
return BookingFilter(
|
||||
sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00Z")
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def landing_response() -> str:
|
||||
landing_response_file = RESPONSES_FOLDER / "landing_response.html"
|
||||
return landing_response_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login_success_response() -> dict:
|
||||
login_success_file = RESPONSES_FOLDER / "login_success.json"
|
||||
return json.loads(login_success_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login_failure_response() -> dict:
|
||||
login_failure_file = RESPONSES_FOLDER / "login_failure.json"
|
||||
return json.loads(login_failure_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def booking_success_response() -> dict:
|
||||
booking_success_file = RESPONSES_FOLDER / "booking_success.json"
|
||||
return json.loads(booking_success_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def booking_failure_response() -> dict:
|
||||
booking_failure_file = RESPONSES_FOLDER / "booking_failure.json"
|
||||
return json.loads(booking_failure_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def booking_success_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
booking_success_response,
|
||||
booking_failure_response,
|
||||
):
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
booking_failure_response,
|
||||
booking_success_response,
|
||||
booking_failure_response,
|
||||
booking_failure_response,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def booking_failure_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
booking_success_response,
|
||||
booking_failure_response,
|
||||
):
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
booking_failure_response,
|
||||
booking_failure_response,
|
||||
booking_failure_response,
|
||||
booking_failure_response,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_bookings_get_response() -> str:
|
||||
user_bookings_file = RESPONSES_FOLDER / "user_bookings_get.html"
|
||||
return user_bookings_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_bookings_list() -> list:
|
||||
user_bookings_file = RESPONSES_FOLDER / "user_bookings_post.json"
|
||||
return json.loads(user_bookings_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_has_ongoing_bookings_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
) -> list:
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_bookings_empty_list() -> list:
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_has_no_ongoing_bookings_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_empty_list,
|
||||
) -> list:
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_empty_list,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancellation_response() -> list:
|
||||
cancellation_response_file = RESPONSES_FOLDER / "cancellation_response.json"
|
||||
return json.loads(cancellation_response_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancellation_by_id_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
cancellation_response,
|
||||
):
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
cancellation_response,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancellation_success_from_start(
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
cancellation_response,
|
||||
):
|
||||
return [
|
||||
landing_response,
|
||||
login_success_response,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
cancellation_response,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancellation_success_booking_filter() -> BookingFilter:
|
||||
return BookingFilter(
|
||||
sport_name="Sport1", date=pendulum.parse("2024-03-21T13:30:00Z")
|
||||
)
|
0
tests/unit_tests/test_booking.py
Normal file
0
tests/unit_tests/test_booking.py
Normal file
0
tests/unit_tests/test_cancellation.py
Normal file
0
tests/unit_tests/test_cancellation.py
Normal file
51
tests/unit_tests/test_config.py
Normal file
51
tests/unit_tests/test_config.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import config
|
||||
from pendulum import DateTime, Timezone
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"SPORT_NAME": "Padel",
|
||||
"DATE_TIME": "2024-02-03T22:38:45Z",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_get_booking_filter():
|
||||
booking_filter = config.get_booking_filter()
|
||||
assert booking_filter.sport_id == "padel"
|
||||
assert booking_filter.date == DateTime(
|
||||
year=2024,
|
||||
month=2,
|
||||
day=3,
|
||||
hour=23,
|
||||
minute=38,
|
||||
second=45,
|
||||
tzinfo=Timezone("Europe/Paris"),
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"LOGIN": "login@user.tld",
|
||||
"PASSWORD": "gloups",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_get_available_user():
|
||||
user = config.get_user()
|
||||
assert user.login == "login@user.tld"
|
||||
assert user.password == "gloups"
|
||||
|
||||
|
||||
def test_read_clubs():
|
||||
clubs = config.get_clubs()
|
||||
assert len(clubs) == 2
|
||||
|
||||
|
||||
def test_get_users():
|
||||
users = config.get_users("tpc")
|
||||
assert len(users) == 2
|
315
tests/unit_tests/test_gestion_sports_connector.py
Normal file
315
tests/unit_tests/test_gestion_sports_connector.py
Normal file
|
@ -0,0 +1,315 @@
|
|||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from aiohttp import ClientSession
|
||||
from connectors import GestionSportsConnector
|
||||
|
||||
|
||||
def make_landing_request_success(aioresponses, connector, landing_response):
|
||||
aioresponses.get(
|
||||
connector.landing_url,
|
||||
status=200,
|
||||
headers={"Set-Cookie": "PHPSESSID=987512"},
|
||||
body=landing_response,
|
||||
)
|
||||
|
||||
|
||||
def make_login_request_fail(aioresponses, connector, login_failure_response):
|
||||
aioresponses.post(
|
||||
connector.login_url,
|
||||
status=200,
|
||||
payload=login_failure_response,
|
||||
)
|
||||
|
||||
|
||||
def make_login_request_success(aioresponses, connector, login_success_response):
|
||||
aioresponses.post(
|
||||
connector.login_url,
|
||||
status=200,
|
||||
headers={"Set-Cookie": "COOK_COMPTE=e2be1;" "COOK_ID_CLUB=22;COOK_ID_USER=666"},
|
||||
payload=login_success_response,
|
||||
)
|
||||
|
||||
|
||||
def set_booking_request(aioresponses, connector, booking_response):
|
||||
aioresponses.post(connector.booking_url, status=200, payload=booking_response)
|
||||
|
||||
|
||||
def set_full_booking_requests_responses(aioresponses, connector, responses_list):
|
||||
make_landing_request_success(aioresponses, connector, responses_list[0])
|
||||
make_login_request_success(aioresponses, connector, responses_list[1])
|
||||
for response in responses_list[2:]:
|
||||
set_booking_request(aioresponses, connector, response)
|
||||
|
||||
|
||||
def set_ongoing_bookings_response(
|
||||
aioresponses, connector, user_bookings_get_response, user_bookings_post_response
|
||||
):
|
||||
set_hash_response(aioresponses, connector, user_bookings_get_response)
|
||||
set_bookings_response(aioresponses, connector, user_bookings_post_response)
|
||||
|
||||
|
||||
def set_hash_response(aioresponses, connector, user_bookings_get_response):
|
||||
aioresponses.get(
|
||||
connector.user_bookings_url, status=200, body=user_bookings_get_response
|
||||
)
|
||||
|
||||
|
||||
def set_bookings_response(aioresponses, connector, user_bookings_post_response):
|
||||
aioresponses.post(
|
||||
connector.user_bookings_url, status=200, payload=user_bookings_post_response
|
||||
)
|
||||
|
||||
|
||||
def set_full_user_bookings_responses(aioresponses, connector, responses):
|
||||
make_landing_request_success(aioresponses, connector, responses[0])
|
||||
make_login_request_success(aioresponses, connector, responses[1])
|
||||
set_ongoing_bookings_response(aioresponses, connector, *responses[2:])
|
||||
|
||||
|
||||
def set_cancellation_response(aioresponses, connector, response):
|
||||
aioresponses.post(connector.booking_cancellation_url, status=200, payload=response)
|
||||
|
||||
|
||||
def set_full_cancellation_by_id_responses(aioresponses, connector, responses):
|
||||
make_landing_request_success(aioresponses, connector, responses[0])
|
||||
make_login_request_success(aioresponses, connector, responses[1])
|
||||
set_hash_response(aioresponses, connector, responses[2])
|
||||
set_cancellation_response(aioresponses, connector, responses[3])
|
||||
|
||||
|
||||
def set_full_cancellation_responses(aioresponses, connector, responses):
|
||||
make_landing_request_success(aioresponses, connector, responses[0])
|
||||
make_login_request_success(aioresponses, connector, responses[1])
|
||||
|
||||
# the request to get the hash is made twice
|
||||
set_hash_response(aioresponses, connector, responses[2])
|
||||
set_hash_response(aioresponses, connector, responses[2])
|
||||
|
||||
set_bookings_response(aioresponses, connector, responses[3])
|
||||
set_cancellation_response(aioresponses, connector, responses[4])
|
||||
|
||||
|
||||
def test_urls(a_club):
|
||||
connector = GestionSportsConnector(a_club)
|
||||
base_url = a_club.booking_platform.url
|
||||
relative_urls = a_club.booking_platform.urls
|
||||
|
||||
relative_landing_url = relative_urls.get("landing-page").path
|
||||
assert connector.landing_url == f"{base_url}/{relative_landing_url}"
|
||||
|
||||
relative_login_url = relative_urls.get("login").path
|
||||
assert connector.login_url == f"{base_url}/{relative_login_url}"
|
||||
|
||||
relative_booking_url = relative_urls.get("booking").path
|
||||
assert connector.booking_url == f"{base_url}/{relative_booking_url}"
|
||||
|
||||
relative_user_bookings_url = relative_urls.get("user-bookings").path
|
||||
assert connector.user_bookings_url == f"{base_url}/{relative_user_bookings_url}"
|
||||
|
||||
relative_cancel_url = relative_urls.get("cancellation").path
|
||||
assert connector.booking_cancellation_url == f"{base_url}/{relative_cancel_url}"
|
||||
|
||||
|
||||
@patch("config.get_resources_folder")
|
||||
def test_urls_payload_templates(mock_resources, a_club):
|
||||
path_to_resources = Path("some/path/to/resource")
|
||||
mock_resources.return_value = path_to_resources
|
||||
|
||||
connector = GestionSportsConnector(a_club)
|
||||
relative_urls = a_club.booking_platform.urls
|
||||
|
||||
login_payload = relative_urls.get("login").payload_template
|
||||
assert connector.login_template == path_to_resources / login_payload
|
||||
|
||||
booking_payload = relative_urls.get("booking").payload_template
|
||||
assert connector.booking_template == path_to_resources / booking_payload
|
||||
|
||||
user_bookings_payload = relative_urls.get("user-bookings").payload_template
|
||||
assert connector.user_bookings_template == path_to_resources / user_bookings_payload
|
||||
|
||||
cancel_payload = relative_urls.get("cancellation").payload_template
|
||||
assert connector.booking_cancel_template == path_to_resources / cancel_payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_landing_page(aioresponses, connector, landing_response):
|
||||
make_landing_request_success(aioresponses, connector, landing_response)
|
||||
|
||||
async with ClientSession() as session:
|
||||
response = await connector.land(session)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.cookies.get("PHPSESSID").value == "987512"
|
||||
assert await response.text() == landing_response
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(aioresponses, connector, user, login_success_response):
|
||||
make_login_request_success(aioresponses, connector, login_success_response)
|
||||
|
||||
async with ClientSession() as session:
|
||||
response = await connector.login(session, user)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.cookies.get("COOK_COMPTE").value == "e2be1"
|
||||
assert response.cookies.get("COOK_ID_CLUB").value == "22"
|
||||
assert response.cookies.get("COOK_ID_USER").value == "666"
|
||||
assert await response.json() == login_success_response
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_failure(aioresponses, connector, user, login_failure_response):
|
||||
make_login_request_fail(aioresponses, connector, login_failure_response)
|
||||
|
||||
async with ClientSession() as session:
|
||||
response = await connector.login(session, user)
|
||||
|
||||
assert response.status == 200
|
||||
assert len(response.cookies) == 0
|
||||
assert await response.json() == login_failure_response
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_booking_success(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
booking_filter,
|
||||
booking_success_from_start,
|
||||
):
|
||||
set_full_booking_requests_responses(
|
||||
aioresponses, connector, booking_success_from_start
|
||||
)
|
||||
|
||||
court_booked = await connector.book(user, booking_filter)
|
||||
|
||||
assert court_booked.id == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_booking_failure(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
booking_filter,
|
||||
booking_failure_from_start,
|
||||
):
|
||||
set_full_booking_requests_responses(
|
||||
aioresponses, connector, booking_failure_from_start
|
||||
)
|
||||
|
||||
court_booked = await connector.book(user, booking_filter)
|
||||
|
||||
assert court_booked is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ongoing_bookings(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
):
|
||||
set_ongoing_bookings_response(
|
||||
aioresponses, connector, user_bookings_get_response, user_bookings_list
|
||||
)
|
||||
|
||||
async with ClientSession() as session:
|
||||
bookings = await connector.get_ongoing_bookings(session)
|
||||
|
||||
assert len(bookings) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ongoing_bookings(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
user_bookings_get_response,
|
||||
user_bookings_list,
|
||||
):
|
||||
set_ongoing_bookings_response(
|
||||
aioresponses, connector, user_bookings_get_response, user_bookings_list
|
||||
)
|
||||
|
||||
async with ClientSession() as session:
|
||||
bookings = await connector.get_ongoing_bookings(session)
|
||||
|
||||
assert len(bookings) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_user_ongoing_bookings(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
user_has_ongoing_bookings_from_start,
|
||||
):
|
||||
set_full_user_bookings_responses(
|
||||
aioresponses, connector, user_has_ongoing_bookings_from_start
|
||||
)
|
||||
|
||||
has_bookings = await connector.has_user_ongoing_booking(user)
|
||||
|
||||
assert has_bookings
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_user_ongoing_bookings(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
user_has_no_ongoing_bookings_from_start,
|
||||
):
|
||||
set_full_user_bookings_responses(
|
||||
aioresponses, connector, user_has_no_ongoing_bookings_from_start
|
||||
)
|
||||
has_bookings = await connector.has_user_ongoing_booking(user)
|
||||
|
||||
assert not has_bookings
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancellation_request(
|
||||
aioresponses, connector, user_bookings_get_response, cancellation_response
|
||||
):
|
||||
set_hash_response(aioresponses, connector, user_bookings_get_response)
|
||||
set_cancellation_response(aioresponses, connector, cancellation_response)
|
||||
|
||||
async with ClientSession() as session:
|
||||
response = await connector.send_cancellation_request(session, 123)
|
||||
|
||||
assert await response.json() == cancellation_response
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_booking_id(
|
||||
aioresponses, connector, user, cancellation_by_id_from_start
|
||||
):
|
||||
set_full_cancellation_by_id_responses(
|
||||
aioresponses, connector, cancellation_by_id_from_start
|
||||
)
|
||||
|
||||
response = await connector.cancel_booking_id(user, 132)
|
||||
|
||||
assert await response.json() == cancellation_by_id_from_start[3]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_booking_success(
|
||||
aioresponses,
|
||||
connector,
|
||||
user,
|
||||
cancellation_success_booking_filter,
|
||||
cancellation_success_from_start,
|
||||
):
|
||||
set_full_cancellation_responses(
|
||||
aioresponses, connector, cancellation_success_from_start
|
||||
)
|
||||
|
||||
response = await connector.cancel_booking(user, cancellation_success_booking_filter)
|
||||
|
||||
assert await response.json() == cancellation_success_from_start[4]
|
Loading…
Add table
Add a link
Reference in a new issue