package slidestest import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "time" ) // exportJobPath finds the status page a started export redirected to: // /d/{sqid}/export/{job}, the job id being rastrillo's own base64url. var exportJobPath = regexp.MustCompile(`/d/[23456789abcdefghjkmnpqrstuvwxyz]{10}/export/[A-Za-z0-9_-]{22}`) // startExport posts the export form and returns the status page it // redirected to. func startExport(t *testing.T, a *testApp, deck string, cookies []*http.Cookie, form url.Values) string { t.Helper() rec := post(t, a, deck+"/export", form, cookies...) if rec.Code != http.StatusSeeOther { t.Fatalf("POST %s/export: status %d, want 303\n%s", deck, rec.Code, rec.Body) } loc := rec.Header().Get("Location") if !exportJobPath.MatchString(loc) { t.Fatalf("POST %s/export: Location = %q, want a job's status page", deck, loc) } return loc } // waitForExport polls the status page's own fragment — the endpoint the // shim polls, so this waits exactly as a browser does — until the job // stops running. Bounded by a context deadline rather than a count of // sleeps, so a slow machine waits longer and a hung job still fails. func waitForExport(t *testing.T, a *testApp, status string, cookies []*http.Cookie) string { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() for { rec := mustGet(t, a, status+"/fragment", http.StatusOK, cookies) body := rec.Body.String() if !strings.Contains(body, "data-poll=") { return body } select { case <-ctx.Done(): t.Fatalf("the export was still running after %v:\n%s", 2*time.Minute, body) case <-time.After(50 * time.Millisecond): } } } // TestExportJobProducesADownload is the whole flow as a person walks // it: the form, the POST, the status page while it works, the fragment // the shim polls, and the file at the end. func TestExportJobProducesADownload(t *testing.T) { a := newApp(t) cookies := signedIn(t, a, allowedAddress) deck := sampleDeck(t, a, cookies) // The viewer offers it, to the person who can read the deck. page := mustGet(t, a, deck, http.StatusOK, cookies).Body.String() if !strings.Contains(page, `href="`+deck+`/export"`) { t.Errorf("the viewer has no export button:\n%s", page) } form := mustGet(t, a, deck+"/export", http.StatusOK, cookies).Body.String() if !strings.Contains(form, `name="notes"`) { t.Errorf("the export form has no with-notes checkbox:\n%s", form) } if !strings.Contains(form, `method="post"`) { t.Errorf("the export form does not post:\n%s", form) } status := startExport(t, a, deck, cookies, url.Values{"notes": {"1"}}) // The status page is a page in its own right, and while the job // runs it says so in a way that works with scripts off. first := mustGet(t, a, status, http.StatusOK, cookies).Body.String() if strings.Contains(first, "data-poll=") && !strings.Contains(first, "http-equiv=\"refresh\"") { t.Errorf("the running status page polls but has no