test: Add tests for ds
Assisted-by: Gemini Flash 2.5 Flash
This commit is contained in:
parent
3f72a321dd
commit
c2a37b748d
4 changed files with 477 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
*.csv
|
||||
__pycache__
|
||||
CLE_*
|
||||
.coverage
|
||||
|
|
95
test/test_card.py
Normal file
95
test/test_card.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# This file is part of sbs
|
||||
#
|
||||
# Copyright (C) 2025 Steve Milner
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Generated by Gemini 2.5 Flash
|
||||
|
||||
import pytest
|
||||
|
||||
from sbs.ds.card import Card
|
||||
|
||||
|
||||
def test_card_initialization():
|
||||
"""
|
||||
Test that the Card object initializes correctly with all provided attributes.
|
||||
"""
|
||||
card = Card(
|
||||
key="PROJ-123",
|
||||
assignee="John Doe",
|
||||
summary="Implement login feature",
|
||||
description="User should be able to log in with username and password.",
|
||||
story_points=5,
|
||||
)
|
||||
|
||||
assert card.key == "PROJ-123"
|
||||
assert card.assignee == "John Doe"
|
||||
assert card.summary == "Implement login feature"
|
||||
assert (
|
||||
card.description == "User should be able to log in with username and password."
|
||||
)
|
||||
assert card.story_points == 5
|
||||
|
||||
|
||||
def test_card_repr_method():
|
||||
"""
|
||||
Test the __repr__ method to ensure it returns the expected string format.
|
||||
"""
|
||||
card = Card(
|
||||
key="PROJ-456",
|
||||
assignee="Jane Smith",
|
||||
summary="Fix bug on dashboard",
|
||||
description="Dashboard not displaying correct data.",
|
||||
story_points=3,
|
||||
)
|
||||
expected_repr = """Key: PROJ-456
|
||||
Assignee: Jane Smith
|
||||
Story Points: 3
|
||||
Summary: Fix bug on dashboard
|
||||
Description: Dashboard not displaying correct data."""
|
||||
assert repr(card) == expected_repr
|
||||
|
||||
|
||||
def test_card_attributes_can_be_accessed():
|
||||
"""
|
||||
Test that individual attributes of the Card object can be accessed after initialization.
|
||||
"""
|
||||
card = Card(
|
||||
key="TASK-001",
|
||||
assignee="Alice Brown",
|
||||
summary="Refactor old code",
|
||||
description="Clean up deprecated functions in module X.",
|
||||
story_points=8,
|
||||
)
|
||||
|
||||
assert card.key == "TASK-001"
|
||||
assert card.assignee == "Alice Brown"
|
||||
assert card.summary == "Refactor old code"
|
||||
assert card.description == "Clean up deprecated functions in module X."
|
||||
assert card.story_points == 8
|
||||
|
||||
|
||||
def test_card_story_points_zero():
|
||||
"""
|
||||
Test initialization with zero story points.
|
||||
"""
|
||||
card = Card(
|
||||
key="BUG-007",
|
||||
assignee="Bob White",
|
||||
summary="Minor text typo",
|
||||
description="Fix typo on homepage footer.",
|
||||
story_points=0,
|
||||
)
|
||||
assert card.story_points == 0
|
||||
|
||||
|
||||
def test_card_empty_strings():
|
||||
"""
|
||||
Test initialization with empty strings for key, assignee, summary, and description.
|
||||
"""
|
||||
card = Card(key="", assignee="", summary="", description="", story_points=1)
|
||||
assert card.key == ""
|
||||
assert card.assignee == ""
|
||||
assert card.summary == ""
|
||||
assert card.description == ""
|
||||
assert card.story_points == 1
|
190
test/test_csvexp.py
Normal file
190
test/test_csvexp.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
# This file is part of sbs
|
||||
#
|
||||
# Copyright (C) 2025 Steve Milner
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Generated by Gemini 2.5 Flash
|
||||
|
||||
import pytest
|
||||
import csv
|
||||
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
from sbs.ds.card import Card
|
||||
from sbs.ds.csvexp import csvexp
|
||||
|
||||
|
||||
def test_csvexp_reads_single_card():
|
||||
"""
|
||||
Test that csvexp correctly reads a single row from a CSV and yields one Card object.
|
||||
"""
|
||||
# Simulate CSV content as a string
|
||||
csv_content = (
|
||||
"Issue key,Assignee,Summary,Description,Custom field (Story Points)\n"
|
||||
"PROJ-1,John Doe,Task 1,Desc 1,5\n"
|
||||
)
|
||||
|
||||
# Use patch.object to mock csv.DictReader's __iter__ method
|
||||
# and mock_open to simulate file reading
|
||||
with patch("builtins.open", mock_open(read_data=csv_content)) as mock_file:
|
||||
# Mock csv.DictReader. We'll manually specify its behavior.
|
||||
# When csv.DictReader is called, it returns an iterable.
|
||||
# We need to ensure that iterable provides the dictionary rows.
|
||||
mock_reader_instance = iter(
|
||||
[
|
||||
{
|
||||
"Issue key": "PROJ-1",
|
||||
"Assignee": "John Doe",
|
||||
"Summary": "Task 1",
|
||||
"Description": "Desc 1",
|
||||
"Custom field (Story Points)": "5",
|
||||
}
|
||||
]
|
||||
)
|
||||
with patch(
|
||||
"csv.DictReader", return_value=mock_reader_instance
|
||||
) as mock_dict_reader:
|
||||
cards = list(csvexp("dummy.csv"))
|
||||
|
||||
# Assertions
|
||||
mock_file.assert_called_once_with(
|
||||
"dummy.csv", "r"
|
||||
) # Check if open was called
|
||||
mock_dict_reader.assert_called_once_with(
|
||||
mock_file()
|
||||
) # Check if DictReader was called with the file handle
|
||||
|
||||
assert len(cards) == 1
|
||||
card = cards[0]
|
||||
assert isinstance(card, Card)
|
||||
assert card.key == "PROJ-1"
|
||||
assert card.assignee == "John Doe"
|
||||
assert card.summary == "Task 1"
|
||||
assert card.description == "Desc 1"
|
||||
assert card.story_points == "5" # Story points are read as string from CSV
|
||||
|
||||
|
||||
def test_csvexp_reads_multiple_cards():
|
||||
"""
|
||||
Test that csvexp correctly reads multiple rows from a CSV and yields multiple Card objects.
|
||||
"""
|
||||
csv_content = (
|
||||
"Issue key,Assignee,Summary,Description,Custom field (Story Points)\n"
|
||||
"PROJ-1,John Doe,Task 1,Desc 1,5\n"
|
||||
"PROJ-2,Jane Smith,Task 2,Desc 2,3\n"
|
||||
"PROJ-3,Peter Jones,Task 3,Desc 3,8\n"
|
||||
)
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=csv_content)) as mock_file:
|
||||
mock_reader_instance = iter(
|
||||
[
|
||||
{
|
||||
"Issue key": "PROJ-1",
|
||||
"Assignee": "John Doe",
|
||||
"Summary": "Task 1",
|
||||
"Description": "Desc 1",
|
||||
"Custom field (Story Points)": "5",
|
||||
},
|
||||
{
|
||||
"Issue key": "PROJ-2",
|
||||
"Assignee": "Jane Smith",
|
||||
"Summary": "Task 2",
|
||||
"Description": "Desc 2",
|
||||
"Custom field (Story Points)": "3",
|
||||
},
|
||||
{
|
||||
"Issue key": "PROJ-3",
|
||||
"Assignee": "Peter Jones",
|
||||
"Summary": "Task 3",
|
||||
"Description": "Desc 3",
|
||||
"Custom field (Story Points)": "8",
|
||||
},
|
||||
]
|
||||
)
|
||||
with patch("csv.DictReader", return_value=mock_reader_instance):
|
||||
cards = list(csvexp("multiple_cards.csv"))
|
||||
|
||||
assert len(cards) == 3
|
||||
|
||||
assert cards[0].key == "PROJ-1"
|
||||
assert cards[1].assignee == "Jane Smith"
|
||||
assert cards[2].story_points == "8"
|
||||
|
||||
|
||||
def test_csvexp_handles_empty_csv():
|
||||
"""
|
||||
Test that csvexp returns an empty list when the CSV file is empty (only header or no data).
|
||||
"""
|
||||
csv_content = "Issue key,Assignee,Summary,Description,Custom field (Story Points)\n"
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=csv_content)) as mock_file:
|
||||
# csv.DictReader on an empty file (after header) will yield nothing
|
||||
mock_reader_instance = iter([])
|
||||
with patch("csv.DictReader", return_value=mock_reader_instance):
|
||||
cards = list(csvexp("empty.csv"))
|
||||
assert len(cards) == 0
|
||||
|
||||
|
||||
def test_csvexp_handles_missing_file():
|
||||
"""
|
||||
Test that csvexp raises a FileNotFoundError if the specified CSV file does not exist.
|
||||
"""
|
||||
with patch("builtins.open", side_effect=FileNotFoundError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
list(csvexp("non_existent.csv"))
|
||||
|
||||
|
||||
def test_csvexp_handles_missing_column():
|
||||
"""
|
||||
Test that csvexp raises a KeyError if a required column is missing from the CSV.
|
||||
"""
|
||||
csv_content = (
|
||||
"Issue key,Assignee,Summary,Description,Missing Column\n"
|
||||
"PROJ-1,John Doe,Task 1,Desc 1,5\n"
|
||||
)
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=csv_content)) as mock_file:
|
||||
# Simulate DictReader providing a row missing a key
|
||||
mock_reader_instance = iter(
|
||||
[
|
||||
{
|
||||
"Issue key": "PROJ-1",
|
||||
"Assignee": "John Doe",
|
||||
"Summary": "Task 1",
|
||||
"Description": "Desc 1",
|
||||
"Missing Column": "5",
|
||||
} # Missing "Custom field (Story Points)"
|
||||
]
|
||||
)
|
||||
with patch("csv.DictReader", return_value=mock_reader_instance):
|
||||
with pytest.raises(KeyError):
|
||||
list(csvexp("missing_column.csv"))
|
||||
|
||||
|
||||
def test_csvexp_ignore_parameter():
|
||||
"""
|
||||
Test that the `ignore` parameter is correctly accepted and does not interfere.
|
||||
"""
|
||||
csv_content = (
|
||||
"Issue key,Assignee,Summary,Description,Custom field (Story Points)\n"
|
||||
"PROJ-1,John Doe,Task 1,Desc 1,5\n"
|
||||
)
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=csv_content)) as mock_file:
|
||||
mock_reader_instance = iter(
|
||||
[
|
||||
{
|
||||
"Issue key": "PROJ-1",
|
||||
"Assignee": "John Doe",
|
||||
"Summary": "Task 1",
|
||||
"Description": "Desc 1",
|
||||
"Custom field (Story Points)": "5",
|
||||
}
|
||||
]
|
||||
)
|
||||
with patch("csv.DictReader", return_value=mock_reader_instance):
|
||||
# Pass an extra keyword argument that should be ignored
|
||||
cards = list(csvexp("dummy.csv", extra_param="value"))
|
||||
|
||||
assert len(cards) == 1
|
||||
assert cards[0].key == "PROJ-1"
|
191
test/test_jql.py
Normal file
191
test/test_jql.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
# This file is part of sbs
|
||||
#
|
||||
# Copyright (C) 2025 Steve Milner
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Generated by Gemini 2.5 Flash
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
from urllib import parse
|
||||
|
||||
from sbs.ds.card import Card
|
||||
from sbs.ds.jql import jira
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response():
|
||||
"""
|
||||
A pytest fixture to create a mock requests.Response object.
|
||||
"""
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
return mock_resp
|
||||
|
||||
|
||||
def test_jira_fetches_and_parses_single_issue(mock_response):
|
||||
"""
|
||||
Test that the jira function correctly fetches and parses a single Jira issue.
|
||||
"""
|
||||
# Define the mock JSON response from Jira API
|
||||
mock_jira_json = {
|
||||
"issues": [
|
||||
{
|
||||
"key": "PROJ-123",
|
||||
"fields": {
|
||||
"assignee": {"name": "John Doe"},
|
||||
"summary": "Implement feature A",
|
||||
"description": "Details for feature A implementation.",
|
||||
"customfield_12310243": 5,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_response.json.return_value = mock_jira_json
|
||||
|
||||
# Patch requests.get to return our mock_response
|
||||
with patch("requests.get", return_value=mock_response) as mock_get:
|
||||
endpoint = "https://jira.example.com/rest/api/2"
|
||||
token = "test_token"
|
||||
query = "project = PROJ AND status = Open"
|
||||
|
||||
cards = list(jira(endpoint, token, query))
|
||||
|
||||
# Assertions
|
||||
expected_fjql = parse.quote_plus(query)
|
||||
expected_fields = parse.quote_plus(
|
||||
"assignee,summary,description,customfield_12310243"
|
||||
)
|
||||
expected_url = f"{endpoint}/search/?fields={expected_fields}&maxResults=100&jql={expected_fjql}"
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
expected_url, headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
assert len(cards) == 1
|
||||
card = cards[0]
|
||||
assert isinstance(card, Card)
|
||||
assert card.key == "PROJ-123"
|
||||
assert card.assignee == "John Doe"
|
||||
assert card.summary == "Implement feature A"
|
||||
assert card.description == "Details for feature A implementation."
|
||||
assert card.story_points == 5
|
||||
|
||||
|
||||
def test_jira_fetches_and_parses_multiple_issues(mock_response):
|
||||
"""
|
||||
Test that the jira function correctly fetches and parses multiple Jira issues.
|
||||
"""
|
||||
mock_jira_json = {
|
||||
"issues": [
|
||||
{
|
||||
"key": "PROJ-1",
|
||||
"fields": {
|
||||
"assignee": {"name": "User A"},
|
||||
"summary": "Task 1",
|
||||
"description": "Description 1",
|
||||
"customfield_12310243": 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "PROJ-2",
|
||||
"fields": {
|
||||
"assignee": {"name": "User B"},
|
||||
"summary": "Task 2",
|
||||
"description": "Description 2",
|
||||
"customfield_12310243": 8,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
mock_response.json.return_value = mock_jira_json
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
cards = list(jira("dummy_endpoint", "dummy_token", "dummy_query"))
|
||||
|
||||
assert len(cards) == 2
|
||||
assert cards[0].key == "PROJ-1"
|
||||
assert cards[1].assignee == "User B"
|
||||
assert cards[1].story_points == 8
|
||||
|
||||
|
||||
def test_jira_handles_issue_with_no_assignee(mock_response):
|
||||
"""
|
||||
Test that the jira function correctly handles issues where the assignee field is missing.
|
||||
It should default the assignee name to an empty string.
|
||||
"""
|
||||
mock_jira_json = {
|
||||
"issues": [
|
||||
{
|
||||
"key": "PROJ-BUG",
|
||||
"fields": {
|
||||
"summary": "Untriaged Bug",
|
||||
"description": "This bug has no assignee.",
|
||||
"customfield_12310243": 1,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_response.json.return_value = mock_jira_json
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
cards = list(jira("dummy_endpoint", "dummy_token", "dummy_query"))
|
||||
|
||||
assert len(cards) == 1
|
||||
card = cards[0]
|
||||
assert card.key == "PROJ-BUG"
|
||||
assert card.assignee == "" # Should be an empty string
|
||||
assert card.summary == "Untriaged Bug"
|
||||
assert card.story_points == 1
|
||||
|
||||
|
||||
def test_jira_handles_empty_issues_list(mock_response):
|
||||
"""
|
||||
Test that the jira function returns an empty list if the Jira API returns no issues.
|
||||
"""
|
||||
mock_jira_json = {"issues": []}
|
||||
mock_response.json.return_value = mock_jira_json
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
cards = list(jira("dummy_endpoint", "dummy_token", "dummy_query"))
|
||||
assert len(cards) == 0
|
||||
|
||||
|
||||
def test_jira_handles_api_error_response(mock_response):
|
||||
"""
|
||||
Test that the jira function gracefully handles a non-200 HTTP status code
|
||||
by letting the requests library's behavior (e.g., raise_for_status) propagate
|
||||
or by handling potential JSON decoding errors.
|
||||
"""
|
||||
mock_response.status_code = 404
|
||||
# Simulate a JSON structure even for an error, as .json() is always called
|
||||
mock_response.json.return_value = {"errorMessages": ["Issue Does Not Exist"]}
|
||||
|
||||
# Patch requests.get to return our mock_response
|
||||
with patch("requests.get", return_value=mock_response) as mock_get:
|
||||
endpoint = "https://jira.example.com/rest/api/2"
|
||||
token = "test_token"
|
||||
query = "project = NONEXISTENT"
|
||||
|
||||
# The original code calls .json() directly, so it expects JSON.
|
||||
# If the API returns a non-200 status and valid JSON, the code
|
||||
# will proceed. If it returns non-JSON, .json() would raise an error.
|
||||
# For this test, we assume .json() works, and we are testing
|
||||
# how the internal logic handles the 'issues' key not being present
|
||||
# or the structure being different.
|
||||
# In this specific case, the KeyError will be raised because 'issues'
|
||||
# is not in the mock_jira_json for an error scenario. list() is used to
|
||||
# call the underlying __next__().
|
||||
with pytest.raises(KeyError):
|
||||
list(jira(endpoint, token, query))
|
||||
|
||||
expected_fjql = parse.quote_plus(query)
|
||||
expected_fields = parse.quote_plus(
|
||||
"assignee,summary,description,customfield_12310243"
|
||||
)
|
||||
expected_url = f"{endpoint}/search/?fields={expected_fields}&maxResults=100&jql={expected_fjql}"
|
||||
|
||||
mock_get.assert_called_once_with(
|
||||
expected_url, headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue