]> git.k1024.org Git - pylibacl.git/blob - tests/test_acls.py
Add type: ignore to tests where we force bad behaviour
[pylibacl.git] / tests / test_acls.py
1 #
2 #
3
4 """Unittests for the posix1e module"""
5
6 #  Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop <iustin@k1024.org>
7 #
8 #  This library is free software; you can redistribute it and/or
9 #  modify it under the terms of the GNU Lesser General Public
10 #  License as published by the Free Software Foundation; either
11 #  version 2.1 of the License, or (at your option) any later version.
12 #
13 #  This library is distributed in the hope that it will be useful,
14 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 #  Lesser General Public License for more details.
17 #
18 #  You should have received a copy of the GNU Lesser General Public
19 #  License along with this library; if not, write to the Free Software
20 #  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 #  02110-1301  USA
22
23
24 import unittest
25 import os
26 import tempfile
27 import sys
28 import platform
29 import re
30 import errno
31 import operator
32 import pytest  # type: ignore
33 import contextlib
34 import pathlib
35 import io
36
37 import posix1e
38 from posix1e import *
39
40 TEST_DIR = os.environ.get("TEST_DIR", ".")
41
42 BASIC_ACL_TEXT = "u::rw,g::r,o::-"
43
44 # Permset permission information
45 PERMSETS = {
46   posix1e.ACL_READ: ("read", posix1e.Permset.read),
47   posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
48   posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
49   }
50
51 ALL_TAGS = [
52   (posix1e.ACL_USER, "user"),
53   (posix1e.ACL_GROUP, "group"),
54   (posix1e.ACL_USER_OBJ, "user object"),
55   (posix1e.ACL_GROUP_OBJ, "group object"),
56   (posix1e.ACL_MASK, "mask"),
57   (posix1e.ACL_OTHER, "other"),
58 ]
59
60 ALL_TAG_VALUES = [i[0] for i in ALL_TAGS]
61 ALL_TAG_DESCS = [i[1] for i in ALL_TAGS]
62
63 # Fixtures and helpers
64
65 def ignore_ioerror(errnum, fn, *args, **kwargs):
66     """Call a function while ignoring some IOErrors.
67
68     This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
69     when doing certain operations on an invalid ACL.
70
71     """
72     try:
73         fn(*args, **kwargs)
74     except IOError as err:
75         if err.errno == errnum:
76             return
77         raise
78
79 @pytest.fixture
80 def testdir():
81     """per-test temp dir based in TEST_DIR"""
82     with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
83         yield dname
84
85 def get_file(path):
86     fh, fname = tempfile.mkstemp(".test", "xattr-", path)
87     return fh, fname
88
89 @contextlib.contextmanager
90 def get_file_name(path):
91     fh, fname = get_file(path)
92     os.close(fh)
93     yield fname
94
95 @contextlib.contextmanager
96 def get_file_fd(path):
97     fd = get_file(path)[0]
98     yield fd
99     os.close(fd)
100
101 @contextlib.contextmanager
102 def get_file_object(path):
103     fd = get_file(path)[0]
104     with os.fdopen(fd) as f:
105         yield f
106
107 @contextlib.contextmanager
108 def get_dir(path):
109     yield tempfile.mkdtemp(".test", "xattr-", path)
110
111 def get_symlink(path, dangling=True):
112     """create a symlink"""
113     fh, fname = get_file(path)
114     os.close(fh)
115     if dangling:
116         os.unlink(fname)
117     sname = fname + ".symlink"
118     os.symlink(fname, sname)
119     return fname, sname
120
121 @contextlib.contextmanager
122 def get_valid_symlink(path):
123     yield get_symlink(path, dangling=False)[1]
124
125 @contextlib.contextmanager
126 def get_dangling_symlink(path):
127     yield get_symlink(path, dangling=True)[1]
128
129 @contextlib.contextmanager
130 def get_file_and_symlink(path):
131     yield get_symlink(path, dangling=False)
132
133 @contextlib.contextmanager
134 def get_file_and_fobject(path):
135     fh, fname = get_file(path)
136     with os.fdopen(fh) as fo:
137         yield fname, fo
138
139 # Wrappers that build upon existing values
140
141 def as_wrapper(call, fn, closer=None):
142     @contextlib.contextmanager
143     def f(path):
144         with call(path) as r:
145             val = fn(r)
146             yield val
147             if closer is not None:
148                 closer(val)
149     return f
150
151 def as_bytes(call):
152     return as_wrapper(call, lambda r: r.encode())
153
154 def as_fspath(call):
155     return as_wrapper(call, pathlib.PurePath)
156
157 def as_iostream(call):
158     opener = lambda f: io.open(f, "r")
159     closer = lambda r: r.close()
160     return as_wrapper(call, opener, closer)
161
162 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
163                                   strict=True)
164 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
165                                   strict=False)
166
167 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
168 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
169 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
170 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
171 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
172 require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT")
173
174 # Note: ACLs are valid only for files/directories, not symbolic links
175 # themselves, so we only create valid symlinks.
176 FILE_P = [
177     get_file_name,
178     as_bytes(get_file_name),
179     pytest.param(as_fspath(get_file_name),
180                  marks=[NOT_BEFORE_36, NOT_PYPY]),
181     get_dir,
182     as_bytes(get_dir),
183     pytest.param(as_fspath(get_dir),
184                  marks=[NOT_BEFORE_36, NOT_PYPY]),
185     get_valid_symlink,
186     as_bytes(get_valid_symlink),
187     pytest.param(as_fspath(get_valid_symlink),
188                  marks=[NOT_BEFORE_36, NOT_PYPY]),
189 ]
190
191 FILE_D = [
192     "file name",
193     "file name (bytes)",
194     "file name (path)",
195     "directory",
196     "directory (bytes)",
197     "directory (path)",
198     "file via symlink",
199     "file via symlink (bytes)",
200     "file via symlink (path)",
201 ]
202
203 FD_P = [
204     get_file_fd,
205     get_file_object,
206     as_iostream(get_file_name),
207 ]
208
209 FD_D = [
210     "file FD",
211     "file object",
212     "file io stream",
213 ]
214
215 ALL_P = FILE_P + FD_P
216 ALL_D = FILE_D + FD_D
217
218 @pytest.fixture(params=FILE_P, ids=FILE_D)
219 def file_subject(testdir, request):
220     with request.param(testdir) as value:
221         yield value
222
223 @pytest.fixture(params=FD_P, ids=FD_D)
224 def fd_subject(testdir, request):
225     with request.param(testdir) as value:
226         yield value
227
228 @pytest.fixture(params=ALL_P, ids=ALL_D)
229 def subject(testdir, request):
230     with request.param(testdir) as value:
231         yield value
232
233
234 class TestLoad:
235     """Load/create tests"""
236     def test_from_file(self, testdir):
237         """Test loading ACLs from a file"""
238         _, fname = get_file(testdir)
239         acl1 = posix1e.ACL(file=fname)
240         assert acl1.valid()
241
242     def test_from_dir(self, testdir):
243         """Test loading ACLs from a directory"""
244         with get_dir(testdir) as dname:
245           acl1 = posix1e.ACL(file=dname)
246           acl2 = posix1e.ACL(filedef=dname)
247           assert acl1.valid()
248         # default ACLs might or might not be valid; missing ones are
249         # not valid, so we don't test acl2 for validity
250
251     def test_from_fd(self, testdir):
252         """Test loading ACLs from a file descriptor"""
253         fd, _ = get_file(testdir)
254         acl1 = posix1e.ACL(fd=fd)
255         assert acl1.valid()
256
257     def test_from_nonexisting(self, testdir):
258         _, fname = get_file(testdir)
259         with pytest.raises(IOError):
260             posix1e.ACL(file="fname"+".no-such-file")
261
262     def test_from_invalid_fd(self, testdir):
263         fd, _ = get_file(testdir)
264         os.close(fd)
265         with pytest.raises(IOError):
266             posix1e.ACL(fd=fd)
267
268     def test_from_empty_invalid(self):
269         """Test creating an empty ACL"""
270         acl1 = posix1e.ACL()
271         assert not acl1.valid()
272
273     def test_from_text(self):
274         """Test creating an ACL from text"""
275         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
276         assert acl1.valid()
277
278     def test_from_acl(self):
279         """Test creating an ACL from an existing ACL"""
280         acl1 = posix1e.ACL()
281         acl2 = posix1e.ACL(acl=acl1)
282         assert acl1 == acl2
283
284     def test_invalid_creation_params(self, testdir):
285         """Test that creating an ACL from multiple objects fails"""
286         fd, _ = get_file(testdir)
287         with pytest.raises(ValueError):
288           posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
289
290     def test_invalid_value_creation(self):
291         """Test that creating an ACL from wrong specification fails"""
292         with pytest.raises(EnvironmentError):
293           posix1e.ACL(text="foobar")
294         with pytest.raises(TypeError):
295           posix1e.ACL(foo="bar")
296
297     def test_uninit(self):
298         """Checks that uninit is actually empty init"""
299         acl = posix1e.ACL.__new__(posix1e.ACL)
300         assert not acl.valid()
301         e = acl.append()
302         e.permset
303         acl.delete_entry(e)
304
305     def test_double_init(self):
306         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
307         assert acl1.valid()
308         acl1.__init__(text=BASIC_ACL_TEXT) # type: ignore
309         assert acl1.valid()
310
311     @pytest.mark.xfail(reason="Unreliable test, re-init doesn't always invalidate children")
312     def test_double_init_breaks_children(self):
313         acl = posix1e.ACL()
314         e = acl.append()
315         e.permset.write = True
316         acl.__init__() # type: ignore
317         with pytest.raises(EnvironmentError):
318             e.permset.write = False
319
320
321 class TestAclExtensions:
322     """ACL extensions checks"""
323
324     @require_acl_from_mode
325     def test_from_mode(self):
326         """Test loading ACLs from an octal mode"""
327         acl1 = posix1e.ACL(mode=0o644)
328         assert acl1.valid()
329
330     @require_acl_check
331     def test_acl_check(self):
332         """Test the acl_check method"""
333         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
334         assert not acl1.check()
335         acl2 = posix1e.ACL()
336         c = acl2.check()
337         assert c == (ACL_MISS_ERROR, 0)
338         assert isinstance(c, tuple)
339         assert c[0] == ACL_MISS_ERROR
340         e = acl2.append()
341         c = acl2.check()
342         assert c == (ACL_ENTRY_ERROR, 0)
343
344     def test_applyto(self, subject):
345         """Test the apply_to function"""
346         # TODO: add read/compare with before, once ACL can be init'ed
347         # from any source.
348         basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
349         basic_acl.applyto(subject)
350         enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
351         assert enhanced_acl.valid()
352         enhanced_acl.applyto(subject)
353
354     def test_apply_to_with_wrong_object(self):
355         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
356         assert acl1.valid()
357         with pytest.raises(TypeError):
358           acl1.applyto(object())
359         with pytest.raises(TypeError):
360           acl1.applyto(object(), object()) # type: ignore
361
362     def test_apply_to_fail(self, testdir):
363         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
364         assert acl1.valid()
365         fd, fname = get_file(testdir)
366         os.close(fd)
367         with pytest.raises(IOError):
368           acl1.applyto(fd)
369         with pytest.raises(IOError, match="no-such-file"):
370           acl1.applyto(fname+".no-such-file")
371
372     @require_extended_check
373     def test_applyto_extended(self, subject):
374         """Test the acl_extended function"""
375         basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
376         basic_acl.applyto(subject)
377         assert not has_extended(subject)
378         enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
379         assert enhanced_acl.valid()
380         enhanced_acl.applyto(subject)
381         assert has_extended(subject)
382
383     @require_extended_check
384     @pytest.mark.parametrize(
385         "gen", [ get_file_and_symlink, get_file_and_fobject ])
386     def test_applyto_extended_mixed(self, testdir, gen):
387         """Test the acl_extended function"""
388         with gen(testdir) as (a, b):
389             basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
390             basic_acl.applyto(a)
391             for item in a, b:
392                 assert not has_extended(item)
393             enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
394             assert enhanced_acl.valid()
395             enhanced_acl.applyto(b)
396             for item in a, b:
397                 assert has_extended(item)
398
399     @require_extended_check
400     def test_extended_fail(self, testdir):
401         fd, fname = get_file(testdir)
402         os.close(fd)
403         with pytest.raises(IOError):
404           has_extended(fd)
405         with pytest.raises(IOError, match="no-such-file"):
406           has_extended(fname+".no-such-file")
407
408     @require_extended_check
409     def test_extended_arg_handling(self):
410       with pytest.raises(TypeError):
411         has_extended() # type: ignore
412       with pytest.raises(TypeError):
413         has_extended(object()) # type: ignore
414
415     @require_equiv_mode
416     def test_equiv_mode(self):
417         """Test the equiv_mode function"""
418         if HAS_ACL_FROM_MODE:
419             for mode in 0o644, 0o755:
420                 acl = posix1e.ACL(mode=mode)
421                 assert acl.equiv_mode() == mode
422         acl = posix1e.ACL(text="u::rw,g::r,o::r")
423         assert acl.equiv_mode() == 0o644
424         acl = posix1e.ACL(text="u::rx,g::-,o::-")
425         assert acl.equiv_mode() == 0o500
426
427     @require_equiv_mode
428     @pytest.mark.xfail(reason="It seems equiv mode always passes, even for empty ACLs")
429     def test_equiv_mode_invalid(self):
430         """Test equiv_mode on invalid ACLs"""
431         a = posix1e.ACL()
432         with pytest.raises(EnvironmentError):
433             a.equiv_mode()
434
435     @require_acl_check
436     def test_to_any_text(self):
437         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
438         assert b"u::" in \
439           acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
440         assert b"user::" in acl.to_any_text()
441
442     @require_acl_check
443     def test_to_any_text_wrong_args(self):
444         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
445         with pytest.raises(TypeError):
446           acl.to_any_text(foo="bar") # type: ignore
447
448
449     @require_acl_check
450     def test_rich_compare(self):
451         acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
452         acl2 = posix1e.ACL(acl=acl1)
453         acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
454         assert acl1 == acl2
455         assert acl1 != acl3
456         with pytest.raises(TypeError):
457           acl1 < acl2 # type: ignore
458         with pytest.raises(TypeError):
459           acl1 >= acl3 # type: ignore
460         assert acl1 != True # type: ignore
461         assert not (acl1 == 1) # type: ignore
462         with pytest.raises(TypeError):
463           acl1 > True # type: ignore
464
465     @require_acl_entry
466     def test_acl_iterator(self):
467         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
468         for entry in acl:
469             assert entry.parent is acl
470
471     @require_copy_ext
472     def test_acl_copy_ext(self):
473         a = posix1e.ACL(text=BASIC_ACL_TEXT)
474         b = posix1e.ACL()
475         c = posix1e.ACL(acl=b)
476         assert a != b
477         assert b == c
478         state = a.__getstate__()
479         b.__setstate__(state)
480         assert a == b
481         assert b != c
482
483
484 class TestWrite:
485     """Write tests"""
486
487     def test_delete_default(self, testdir):
488         """Test removing the default ACL"""
489         with get_dir(testdir) as dname:
490           posix1e.delete_default(dname)
491
492     def test_delete_default_fail(self, testdir):
493         """Test removing the default ACL"""
494         with get_file_name(testdir) as fname:
495             with pytest.raises(IOError, match="no-such-file"):
496                 posix1e.delete_default(fname+".no-such-file")
497
498     @NOT_PYPY
499     def test_delete_default_wrong_arg(self):
500         with pytest.raises(TypeError):
501           posix1e.delete_default(object()) # type: ignore
502
503     def test_reapply(self, testdir):
504         """Test re-applying an ACL"""
505         fd, fname = get_file(testdir)
506         acl1 = posix1e.ACL(fd=fd)
507         acl1.applyto(fd)
508         acl1.applyto(fname)
509         with get_dir(testdir) as dname:
510           acl2 = posix1e.ACL(file=fname)
511           acl2.applyto(dname)
512
513
514
515 @require_acl_entry
516 class TestModification:
517     """ACL modification tests"""
518
519     def checkRef(self, obj):
520         """Checks if a given obj has a 'sane' refcount"""
521         if platform.python_implementation() == "PyPy":
522             return
523         ref_cnt = sys.getrefcount(obj)
524         # FIXME: hardcoded value for the max ref count... but I've
525         # seen it overflow on bad reference counting, so it's better
526         # to be safe
527         if ref_cnt < 2 or ref_cnt > 1024:
528             pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
529                         ref_cnt)
530
531     def test_str(self):
532         """Test str() of an ACL."""
533         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
534         str_acl = str(acl)
535         self.checkRef(str_acl)
536
537     def test_append(self):
538         """Test append a new Entry to the ACL"""
539         acl = posix1e.ACL()
540         e = acl.append()
541         e.tag_type = posix1e.ACL_OTHER
542         ignore_ioerror(errno.EINVAL, acl.calc_mask)
543         str_format = str(e)
544         self.checkRef(str_format)
545         e2 = acl.append(e)
546         ignore_ioerror(errno.EINVAL, acl.calc_mask)
547         assert not acl.valid()
548
549     def test_wrong_append(self):
550         """Test append a new Entry to the ACL based on wrong object type"""
551         acl = posix1e.ACL()
552         with pytest.raises(TypeError):
553           acl.append(object()) # type: ignore
554
555     @pytest.mark.xfail(reason="Behaviour not conform to specification")
556     def test_append_invalid_source(self):
557         a = posix1e.ACL()
558         b = posix1e.ACL()
559         f = b.append()
560         b.delete_entry(f)
561         with pytest.raises(EnvironmentError):
562             f.permset.write = True
563         with pytest.raises(EnvironmentError):
564             e = a.append(f)
565
566     def test_entry_creation(self):
567         acl = posix1e.ACL()
568         e = posix1e.Entry(acl)
569         ignore_ioerror(errno.EINVAL, acl.calc_mask)
570         str_format = str(e)
571         self.checkRef(str_format)
572
573     def test_entry_failed_creation(self):
574         # Checks for partial initialisation and deletion on error
575         # path.
576         with pytest.raises(TypeError):
577           posix1e.Entry(object()) # type: ignore
578
579     def test_entry_reinitialisations(self):
580         a = posix1e.ACL()
581         b = posix1e.ACL()
582         e = posix1e.Entry(a)
583         e.__init__(a) # type: ignore
584         with pytest.raises(ValueError, match="different parent"):
585             e.__init__(b) # type: ignore
586
587     @NOT_PYPY
588     def test_entry_reinit_leaks_refcount(self):
589         acl = posix1e.ACL()
590         e = acl.append()
591         ref = sys.getrefcount(acl)
592         e.__init__(acl) # type: ignore
593         assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
594
595     def test_delete(self):
596         """Test delete Entry from the ACL"""
597         acl = posix1e.ACL()
598         e = acl.append()
599         e.tag_type = posix1e.ACL_OTHER
600         ignore_ioerror(errno.EINVAL, acl.calc_mask)
601         acl.delete_entry(e)
602         ignore_ioerror(errno.EINVAL, acl.calc_mask)
603
604     def test_double_delete(self):
605         """Test delete Entry from the ACL"""
606         # This is not entirely valid/correct, since the entry object
607         # itself is invalid after the first deletion, so we're
608         # actually testing deleting an invalid object, not a
609         # non-existing entry...
610         acl = posix1e.ACL()
611         e = acl.append()
612         e.tag_type = posix1e.ACL_OTHER
613         ignore_ioerror(errno.EINVAL, acl.calc_mask)
614         acl.delete_entry(e)
615         ignore_ioerror(errno.EINVAL, acl.calc_mask)
616         with pytest.raises(EnvironmentError):
617           acl.delete_entry(e)
618
619     def test_delete_unowned(self):
620         """Test delete Entry from the ACL"""
621         a = posix1e.ACL()
622         b = posix1e.ACL()
623         e = a.append()
624         e.tag_type = posix1e.ACL_OTHER
625         with pytest.raises(ValueError, match="un-owned entry"):
626             b.delete_entry(e)
627
628     # This currently fails as this deletion seems to be accepted :/
629     @pytest.mark.xfail(reason="Entry deletion is unreliable")
630     def testDeleteInvalidEntry(self):
631         """Test delete foreign Entry from the ACL"""
632         acl1 = posix1e.ACL()
633         acl2 = posix1e.ACL()
634         e = acl1.append()
635         e.tag_type = posix1e.ACL_OTHER
636         ignore_ioerror(errno.EINVAL, acl1.calc_mask)
637         with pytest.raises(EnvironmentError):
638           acl2.delete_entry(e)
639
640     def test_delete_invalid_object(self):
641         """Test delete a non-Entry from the ACL"""
642         acl = posix1e.ACL()
643         with pytest.raises(TypeError):
644           acl.delete_entry(object()) # type: ignore
645
646     def test_double_entries(self):
647         """Test double entries"""
648         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
649         assert acl.valid()
650         for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
651                          posix1e.ACL_OTHER):
652             e = acl.append()
653             e.tag_type = tag_type
654             e.permset.clear()
655             assert not acl.valid(), ("ACL containing duplicate entries"
656                                      " should not be valid")
657             acl.delete_entry(e)
658
659     def test_multiple_good_entries(self):
660         """Test multiple valid entries"""
661         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
662         assert acl.valid()
663         for tag_type in (posix1e.ACL_USER,
664                          posix1e.ACL_GROUP):
665             for obj_id in range(5):
666                 e = acl.append()
667                 e.tag_type = tag_type
668                 e.qualifier = obj_id
669                 e.permset.clear()
670                 acl.calc_mask()
671                 assert acl.valid(), ("ACL should be able to hold multiple"
672                                      " user/group entries")
673
674     def test_multiple_bad_entries(self):
675         """Test multiple invalid entries"""
676         for tag_type in (posix1e.ACL_USER,
677                          posix1e.ACL_GROUP):
678             acl = posix1e.ACL(text=BASIC_ACL_TEXT)
679             assert acl.valid()
680             e1 = acl.append()
681             e1.tag_type = tag_type
682             e1.qualifier = 0
683             e1.permset.clear()
684             acl.calc_mask()
685             assert acl.valid(), ("ACL should be able to add a"
686                                  " user/group entry")
687             e2 = acl.append()
688             e2.tag_type = tag_type
689             e2.qualifier = 0
690             e2.permset.clear()
691             ignore_ioerror(errno.EINVAL, acl.calc_mask)
692             assert not acl.valid(), ("ACL should not validate when"
693                                      " containing two duplicate entries")
694             acl.delete_entry(e1)
695             # FreeBSD trips over itself here and can't delete the
696             # entry, even though it still exists.
697             ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
698
699     def test_copy(self):
700         acl = ACL()
701         e1 = acl.append()
702         e1.tag_type = ACL_USER
703         p1 = e1.permset
704         p1.clear()
705         p1.read = True
706         p1.write = True
707         e2 = acl.append()
708         e2.tag_type = ACL_GROUP
709         p2 = e2.permset
710         p2.clear()
711         p2.read = True
712         assert not p2.write
713         e2.copy(e1)
714         assert p2.write
715         assert e1.tag_type == e2.tag_type
716
717     def test_copy_wrong_arg(self):
718         acl = ACL()
719         e = acl.append()
720         with pytest.raises(TypeError):
721           e.copy(object()) # type: ignore
722
723     def test_set_permset(self):
724         acl = ACL()
725         e1 = acl.append()
726         e1.tag_type = ACL_USER
727         p1 = e1.permset
728         p1.clear()
729         p1.read = True
730         p1.write = True
731         e2 = acl.append()
732         e2.tag_type = ACL_GROUP
733         p2 = e2.permset
734         p2.clear()
735         p2.read = True
736         assert not p2.write
737         e2.permset = p1
738         assert e2.permset.write
739         assert e2.tag_type == ACL_GROUP
740
741     def test_set_permset_wrong_arg(self):
742         acl = ACL()
743         e = acl.append()
744         with pytest.raises(TypeError):
745           e.permset = object() # type: ignore
746
747     def test_permset_creation(self):
748         acl = ACL()
749         e = acl.append()
750         p1 = e.permset
751         p2 = Permset(e)
752         #assert p1 == p2
753
754     def test_permset_creation_wrong_arg(self):
755         with pytest.raises(TypeError):
756           Permset(object()) # type: ignore
757
758     def test_permset_reinitialisations(self):
759         a = posix1e.ACL()
760         e = posix1e.Entry(a)
761         f = posix1e.Entry(a)
762         p = e.permset
763         p.__init__(e) # type: ignore
764         with pytest.raises(ValueError, match="different parent"):
765             p.__init__(f) # type: ignore
766
767     @NOT_PYPY
768     def test_permset_reinit_leaks_refcount(self):
769         acl = posix1e.ACL()
770         e = acl.append()
771         p = e.permset
772         ref = sys.getrefcount(e)
773         p.__init__(e) # type: ignore
774         assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..."
775
776     def test_permset(self):
777         """Test permissions"""
778         acl = posix1e.ACL()
779         e = acl.append()
780         ps = e.permset
781         ps.clear()
782         str_ps = str(ps)
783         self.checkRef(str_ps)
784         for perm in PERMSETS:
785             str_ps = str(ps)
786             txt = PERMSETS[perm][0]
787             self.checkRef(str_ps)
788             assert not ps.test(perm), ("Empty permission set should not"
789                                        " have permission '%s'" % txt)
790             ps.add(perm)
791             assert ps.test(perm), ("Permission '%s' should exist"
792                                    " after addition" % txt)
793             str_ps = str(ps)
794             self.checkRef(str_ps)
795             ps.delete(perm)
796             assert not ps.test(perm), ("Permission '%s' should not exist"
797                                        " after deletion" % txt)
798
799     def test_permset_via_accessors(self):
800         """Test permissions"""
801         acl = posix1e.ACL()
802         e = acl.append()
803         ps = e.permset
804         ps.clear()
805         str_ps = str(ps)
806         self.checkRef(str_ps)
807         def getter(perm):
808             return PERMSETS[perm][1].__get__(ps) # type: ignore
809         def setter(parm, value):
810             return PERMSETS[perm][1].__set__(ps, value) # type: ignore
811         for perm in PERMSETS:
812             str_ps = str(ps)
813             self.checkRef(str_ps)
814             txt = PERMSETS[perm][0]
815             assert not getter(perm), ("Empty permission set should not"
816                                       " have permission '%s'" % txt)
817             setter(perm, True)
818             assert ps.test(perm), ("Permission '%s' should exist"
819                                    " after addition" % txt)
820             assert getter(perm), ("Permission '%s' should exist"
821                                   " after addition" % txt)
822             str_ps = str(ps)
823             self.checkRef(str_ps)
824             setter(perm, False)
825             assert not ps.test(perm), ("Permission '%s' should not exist"
826                                        " after deletion" % txt)
827             assert not getter(perm), ("Permission '%s' should not exist"
828                                       " after deletion" % txt)
829
830     def test_permset_invalid_type(self):
831         acl = posix1e.ACL()
832         e = acl.append()
833         ps = e.permset
834         ps.clear()
835         with pytest.raises(TypeError):
836           ps.add("foobar") # type: ignore
837         with pytest.raises(TypeError):
838           ps.delete("foobar") # type: ignore
839         with pytest.raises(TypeError):
840           ps.test("foobar") # type: ignore
841         with pytest.raises(ValueError):
842           ps.write = object() # type: ignore
843
844     def test_qualifier_values(self):
845         """Tests qualifier correct store/retrieval"""
846         acl = posix1e.ACL()
847         e = acl.append()
848         # work around deprecation warnings
849         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
850             qualifier = 1
851             e.tag_type = tag
852             while True:
853                 if tag == posix1e.ACL_USER:
854                     regex = re.compile("user with uid %d" % qualifier)
855                 else:
856                     regex = re.compile("group with gid %d" % qualifier)
857                 try:
858                     e.qualifier = qualifier
859                 except OverflowError:
860                     # reached overflow condition, break
861                     break
862                 assert e.qualifier == qualifier
863                 assert regex.search(str(e)) is not None
864                 qualifier *= 2
865
866     def test_qualifier_overflow(self):
867         """Tests qualifier overflow handling"""
868         acl = posix1e.ACL()
869         e = acl.append()
870         qualifier = sys.maxsize * 2
871         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
872             e.tag_type = tag
873             with pytest.raises(OverflowError):
874                 e.qualifier = qualifier
875
876     def test_negative_qualifier(self):
877         """Tests negative qualifier handling"""
878         # Note: this presumes that uid_t/gid_t in C are unsigned...
879         acl = posix1e.ACL()
880         e = acl.append()
881         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
882             e.tag_type = tag
883             for qualifier in [-10, -5, -1]:
884                 with pytest.raises(OverflowError):
885                     e.qualifier = qualifier
886
887     def test_invalid_qualifier(self):
888         """Tests invalid qualifier handling"""
889         acl = posix1e.ACL()
890         e = acl.append()
891         with pytest.raises(TypeError):
892           e.qualifier = object() # type: ignore
893         with pytest.raises((TypeError, AttributeError)):
894           del e.qualifier
895
896     def test_qualifier_on_wrong_tag(self):
897         """Tests qualifier setting on wrong tag"""
898         acl = posix1e.ACL()
899         e = acl.append()
900         e.tag_type = posix1e.ACL_OTHER
901         with pytest.raises(TypeError):
902           e.qualifier = 1
903         with pytest.raises(TypeError):
904           e.qualifier
905
906     @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
907     def test_tag_types(self, tag):
908         """Tests tag type correct set/get"""
909         acl = posix1e.ACL()
910         e = acl.append()
911         e.tag_type = tag
912         assert e.tag_type == tag
913         # check we can show all tag types without breaking
914         assert str(e)
915
916     @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
917     @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
918     def test_tag_overwrite(self, src_tag, dst_tag):
919         """Tests tag type correct set/get"""
920         acl = posix1e.ACL()
921         e = acl.append()
922         e.tag_type = src_tag
923         assert e.tag_type == src_tag
924         assert str(e)
925         e.tag_type = dst_tag
926         assert e.tag_type == dst_tag
927         assert str(e)
928
929     def test_invalid_tags(self):
930         """Tests tag type incorrect set/get"""
931         acl = posix1e.ACL()
932         e = acl.append()
933         with pytest.raises(TypeError):
934           e.tag_type = object() # type: ignore
935         e.tag_type = posix1e.ACL_USER_OBJ
936         # For some reason, PyPy raises AttributeError. Strange...
937         with pytest.raises((TypeError, AttributeError)):
938           del e.tag_type
939
940     def test_tag_wrong_overwrite(self):
941         acl = posix1e.ACL()
942         e = acl.append()
943         e.tag_type = posix1e.ACL_USER_OBJ
944         tag = max(ALL_TAG_VALUES) + 1
945         with pytest.raises(EnvironmentError):
946           e.tag_type = tag
947         # Check tag is still valid.
948         assert e.tag_type == posix1e.ACL_USER_OBJ
949
950 if __name__ == "__main__":
951     unittest.main()