Skip to content

Commit ae82436

Browse files
committed
test(api): add anniversaries ICS endpoint tests
1 parent f9c8e05 commit ae82436

1 file changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#
2+
# Gramps Web API - A RESTful API for the Gramps genealogy program
3+
#
4+
# Copyright (C) 2026 Gramps Web contributors
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as published by
8+
# the Free Software Foundation; either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
12+
"""Tests for anniversaries ICS endpoint."""
13+
14+
import unittest
15+
from unittest.mock import patch
16+
from uuid import uuid4
17+
18+
from gramps_webapi.auth import (
19+
add_user,
20+
get_user_details,
21+
rotate_user_access_token,
22+
)
23+
from gramps_webapi.auth.const import (
24+
ACCESS_TOKEN_SCOPE_ANNIVERSARIES_ICS,
25+
ROLE_DISABLED,
26+
ROLE_GUEST,
27+
ROLE_OWNER,
28+
)
29+
30+
from . import BASE_URL, get_test_client
31+
from .util import fetch_header
32+
33+
ICS_URL = BASE_URL + "/anniversaries.ics"
34+
TOKEN_URL = BASE_URL + "/users/-/access-tokens/anniversaries_ics/"
35+
36+
37+
class TestAnniversariesIcs(unittest.TestCase):
38+
"""Test cases for anniversaries ICS endpoint."""
39+
40+
@classmethod
41+
def setUpClass(cls):
42+
"""Test class setup."""
43+
cls.client = get_test_client()
44+
45+
def _create_token(self, role=ROLE_OWNER):
46+
"""Create or rotate token for role and return (header, token)."""
47+
header = fetch_header(self.client, role=role)
48+
rv = self.client.post(TOKEN_URL, headers=header)
49+
self.assertEqual(rv.status_code, 200)
50+
self.assertTrue(rv.json["active"])
51+
token = rv.json["token"]
52+
self.assertIsNotNone(token)
53+
return header, token
54+
55+
def test_public_feed_requires_valid_token(self):
56+
"""Public feed requires token query parameter and valid token value."""
57+
rv = self.client.get(ICS_URL)
58+
self.assertEqual(rv.status_code, 422)
59+
60+
rv = self.client.get(f"{ICS_URL}?token=invalid-token")
61+
self.assertEqual(rv.status_code, 401)
62+
63+
def test_feed_access_and_revoke_flow(self):
64+
"""Public feed works with valid token and fails after revocation."""
65+
header, token = self._create_token()
66+
67+
rv = self.client.get(f"{ICS_URL}?token={token}")
68+
self.assertEqual(rv.status_code, 200)
69+
self.assertIn("text/calendar", rv.content_type)
70+
text = rv.data.decode("utf-8")
71+
self.assertIn("BEGIN:VCALENDAR", text)
72+
self.assertIn("END:VCALENDAR", text)
73+
self.assertIn("RRULE:FREQ=YEARLY", text)
74+
75+
rv = self.client.delete(TOKEN_URL, headers=header)
76+
self.assertEqual(rv.status_code, 200)
77+
78+
rv = self.client.get(f"{ICS_URL}?token={token}")
79+
self.assertEqual(rv.status_code, 401)
80+
81+
def test_public_feed_event_type_filter(self):
82+
"""event_types filter limits event types included in ICS."""
83+
_, token = self._create_token(role=ROLE_OWNER)
84+
rv = self.client.get(f"{ICS_URL}?token={token}&event_types=Birth")
85+
self.assertEqual(rv.status_code, 200)
86+
text = rv.data.decode("utf-8")
87+
self.assertIn("Type: Birth", text)
88+
self.assertNotIn("Type: Death", text)
89+
90+
def test_public_feed_generation_depth_validation(self):
91+
"""generation_depth is bounded between 1 and 9."""
92+
_, token = self._create_token(role=ROLE_OWNER)
93+
rv = self.client.get(f"{ICS_URL}?token={token}&generation_depth=0")
94+
self.assertEqual(rv.status_code, 422)
95+
rv = self.client.get(f"{ICS_URL}?token={token}&generation_depth=10")
96+
self.assertEqual(rv.status_code, 422)
97+
98+
def test_public_feed_anchor_scope_for_owner_and_guest(self):
99+
"""Anchor scope query works for both owner and guest roles."""
100+
# Use a known person ID from example_gramps.
101+
_, owner_token = self._create_token(role=ROLE_OWNER)
102+
rv = self.client.get(
103+
f"{ICS_URL}?token={owner_token}&anchor_gramps_id=I0044"
104+
)
105+
self.assertEqual(rv.status_code, 200)
106+
107+
_, guest_token = self._create_token(role=ROLE_GUEST)
108+
rv = self.client.get(
109+
f"{ICS_URL}?token={guest_token}&anchor_gramps_id=I0044"
110+
)
111+
self.assertEqual(rv.status_code, 200)
112+
113+
def test_public_feed_invalid_anchor(self):
114+
"""Unknown anchor Gramps ID returns 404."""
115+
_, token = self._create_token(role=ROLE_OWNER)
116+
rv = self.client.get(
117+
f"{ICS_URL}?token={token}&anchor_gramps_id=NOT_A_REAL_GRMPS_ID"
118+
)
119+
self.assertEqual(rv.status_code, 404)
120+
121+
def test_public_feed_disabled_user(self):
122+
"""Disabled users cannot use access tokens."""
123+
username = f"disabled-ics-{uuid4().hex[:8]}"
124+
with self.client.application.app_context():
125+
tree = get_user_details("owner")["tree"]
126+
add_user(
127+
name=username,
128+
password="secret",
129+
role=ROLE_DISABLED,
130+
tree=tree,
131+
)
132+
token = rotate_user_access_token(
133+
username, ACCESS_TOKEN_SCOPE_ANNIVERSARIES_ICS
134+
)
135+
rv = self.client.get(f"{ICS_URL}?token={token}")
136+
self.assertEqual(rv.status_code, 403)
137+
138+
def test_public_feed_disabled_tree(self):
139+
"""If the tree is disabled, feed returns 503."""
140+
_, token = self._create_token(role=ROLE_OWNER)
141+
with patch(
142+
"gramps_webapi.api.resources.anniversaries.is_tree_disabled",
143+
return_value=True,
144+
):
145+
rv = self.client.get(f"{ICS_URL}?token={token}")
146+
self.assertEqual(rv.status_code, 503)

0 commit comments

Comments
 (0)