diff --git a/concat_test.go b/concat_test.go index 0a1a108..f8e31e8 100644 --- a/concat_test.go +++ b/concat_test.go @@ -4,200 +4,205 @@ import ( "net/http" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" . "github.com/tus/tusd" ) -type concatPartialStore struct { - t *assert.Assertions - zeroStore -} +func TestConcat(t *testing.T) { + SubTest(t, "ExtensionDiscovery", func(t *testing.T, store *MockFullDataStore) { + composer := NewStoreComposer() + composer.UseCore(store) + composer.UseConcater(store) -func (s concatPartialStore) NewUpload(info FileInfo) (string, error) { - s.t.True(info.IsPartial) - s.t.False(info.IsFinal) - s.t.Nil(info.PartialUploads) + handler, _ := NewHandler(Config{ + StoreComposer: composer, + }) - return "foo", nil -} - -func (s concatPartialStore) GetInfo(id string) (FileInfo, error) { - return FileInfo{ - IsPartial: true, - }, nil -} - -func (s concatPartialStore) ConcatUploads(id string, uploads []string) error { - return nil -} - -func TestConcatPartial(t *testing.T) { - handler, _ := NewHandler(Config{ - MaxSize: 400, - BasePath: "files", - DataStore: concatPartialStore{ - t: assert.New(t), - }, + (&httpTest{ + Method: "OPTIONS", + Code: http.StatusOK, + ResHeader: map[string]string{ + "Tus-Extension": "creation,creation-with-upload,concatenation", + }, + }).Run(handler, t) }) - (&httpTest{ - Name: "Successful OPTIONS request", - Method: "OPTIONS", - URL: "", - ResHeader: map[string]string{ - "Tus-Extension": "creation,creation-with-upload,concatenation", - }, - Code: http.StatusOK, - }).Run(handler, t) + SubTest(t, "Partial", func(t *testing.T, store *MockFullDataStore) { + SubTest(t, "Create", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + IsPartial: true, + IsFinal: false, + PartialUploads: nil, + MetaData: make(map[string]string), + }).Return("foo", nil) - (&httpTest{ - Name: "Successful POST request", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Concat": "partial", - }, - Code: http.StatusCreated, - }).Run(handler, t) + handler, _ := NewHandler(Config{ + BasePath: "files", + DataStore: store, + }) - (&httpTest{ - Name: "Successful HEAD request", - Method: "HEAD", - URL: "foo", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusOK, - ResHeader: map[string]string{ - "Upload-Concat": "partial", - }, - }).Run(handler, t) -} + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Upload-Concat": "partial", + }, + Code: http.StatusCreated, + }).Run(handler, t) + }) -type concatFinalStore struct { - t *assert.Assertions - zeroStore -} + SubTest(t, "Status", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("foo").Return(FileInfo{ + IsPartial: true, + }, nil) -func (s concatFinalStore) NewUpload(info FileInfo) (string, error) { - s.t.False(info.IsPartial) - s.t.True(info.IsFinal) - s.t.Equal([]string{"a", "b"}, info.PartialUploads) + handler, _ := NewHandler(Config{ + BasePath: "files", + DataStore: store, + }) - return "foo", nil -} - -func (s concatFinalStore) GetInfo(id string) (FileInfo, error) { - if id == "a" || id == "b" { - return FileInfo{ - IsPartial: true, - Size: 5, - Offset: 5, - }, nil - } - - if id == "c" { - return FileInfo{ - IsPartial: true, - Size: 5, - Offset: 3, - }, nil - } - - if id == "foo" { - return FileInfo{ - IsFinal: true, - PartialUploads: []string{"a", "b"}, - Size: 10, - Offset: 10, - }, nil - } - - return FileInfo{}, ErrNotFound -} - -func (s concatFinalStore) ConcatUploads(id string, uploads []string) error { - s.t.Equal("foo", id) - s.t.Equal([]string{"a", "b"}, uploads) - - return nil -} - -func TestConcatFinal(t *testing.T) { - a := assert.New(t) - - handler, _ := NewHandler(Config{ - MaxSize: 400, - BasePath: "files", - DataStore: concatFinalStore{ - t: a, - }, - NotifyCompleteUploads: true, + (&httpTest{ + Method: "HEAD", + URL: "foo", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusOK, + ResHeader: map[string]string{ + "Upload-Concat": "partial", + }, + }).Run(handler, t) + }) }) - c := make(chan FileInfo, 1) - handler.CompleteUploads = c + SubTest(t, "Final", func(t *testing.T, store *MockFullDataStore) { + SubTest(t, "Create", func(t *testing.T, store *MockFullDataStore) { + a := assert.New(t) - (&httpTest{ - Name: "Successful POST request", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Concat": "final; http://tus.io/files/a /files/b/", - }, - Code: http.StatusCreated, - }).Run(handler, t) + gomock.InOrder( + store.EXPECT().GetInfo("a").Return(FileInfo{ + IsPartial: true, + Size: 5, + Offset: 5, + }, nil), + store.EXPECT().GetInfo("b").Return(FileInfo{ + IsPartial: true, + Size: 5, + Offset: 5, + }, nil), + store.EXPECT().NewUpload(FileInfo{ + Size: 10, + IsPartial: false, + IsFinal: true, + PartialUploads: []string{"a", "b"}, + MetaData: make(map[string]string), + }).Return("foo", nil), + store.EXPECT().ConcatUploads("foo", []string{"a", "b"}).Return(nil), + ) - info := <-c - a.Equal("foo", info.ID) - a.Equal(int64(10), info.Size) - a.Equal(int64(10), info.Offset) - a.False(info.IsPartial) - a.True(info.IsFinal) - a.Equal([]string{"a", "b"}, info.PartialUploads) + handler, _ := NewHandler(Config{ + BasePath: "files", + DataStore: store, + NotifyCompleteUploads: true, + }) - (&httpTest{ - Name: "Successful HEAD request", - Method: "HEAD", - URL: "foo", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusOK, - ResHeader: map[string]string{ - "Upload-Concat": "final; http://tus.io/files/a http://tus.io/files/b", - "Upload-Length": "10", - "Upload-Offset": "10", - }, - }).Run(handler, t) + c := make(chan FileInfo, 1) + handler.CompleteUploads = c - (&httpTest{ - Name: "Concatenating non finished upload (id: c)", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Concat": "final; http://tus.io/files/c", - }, - Code: http.StatusBadRequest, - }).Run(handler, t) + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Concat": "final; http://tus.io/files/a /files/b/", + }, + Code: http.StatusCreated, + }).Run(handler, t) - handler, _ = NewHandler(Config{ - MaxSize: 9, - BasePath: "files", - DataStore: concatFinalStore{ - t: assert.New(t), - }, + info := <-c + a.Equal("foo", info.ID) + a.EqualValues(10, info.Size) + a.EqualValues(10, info.Offset) + a.False(info.IsPartial) + a.True(info.IsFinal) + a.Equal([]string{"a", "b"}, info.PartialUploads) + }) + + SubTest(t, "Status", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("foo").Return(FileInfo{ + IsFinal: true, + PartialUploads: []string{"a", "b"}, + Size: 10, + Offset: 10, + }, nil) + + handler, _ := NewHandler(Config{ + BasePath: "files", + DataStore: store, + }) + + (&httpTest{ + Method: "HEAD", + URL: "foo", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusOK, + ResHeader: map[string]string{ + "Upload-Concat": "final; http://tus.io/files/a http://tus.io/files/b", + "Upload-Length": "10", + "Upload-Offset": "10", + }, + }).Run(handler, t) + }) + + SubTest(t, "CreateWithUnfinishedFail", func(t *testing.T, store *MockFullDataStore) { + // This upload is still unfinished (mismatching offset and size) and + // will therefore cause the POST request to fail. + store.EXPECT().GetInfo("c").Return(FileInfo{ + IsPartial: true, + Size: 5, + Offset: 3, + }, nil) + + handler, _ := NewHandler(Config{ + BasePath: "files", + DataStore: store, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Concat": "final; http://tus.io/files/c", + }, + Code: http.StatusBadRequest, + }).Run(handler, t) + }) + + SubTest(t, "CreateExceedingMaxSizeFail", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("huge").Return(FileInfo{ + Size: 1000, + Offset: 1000, + }, nil) + + handler, _ := NewHandler(Config{ + MaxSize: 100, + BasePath: "files", + DataStore: store, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Concat": "final; /files/huge", + }, + Code: http.StatusRequestEntityTooLarge, + }).Run(handler, t) + }) }) - - (&httpTest{ - Name: "Exceeding MaxSize", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Concat": "final; http://tus.io/files/a /files/b/", - }, - Code: http.StatusRequestEntityTooLarge, - }).Run(handler, t) } diff --git a/get_test.go b/get_test.go index 5796dde..4c9ecb5 100644 --- a/get_test.go +++ b/get_test.go @@ -1,37 +1,14 @@ package tusd_test import ( - "io" "net/http" - "os" "strings" "testing" + "github.com/golang/mock/gomock" . "github.com/tus/tusd" ) -type getStore struct { - zeroStore -} - -func (s getStore) GetInfo(id string) (FileInfo, error) { - if id != "yes" { - return FileInfo{}, os.ErrNotExist - } - - return FileInfo{ - Offset: 5, - Size: 20, - MetaData: map[string]string{ - "filename": "file.jpg\"evil", - }, - }, nil -} - -func (s getStore) GetReader(id string) (io.Reader, error) { - return reader, nil -} - type closingStringReader struct { *strings.Reader closed bool @@ -47,23 +24,35 @@ var reader = &closingStringReader{ } func TestGet(t *testing.T) { - handler, _ := NewHandler(Config{ - DataStore: getStore{}, + SubTest(t, "Successful download", func(t *testing.T, store *MockFullDataStore) { + gomock.InOrder( + store.EXPECT().GetInfo("yes").Return(FileInfo{ + Offset: 5, + Size: 20, + MetaData: map[string]string{ + "filename": "file.jpg\"evil", + }, + }, nil), + store.EXPECT().GetReader("yes").Return(reader, nil), + ) + + handler, _ := NewHandler(Config{ + DataStore: store, + }) + + (&httpTest{ + Method: "GET", + URL: "yes", + Code: http.StatusOK, + ResBody: "hello", + ResHeader: map[string]string{ + "Content-Length": "5", + "Content-Disposition": `inline;filename="file.jpg\"evil"`, + }, + }).Run(handler, t) + + if !reader.closed { + t.Error("expected reader to be closed") + } }) - - (&httpTest{ - Name: "Successful download", - Method: "GET", - URL: "yes", - Code: http.StatusOK, - ResBody: "hello", - ResHeader: map[string]string{ - "Content-Length": "5", - "Content-Disposition": `inline;filename="file.jpg\"evil"`, - }, - }).Run(handler, t) - - if !reader.closed { - t.Error("expected reader to be closed") - } } diff --git a/handler_mock_test.go b/handler_mock_test.go new file mode 100644 index 0000000..fd292fd --- /dev/null +++ b/handler_mock_test.go @@ -0,0 +1,96 @@ +// Automatically generated by MockGen. DO NOT EDIT! +// Source: handler_test.go + +package tusd_test + +import ( + io "io" + + gomock "github.com/golang/mock/gomock" + tusd "github.com/tus/tusd" +) + +// Mock of FullDataStore interface +type MockFullDataStore struct { + ctrl *gomock.Controller + recorder *_MockFullDataStoreRecorder +} + +// Recorder for MockFullDataStore (not exported) +type _MockFullDataStoreRecorder struct { + mock *MockFullDataStore +} + +func NewMockFullDataStore(ctrl *gomock.Controller) *MockFullDataStore { + mock := &MockFullDataStore{ctrl: ctrl} + mock.recorder = &_MockFullDataStoreRecorder{mock} + return mock +} + +func (_m *MockFullDataStore) EXPECT() *_MockFullDataStoreRecorder { + return _m.recorder +} + +func (_m *MockFullDataStore) NewUpload(info tusd.FileInfo) (string, error) { + ret := _m.ctrl.Call(_m, "NewUpload", info) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockFullDataStoreRecorder) NewUpload(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "NewUpload", arg0) +} + +func (_m *MockFullDataStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) { + ret := _m.ctrl.Call(_m, "WriteChunk", id, offset, src) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockFullDataStoreRecorder) WriteChunk(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "WriteChunk", arg0, arg1, arg2) +} + +func (_m *MockFullDataStore) GetInfo(id string) (tusd.FileInfo, error) { + ret := _m.ctrl.Call(_m, "GetInfo", id) + ret0, _ := ret[0].(tusd.FileInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockFullDataStoreRecorder) GetInfo(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetInfo", arg0) +} + +func (_m *MockFullDataStore) Terminate(id string) error { + ret := _m.ctrl.Call(_m, "Terminate", id) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockFullDataStoreRecorder) Terminate(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "Terminate", arg0) +} + +func (_m *MockFullDataStore) ConcatUploads(destination string, partialUploads []string) error { + ret := _m.ctrl.Call(_m, "ConcatUploads", destination, partialUploads) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockFullDataStoreRecorder) ConcatUploads(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "ConcatUploads", arg0, arg1) +} + +func (_m *MockFullDataStore) GetReader(id string) (io.Reader, error) { + ret := _m.ctrl.Call(_m, "GetReader", id) + ret0, _ := ret[0].(io.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockFullDataStoreRecorder) GetReader(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetReader", arg0) +} diff --git a/handler_test.go b/handler_test.go index 3a32813..748d325 100644 --- a/handler_test.go +++ b/handler_test.go @@ -1,15 +1,18 @@ package tusd_test import ( + "fmt" "io" + "io/ioutil" "net/http" "net/http/httptest" - "os" + "reflect" "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/golang/mock/gomock" + "github.com/tus/tusd" . "github.com/tus/tusd" ) @@ -26,6 +29,13 @@ func (store zeroStore) GetInfo(id string) (FileInfo, error) { return FileInfo{}, nil } +type FullDataStore interface { + tusd.DataStore + tusd.TerminaterDataStore + tusd.ConcaterDataStore + tusd.GetReaderDataStore +} + type httpTest struct { Name string @@ -41,8 +51,6 @@ type httpTest struct { } func (test *httpTest) Run(handler http.Handler, t *testing.T) *httptest.ResponseRecorder { - t.Logf("'%s' in %s", test.Name, assert.CallerInfo()[1]) - req, _ := http.NewRequest(test.Method, test.URL, test.ReqBody) // Add headers @@ -73,33 +81,61 @@ func (test *httpTest) Run(handler http.Handler, t *testing.T) *httptest.Response return w } -type methodOverrideStore struct { - zeroStore - t *testing.T - called bool +func SubTest(t *testing.T, name string, runTest func(*testing.T, *MockFullDataStore)) { + t.Run(name, func(subT *testing.T) { + //subT.Parallel() + + ctrl := gomock.NewController(subT) + defer ctrl.Finish() + + store := NewMockFullDataStore(ctrl) + + runTest(subT, store) + }) } -func (s methodOverrideStore) GetInfo(id string) (FileInfo, error) { - if id != "yes" { - return FileInfo{}, os.ErrNotExist +type ReaderMatcher struct { + expect string +} + +func NewReaderMatcher(expect string) gomock.Matcher { + return ReaderMatcher{ + expect: expect, + } +} + +func (m ReaderMatcher) Matches(x interface{}) bool { + input, ok := x.(io.Reader) + if !ok { + return false } - return FileInfo{ - Offset: 5, - Size: 20, - }, nil + bytes, err := ioutil.ReadAll(input) + if err != nil { + panic(err) + } + + readStr := string(bytes) + return reflect.DeepEqual(m.expect, readStr) } -func (s *methodOverrideStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) { - s.called = true - - return 5, nil +func (m ReaderMatcher) String() string { + return fmt.Sprintf("reads to %s", m.expect) } func TestMethodOverride(t *testing.T) { - store := &methodOverrideStore{ - t: t, - } + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + store := NewMockFullDataStore(mockCtrl) + + store.EXPECT().GetInfo("yes").Return(FileInfo{ + Offset: 5, + Size: 20, + }, nil) + + store.EXPECT().WriteChunk("yes", int64(5), NewReaderMatcher("hello")).Return(int64(5), nil) + handler, _ := NewHandler(Config{ DataStore: store, }) @@ -120,8 +156,4 @@ func TestMethodOverride(t *testing.T) { "Upload-Offset": "10", }, }).Run(handler, t) - - if !store.called { - t.Fatal("WriteChunk implementation not called") - } } diff --git a/head_test.go b/head_test.go index 90c5554..531149c 100644 --- a/head_test.go +++ b/head_test.go @@ -8,67 +8,65 @@ import ( . "github.com/tus/tusd" ) -type headStore struct { - zeroStore -} - -func (s headStore) GetInfo(id string) (FileInfo, error) { - if id != "yes" { - return FileInfo{}, os.ErrNotExist - } - - return FileInfo{ - Offset: 11, - Size: 44, - MetaData: map[string]string{ - "name": "lunrjs.png", - "type": "image/png", - }, - }, nil -} - func TestHead(t *testing.T) { - handler, _ := NewHandler(Config{ - BasePath: "https://buy.art/", - DataStore: headStore{}, + SubTest(t, "Successful HEAD request", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("yes").Return(FileInfo{ + Offset: 11, + Size: 44, + MetaData: map[string]string{ + "name": "lunrjs.png", + "type": "image/png", + }, + }, nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + }) + + res := (&httpTest{ + Name: "Successful request", + Method: "HEAD", + URL: "yes", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusOK, + ResHeader: map[string]string{ + "Upload-Offset": "11", + "Upload-Length": "44", + "Cache-Control": "no-store", + }, + }).Run(handler, t) + + // Since the order of a map is not guaranteed in Go, we need to be prepared + // for the case, that the order of the metadata may have been changed + if v := res.Header().Get("Upload-Metadata"); v != "name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n" && + v != "type aW1hZ2UvcG5n,name bHVucmpzLnBuZw==" { + t.Errorf("Expected valid metadata (got '%s')", v) + } }) - res := (&httpTest{ - Name: "Successful request", - Method: "HEAD", - URL: "yes", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusOK, - ResHeader: map[string]string{ - "Upload-Offset": "11", - "Upload-Length": "44", - "Cache-Control": "no-store", - }, - }).Run(handler, t) + SubTest(t, "Non-existing file", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("no").Return(FileInfo{}, os.ErrNotExist) - // Since the order of a map is not guaranteed in Go, we need to be prepared - // for the case, that the order of the metadata may have been changed - if v := res.Header().Get("Upload-Metadata"); v != "name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n" && - v != "type aW1hZ2UvcG5n,name bHVucmpzLnBuZw==" { - t.Errorf("Expected valid metadata (got '%s')", v) - } + handler, _ := NewHandler(Config{ + DataStore: store, + }) - res = (&httpTest{ - Name: "Non-existing file", - Method: "HEAD", - URL: "no", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusNotFound, - ResHeader: map[string]string{ - "Content-Length": "0", - }, - }).Run(handler, t) + res := (&httpTest{ + Method: "HEAD", + URL: "no", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusNotFound, + ResHeader: map[string]string{ + "Content-Length": "0", + }, + }).Run(handler, t) - if string(res.Body.Bytes()) != "" { - t.Errorf("Expected empty body for failed HEAD request") - } + if string(res.Body.Bytes()) != "" { + t.Errorf("Expected empty body for failed HEAD request") + } + }) } diff --git a/options_test.go b/options_test.go index 2d89ddf..26febd8 100644 --- a/options_test.go +++ b/options_test.go @@ -9,7 +9,7 @@ import ( func TestOptions(t *testing.T) { store := NewStoreComposer() - store.UseCore(zeroStore{}) + store.UseCore(NewMockFullDataStore(nil)) handler, _ := NewHandler(Config{ StoreComposer: store, MaxSize: 400, diff --git a/post_test.go b/post_test.go index ae0a3d1..580a42e 100644 --- a/post_test.go +++ b/post_test.go @@ -2,228 +2,276 @@ package tusd_test import ( "bytes" - "io" - "io/ioutil" "net/http" "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/golang/mock/gomock" . "github.com/tus/tusd" ) -type postStore struct { - t *assert.Assertions - zeroStore -} - -func (s postStore) NewUpload(info FileInfo) (string, error) { - s.t.Equal(int64(300), info.Size) - - metaData := info.MetaData - s.t.Equal(2, len(metaData)) - s.t.Equal("hello", metaData["foo"]) - s.t.Equal("world", metaData["bar"]) - - return "foo", nil -} - -func (s postStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) { - s.t.Equal(int64(0), offset) - - data, err := ioutil.ReadAll(src) - s.t.Nil(err) - s.t.Equal("hello", string(data)) - - return 5, nil -} - -func (s postStore) ConcatUploads(id string, uploads []string) error { - s.t.True(false, "concatenation should not be attempted") - return nil -} - func TestPost(t *testing.T) { - a := assert.New(t) + SubTest(t, "Create", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{ + "foo": "hello", + "bar": "world", + }, + }).Return("foo", nil) - handler, _ := NewHandler(Config{ - MaxSize: 400, - BasePath: "files", - DataStore: postStore{ - t: a, - }, + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + }, + }).Run(handler, t) }) - (&httpTest{ - Name: "Successful request", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - }, - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "http://tus.io/files/foo", - }, - }).Run(handler, t) + SubTest(t, "CreateExceedingMaxSizeFail", func(t *testing.T, store *MockFullDataStore) { + handler, _ := NewHandler(Config{ + MaxSize: 400, + DataStore: store, + BasePath: "/files/", + }) - (&httpTest{ - Name: "Exceeding MaxSize", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "500", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - }, - Code: http.StatusRequestEntityTooLarge, - }).Run(handler, t) - - (&httpTest{ - Name: "Ignore Forwarded headers", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "X-Forwarded-Host": "foo.com", - "X-Forwarded-Proto": "https", - }, - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "http://tus.io/files/foo", - }, - }).Run(handler, t) - - handler, _ = NewHandler(Config{ - MaxSize: 400, - BasePath: "files", - DataStore: postStore{ - t: a, - }, - RespectForwardedHeaders: true, + (&httpTest{ + Name: "Exceeding MaxSize", + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "500", + "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", + }, + Code: http.StatusRequestEntityTooLarge, + }).Run(handler, t) }) - (&httpTest{ - Name: "Respect X-Forwarded-* headers", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "X-Forwarded-Host": "foo.com", - "X-Forwarded-Proto": "https", - }, - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "https://foo.com/files/foo", - }, - }).Run(handler, t) + SubTest(t, "ForwardHeaders", func(t *testing.T, store *MockFullDataStore) { + SubTest(t, "IgnoreXForwarded", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) - (&httpTest{ - Name: "Respect Forwarded headers", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "X-Forwarded-Host": "bar.com", - "X-Forwarded-Proto": "http", - "Forwarded": "proto=https,host=foo.com", - }, - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "https://foo.com/files/foo", - }, - }).Run(handler, t) + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) - (&httpTest{ - Name: "Filter forwarded protocol", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "X-Forwarded-Proto": "aaa", - "Forwarded": "proto=bbb", - }, - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "http://tus.io/files/foo", - }, - }).Run(handler, t) -} - -func TestPostWithUpload(t *testing.T) { - a := assert.New(t) - - handler, _ := NewHandler(Config{ - MaxSize: 400, - BasePath: "files", - DataStore: postStore{ - t: a, - }, - }) - - (&httpTest{ - Name: "Successful request", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Content-Type": "application/offset+octet-stream", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - }, - ReqBody: strings.NewReader("hello"), - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "http://tus.io/files/foo", - "Upload-Offset": "5", - }, - }).Run(handler, t) - - (&httpTest{ - Name: "Exceeding upload size", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Content-Type": "application/offset+octet-stream", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - }, - ReqBody: bytes.NewReader(make([]byte, 400)), - Code: http.StatusRequestEntityTooLarge, - }).Run(handler, t) - - (&httpTest{ - Name: "Incorrect content type", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "Content-Type": "application/false", - }, - ReqBody: strings.NewReader("hello"), - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Location": "http://tus.io/files/foo", - "Upload-Offset": "", - }, - }).Run(handler, t) - - (&httpTest{ - Name: "Upload and final concatenation", - Method: "POST", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - "Upload-Length": "300", - "Content-Type": "application/offset+octet-stream", - "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", - "Upload-Concat": "final; http://tus.io/files/a http://tus.io/files/b", - }, - ReqBody: strings.NewReader("hello"), - Code: http.StatusForbidden, - }).Run(handler, t) + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "X-Forwarded-Host": "foo.com", + "X-Forwarded-Proto": "https", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + }, + }).Run(handler, t) + }) + + SubTest(t, "RespectXForwarded", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + RespectForwardedHeaders: true, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "X-Forwarded-Host": "foo.com", + "X-Forwarded-Proto": "https", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "https://foo.com/files/foo", + }, + }).Run(handler, t) + }) + + SubTest(t, "RespectForwarded", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + RespectForwardedHeaders: true, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "X-Forwarded-Host": "bar.com", + "X-Forwarded-Proto": "http", + "Forwarded": "proto=https,host=foo.com", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "https://foo.com/files/foo", + }, + }).Run(handler, t) + }) + + SubTest(t, "FilterForwardedProtocol", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + RespectForwardedHeaders: true, + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "X-Forwarded-Proto": "aaa", + "Forwarded": "proto=bbb", + }, + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + }, + }).Run(handler, t) + }) + }) + + SubTest(t, "WithUpload", func(t *testing.T, store *MockFullDataStore) { + SubTest(t, "Create", func(t *testing.T, store *MockFullDataStore) { + gomock.InOrder( + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{ + "foo": "hello", + "bar": "world", + }, + }).Return("foo", nil), + store.EXPECT().WriteChunk("foo", int64(0), NewReaderMatcher("hello")).Return(int64(5), nil), + ) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Content-Type": "application/offset+octet-stream", + "Upload-Metadata": "foo aGVsbG8=, bar d29ybGQ=", + }, + ReqBody: strings.NewReader("hello"), + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + "Upload-Offset": "5", + }, + }).Run(handler, t) + }) + + SubTest(t, "CreateExceedingUploadSize", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Content-Type": "application/offset+octet-stream", + }, + ReqBody: bytes.NewReader(make([]byte, 400)), + Code: http.StatusRequestEntityTooLarge, + }).Run(handler, t) + }) + + SubTest(t, "IncorrectContentType", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().NewUpload(FileInfo{ + Size: 300, + MetaData: map[string]string{}, + }).Return("foo", nil) + + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) + + (&httpTest{ + Name: "Incorrect content type", + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Content-Type": "application/false", + }, + ReqBody: strings.NewReader("hello"), + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Location": "http://tus.io/files/foo", + "Upload-Offset": "", + }, + }).Run(handler, t) + }) + + SubTest(t, "UploadToFinalUpload", func(t *testing.T, store *MockFullDataStore) { + handler, _ := NewHandler(Config{ + DataStore: store, + BasePath: "/files/", + }) + + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + "Upload-Length": "300", + "Content-Type": "application/offset+octet-stream", + "Upload-Concat": "final; http://tus.io/files/a http://tus.io/files/b", + }, + ReqBody: strings.NewReader("hello"), + Code: http.StatusForbidden, + }).Run(handler, t) + }) + }) } diff --git a/terminate_test.go b/terminate_test.go index 55b6293..91f9294 100644 --- a/terminate_test.go +++ b/terminate_test.go @@ -9,75 +9,71 @@ import ( "github.com/stretchr/testify/assert" ) -type terminateStore struct { - t *testing.T - zeroStore -} - -func (s terminateStore) GetInfo(id string) (FileInfo, error) { - return FileInfo{ - ID: id, - Size: 10, - }, nil -} - -func (s terminateStore) Terminate(id string) error { - if id != "foo" { - s.t.Fatal("unexpected id") - } - return nil -} - func TestTerminate(t *testing.T) { - handler, _ := NewHandler(Config{ - DataStore: terminateStore{ - t: t, - }, - NotifyTerminatedUploads: true, + SubTest(t, "ExtensionDiscovery", func(t *testing.T, store *MockFullDataStore) { + composer := NewStoreComposer() + composer.UseCore(store) + composer.UseTerminater(store) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + }) + + (&httpTest{ + Method: "OPTIONS", + Code: http.StatusOK, + ResHeader: map[string]string{ + "Tus-Extension": "creation,creation-with-upload,termination", + }, + }).Run(handler, t) }) - c := make(chan FileInfo, 1) - handler.TerminatedUploads = c + SubTest(t, "Termination", func(t *testing.T, store *MockFullDataStore) { + store.EXPECT().GetInfo("foo").Return(FileInfo{ + ID: "foo", + Size: 10, + }, nil) + store.EXPECT().Terminate("foo").Return(nil) - (&httpTest{ - Name: "Successful OPTIONS request", - Method: "OPTIONS", - URL: "", - ResHeader: map[string]string{ - "Tus-Extension": "creation,creation-with-upload,termination", - }, - Code: http.StatusOK, - }).Run(handler, t) + handler, _ := NewHandler(Config{ + DataStore: store, + NotifyTerminatedUploads: true, + }) - (&httpTest{ - Name: "Successful request", - Method: "DELETE", - URL: "foo", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusNoContent, - }).Run(handler, t) + c := make(chan FileInfo, 1) + handler.TerminatedUploads = c - info := <-c + (&httpTest{ + Method: "DELETE", + URL: "foo", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusNoContent, + }).Run(handler, t) - a := assert.New(t) - a.Equal("foo", info.ID) - a.Equal(int64(10), info.Size) -} + info := <-c -func TestTerminateNotImplemented(t *testing.T) { - handler, _ := NewHandler(Config{ - DataStore: zeroStore{}, + a := assert.New(t) + a.Equal("foo", info.ID) + a.Equal(int64(10), info.Size) }) - (&httpTest{ - Name: "TerminaterDataStore not implemented", - Method: "DELETE", - URL: "foo", - ReqHeader: map[string]string{ - "Tus-Resumable": "1.0.0", - }, - Code: http.StatusMethodNotAllowed, - }).Run(handler, t) + SubTest(t, "NotProvided", func(t *testing.T, store *MockFullDataStore) { + composer := NewStoreComposer() + composer.UseCore(store) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + }) + + (&httpTest{ + Method: "DELETE", + URL: "foo", + ReqHeader: map[string]string{ + "Tus-Resumable": "1.0.0", + }, + Code: http.StatusMethodNotAllowed, + }).Run(handler, t) + }) }