package handlers_test import ( "compress/gzip" "encoding/json" "net/http" "net/http/httptest" "net/url" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/database" ) // downloadPath is the archive download route of a target. func downloadPath(webhookID, targetID string) string { return "/hook/" + webhookID + "/targets/" + targetID + "/download" } // renameTarget submits the edit form renaming a target to Renamed. func renameTarget( env *sourceTestEnv, webhookID, targetID string, ) *httptest.ResponseRecorder { form := url.Values{} form.Set("name", "Renamed") return submitTargetEdit(env, webhookID, targetID, form) } // TestHandleTargetDownload proves a database target's archive // downloads as a gzipped JSON attachment named for the webhook, the // target and the time, here with no archive file yet, so with no // rows; and that a target of another type has no download. func TestHandleTargetDownload(t *testing.T) { t.Parallel() env := setupSourceTest(t) wh := seedWebhookWithRetention(t, env.db, 7) archive := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase) logTarget := seedTarget(t, env.db, wh.ID, database.TargetTypeLog) w := serveTarget( env, http.MethodGet, downloadPath(wh.ID, archive.ID), nil, ) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) assert.Equal(t, "application/gzip", w.Header().Get("Content-Type")) assert.Regexp(t, `^attachment; filename="archive-seeded-t-database-`+ `\d{8}T\d{6}Z\.json\.gz"$`, w.Header().Get("Content-Disposition"), ) zr, err := gzip.NewReader(w.Body) require.NoError(t, err) var got map[string]json.RawMessage require.NoError(t, json.NewDecoder(zr).Decode(&got)) assert.JSONEq(t, `{"id":"`+archive.ID+`","name":"t-database"}`, string(got["target"]), ) assert.JSONEq(t, `[]`, string(got["archived_events"])) w = serveTarget( env, http.MethodGet, downloadPath(wh.ID, logTarget.ID), nil, ) assert.Equal(t, http.StatusNotFound, w.Code) } // TestHandleTargetDownload_WaitsForRename proves a download reads the // target's names and opens its archive under the lock a rename holds: // started while an edit is renaming the archive, it waits, and is // named for the target's new name. func TestHandleTargetDownload_WaitsForRename(t *testing.T) { t.Parallel() env := setupSourceTest(t) wh := seedWebhookWithRetention(t, env.db, 7) archive := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase) renaming, release := env.archives.BlockNextRename() edited := make(chan *httptest.ResponseRecorder, 1) go func() { edited <- renameTarget(env, wh.ID, archive.ID) }() <-renaming downloaded := make(chan *httptest.ResponseRecorder, 1) go func() { downloaded <- serveTarget( env, http.MethodGet, downloadPath(wh.ID, archive.ID), nil, ) }() select { case <-downloaded: release() t.Fatal("the download did not wait for the rename") case <-time.After(100 * time.Millisecond): } release() require.Equal(t, http.StatusSeeOther, (<-edited).Code) w := <-downloaded require.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Header().Get("Content-Disposition"), "archive-seeded-renamed-", ) } // stalledWriter is a response writer whose first write waits until // resume is closed, closing writing when it starts to wait. type stalledWriter struct { *httptest.ResponseRecorder once sync.Once writing chan struct{} resume chan struct{} } func (s *stalledWriter) Write(b []byte) (int, error) { s.once.Do(func() { close(s.writing) <-s.resume }) return s.ResponseRecorder.Write(b) } // TestHandleTargetDownload_StreamsWithoutTheLock proves a download // lets go of the rename lock once its archive is open: while the // download is stalled writing, an edit can still rename the target. func TestHandleTargetDownload_StreamsWithoutTheLock(t *testing.T) { t.Parallel() env := setupSourceTest(t) wh := seedWebhookWithRetention(t, env.db, 7) archive := seedTarget(t, env.db, wh.ID, database.TargetTypeDatabase) req := httptest.NewRequestWithContext( t.Context(), http.MethodGet, downloadPath(wh.ID, archive.ID), nil, ) for _, c := range env.cookies { req.AddCookie(c) } sw := &stalledWriter{ ResponseRecorder: httptest.NewRecorder(), writing: make(chan struct{}), resume: make(chan struct{}), } downloaded := make(chan struct{}) go func() { targetRouter(env).ServeHTTP(sw, req) close(downloaded) }() <-sw.writing edited := make(chan *httptest.ResponseRecorder, 1) go func() { edited <- renameTarget(env, wh.ID, archive.ID) }() select { case w := <-edited: assert.Equal(t, http.StatusSeeOther, w.Code) case <-time.After(10 * time.Second): t.Error("the rename waited for the download") } close(sw.resume) <-downloaded assert.Equal(t, http.StatusOK, sw.Code) }