package e2e import ( "bytes" "context" "net/http" "net/url" "os/exec" "strconv" "strings" "testing" "github.com/barerepo/server/internal/token" ) // Chapter 22.5: a run attaches its output to a release, and the job token carries the permission. func TestABuildAttachesAFileToARelease(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") run(t, work, "git", "tag", "-a", "v1.0.0", "-m", "first release") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "v1.0.0") jobID, jobToken := takeJob(t, in, "john", "john/johnbot") body := bytes.Repeat([]byte("a compiled binary, more or less\n"), 100) if code := upload(t, in, jobToken, jobID, "v1.0.0", "johnbot-linux-amd64", body); code != http.StatusNoContent { t.Fatalf("the upload answered %d", code) } // The releases page names the file and its size, as releases.html has it. code, _, page := get(t, in.http.URL+"/john/johnbot/releases") if code != http.StatusOK { t.Fatalf("the releases page answered %d", code) } if !strings.Contains(page, "johnbot-linux-amd64") { t.Errorf("the releases page does not name the attached file:\n%s", page) } if !strings.Contains(page, "3.1kb") { t.Errorf("the releases page does not give the file's size:\n%s", page) } // And it downloads, byte for byte, as an attachment and never as a page. url := in.http.URL + "/john/johnbot/release/v1.0.0/johnbot-linux-amd64" resp, err := http.Get(url) if err != nil { t.Fatal(err) } defer resp.Body.Close() got := readAll(t, resp) if got != string(body) { t.Errorf("the download is %d bytes, wanted %d", len(got), len(body)) } if d := resp.Header.Get("Content-Disposition"); !strings.HasPrefix(d, "attachment") { t.Errorf("an attached file is served as %q, which a browser may render", d) } if resp.Header.Get("X-Content-Type-Options") != "nosniff" { t.Error("the download does not say nosniff, so a browser may guess its type") } } // A tag that is not in the repository has no release to attach to. func TestAnArtifactNeedsARealTag(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() in := newInstance(t) john := in.account("john") seed(t, in, john, "john", "johnbot") jobID, jobToken := takeJob(t, in, "john", "john/johnbot") if code := upload(t, in, jobToken, jobID, "v9.9.9", "x", []byte("hi")); code != http.StatusNotFound { t.Errorf("attaching to a tag that is not here answered %d", code) } // A name that would leave the directory is not a file name. if code := upload(t, in, jobToken, jobID, "v1.0.0", "../escape", []byte("hi")); code == http.StatusNoContent { t.Error("a file name with a path in it was accepted") } // A token for the repository that is not this job's token is not enough. 22.5. other, _, err := in.db.CreateToken(ctx, token.Git, "john", "john/johnbot", "a plain git token") if err != nil { t.Fatal(err) } if code := upload(t, in, other, jobID, "v1.0.0", "x", []byte("hi")); code != http.StatusUnauthorized { t.Errorf("a token that is not this job's uploaded anyway: %d", code) } } func upload(t *testing.T, in *instance, token string, jobID int64, tag, file string, body []byte) int { t.Helper() req, err := http.NewRequest(http.MethodPost, in.http.URL+"/runner/artifact", bytes.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Barerepo-Token", token) req.Header.Set("Barerepo-Job", strconv.FormatInt(jobID, 10)) req.Header.Set("Barerepo-Tag", tag) req.Header.Set("Barerepo-File", file) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() return resp.StatusCode } // takeJob makes a runner, queues a job, takes it, and issues the job token the poll would have. func takeJob(t *testing.T, in *instance, account, repo string) (int64, string) { id, jobToken, _ := takeJobAs(t, in, account, repo) return id, jobToken } // takeJobAs also hands back the runner's own token, which is what the log and done calls carry. func takeJobAs(t *testing.T, in *instance, account, repo string) (int64, string, string) { t.Helper() ctx := context.Background() runnerToken, tok, err := in.db.CreateToken(ctx, token.Runner, account, repo, "a runner") if err != nil { t.Fatal(err) } runner, err := in.db.AttachRunner(ctx, tok.ID, repo, "uproar.local", "linux", "amd64", nil) if err != nil { t.Fatal(err) } if _, err := in.db.QueueJob(ctx, repo, "refs/heads/master", "abc", "go build", ""); err != nil { t.Fatal(err) } job, err := in.db.TakeJob(ctx, repo, *runner) if err != nil || job == nil { t.Fatalf("the runner could not take a job: %v", err) } jobToken, _, err := in.db.CreateToken(ctx, token.Git, account, repo, "job "+strconv.FormatInt(job.ID, 10)) if err != nil { t.Fatal(err) } return job.ID, jobToken, runnerToken } // A repository that moves takes its attached files, which are not in git and move with nothing else. func TestARenameCarriesTheAttachedFiles(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") in.account("lisa") work := seed(t, in, john, "john", "johnbot") run(t, work, "git", "tag", "-a", "v1.0.0", "-m", "first release") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "v1.0.0") jobID, jobToken := takeJob(t, in, "john", "john/johnbot") if code := upload(t, in, jobToken, jobID, "v1.0.0", "bot-linux", []byte("binary")); code != http.StatusNoContent { t.Fatalf("the upload answered %d", code) } resp := post(t, in, "john", "/john/johnbot/rename", url.Values{"name": {"ircbot"}}) if resp.StatusCode != http.StatusFound { t.Fatalf("the rename answered %d", resp.StatusCode) } if _, _, page := get(t, in.http.URL+"/john/ircbot/releases"); !strings.Contains(page, "bot-linux") { t.Errorf("the rename left the attached file behind:\n%s", page) } code, _, body := get(t, in.http.URL+"/john/ircbot/release/v1.0.0/bot-linux") if code != http.StatusOK || body != "binary" { t.Errorf("the file does not download at the new name: %d %q", code, body) } // A transfer moves the owner, which is the other half of the path the files live under. resp = post(t, in, "john", "/john/ircbot/transfer", url.Values{"owner": {"lisa"}, "confirm": {"ircbot"}}) if resp.StatusCode != http.StatusFound { t.Fatalf("the transfer answered %d", resp.StatusCode) } if _, _, page := get(t, in.http.URL+"/lisa/ircbot/releases"); !strings.Contains(page, "bot-linux") { t.Errorf("the transfer left the attached file behind:\n%s", page) } }