1#

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>

3#

4# This program is free software: you can redistribute it and/or modify

5# it under the terms of the GNU Lesser General Public License as published by

6# the Free Software Foundation, either version 3 of the License, or

7# (at your option) any later version.

8#

9# This program is distributed in the hope that it will be useful,

10# but WITHOUT ANY WARRANTY; without even the implied warranty of

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

12# GNU Lesser General Public License for more details.

13#

14# You should have received a copy of the GNU Lesser General Public License

15# along with this program. If not, see <http://www.gnu.org/licenses/>.

16

17 import inspect

18 import os

19 import re

20 from base64 import standard_b64decode, standard_b64encode

21

22 from vcr import VCR

23 from vcr.matchers import body

24

25

26 def _gitlabracadabra_func_path_generator(function):

27 func_file = inspect.getfile(function)

28 method_name = function.__name__ # test_no_create

29 instance_name = function.__self__.__class__.__name__ # TestUser

30 fixture_name = re.sub(r"^Test", r"", instance_name) + "_" + re.sub(r"^test_", r"", method_name) + ".yaml"

31 return os.path.join(

32 os.path.dirname(func_file),

33 "fixtures",

34 fixture_name,

35 )

36

37

38 def _gitlabracadabra_uri_matcher(r1, r2):

39 r1_uri = r1.uri

40 r2_uri = r2.uri

41 # Workaround 'all=True' in API calls

42 # with python-gitlab < 1.8.0

43 # See https://github.com/python-gitlab/python-gitlab/pull/701

44 if r1_uri.endswith("all=True"): 44 ↛ 45 line 44 didn't jump to line 45 because the condition on line 44 was never true

45 r1_uri = r1_uri[0:-9]

46 # Ignore host and port

47 r1_uri = re.sub(r"^https?://[^:/]+(:\d+)?/", "http://localhost/", r1_uri)

48 r2_uri = re.sub(r"^https?://[^:/]+(:\d+)?/", "http://localhost/", r2_uri)

49 return r1_uri == r2_uri

50

51

52 def _gitlabracadabra_body_matcher(r1, r2):

53 if r1.method == "POST" and r1.method == r2.method and r1.url == r2.url:

54 _, _, r1_boundary = r1.headers.get("Content-Type", "").partition("multipart/form-data; boundary=")

55 _, _, r2_boundary = r2.headers.get("Content-Type", "").partition("multipart/form-data; boundary=")

56 return (

57 r1_boundary and r2_boundary and r1.body.split(r1_boundary.encode()) == r2.body.split(r2_boundary.encode())

58 )

59 return body(r1, r2)

60

61

62 def _gitlabracadabra_headers(headers: dict[str, str]) -> dict[str, str]:

63 new_headers: dict[str, str] = {}

64 for header_name, header_value in headers.items():

65 if header_name == "Accept" and header_value != "*/*":

66 header_value = "boilerplate"

67 elif header_name == "Authorization":

68 if header_value.startswith("Basic "):

69 userpass = standard_b64decode(header_value.removeprefix("Basic ")).split(b":")

70 userpass[1] = b"my-pass"

71 basic_value = standard_b64encode(b":".join(userpass)).decode("ascii")

72 header_value = f"Basic {basic_value}"

73 elif header_value.startswith("Bearer "): 73 ↛ 84 line 73 didn't jump to line 84 because the condition on line 73 was always true

74 header_value = "Bearer some-value"

75 elif header_name == "Content-Length":

76 header_value = "42"

77 elif header_name == "Content-Type":

78 if header_value.startswith("multipart/form-data; boundary="):

79 header_value = "multipart/form-data; boundary=some-boundary"

80 elif header_name == "PRIVATE-TOKEN":

81 header_value = "MY-TOKEN"

82 elif header_name in {"Accept-Encoding", "Content-type", "Cookie", "User-Agent"}:

83 continue

84 new_headers[header_name] = header_value

85 return new_headers

86

87

88 def _gitlabracadabra_headers_matcher(r1, r2):

89 r1_headers = _gitlabracadabra_headers(r1.headers)

90 r2_headers = _gitlabracadabra_headers(r2.headers)

91 if r1_headers != r2_headers:

92 msg = f"{r1_headers} != {r2_headers}"

93 raise AssertionError(msg)

94 return r1_headers == r2_headers # compat

95

96

97 my_vcr = VCR(

98 match_on=["method", "gitlabracadabra_uri", "body", "gitlabracadabra_headers"],

99 func_path_generator=_gitlabracadabra_func_path_generator,

100 record_mode="none", # change to 'once' or 'new_episodes'

101 inject_cassette=True,

102 decode_compressed_response=True,

103)

104 my_vcr.register_matcher("gitlabracadabra_uri", _gitlabracadabra_uri_matcher)

105 my_vcr.register_matcher("gitlabracadabra_body", _gitlabracadabra_body_matcher)

106 my_vcr.register_matcher("gitlabracadabra_headers", _gitlabracadabra_headers_matcher)