File size: 5,086 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { describe, it, expect } from 'vitest';
import { isPathWithinScope, writeTargets, checkWriteScope } from '../write-scope';

describe('isPathWithinScope', () => {
  it('allows paths under the scope', () => {
    expect(isPathWithinScope('/.interviews/', '/.interviews/a.md')).toBe(true);
    expect(isPathWithinScope('/.interviews/', '/.interviews/sub/b.md')).toBe(true);
  });

  it('denies paths outside the scope', () => {
    expect(isPathWithinScope('/.interviews/', '/index.html')).toBe(false);
    expect(isPathWithinScope('/.interviews/', '/styles/app.css')).toBe(false);
  });

  it('denies a sibling prefix that is not actually inside the scope dir', () => {
    expect(isPathWithinScope('/.interviews/', '/.interviewsX/a.md')).toBe(false);
  });

  it('rejects any path containing ..', () => {
    expect(isPathWithinScope('/.interviews/', '/.interviews/../index.html')).toBe(false);
    expect(isPathWithinScope('/.interviews/', '/.interviews/../../etc/x')).toBe(false);
  });

  it('normalizes a scope given without a trailing slash', () => {
    expect(isPathWithinScope('/.interviews', '/.interviews/a.md')).toBe(true);
    expect(isPathWithinScope('/.interviews', '/index.html')).toBe(false);
  });

  it('allows the scope directory itself (e.g. mkdir of the scope dir)', () => {
    expect(isPathWithinScope('/.interviews/', '/.interviews')).toBe(true);
    expect(isPathWithinScope('/.interviews', '/.interviews')).toBe(true);
  });
});

describe('writeTargets', () => {
  it('returns the redirect target for cat/echo > and >>', () => {
    expect(writeTargets(['cat', '>', '/index.html'])).toEqual(['/index.html']);
    expect(writeTargets(['echo', 'hi', '>>', '/a.txt'])).toEqual(['/a.txt']);
  });

  it('returns the first path arg for ss (skipping flags)', () => {
    expect(writeTargets(['ss', '/foo.md'])).toEqual(['/foo.md']);
    expect(writeTargets(['ss', '--entity', '/foo.md'])).toEqual(['/foo.md']);
  });

  it('returns the file arg for sed -i', () => {
    expect(writeTargets(['sed', '-i', 's/a/b/', '/foo.md'])).toEqual(['/foo.md']);
  });

  it('returns the path for touch and mkdir', () => {
    expect(writeTargets(['touch', '/foo.md'])).toEqual(['/foo.md']);
    expect(writeTargets(['mkdir', '-p', '/a/b'])).toEqual(['/a/b']);
  });

  it('returns the destination for cp and mv', () => {
    expect(writeTargets(['cp', '/src.md', '/dst.md'])).toEqual(['/dst.md']);
    expect(writeTargets(['mv', '/src.md', '/dst.md'])).toEqual(['/dst.md']);
  });

  it('returns the --out target for generate-image, else the default /.generated/', () => {
    expect(writeTargets(['generate-image', 'a dragon', '--out', '/images/d.png'])).toEqual(['/images/d.png']);
    expect(writeTargets(['generate-image', 'a dragon', '-o', '/images/d.png'])).toEqual(['/images/d.png']);
    expect(writeTargets(['generate-image', 'a dragon'])).toEqual(['/.generated/']);
  });

  it('normalizes a relative target to an absolute path', () => {
    expect(writeTargets(['cat', '>', 'feature.md'])).toEqual(['/feature.md']);
  });

  it('returns [] for read commands', () => {
    expect(writeTargets(['cat', '/foo.md'])).toEqual([]);
    expect(writeTargets(['rg', 'pattern', '/'])).toEqual([]);
  });

  it('returns null (fail-closed) for a write whose target cannot be determined', () => {
    expect(writeTargets(['cat', '>'])).toBeNull();
    expect(writeTargets(['ss'])).toBeNull();
  });
});

describe('checkWriteScope', () => {
  it('allows everything when no scope is set', () => {
    expect(checkWriteScope(['cat', '>', '/index.html'], undefined).allowed).toBe(true);
  });

  it('allows reads anywhere even under a scope', () => {
    expect(checkWriteScope(['cat', '/index.html'], '/.interviews/').allowed).toBe(true);
    expect(checkWriteScope(['rg', 'x', '/'], '/.interviews/').allowed).toBe(true);
  });

  it('allows writes inside the scope', () => {
    expect(checkWriteScope(['cat', '>', '/.interviews/a.md'], '/.interviews/').allowed).toBe(true);
    expect(checkWriteScope(['ss', '/.interviews/a.md'], '/.interviews/').allowed).toBe(true);
  });

  it('allows mkdir of the scope directory itself', () => {
    expect(checkWriteScope(['mkdir', '-p', '/.interviews'], '/.interviews/').allowed).toBe(true);
  });

  it('denies writes outside the scope, with a reason', () => {
    const r = checkWriteScope(['cat', '>', '/index.html'], '/.interviews/');
    expect(r.allowed).toBe(false);
    expect(r.reason).toContain('/.interviews/');
  });

  it('denies fail-closed when a write target cannot be parsed', () => {
    const r = checkWriteScope(['ss'], '/.interviews/');
    expect(r.allowed).toBe(false);
  });

  it('denies generate-image writing outside the scope (incl. its default /.generated/)', () => {
    expect(checkWriteScope(['generate-image', 'x', '--out', '/images/x.png'], '/.interviews/').allowed).toBe(false);
    expect(checkWriteScope(['generate-image', 'x'], '/.interviews/').allowed).toBe(false);
    expect(checkWriteScope(['generate-image', 'x', '--out', '/.interviews/x.png'], '/.interviews/').allowed).toBe(true);
  });
});