1package testflight_test
2
3import (
4	"encoding/json"
5	"fmt"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"os"
10	"os/exec"
11	"regexp"
12	"runtime"
13	"strconv"
14	"strings"
15	"testing"
16	"time"
17
18	. "github.com/onsi/ginkgo"
19	. "github.com/onsi/gomega"
20
21	"github.com/concourse/concourse/go-concourse/concourse"
22	uuid "github.com/nu7hatch/gouuid"
23	"github.com/onsi/gomega/gexec"
24)
25
26const testflightFlyTarget = "tf"
27const adminFlyTarget = "tf-admin"
28
29const pipelinePrefix = "tf-pipeline"
30const teamName = "testflight"
31
32var flyTarget string
33
34type suiteConfig struct {
35	FlyBin      string `json:"fly"`
36	ATCURL      string `json:"atc_url"`
37	ATCUsername string `json:"atc_username"`
38	ATCPassword string `json:"atc_password"`
39	DownloadCLI bool   `json:"download_cli"`
40}
41
42var (
43	config = suiteConfig{
44		ATCURL:      "http://localhost:8080",
45		ATCUsername: "test",
46		ATCPassword: "test",
47	}
48
49	pipelineName string
50	tmp          string
51)
52
53func TestTestflight(t *testing.T) {
54	RegisterFailHandler(Fail)
55	RunSpecs(t, "TestFlight Suite")
56}
57
58var _ = SynchronizedBeforeSuite(func() []byte {
59	atcURL := os.Getenv("ATC_URL")
60	if atcURL != "" {
61		config.ATCURL = atcURL
62	}
63
64	var err error
65	downloadCLI := os.Getenv("DOWNLOAD_CLI")
66	if downloadCLI != "" {
67		config.DownloadCLI, err = strconv.ParseBool(downloadCLI)
68		Expect(err).ToNot(HaveOccurred())
69	}
70
71	if config.DownloadCLI {
72		config.FlyBin, err = downloadFly(config.ATCURL)
73		Expect(err).ToNot(HaveOccurred())
74	} else {
75		config.FlyBin, err = gexec.Build("github.com/concourse/concourse/fly")
76		Expect(err).ToNot(HaveOccurred())
77	}
78
79	atcUsername := os.Getenv("ATC_USERNAME")
80	if atcUsername != "" {
81		config.ATCUsername = atcUsername
82	}
83
84	atcPassword := os.Getenv("ATC_PASSWORD")
85	if atcPassword != "" {
86		config.ATCPassword = atcPassword
87	}
88
89	payload, err := json.Marshal(config)
90	Expect(err).ToNot(HaveOccurred())
91
92	Eventually(func() *gexec.Session {
93		login := spawnFlyLogin(adminFlyTarget)
94		<-login.Exited
95		return login
96	}, 2*time.Minute, time.Second).Should(gexec.Exit(0))
97
98	fly("-t", adminFlyTarget, "set-team", "--non-interactive", "-n", teamName, "--local-user", config.ATCUsername)
99	wait(spawnFlyLogin(testflightFlyTarget, "-n", teamName), false)
100
101	for _, ps := range flyTable("-t", adminFlyTarget, "pipelines") {
102		name := ps["name"]
103		if strings.HasPrefix(name, pipelinePrefix) {
104			fly("-t", adminFlyTarget, "destroy-pipeline", "-n", "-p", name)
105		}
106	}
107
108	for _, ps := range flyTable("-t", testflightFlyTarget, "pipelines") {
109		name := ps["name"]
110		if strings.HasPrefix(name, pipelinePrefix) {
111			fly("-t", testflightFlyTarget, "destroy-pipeline", "-n", "-p", name)
112		}
113	}
114
115	return payload
116}, func(data []byte) {
117	err := json.Unmarshal(data, &config)
118	Expect(err).ToNot(HaveOccurred())
119})
120
121var _ = SynchronizedAfterSuite(func() {
122}, func() {
123	os.Remove(config.FlyBin)
124})
125
126var _ = BeforeEach(func() {
127	SetDefaultEventuallyTimeout(5 * time.Minute)
128	SetDefaultEventuallyPollingInterval(time.Second)
129	SetDefaultConsistentlyDuration(time.Minute)
130	SetDefaultConsistentlyPollingInterval(time.Second)
131
132	var err error
133	tmp, err = ioutil.TempDir("", "testflight-tmp")
134	Expect(err).ToNot(HaveOccurred())
135
136	flyTarget = testflightFlyTarget
137
138	pipelineName = randomPipelineName()
139})
140
141var _ = AfterEach(func() {
142	Expect(os.RemoveAll(tmp)).To(Succeed())
143
144	fly("destroy-pipeline", "-n", "-p", pipelineName)
145})
146
147func downloadFly(atcUrl string) (string, error) {
148	client := concourse.NewClient(atcUrl, http.DefaultClient, false)
149	readCloser, _, err := client.GetCLIReader("amd64", runtime.GOOS)
150	if err != nil {
151		return "", err
152	}
153	outFile, err := ioutil.TempFile("", "fly")
154	if err != nil {
155		return "", err
156	}
157	defer outFile.Close()
158	_, err = io.Copy(outFile, readCloser)
159	if err != nil {
160		return "", err
161	}
162	err = outFile.Chmod(0755)
163	if err != nil {
164		return "", err
165	}
166	return outFile.Name(), nil
167}
168
169func randomPipelineName() string {
170	guid, err := uuid.NewV4()
171	Expect(err).ToNot(HaveOccurred())
172
173	return fmt.Sprintf("%s-%d-%s", pipelinePrefix, GinkgoParallelNode(), guid)
174}
175
176func fly(argv ...string) *gexec.Session {
177	sess := spawnFly(argv...)
178	wait(sess, false)
179	return sess
180}
181
182func flyIn(dir string, argv ...string) *gexec.Session {
183	sess := spawnFlyIn(dir, argv...)
184	wait(sess, false)
185	return sess
186}
187
188func flyUnsafe(argv ...string) *gexec.Session {
189	sess := spawnFly(argv...)
190	wait(sess, true)
191	return sess
192}
193
194func spawnFlyLogin(target string, args ...string) *gexec.Session {
195	return spawn(config.FlyBin, append([]string{"-t", target, "login", "-c", config.ATCURL, "-u", config.ATCUsername, "-p", config.ATCPassword}, args...)...)
196}
197
198func spawnFly(argv ...string) *gexec.Session {
199	return spawn(config.FlyBin, append([]string{"-t", flyTarget}, argv...)...)
200}
201
202func spawnFlyIn(dir string, argv ...string) *gexec.Session {
203	return spawnIn(dir, config.FlyBin, append([]string{"-t", flyTarget}, argv...)...)
204}
205
206func spawn(argc string, argv ...string) *gexec.Session {
207	By("running: " + argc + " " + strings.Join(argv, " "))
208	cmd := exec.Command(argc, argv...)
209	session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
210	Expect(err).ToNot(HaveOccurred())
211	return session
212}
213
214func spawnIn(dir string, argc string, argv ...string) *gexec.Session {
215	By("running in " + dir + ": " + argc + " " + strings.Join(argv, " "))
216	cmd := exec.Command(argc, argv...)
217	cmd.Dir = dir
218	session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
219	Expect(err).ToNot(HaveOccurred())
220	return session
221}
222
223func wait(session *gexec.Session, allowNonZero bool) {
224	<-session.Exited
225	if !allowNonZero {
226		Expect(session.ExitCode()).To(Equal(0), "Output: "+string(session.Out.Contents()))
227	}
228}
229
230var colSplit = regexp.MustCompile(`\s{2,}`)
231
232func flyTable(argv ...string) []map[string]string {
233	session := spawnFly(append([]string{"--print-table-headers"}, argv...)...)
234	<-session.Exited
235	Expect(session.ExitCode()).To(Equal(0))
236
237	result := []map[string]string{}
238	var headers []string
239
240	rows := strings.Split(string(session.Out.Contents()), "\n")
241	for i, row := range rows {
242		columns := colSplit.Split(strings.TrimSpace(row), -1)
243
244		if i == 0 {
245			headers = columns
246			continue
247		}
248
249		if row == "" {
250			continue
251		}
252
253		result = append(result, map[string]string{})
254
255		Expect(columns).To(HaveLen(len(headers)))
256
257		for j, header := range headers {
258			if header == "" || columns[j] == "" {
259				continue
260			}
261
262			result[i-1][header] = columns[j]
263		}
264	}
265
266	return result
267}
268
269func setAndUnpausePipeline(config string, args ...string) {
270	setPipeline(config, args...)
271	fly("unpause-pipeline", "-p", pipelineName)
272}
273
274func setPipeline(config string, args ...string) {
275	sp := []string{"set-pipeline", "-n", "-p", pipelineName, "-c", config}
276	fly(append(sp, args...)...)
277}
278
279func inPipeline(thing string) string {
280	return pipelineName + "/" + thing
281}
282
283func newMockVersion(resourceName string, tag string) string {
284	guid, err := uuid.NewV4()
285	Expect(err).ToNot(HaveOccurred())
286
287	version := guid.String() + "-" + tag
288
289	fly("check-resource", "-r", inPipeline(resourceName), "-f", "version:"+version)
290
291	return version
292}
293
294func waitForBuildAndWatch(jobName string, buildName ...string) *gexec.Session {
295	args := []string{"watch", "-j", inPipeline(jobName)}
296
297	if len(buildName) > 0 {
298		args = append(args, "-b", buildName[0])
299	}
300
301	keepPollingCheck := regexp.MustCompile("job has no builds|build not found|failed to get build")
302	for {
303		session := spawnFly(args...)
304		<-session.Exited
305
306		if session.ExitCode() == 1 {
307			output := strings.TrimSpace(string(session.Err.Contents()))
308			if keepPollingCheck.MatchString(output) {
309				// build hasn't started yet; keep polling
310				time.Sleep(time.Second)
311				continue
312			}
313		}
314
315		return session
316	}
317}
318
319func withFlyTarget(target string, f func()) {
320	before := flyTarget
321	flyTarget = target
322	f()
323	flyTarget = before
324}
325