• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uw-it-aca / canvas-training-provisioner / 20397777188

20 Dec 2025 05:35PM UTC coverage: 91.086% (+1.7%) from 89.379%
20397777188

Pull #30

github

web-flow
Merge pull request #37 from uw-it-aca/task/test-enrollment-logic

Handle redeletes and reenable cronjobs
Pull Request #30: Develop

631 of 678 new or added lines in 12 files covered. (93.07%)

6 existing lines in 1 file now uncovered.

2248 of 2468 relevant lines covered (91.09%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.62
/training_provisioner/test/test_cache.py
1
# Copyright 2025 UW-IT, University of Washington
2
# SPDX-License-Identifier: Apache-2.0
3

4
from django.test import TestCase
1✔
5
from unittest.mock import patch, MagicMock
1✔
6
import sys
1✔
7

8
# Mock the missing memcached_clients module
9
if 'memcached_clients' not in sys.modules:
1✔
10
    memcached_mock = MagicMock()
1✔
11
    memcached_mock.RestclientPymemcacheClient = MagicMock
1✔
12
    sys.modules['memcached_clients'] = memcached_mock
1✔
13

14
# Try to import the cache module
15
try:
1✔
16
    from training_provisioner.cache import (
1✔
17
        RestClientsCache, ONE_MINUTE, ONE_HOUR, ONE_DAY, ONE_WEEK, ONE_MONTH
18
    )
19
    CACHE_AVAILABLE = True
1✔
NEW
20
except ImportError as e:
×
NEW
21
    CACHE_AVAILABLE = False
×
NEW
22
    print(f"Cache import failed: {e}")
×
23

24

25
class RestClientsCacheTest(TestCase):
1✔
26
    def setUp(self):
1✔
27
        """Set up test cache instance."""
28
        self.cache = RestClientsCache()
1✔
29

30
    def test_time_constants(self):
1✔
31
        """Test that time constants are correctly defined."""
32
        self.assertEqual(ONE_MINUTE, 60)
1✔
33
        self.assertEqual(ONE_HOUR, 60 * 60)
1✔
34
        self.assertEqual(ONE_DAY, 60 * 60 * 24)
1✔
35
        self.assertEqual(ONE_WEEK, 60 * 60 * 24 * 7)
1✔
36
        self.assertEqual(ONE_MONTH, 60 * 60 * 24 * 30)
1✔
37

38
    def test_canvas_courses_api_cache_expiration(self):
1✔
39
        """Test cache expiration for Canvas courses API endpoints."""
40
        # Test various Canvas courses API URLs
41
        test_cases = [
1✔
42
            '/api/v1/courses/123',
43
            '/api/v2/courses/456/enrollments',
44
            '/api/v1/courses/789/sections',
45
            '/api/v3/courses/101/assignments',
46
        ]
47

48
        for url in test_cases:
1✔
49
            with self.subTest(url=url):
1✔
50
                expiration = self.cache.get_cache_expiration_time(
1✔
51
                    'canvas', url)
52
                self.assertEqual(expiration, ONE_HOUR * 10)
1✔
53

54
    def test_canvas_courses_api_with_status_codes(self):
1✔
55
        """Test cache expiration for Canvas courses API with different status
56
          codes.
57
        """
58
        url = '/api/v1/courses/123'
1✔
59

60
        # Test with various status codes
61
        status_codes = [200, 201, 404, 500]
1✔
62
        for status in status_codes:
1✔
63
            with self.subTest(status=status):
1✔
64
                expiration = self.cache.get_cache_expiration_time(
1✔
65
                    'canvas', url, status)
66
                self.assertEqual(expiration, ONE_HOUR * 10)
1✔
67

68
    def test_canvas_non_courses_api_urls(self):
1✔
69
        """Test that non-courses Canvas API URLs return None
70
        (default behavior).
71
        """
72
        test_cases = [
1✔
73
            '/api/v1/accounts/123',
74
            '/api/v1/users/456',
75
            '/api/v2/enrollments',
76
            '/api/v1/sections/789',
77
            '/some/other/endpoint',
78
            '',
79
        ]
80

81
        for url in test_cases:
1✔
82
            with self.subTest(url=url):
1✔
83
                expiration = self.cache.get_cache_expiration_time(
1✔
84
                    'canvas', url)
85
                self.assertIsNone(expiration)
1✔
86

87
    def test_non_canvas_services(self):
1✔
88
        """Test that non-canvas services return None (default behavior)."""
89
        test_services = [
1✔
90
            'sws',
91
            'pws',
92
            'gws',
93
            'uwnetid',
94
            'some_other_service',
95
            '',
96
        ]
97

98
        url = '/api/v1/courses/123'
1✔
99
        for service in test_services:
1✔
100
            with self.subTest(service=service):
1✔
101
                expiration = self.cache.get_cache_expiration_time(service, url)
1✔
102
                self.assertIsNone(expiration)
1✔
103

104
    def test_edge_cases(self):
1✔
105
        """Test edge cases and malformed inputs."""
106
        # Test with None values - should handle gracefully
107
        expiration = self.cache.get_cache_expiration_time(
1✔
108
            None, '/api/v1/courses/123')
109
        self.assertIsNone(expiration)
1✔
110

111
        # Test with None URL - this will cause a TypeError in the current
112
        # implementation which is the expected behavior since re.match expects
113
        # a string
114
        with self.assertRaises(TypeError):
1✔
115
            self.cache.get_cache_expiration_time('canvas', None)
1✔
116

117
        # Test with empty strings
118
        expiration = self.cache.get_cache_expiration_time(
1✔
119
            '', '/api/v1/courses/123')
120
        self.assertIsNone(expiration)
1✔
121

122
        expiration = self.cache.get_cache_expiration_time('canvas', '')
1✔
123
        self.assertIsNone(expiration)
1✔
124

125
    def test_courses_api_regex_patterns(self):
1✔
126
        """Test the regex pattern matching for courses API endpoints."""
127
        # Test URLs that should match (single digit version numbers)
128
        matching_urls = [
1✔
129
            '/api/v1/courses/',
130
            '/api/v1/courses/123',
131
            '/api/v2/courses/456/enrollments',
132
            '/api/v9/courses/test',
133
        ]
134

135
        for url in matching_urls:
1✔
136
            with self.subTest(url=url, should_match=True):
1✔
137
                expiration = self.cache.get_cache_expiration_time(
1✔
138
                    'canvas', url)
139
                self.assertEqual(expiration, ONE_HOUR * 10)
1✔
140

141
        # Test URLs that should NOT match
142
        non_matching_urls = [
1✔
143
            '/api/courses/123',  # missing version
144
            'api/v1/courses/123',  # missing leading slash
145
            '/api/v1/course/123',  # singular "course"
146
            '/api/v/courses/123',  # invalid version format
147
            '/api/v1/courses',  # missing trailing slash
148
            '/api/v1/something/courses/123',  # extra path segment
149
            '/api/v10/courses/test',  # multi-digit (doesn't match \d pattern)
150
            '/api/v999/courses/some-course-id',  # multi-digit version
151
        ]
152

153
        for url in non_matching_urls:
1✔
154
            with self.subTest(url=url, should_match=False):
1✔
155
                expiration = self.cache.get_cache_expiration_time(
1✔
156
                    'canvas', url)
157
                self.assertIsNone(expiration)
1✔
158

159
    def test_inheritance_from_parent_class(self):
1✔
160
        """Test that RestClientsCache properly inherits from
161
           RestclientPymemcacheClient.
162
        """
163
        from memcached_clients import RestclientPymemcacheClient
1✔
164
        self.assertTrue(issubclass(RestClientsCache,
1✔
165
                                   RestclientPymemcacheClient))
166

167
    def test_method_signature(self):
1✔
168
        """Test that the method signature is correct."""
169
        import inspect
1✔
170
        signature = inspect.signature(self.cache.get_cache_expiration_time)
1✔
171

172
        # Check parameter names and defaults
173
        params = signature.parameters
1✔
174
        self.assertIn('service', params)
1✔
175
        self.assertIn('url', params)
1✔
176
        self.assertIn('status', params)
1✔
177
        self.assertEqual(params['status'].default, 200)
1✔
178

179

180
# Test that can run even without cache dependencies
181
class CacheConstantsTest(TestCase):
1✔
182
    """Test cache constants that should always be available."""
183

184
    def test_time_constants_defined(self):
1✔
185
        """Test that time constants are properly defined in the module."""
186
        if CACHE_AVAILABLE:
1✔
187
            # If cache is available, test the actual constants
188
            self.assertEqual(ONE_MINUTE, 60)
1✔
189
            self.assertEqual(ONE_HOUR, 60 * 60)
1✔
190
            self.assertEqual(ONE_DAY, 60 * 60 * 24)
1✔
191
            self.assertEqual(ONE_WEEK, 60 * 60 * 24 * 7)
1✔
192
            self.assertEqual(ONE_MONTH, 60 * 60 * 24 * 30)
1✔
193
        else:
194
            # If cache isn't available, just verify the module can be analyzed
NEW
195
            import ast
×
NEW
196
            import os
×
197

NEW
198
            cache_file = os.path.join(os.path.dirname(__file__), '..',
×
199
                                      'cache.py')
NEW
200
            with open(cache_file, 'r') as f:
×
NEW
201
                content = f.read()
×
202

203
            # Parse the AST to verify constants are defined
NEW
204
            tree = ast.parse(content)
×
NEW
205
            assignments = [node for node in ast.walk(tree) if isinstance(
×
206
                node, ast.Assign)]
NEW
207
            constant_names = []
×
NEW
208
            for assign in assignments:
×
NEW
209
                for target in assign.targets:
×
NEW
210
                    if isinstance(target, ast.Name):
×
NEW
211
                        constant_names.append(target.id)
×
212

NEW
213
            expected_constants = ['ONE_MINUTE', 'ONE_HOUR', 'ONE_DAY',
×
214
                                  'ONE_WEEK', 'ONE_MONTH']
NEW
215
            for constant in expected_constants:
×
NEW
216
                self.assertIn(constant, constant_names, f"Constant {constant} "
×
217
                              "should be defined")
218

219
    def test_cache_module_structure(self):
1✔
220
        """Test that the cache module has the expected structure."""
221
        import ast
1✔
222
        import os
1✔
223

224
        cache_file = os.path.join(os.path.dirname(__file__), '..', 'cache.py')
1✔
225
        with open(cache_file, 'r') as f:
1✔
226
            content = f.read()
1✔
227

228
        # Parse AST to verify module structure
229
        tree = ast.parse(content)
1✔
230

231
        # Check for class definition
232
        classes = [node for node in ast.walk(tree) if isinstance(
1✔
233
            node, ast.ClassDef)]
234
        class_names = [cls.name for cls in classes]
1✔
235
        self.assertIn('RestClientsCache', class_names,
1✔
236
                      "RestClientsCache class should be defined")
237

238
        # Check for method definition
239
        methods = []
1✔
240
        for cls in classes:
1✔
241
            if cls.name == 'RestClientsCache':
1✔
242
                methods = [node.name for node in cls.body if isinstance(
1✔
243
                    node, ast.FunctionDef)]
244

245
        self.assertIn('get_cache_expiration_time', methods,
1✔
246
                      "get_cache_expiration_time method should be defined")
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc