1package resource_test
2
3import (
4	"errors"
5	"os"
6	"path/filepath"
7	"strings"
8
9	fakesys "github.com/cloudfoundry/bosh-utils/system/fakes"
10	. "github.com/onsi/ginkgo"
11	. "github.com/onsi/gomega"
12
13	fakecrypto "github.com/cloudfoundry/bosh-cli/crypto/fakes"
14	. "github.com/cloudfoundry/bosh-cli/release/resource"
15)
16
17var _ = Describe("FingerprinterImpl", func() {
18	var (
19		digestCalculator *fakecrypto.FakeDigestCalculator
20		fs               *fakesys.FakeFileSystem
21		fingerprinter    FingerprinterImpl
22		followSymlinks   bool
23	)
24
25	BeforeEach(func() {
26		digestCalculator = fakecrypto.NewFakeDigestCalculator()
27		fs = fakesys.NewFakeFileSystem()
28	})
29
30	JustBeforeEach(func() {
31		fingerprinter = NewFingerprinterImpl(digestCalculator, fs, followSymlinks)
32	})
33
34	Context("successfully creating a fingerprint", func() {
35		var (
36			chunks []string
37			files  []File
38		)
39
40		BeforeEach(func() {
41			files = []File{
42				NewFile(filepath.Join("/", "tmp", "file2"), filepath.Join("/", "tmp")),
43				NewFile(filepath.Join("/", "tmp", "file1"), filepath.Join("/", "tmp")),
44				NewFile(filepath.Join("/", "tmp", "file3"), filepath.Join("/", "tmp")),
45				NewFile(filepath.Join("/", "tmp", "rel", "file4"), filepath.Join("/", "tmp")),
46			}
47
48			excludeModeFile := NewFile(filepath.Join("/", "tmp", "file5"), filepath.Join("/", "tmp"))
49			excludeModeFile.ExcludeMode = true
50			files = append(files, excludeModeFile)
51
52			basenameFile := NewFile(filepath.Join("/", "tmp", "rel", "file6"), filepath.Join("/", "tmp"))
53			basenameFile.UseBasename = true
54			files = append(files, basenameFile)
55
56			fs.RegisterOpenFile(filepath.Join("/", "tmp", "file1"), &fakesys.FakeFile{
57				Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeDir},
58			})
59
60			fs.RegisterOpenFile(filepath.Join("/", "tmp", "file2"), &fakesys.FakeFile{
61				Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeFile},
62			})
63
64			fs.RegisterOpenFile(filepath.Join("/", "tmp", "file3"), &fakesys.FakeFile{
65				Stats: &fakesys.FakeFileStats{
66					FileType: fakesys.FakeFileTypeFile,
67					FileMode: os.FileMode(0111),
68				},
69			})
70
71			fs.RegisterOpenFile(filepath.Join("/", "tmp", "rel", "file4"), &fakesys.FakeFile{
72				Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeFile},
73			})
74
75			fs.RegisterOpenFile(filepath.Join("/", "tmp", "file5"), &fakesys.FakeFile{
76				Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeFile},
77			})
78
79			fs.RegisterOpenFile(filepath.Join("/", "tmp", "rel", "file6"), &fakesys.FakeFile{
80				Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeFile},
81			})
82
83			digestCalculator.SetCalculateBehavior(map[string]fakecrypto.CalculateInput{
84				// file1 directory is not sha1-ed
85				filepath.Join("/", "tmp", "file2"):        fakecrypto.CalculateInput{DigestStr: "file2-sha1"},
86				filepath.Join("/", "tmp", "file3"):        fakecrypto.CalculateInput{DigestStr: "file3-sha1"},
87				filepath.Join("/", "tmp", "rel", "file4"): fakecrypto.CalculateInput{DigestStr: "file4-sha1"},
88				filepath.Join("/", "tmp", "file5"):        fakecrypto.CalculateInput{DigestStr: "file5-sha1"},
89				filepath.Join("/", "tmp", "rel", "file6"): fakecrypto.CalculateInput{DigestStr: "file6-sha1"},
90			})
91
92			chunks = []string{
93				"v2",             // version
94				"file1", "40755", // dir perms
95				"file2", "file2-sha1", "100644", // regular file perms
96				"file3", "file3-sha1", "100755", // exec file perms
97				"file5", "file5-sha1", // excludes mode
98				"rel/file4", "file4-sha1", "100644", // relative file
99				"file6", "file6-sha1", "100644", // uses basename
100				"chunk1", ",chunk2", // sorted chunks
101			}
102		})
103
104		It("fingerprints all files", func() {
105			digestCalculator.CalculateStringInputs = map[string]string{
106				strings.Join(chunks, ""): "fp",
107			}
108
109			fp, err := fingerprinter.Calculate(files, []string{"chunk2", "chunk1"})
110			Expect(err).ToNot(HaveOccurred())
111			Expect(fp).To(Equal("fp"))
112		})
113
114		It("trims `sha256` algorithm info from resulting fingerprint string", func() {
115			digestCalculator.CalculateStringInputs = map[string]string{
116				strings.Join(chunks, ""): "sha256:asdfasdfasdfasdf",
117			}
118
119			fp, err := fingerprinter.Calculate(files, []string{"chunk2", "chunk1"})
120			Expect(err).ToNot(HaveOccurred())
121			Expect(fp).To(Equal("asdfasdfasdfasdf"))
122		})
123	})
124
125	It("returns an error when the resulting checksum contains unexpected content so it does not pass incompatible fingerprints to director", func() {
126		files := []File{NewFile(filepath.Join("/", "tmp", "file"), filepath.Join("/", "tmp"))}
127		fs.WriteFileString(filepath.Join("/", "tmp", "file"), "stuff")
128
129		digestCalculator.CalculateStringInputs = map[string]string{
130			strings.Join([]string{"v2", "file", "100644"}, ""): "whatTheAlgorithmIsThat!:asdfasdfasdfasdf",
131		}
132
133		_, err := fingerprinter.Calculate(files, []string{})
134
135		Expect(err).To(HaveOccurred())
136		Expect(err.Error()).To(ContainSubstring("Generated fingerprint contains unexpected characters 'whatTheAlgorithmIsThat!:asdfasdfasdfasdf'"))
137	})
138
139	Context("when following symlinks", func() {
140		BeforeEach(func() {
141			followSymlinks = true
142		})
143
144		It("Includes symlink target in fingerprint calculation", func() {
145			files := []File{
146				NewFile(filepath.Join("/", "tmp", "regular"), filepath.Join("/", "tmp")),
147				NewFile(filepath.Join("/", "tmp", "symlink"), filepath.Join("/", "tmp")),
148			}
149
150			fs.WriteFileString(filepath.Join("/", "tmp", "regular"), "")
151			fs.Symlink(filepath.Join("/", "tmp", "regular"), filepath.Join("/", "tmp", "symlink"))
152
153			digestCalculator.SetCalculateBehavior(map[string]fakecrypto.CalculateInput{
154				filepath.Join("/", "tmp", "regular"): fakecrypto.CalculateInput{DigestStr: "regular-sha1"},
155			})
156
157			chunks := []string{
158				"v2", // version
159				"regular", "regular-sha1", "100644",
160				"symlink", "regular-sha1", "100644",
161				"chunk1", ",chunk2", // sorted chunks
162			}
163
164			digestCalculator.CalculateStringInputs = map[string]string{
165				strings.Join(chunks, ""): "fp",
166			}
167
168			fp, err := fingerprinter.Calculate(files, []string{"chunk2", "chunk1"})
169			Expect(err).ToNot(HaveOccurred())
170			Expect(fp).To(Equal("fp"))
171		})
172	})
173
174	Context("when not following symlinks", func() {
175		BeforeEach(func() {
176			followSymlinks = false
177		})
178
179		It("Includes symlink target in fingerprint calculation", func() {
180			files := []File{
181				NewFile(filepath.Join("/", "tmp", "regular"), filepath.Join("/", "tmp")),
182				NewFile(filepath.Join("/", "tmp", "symlink"), filepath.Join("/", "tmp")),
183			}
184
185			fs.WriteFileString(filepath.Join("/", "tmp", "regular"), "")
186			fs.Symlink("nothing", filepath.Join("/", "tmp", "symlink"))
187
188			digestCalculator.SetCalculateBehavior(map[string]fakecrypto.CalculateInput{
189				filepath.Join("/", "tmp", "regular"): fakecrypto.CalculateInput{DigestStr: "regular-sha1"},
190			})
191
192			chunks := []string{
193				"v2", // version
194				"regular", "regular-sha1", "100644",
195				"symlink", "symlink-target-sha1", "symlink",
196				"chunk1", ",chunk2", // sorted chunks
197			}
198
199			digestCalculator.CalculateStringInputs = map[string]string{
200				"nothing":                "symlink-target-sha1",
201				strings.Join(chunks, ""): "fp",
202			}
203
204			fp, err := fingerprinter.Calculate(files, []string{"chunk2", "chunk1"})
205			Expect(err).ToNot(HaveOccurred())
206			Expect(fp).To(Equal("fp"))
207		})
208	})
209
210	It("returns error if stating file fails", func() {
211		fs.RegisterOpenFile(filepath.Join("/", "tmp", "file2"), &fakesys.FakeFile{
212			StatErr: errors.New("fake-err"),
213		})
214
215		_, err := fingerprinter.Calculate([]File{NewFile(filepath.Join("/", "tmp", "file2"), filepath.Join("/", "tmp"))}, nil)
216		Expect(err).To(HaveOccurred())
217		Expect(err.Error()).To(ContainSubstring("fake-err"))
218	})
219
220	It("returns error if calculating file sha1 fails", func() {
221		fs.RegisterOpenFile(filepath.Join("/", "tmp", "file2"), &fakesys.FakeFile{
222			Stats: &fakesys.FakeFileStats{FileType: fakesys.FakeFileTypeFile},
223		})
224
225		digestCalculator.SetCalculateBehavior(map[string]fakecrypto.CalculateInput{
226			filepath.Join("/", "tmp", "file2"): fakecrypto.CalculateInput{Err: errors.New("fake-err")},
227		})
228
229		_, err := fingerprinter.Calculate([]File{NewFile(filepath.Join("/", "tmp", "file2"), filepath.Join("/", "tmp"))}, nil)
230		Expect(err).To(HaveOccurred())
231		Expect(err.Error()).To(ContainSubstring("fake-err"))
232	})
233})
234