Accept "git upload-pack" etc, for future compatibility.
[gitosis.git] / gitosis / test / test_serve.py
1 from nose.tools import eq_ as eq
2 from gitosis.test.util import assert_raises
3
4 import logging
5 import os
6 from cStringIO import StringIO
7 from ConfigParser import RawConfigParser
8
9 from gitosis import serve
10 from gitosis import repository
11
12 from gitosis.test import util
13
14 def test_bad_newLine():
15     cfg = RawConfigParser()
16     e = assert_raises(
17         serve.CommandMayNotContainNewlineError,
18         serve.serve,
19         cfg=cfg,
20         user='jdoe',
21         command='ev\nil',
22         )
23     eq(str(e), 'Command may not contain newline')
24     assert isinstance(e, serve.ServingError)
25
26 def test_bad_dash_noargs():
27     cfg = RawConfigParser()
28     e = assert_raises(
29         serve.UnknownCommandError,
30         serve.serve,
31         cfg=cfg,
32         user='jdoe',
33         command='git-upload-pack',
34         )
35     eq(str(e), 'Unknown command denied')
36     assert isinstance(e, serve.ServingError)
37
38 def test_bad_space_noargs():
39     cfg = RawConfigParser()
40     e = assert_raises(
41         serve.UnknownCommandError,
42         serve.serve,
43         cfg=cfg,
44         user='jdoe',
45         command='git upload-pack',
46         )
47     eq(str(e), 'Unknown command denied')
48     assert isinstance(e, serve.ServingError)
49
50 def test_bad_command():
51     cfg = RawConfigParser()
52     e = assert_raises(
53         serve.UnknownCommandError,
54         serve.serve,
55         cfg=cfg,
56         user='jdoe',
57         command="evil 'foo'",
58         )
59     eq(str(e), 'Unknown command denied')
60     assert isinstance(e, serve.ServingError)
61
62 def test_bad_unsafeArguments_notQuoted():
63     cfg = RawConfigParser()
64     e = assert_raises(
65         serve.UnsafeArgumentsError,
66         serve.serve,
67         cfg=cfg,
68         user='jdoe',
69         command="git-upload-pack foo",
70         )
71     eq(str(e), 'Arguments to command look dangerous')
72     assert isinstance(e, serve.ServingError)
73
74 def test_bad_unsafeArguments_badCharacters():
75     cfg = RawConfigParser()
76     e = assert_raises(
77         serve.UnsafeArgumentsError,
78         serve.serve,
79         cfg=cfg,
80         user='jdoe',
81         command="git-upload-pack 'ev!l'",
82         )
83     eq(str(e), 'Arguments to command look dangerous')
84     assert isinstance(e, serve.ServingError)
85
86 def test_bad_unsafeArguments_dotdot():
87     cfg = RawConfigParser()
88     e = assert_raises(
89         serve.UnsafeArgumentsError,
90         serve.serve,
91         cfg=cfg,
92         user='jdoe',
93         command="git-upload-pack 'something/../evil'",
94         )
95     eq(str(e), 'Arguments to command look dangerous')
96     assert isinstance(e, serve.ServingError)
97
98 def test_bad_forbiddenCommand_read_dash():
99     cfg = RawConfigParser()
100     e = assert_raises(
101         serve.ReadAccessDenied,
102         serve.serve,
103         cfg=cfg,
104         user='jdoe',
105         command="git-upload-pack 'foo'",
106         )
107     eq(str(e), 'Repository read access denied')
108     assert isinstance(e, serve.AccessDenied)
109     assert isinstance(e, serve.ServingError)
110
111 def test_bad_forbiddenCommand_read_space():
112     cfg = RawConfigParser()
113     e = assert_raises(
114         serve.ReadAccessDenied,
115         serve.serve,
116         cfg=cfg,
117         user='jdoe',
118         command="git upload-pack 'foo'",
119         )
120     eq(str(e), 'Repository read access denied')
121     assert isinstance(e, serve.AccessDenied)
122     assert isinstance(e, serve.ServingError)
123
124 def test_bad_forbiddenCommand_write_noAccess_dash():
125     cfg = RawConfigParser()
126     e = assert_raises(
127         serve.ReadAccessDenied,
128         serve.serve,
129         cfg=cfg,
130         user='jdoe',
131         command="git-receive-pack 'foo'",
132         )
133     # error message talks about read in an effort to make it more
134     # obvious that jdoe doesn't have *even* read access
135     eq(str(e), 'Repository read access denied')
136     assert isinstance(e, serve.AccessDenied)
137     assert isinstance(e, serve.ServingError)
138
139 def test_bad_forbiddenCommand_write_noAccess_space():
140     cfg = RawConfigParser()
141     e = assert_raises(
142         serve.ReadAccessDenied,
143         serve.serve,
144         cfg=cfg,
145         user='jdoe',
146         command="git receive-pack 'foo'",
147         )
148     # error message talks about read in an effort to make it more
149     # obvious that jdoe doesn't have *even* read access
150     eq(str(e), 'Repository read access denied')
151     assert isinstance(e, serve.AccessDenied)
152     assert isinstance(e, serve.ServingError)
153
154 def test_bad_forbiddenCommand_write_readAccess_dash():
155     cfg = RawConfigParser()
156     cfg.add_section('group foo')
157     cfg.set('group foo', 'members', 'jdoe')
158     cfg.set('group foo', 'readonly', 'foo')
159     e = assert_raises(
160         serve.WriteAccessDenied,
161         serve.serve,
162         cfg=cfg,
163         user='jdoe',
164         command="git-receive-pack 'foo'",
165         )
166     eq(str(e), 'Repository write access denied')
167     assert isinstance(e, serve.AccessDenied)
168     assert isinstance(e, serve.ServingError)
169
170 def test_bad_forbiddenCommand_write_readAccess_space():
171     cfg = RawConfigParser()
172     cfg.add_section('group foo')
173     cfg.set('group foo', 'members', 'jdoe')
174     cfg.set('group foo', 'readonly', 'foo')
175     e = assert_raises(
176         serve.WriteAccessDenied,
177         serve.serve,
178         cfg=cfg,
179         user='jdoe',
180         command="git receive-pack 'foo'",
181         )
182     eq(str(e), 'Repository write access denied')
183     assert isinstance(e, serve.AccessDenied)
184     assert isinstance(e, serve.ServingError)
185
186 def test_simple_read_dash():
187     tmp = util.maketemp()
188     repository.init(os.path.join(tmp, 'foo.git'))
189     cfg = RawConfigParser()
190     cfg.add_section('gitosis')
191     cfg.set('gitosis', 'repositories', tmp)
192     cfg.add_section('group foo')
193     cfg.set('group foo', 'members', 'jdoe')
194     cfg.set('group foo', 'readonly', 'foo')
195     got = serve.serve(
196         cfg=cfg,
197         user='jdoe',
198         command="git-upload-pack 'foo'",
199         )
200     eq(got, "git-upload-pack '%s/foo.git'" % tmp)
201
202 def test_simple_read_space():
203     tmp = util.maketemp()
204     repository.init(os.path.join(tmp, 'foo.git'))
205     cfg = RawConfigParser()
206     cfg.add_section('gitosis')
207     cfg.set('gitosis', 'repositories', tmp)
208     cfg.add_section('group foo')
209     cfg.set('group foo', 'members', 'jdoe')
210     cfg.set('group foo', 'readonly', 'foo')
211     got = serve.serve(
212         cfg=cfg,
213         user='jdoe',
214         command="git upload-pack 'foo'",
215         )
216     eq(got, "git upload-pack '%s/foo.git'" % tmp)
217
218 def test_simple_write_dash():
219     tmp = util.maketemp()
220     repository.init(os.path.join(tmp, 'foo.git'))
221     cfg = RawConfigParser()
222     cfg.add_section('gitosis')
223     cfg.set('gitosis', 'repositories', tmp)
224     cfg.add_section('group foo')
225     cfg.set('group foo', 'members', 'jdoe')
226     cfg.set('group foo', 'writable', 'foo')
227     got = serve.serve(
228         cfg=cfg,
229         user='jdoe',
230         command="git-receive-pack 'foo'",
231         )
232     eq(got, "git-receive-pack '%s/foo.git'" % tmp)
233
234 def test_simple_write_space():
235     tmp = util.maketemp()
236     repository.init(os.path.join(tmp, 'foo.git'))
237     cfg = RawConfigParser()
238     cfg.add_section('gitosis')
239     cfg.set('gitosis', 'repositories', tmp)
240     cfg.add_section('group foo')
241     cfg.set('group foo', 'members', 'jdoe')
242     cfg.set('group foo', 'writable', 'foo')
243     got = serve.serve(
244         cfg=cfg,
245         user='jdoe',
246         command="git receive-pack 'foo'",
247         )
248     eq(got, "git receive-pack '%s/foo.git'" % tmp)
249
250 def test_push_inits_if_needed():
251     # a push to a non-existent repository (but where config authorizes
252     # you to do that) will create the repository on the fly
253     tmp = util.maketemp()
254     cfg = RawConfigParser()
255     cfg.add_section('gitosis')
256     repositories = os.path.join(tmp, 'repositories')
257     os.mkdir(repositories)
258     cfg.set('gitosis', 'repositories', repositories)
259     generated = os.path.join(tmp, 'generated')
260     os.mkdir(generated)
261     cfg.set('gitosis', 'generate-files-in', generated)
262     cfg.add_section('group foo')
263     cfg.set('group foo', 'members', 'jdoe')
264     cfg.set('group foo', 'writable', 'foo')
265     serve.serve(
266         cfg=cfg,
267         user='jdoe',
268         command="git-receive-pack 'foo'",
269         )
270     eq(os.listdir(repositories), ['foo.git'])
271     assert os.path.isfile(os.path.join(repositories, 'foo.git', 'HEAD'))
272
273 def test_push_inits_if_needed_haveExtension():
274     # a push to a non-existent repository (but where config authorizes
275     # you to do that) will create the repository on the fly
276     tmp = util.maketemp()
277     cfg = RawConfigParser()
278     cfg.add_section('gitosis')
279     repositories = os.path.join(tmp, 'repositories')
280     os.mkdir(repositories)
281     cfg.set('gitosis', 'repositories', repositories)
282     generated = os.path.join(tmp, 'generated')
283     os.mkdir(generated)
284     cfg.set('gitosis', 'generate-files-in', generated)
285     cfg.add_section('group foo')
286     cfg.set('group foo', 'members', 'jdoe')
287     cfg.set('group foo', 'writable', 'foo')
288     serve.serve(
289         cfg=cfg,
290         user='jdoe',
291         command="git-receive-pack 'foo.git'",
292         )
293     eq(os.listdir(repositories), ['foo.git'])
294     assert os.path.isfile(os.path.join(repositories, 'foo.git', 'HEAD'))
295
296 def test_push_inits_subdir_parent_missing():
297     tmp = util.maketemp()
298     cfg = RawConfigParser()
299     cfg.add_section('gitosis')
300     repositories = os.path.join(tmp, 'repositories')
301     os.mkdir(repositories)
302     cfg.set('gitosis', 'repositories', repositories)
303     generated = os.path.join(tmp, 'generated')
304     os.mkdir(generated)
305     cfg.set('gitosis', 'generate-files-in', generated)
306     cfg.add_section('group foo')
307     cfg.set('group foo', 'members', 'jdoe')
308     cfg.set('group foo', 'writable', 'foo/bar')
309     serve.serve(
310         cfg=cfg,
311         user='jdoe',
312         command="git-receive-pack 'foo/bar.git'",
313         )
314     eq(os.listdir(repositories), ['foo'])
315     foo = os.path.join(repositories, 'foo')
316     util.check_mode(foo, 0750, is_dir=True)
317     eq(os.listdir(foo), ['bar.git'])
318     assert os.path.isfile(os.path.join(repositories, 'foo', 'bar.git', 'HEAD'))
319
320 def test_push_inits_subdir_parent_exists():
321     tmp = util.maketemp()
322     cfg = RawConfigParser()
323     cfg.add_section('gitosis')
324     repositories = os.path.join(tmp, 'repositories')
325     os.mkdir(repositories)
326     foo = os.path.join(repositories, 'foo')
327     # silly mode on purpose; not to be touched
328     os.mkdir(foo, 0751)
329     cfg.set('gitosis', 'repositories', repositories)
330     generated = os.path.join(tmp, 'generated')
331     os.mkdir(generated)
332     cfg.set('gitosis', 'generate-files-in', generated)
333     cfg.add_section('group foo')
334     cfg.set('group foo', 'members', 'jdoe')
335     cfg.set('group foo', 'writable', 'foo/bar')
336     serve.serve(
337         cfg=cfg,
338         user='jdoe',
339         command="git-receive-pack 'foo/bar.git'",
340         )
341     eq(os.listdir(repositories), ['foo'])
342     util.check_mode(foo, 0751, is_dir=True)
343     eq(os.listdir(foo), ['bar.git'])
344     assert os.path.isfile(os.path.join(repositories, 'foo', 'bar.git', 'HEAD'))
345
346 def test_push_inits_if_needed_existsWithExtension():
347     tmp = util.maketemp()
348     cfg = RawConfigParser()
349     cfg.add_section('gitosis')
350     repositories = os.path.join(tmp, 'repositories')
351     os.mkdir(repositories)
352     cfg.set('gitosis', 'repositories', repositories)
353     cfg.add_section('group foo')
354     cfg.set('group foo', 'members', 'jdoe')
355     cfg.set('group foo', 'writable', 'foo')
356     os.mkdir(os.path.join(repositories, 'foo.git'))
357     serve.serve(
358         cfg=cfg,
359         user='jdoe',
360         command="git-receive-pack 'foo'",
361         )
362     eq(os.listdir(repositories), ['foo.git'])
363     # it should *not* have HEAD here as we just mkdirred it and didn't
364     # create it properly, and the mock repo didn't have anything in
365     # it.. having HEAD implies serve ran git init, which is supposed
366     # to be unnecessary here
367     eq(os.listdir(os.path.join(repositories, 'foo.git')), [])
368
369 def test_push_inits_no_stdout_spam():
370     # git init has a tendency to spew to stdout, and that confuses
371     # e.g. a git push
372     tmp = util.maketemp()
373     cfg = RawConfigParser()
374     cfg.add_section('gitosis')
375     repositories = os.path.join(tmp, 'repositories')
376     os.mkdir(repositories)
377     cfg.set('gitosis', 'repositories', repositories)
378     generated = os.path.join(tmp, 'generated')
379     os.mkdir(generated)
380     cfg.set('gitosis', 'generate-files-in', generated)
381     cfg.add_section('group foo')
382     cfg.set('group foo', 'members', 'jdoe')
383     cfg.set('group foo', 'writable', 'foo')
384     old_stdout = os.dup(1)
385     try:
386         new_stdout = os.tmpfile()
387         os.dup2(new_stdout.fileno(), 1)
388         serve.serve(
389             cfg=cfg,
390             user='jdoe',
391             command="git-receive-pack 'foo'",
392             )
393     finally:
394         os.dup2(old_stdout, 1)
395         os.close(old_stdout)
396     new_stdout.seek(0)
397     got = new_stdout.read()
398     new_stdout.close()
399     eq(got, '')
400     eq(os.listdir(repositories), ['foo.git'])
401     assert os.path.isfile(os.path.join(repositories, 'foo.git', 'HEAD'))
402
403 def test_push_inits_sets_description():
404     tmp = util.maketemp()
405     cfg = RawConfigParser()
406     cfg.add_section('gitosis')
407     repositories = os.path.join(tmp, 'repositories')
408     os.mkdir(repositories)
409     cfg.set('gitosis', 'repositories', repositories)
410     generated = os.path.join(tmp, 'generated')
411     os.mkdir(generated)
412     cfg.set('gitosis', 'generate-files-in', generated)
413     cfg.add_section('group foo')
414     cfg.set('group foo', 'members', 'jdoe')
415     cfg.set('group foo', 'writable', 'foo')
416     cfg.add_section('repo foo')
417     cfg.set('repo foo', 'description', 'foodesc')
418     serve.serve(
419         cfg=cfg,
420         user='jdoe',
421         command="git-receive-pack 'foo'",
422         )
423     eq(os.listdir(repositories), ['foo.git'])
424     path = os.path.join(repositories, 'foo.git', 'description')
425     assert os.path.exists(path)
426     got = util.readFile(path)
427     eq(got, 'foodesc\n')
428
429 def test_push_inits_updates_projects_list():
430     tmp = util.maketemp()
431     cfg = RawConfigParser()
432     cfg.add_section('gitosis')
433     repositories = os.path.join(tmp, 'repositories')
434     os.mkdir(repositories)
435     cfg.set('gitosis', 'repositories', repositories)
436     generated = os.path.join(tmp, 'generated')
437     os.mkdir(generated)
438     cfg.set('gitosis', 'generate-files-in', generated)
439     cfg.add_section('group foo')
440     cfg.set('group foo', 'members', 'jdoe')
441     cfg.set('group foo', 'writable', 'foo')
442     cfg.add_section('repo foo')
443     cfg.set('repo foo', 'gitweb', 'yes')
444     os.mkdir(os.path.join(repositories, 'gitosis-admin.git'))
445     serve.serve(
446         cfg=cfg,
447         user='jdoe',
448         command="git-receive-pack 'foo'",
449         )
450     eq(
451         sorted(os.listdir(repositories)),
452         sorted(['foo.git', 'gitosis-admin.git']),
453         )
454     path = os.path.join(generated, 'projects.list')
455     assert os.path.exists(path)
456     got = util.readFile(path)
457     eq(got, 'foo.git\n')
458
459 def test_push_inits_sets_export_ok():
460     tmp = util.maketemp()
461     cfg = RawConfigParser()
462     cfg.add_section('gitosis')
463     repositories = os.path.join(tmp, 'repositories')
464     os.mkdir(repositories)
465     cfg.set('gitosis', 'repositories', repositories)
466     generated = os.path.join(tmp, 'generated')
467     os.mkdir(generated)
468     cfg.set('gitosis', 'generate-files-in', generated)
469     cfg.add_section('group foo')
470     cfg.set('group foo', 'members', 'jdoe')
471     cfg.set('group foo', 'writable', 'foo')
472     cfg.add_section('repo foo')
473     cfg.set('repo foo', 'daemon', 'yes')
474     serve.serve(
475         cfg=cfg,
476         user='jdoe',
477         command="git-receive-pack 'foo'",
478         )
479     eq(os.listdir(repositories), ['foo.git'])
480     path = os.path.join(repositories, 'foo.git', 'git-daemon-export-ok')
481     assert os.path.exists(path)
482
483 def test_absolute():
484     # as the only convenient way to use non-standard SSH ports with
485     # git is via the ssh://user@host:port/path syntax, and that syntax
486     # forces absolute urls, just force convert absolute paths to
487     # relative paths; you'll never really want absolute paths via
488     # gitosis, anyway.
489     tmp = util.maketemp()
490     repository.init(os.path.join(tmp, 'foo.git'))
491     cfg = RawConfigParser()
492     cfg.add_section('gitosis')
493     cfg.set('gitosis', 'repositories', tmp)
494     cfg.add_section('group foo')
495     cfg.set('group foo', 'members', 'jdoe')
496     cfg.set('group foo', 'readonly', 'foo')
497     got = serve.serve(
498         cfg=cfg,
499         user='jdoe',
500         command="git-upload-pack '/foo'",
501         )
502     eq(got, "git-upload-pack '%s/foo.git'" % tmp)
503
504 def test_typo_writeable():
505     tmp = util.maketemp()
506     repository.init(os.path.join(tmp, 'foo.git'))
507     cfg = RawConfigParser()
508     cfg.add_section('gitosis')
509     cfg.set('gitosis', 'repositories', tmp)
510     cfg.add_section('group foo')
511     cfg.set('group foo', 'members', 'jdoe')
512     cfg.set('group foo', 'writeable', 'foo')
513     log = logging.getLogger('gitosis.serve')
514     buf = StringIO()
515     handler = logging.StreamHandler(buf)
516     log.addHandler(handler)
517     try:
518         got = serve.serve(
519             cfg=cfg,
520             user='jdoe',
521             command="git-receive-pack 'foo'",
522             )
523     finally:
524         log.removeHandler(handler)
525     eq(got, "git-receive-pack '%s/foo.git'" % tmp)
526     handler.flush()
527     eq(
528         buf.getvalue(),
529         "Repository 'foo' config has typo \"writeable\", shou"
530         +"ld be \"writable\"\n",
531         )