diff --git a/.gitignore b/.gitignore index 9b06bd83..100deb8b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ debug *.tar.gz build -.idea \ No newline at end of file +.idea +.vscode diff --git a/api/api.go b/api/api.go index cf89e7e8..5376c4c4 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,5 @@ package api -//go:generate moq -out ../mocks/generated_auth_mocks.go -pkg mocks . AuthHandler //go:generate moq -out ../mocks/mocks.go -pkg mocks . DownloadsGenerator import ( diff --git a/api/dimensions.go b/api/dimensions.go index 26f67be8..2c6f832f 100644 --- a/api/dimensions.go +++ b/api/dimensions.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strconv" + "strings" errs "github.com/ONSdigital/dp-dataset-api/apierrors" "github.com/ONSdigital/dp-dataset-api/models" @@ -15,6 +16,13 @@ import ( "github.com/gorilla/mux" ) +const maxIDs = 200 + +// MaxIDs returns the maximum number of IDs acceptable in a list +var MaxIDs = func() int { + return maxIDs +} + func (api *DatasetAPI) getDimensions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) @@ -141,6 +149,26 @@ func getPositiveIntQueryParameter(queryVars url.Values, varKey string, defaultVa return val, nil } +// getQueryParamListValues obtains a list of strings from the provided queryVars, +// by parsing all values with key 'varKey' and splitting the values by commas, if they contain commas. +// Up to maxNumItems values are allowed in total. +func getQueryParamListValues(queryVars url.Values, varKey string, maxNumItems int) (items []string, err error) { + // get query parameters values for the provided key + values, found := queryVars[varKey] + if !found { + return []string{}, nil + } + + // each value may contain a simple value or a list of values, in a comma-separated format + for _, value := range values { + items = append(items, strings.Split(value, ",")...) + if len(items) > maxNumItems { + return []string{}, errs.ErrTooManyQueryParameters + } + } + return items, nil +} + func (api *DatasetAPI) getDimensionOptions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) @@ -157,20 +185,32 @@ func (api *DatasetAPI) getDimensionOptions(w http.ResponseWriter, r *http.Reques state = models.PublishedState } - // get limit from query parameters, or default value - limit, err := getPositiveIntQueryParameter(r.URL.Query(), "limit", api.defaultLimit) + // get list of option IDs that we want to get + ids, err := getQueryParamListValues(r.URL.Query(), "id", MaxIDs()) if err != nil { logData["query_params"] = r.URL.RawQuery - handleDimensionsErr(ctx, w, "failed to obtain limit from request query paramters", err, logData) + handleDimensionsErr(ctx, w, "failed to obtain list of IDs from request query parameters", err, logData) return } - // get offset from query parameters, or default value - offset, err := getPositiveIntQueryParameter(r.URL.Query(), "offset", api.defaultOffset) - if err != nil { - logData["query_params"] = r.URL.RawQuery - handleDimensionsErr(ctx, w, "failed to obtain offset from request query paramters", err, logData) - return + // if no list of IDs is provided, get offset and limit + var offset, limit int + if len(ids) == 0 { + // get limit from query parameters, or default value + limit, err = getPositiveIntQueryParameter(r.URL.Query(), "limit", api.defaultLimit) + if err != nil { + logData["query_params"] = r.URL.RawQuery + handleDimensionsErr(ctx, w, "failed to obtain limit from request query parameters", err, logData) + return + } + + // get offset from query parameters, or default value + offset, err = getPositiveIntQueryParameter(r.URL.Query(), "offset", api.defaultOffset) + if err != nil { + logData["query_params"] = r.URL.RawQuery + handleDimensionsErr(ctx, w, "failed to obtain offset from request query parameters", err, logData) + return + } } // ger version for provided dataset, edition and versionID @@ -187,11 +227,21 @@ func (api *DatasetAPI) getDimensionOptions(w http.ResponseWriter, r *http.Reques return } - // get sorted dimension options, starting at offset index, with a limit on the number of items - results, err := api.dataStore.Backend.GetDimensionOptions(version, dimension, offset, limit) - if err != nil { - handleDimensionsErr(ctx, w, "failed to get a list of dimension options", err, logData) - return + var results *models.DimensionOptionResults + if len(ids) == 0 { + // get sorted dimension options, starting at offset index, with a limit on the number of items + results, err = api.dataStore.Backend.GetDimensionOptions(version, dimension, offset, limit) + if err != nil { + handleDimensionsErr(ctx, w, "failed to get a list of dimension options", err, logData) + return + } + } else { + // get dimension options from the provided list of IDs, sorted by option + results, err = api.dataStore.Backend.GetDimensionOptionsFromIDs(version, dimension, ids) + if err != nil { + handleDimensionsErr(ctx, w, "failed to get a list of dimension options", err, logData) + return + } } // populate links diff --git a/api/dimensions_test.go b/api/dimensions_test.go index 7ad6fa4c..e724b259 100644 --- a/api/dimensions_test.go +++ b/api/dimensions_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "sort" "testing" errs "github.com/ONSdigital/dp-dataset-api/apierrors" @@ -143,7 +144,7 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { // testing DataStore with 5 dimension options mockedDataStore := &storetest.StorerMock{ GetVersionFunc: func(datasetID, edition, version, state string) (*models.Version, error) { - return &models.Version{State: models.AssociatedState}, nil + return &models.Version{State: models.AssociatedState, ID: "v1"}, nil }, GetDimensionOptionsFunc: func(version *models.Version, dimensions string, offset, limit int) (*models.DimensionOptionResults, error) { if offset > 4 { @@ -174,6 +175,26 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { Offset: offset, }, nil }, + GetDimensionOptionsFromIDsFunc: func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { + ret := &models.DimensionOptionResults{TotalCount: 5} + sort.Strings(ids) + for _, id := range ids { + switch id { + case "op1": + ret.Items = append(ret.Items, models.PublicDimensionOption{Option: "op1"}) + case "op2": + ret.Items = append(ret.Items, models.PublicDimensionOption{Option: "op2"}) + case "op3": + ret.Items = append(ret.Items, models.PublicDimensionOption{Option: "op3"}) + case "op4": + ret.Items = append(ret.Items, models.PublicDimensionOption{Option: "op4"}) + case "op5": + ret.Items = append(ret.Items, models.PublicDimensionOption{Option: "op5"}) + } + } + ret.Count = len(ret.Items) + return ret, nil + }, } // permissions mocks @@ -189,11 +210,22 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { } // func to validate expected calls - validateCalls := func() { + validateCalls := func(expectedIDs *[]string) { So(datasetPermissions.Required.Calls, ShouldEqual, 1) So(permissions.Required.Calls, ShouldEqual, 0) So(len(mockedDataStore.GetVersionCalls()), ShouldEqual, 1) - So(len(mockedDataStore.GetDimensionOptionsCalls()), ShouldEqual, 1) + if expectedIDs == nil { + So(mockedDataStore.GetDimensionOptionsCalls(), ShouldHaveLength, 1) + So(mockedDataStore.GetDimensionOptionsFromIDsCalls(), ShouldHaveLength, 0) + So(mockedDataStore.GetDimensionOptionsCalls()[0].Dimension, ShouldEqual, "age") + So(mockedDataStore.GetDimensionOptionsCalls()[0].Version.ID, ShouldEqual, "v1") + } else { + So(mockedDataStore.GetDimensionOptionsCalls(), ShouldHaveLength, 0) + So(mockedDataStore.GetDimensionOptionsFromIDsCalls(), ShouldHaveLength, 1) + So(mockedDataStore.GetDimensionOptionsFromIDsCalls()[0].Dimension, ShouldEqual, "age") + So(mockedDataStore.GetDimensionOptionsFromIDsCalls()[0].Version.ID, ShouldEqual, "v1") + So(mockedDataStore.GetDimensionOptionsFromIDsCalls()[0].Ids, ShouldResemble, *expectedIDs) + } } // expected Links structure for the requested dataset version @@ -229,7 +261,7 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { } So(w.Code, ShouldEqual, http.StatusOK) validateBody(w.Body.Bytes(), expectedResponse) - validateCalls() + validateCalls(nil) }) }) @@ -250,7 +282,7 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { } So(w.Code, ShouldEqual, http.StatusOK) validateBody(w.Body.Bytes(), expectedResponse) - validateCalls() + validateCalls(nil) }) }) @@ -272,7 +304,7 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { } So(w.Code, ShouldEqual, http.StatusOK) validateBody(w.Body.Bytes(), expectedResponse) - validateCalls() + validateCalls(nil) }) }) @@ -296,7 +328,50 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { } So(w.Code, ShouldEqual, http.StatusOK) validateBody(w.Body.Bytes(), expectedResponse) - validateCalls() + validateCalls(nil) + }) + }) + + Convey("When a valid dimension and list of existing IDs is provided in more than one parameter, in comma-separated format", func() { + r := httptest.NewRequest("GET", "http://localhost:22000/datasets/123/editions/2017/versions/1/dimensions/age/options?id=op1,op3&id=op5", nil) + w := call(r) + + Convey("Then the call succeeds with 200 OK code, expected body and calls", func() { + expectedResponse := models.DimensionOptionResults{ + Items: []models.PublicDimensionOption{ + {Option: "op1", Links: expectedLinks}, + {Option: "op3", Links: expectedLinks}, + {Option: "op5", Links: expectedLinks}, + }, + Count: 3, + Offset: 0, + Limit: 0, + TotalCount: 5, + } + So(w.Code, ShouldEqual, http.StatusOK) + validateBody(w.Body.Bytes(), expectedResponse) + validateCalls(&[]string{"op1", "op3", "op5"}) + }) + }) + + Convey("When a valid offset, limit and dimension and list of existing IDs are provided", func() { + r := httptest.NewRequest("GET", "http://localhost:22000/datasets/123/editions/2017/versions/1/dimensions/age/options?id=op1,op3&offset=0&limit=1", nil) + w := call(r) + + Convey("Then the call succeeds with 200 OK code, the list of IDs take precedence (offset and limit are ignored), and the expected body and calls are performed", func() { + expectedResponse := models.DimensionOptionResults{ + Items: []models.PublicDimensionOption{ + {Option: "op1", Links: expectedLinks}, + {Option: "op3", Links: expectedLinks}, + }, + Count: 2, + Offset: 0, + Limit: 0, + TotalCount: 5, + } + So(w.Code, ShouldEqual, http.StatusOK) + validateBody(w.Body.Bytes(), expectedResponse) + validateCalls(&[]string{"op1", "op3"}) }) }) }) @@ -306,6 +381,8 @@ func TestGetDimensionOptionsReturnsOk(t *testing.T) { func TestGetDimensionOptionsReturnsErrors(t *testing.T) { t.Parallel() + MaxIDs = func() int { return 5 } + Convey("Given a set of mocked dependencies", t, func() { // permissions mocks @@ -339,6 +416,16 @@ func TestGetDimensionOptionsReturnsErrors(t *testing.T) { So(datasetPermissions.Required.Calls, ShouldEqual, 1) So(permissions.Required.Calls, ShouldEqual, 0) }) + + Convey("Then providing more IDs than the maximum allowed results in 400 BadRequest response", func() { + r := httptest.NewRequest("GET", "http://localhost:22000/datasets/123/editions/2017/versions/1/dimensions/age/options?id=id1,id2,id3&id=id4,id5,id6", nil) + w := call(r) + + So(w.Code, ShouldEqual, http.StatusBadRequest) + So(w.Body.String(), ShouldContainSubstring, errs.ErrTooManyQueryParameters.Error()) + So(datasetPermissions.Required.Calls, ShouldEqual, 1) + So(permissions.Required.Calls, ShouldEqual, 0) + }) }) Convey("When the version doesn't exist in a request for dimension options, then return not found", t, func() { @@ -387,6 +474,31 @@ func TestGetDimensionOptionsReturnsErrors(t *testing.T) { So(len(mockedDataStore.GetDimensionOptionsCalls()), ShouldEqual, 1) }) + Convey("When an internal error causes failure to retrieve dimension options from IDs, then return internal server error", t, func() { + r := httptest.NewRequest("GET", "http://localhost:22000/datasets/123/editions/2017/versions/1/dimensions/age/options?id=id1", nil) + w := httptest.NewRecorder() + mockedDataStore := &storetest.StorerMock{ + GetVersionFunc: func(datasetID, edition, version, state string) (*models.Version, error) { + return &models.Version{State: models.AssociatedState}, nil + }, + GetDimensionOptionsFromIDsFunc: func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { + return nil, errs.ErrInternalServer + }, + } + + datasetPermissions := getAuthorisationHandlerMock() + permissions := getAuthorisationHandlerMock() + api := GetAPIWithMocks(mockedDataStore, &mocks.DownloadsGeneratorMock{}, datasetPermissions, permissions) + api.Router.ServeHTTP(w, r) + + So(w.Code, ShouldEqual, http.StatusInternalServerError) + So(w.Body.String(), ShouldContainSubstring, errs.ErrInternalServer.Error()) + So(datasetPermissions.Required.Calls, ShouldEqual, 1) + So(permissions.Required.Calls, ShouldEqual, 0) + So(len(mockedDataStore.GetVersionCalls()), ShouldEqual, 1) + So(len(mockedDataStore.GetDimensionOptionsFromIDsCalls()), ShouldEqual, 1) + }) + Convey("When the version has an invalid state return internal server error", t, func() { r := httptest.NewRequest("GET", "http://localhost:22000/datasets/123/editions/2017/versions/1/dimensions/age/options", nil) w := httptest.NewRecorder() diff --git a/apierrors/errors.go b/apierrors/errors.go index 93cbd46a..8c69a6d1 100644 --- a/apierrors/errors.go +++ b/apierrors/errors.go @@ -26,6 +26,7 @@ var ( ErrInternalServer = errors.New("internal error") ErrInsertedObservationsInvalidSyntax = errors.New("inserted observation request parameter not an integer") ErrInvalidQueryParameter = errors.New("invalid query parameter") + ErrTooManyQueryParameters = errors.New("too many query parameters have been provided") ErrMetadataVersionNotFound = errors.New("version not found") ErrMissingJobProperties = errors.New("missing job properties") ErrMissingParameters = errors.New("missing properties in JSON") @@ -63,6 +64,7 @@ var ( BadRequestMap = map[error]bool{ ErrInsertedObservationsInvalidSyntax: true, ErrInvalidQueryParameter: true, + ErrTooManyQueryParameters: true, ErrMissingJobProperties: true, ErrMissingParameters: true, ErrUnableToParseJSON: true, diff --git a/config/config.go b/config/config.go index 741930e7..484b700c 100644 --- a/config/config.go +++ b/config/config.go @@ -68,7 +68,7 @@ func Get() (*Configuration, error) { BindAddr: "localhost:27017", Collection: "datasets", Database: "datasets", - Limit: 0, + Limit: 100, Offset: 0, }, } diff --git a/config/config_test.go b/config/config_test.go index d56bcce7..5e62c954 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -33,7 +33,7 @@ func TestSpec(t *testing.T) { So(cfg.MongoConfig.BindAddr, ShouldEqual, "localhost:27017") So(cfg.MongoConfig.Collection, ShouldEqual, "datasets") So(cfg.MongoConfig.Database, ShouldEqual, "datasets") - So(cfg.MongoConfig.Limit, ShouldEqual, 0) + So(cfg.MongoConfig.Limit, ShouldEqual, 100) So(cfg.MongoConfig.Offset, ShouldEqual, 0) So(cfg.EnablePermissionsAuth, ShouldBeFalse) So(cfg.HealthCheckCriticalTimeout, ShouldEqual, 90*time.Second) diff --git a/mongo/dimension_store.go b/mongo/dimension_store.go index 336324b9..9cb5b7d4 100644 --- a/mongo/dimension_store.go +++ b/mongo/dimension_store.go @@ -11,6 +11,7 @@ import ( ) const dimensionOptions = "dimension.options" +const maxIDs = 1000 // GetDimensionsFromInstance returns a list of dimensions and their options for an instance resource func (m *Mongo) GetDimensionsFromInstance(id string) (*models.DimensionNodeResults, error) { @@ -120,3 +121,44 @@ func (m *Mongo) GetDimensionOptions(version *models.Version, dimension string, o Limit: limit, }, nil } + +// GetDimensionOptionsFromIDs returns dimension options for a dimension within a dataset, whose IDs match the provided list of IDs +func (m *Mongo) GetDimensionOptionsFromIDs(version *models.Version, dimension string, IDs []string) (*models.DimensionOptionResults, error) { + if len(IDs) > maxIDs { + return nil, errors.New("too many IDs provided") + } + + s := m.Session.Copy() + defer s.Close() + + selectorAll := bson.M{"instance_id": version.ID, "name": dimension} + selectorInList := bson.M{"instance_id": version.ID, "name": dimension, "option": bson.M{"$in": IDs}} + + // count total number of options in dimension + q := s.DB(m.Database).C(dimensionOptions).Find(selectorAll) + totalCount, err := q.Count() + if err != nil { + return nil, err + } + + // obtain only options matching the provided IDs + q = s.DB(m.Database).C(dimensionOptions).Find(selectorInList) + + var values []models.PublicDimensionOption + iter := q.Sort("option").Iter() + if err := iter.All(&values); err != nil { + return nil, err + } + + for i := 0; i < len(values); i++ { + values[i].Links.Version = *version.Links.Self + } + + return &models.DimensionOptionResults{ + Items: values, + Count: len(values), + TotalCount: totalCount, + Offset: 0, + Limit: 0, + }, nil +} diff --git a/service/mock/initialiser.go b/service/mock/initialiser.go index 20221b3c..f4b5d785 100644 --- a/service/mock/initialiser.go +++ b/service/mock/initialiser.go @@ -5,13 +5,12 @@ package mock import ( "context" - "net/http" - "sync" - "github.com/ONSdigital/dp-dataset-api/config" "github.com/ONSdigital/dp-dataset-api/service" "github.com/ONSdigital/dp-dataset-api/store" - kafka "github.com/ONSdigital/dp-kafka/v2" + "github.com/ONSdigital/dp-kafka/v2" + "net/http" + "sync" ) var ( @@ -22,7 +21,7 @@ var ( lockInitialiserMockDoGetMongoDB sync.RWMutex ) -// Ensure, that InitialiserMock does implement Initialiser. +// Ensure, that InitialiserMock does implement service.Initialiser. // If this is not the case, regenerate this file with moq. var _ service.Initialiser = &InitialiserMock{} diff --git a/store/datastore.go b/store/datastore.go index 78551f9c..0f086853 100644 --- a/store/datastore.go +++ b/store/datastore.go @@ -30,6 +30,7 @@ type dataMongoDB interface { GetDimensionsFromInstance(ID string) (*models.DimensionNodeResults, error) GetDimensions(datasetID, versionID string) ([]bson.M, error) GetDimensionOptions(version *models.Version, dimension string, offset, limit int) (*models.DimensionOptionResults, error) + GetDimensionOptionsFromIDs(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) GetEdition(ID, editionID, state string) (*models.EditionUpdate, error) GetEditions(ctx context.Context, ID, state string) (*models.EditionUpdateResults, error) GetInstances(ctx context.Context, states []string, datasets []string) (*models.InstanceResults, error) diff --git a/store/datastoretest/datastore.go b/store/datastoretest/datastore.go index d10cbaa6..760f6dee 100755 --- a/store/datastoretest/datastore.go +++ b/store/datastoretest/datastore.go @@ -6,6 +6,7 @@ package storetest import ( "context" "github.com/ONSdigital/dp-dataset-api/models" + "github.com/ONSdigital/dp-dataset-api/store" "github.com/ONSdigital/dp-graph/v2/observation" "github.com/globalsign/mgo/bson" "sync" @@ -23,6 +24,7 @@ var ( lockStorerMockGetDataset sync.RWMutex lockStorerMockGetDatasets sync.RWMutex lockStorerMockGetDimensionOptions sync.RWMutex + lockStorerMockGetDimensionOptionsFromIDs sync.RWMutex lockStorerMockGetDimensions sync.RWMutex lockStorerMockGetDimensionsFromInstance sync.RWMutex lockStorerMockGetEdition sync.RWMutex @@ -50,6 +52,10 @@ var ( lockStorerMockUpsertVersion sync.RWMutex ) +// Ensure, that StorerMock does implement store.Storer. +// If this is not the case, regenerate this file with moq. +var _ store.Storer = &StorerMock{} + // StorerMock is a mock implementation of store.Storer. // // func TestSomethingThatUsesStorer(t *testing.T) { @@ -89,6 +95,9 @@ var ( // GetDimensionOptionsFunc: func(version *models.Version, dimension string, offset int, limit int) (*models.DimensionOptionResults, error) { // panic("mock out the GetDimensionOptions method") // }, +// GetDimensionOptionsFromIDsFunc: func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { +// panic("mock out the GetDimensionOptionsFromIDs method") +// }, // GetDimensionsFunc: func(datasetID string, versionID string) ([]bson.M, error) { // panic("mock out the GetDimensions method") // }, @@ -204,6 +213,9 @@ type StorerMock struct { // GetDimensionOptionsFunc mocks the GetDimensionOptions method. GetDimensionOptionsFunc func(version *models.Version, dimension string, offset int, limit int) (*models.DimensionOptionResults, error) + // GetDimensionOptionsFromIDsFunc mocks the GetDimensionOptionsFromIDs method. + GetDimensionOptionsFromIDsFunc func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) + // GetDimensionsFunc mocks the GetDimensions method. GetDimensionsFunc func(datasetID string, versionID string) ([]bson.M, error) @@ -358,6 +370,15 @@ type StorerMock struct { // Limit is the limit argument value. Limit int } + // GetDimensionOptionsFromIDs holds details about calls to the GetDimensionOptionsFromIDs method. + GetDimensionOptionsFromIDs []struct { + // Version is the version argument value. + Version *models.Version + // Dimension is the dimension argument value. + Dimension string + // Ids is the ids argument value. + Ids []string + } // GetDimensions holds details about calls to the GetDimensions method. GetDimensions []struct { // DatasetID is the datasetID argument value. @@ -949,6 +970,45 @@ func (mock *StorerMock) GetDimensionOptionsCalls() []struct { return calls } +// GetDimensionOptionsFromIDs calls GetDimensionOptionsFromIDsFunc. +func (mock *StorerMock) GetDimensionOptionsFromIDs(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { + if mock.GetDimensionOptionsFromIDsFunc == nil { + panic("StorerMock.GetDimensionOptionsFromIDsFunc: method is nil but Storer.GetDimensionOptionsFromIDs was just called") + } + callInfo := struct { + Version *models.Version + Dimension string + Ids []string + }{ + Version: version, + Dimension: dimension, + Ids: ids, + } + lockStorerMockGetDimensionOptionsFromIDs.Lock() + mock.calls.GetDimensionOptionsFromIDs = append(mock.calls.GetDimensionOptionsFromIDs, callInfo) + lockStorerMockGetDimensionOptionsFromIDs.Unlock() + return mock.GetDimensionOptionsFromIDsFunc(version, dimension, ids) +} + +// GetDimensionOptionsFromIDsCalls gets all the calls that were made to GetDimensionOptionsFromIDs. +// Check the length with: +// len(mockedStorer.GetDimensionOptionsFromIDsCalls()) +func (mock *StorerMock) GetDimensionOptionsFromIDsCalls() []struct { + Version *models.Version + Dimension string + Ids []string +} { + var calls []struct { + Version *models.Version + Dimension string + Ids []string + } + lockStorerMockGetDimensionOptionsFromIDs.RLock() + calls = mock.calls.GetDimensionOptionsFromIDs + lockStorerMockGetDimensionOptionsFromIDs.RUnlock() + return calls +} + // GetDimensions calls GetDimensionsFunc. func (mock *StorerMock) GetDimensions(datasetID string, versionID string) ([]bson.M, error) { if mock.GetDimensionsFunc == nil { diff --git a/store/datastoretest/mongo.go b/store/datastoretest/mongo.go index e4535555..0c018a10 100644 --- a/store/datastoretest/mongo.go +++ b/store/datastoretest/mongo.go @@ -6,6 +6,7 @@ package storetest import ( "context" "github.com/ONSdigital/dp-dataset-api/models" + "github.com/ONSdigital/dp-dataset-api/store" "github.com/ONSdigital/dp-healthcheck/healthcheck" "github.com/globalsign/mgo/bson" "sync" @@ -24,6 +25,7 @@ var ( lockMongoDBMockGetDataset sync.RWMutex lockMongoDBMockGetDatasets sync.RWMutex lockMongoDBMockGetDimensionOptions sync.RWMutex + lockMongoDBMockGetDimensionOptionsFromIDs sync.RWMutex lockMongoDBMockGetDimensions sync.RWMutex lockMongoDBMockGetDimensionsFromInstance sync.RWMutex lockMongoDBMockGetEdition sync.RWMutex @@ -49,6 +51,10 @@ var ( lockMongoDBMockUpsertVersion sync.RWMutex ) +// Ensure, that MongoDBMock does implement store.MongoDB. +// If this is not the case, regenerate this file with moq. +var _ store.MongoDB = &MongoDBMock{} + // MongoDBMock is a mock implementation of store.MongoDB. // // func TestSomethingThatUsesMongoDB(t *testing.T) { @@ -91,6 +97,9 @@ var ( // GetDimensionOptionsFunc: func(version *models.Version, dimension string, offset int, limit int) (*models.DimensionOptionResults, error) { // panic("mock out the GetDimensionOptions method") // }, +// GetDimensionOptionsFromIDsFunc: func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { +// panic("mock out the GetDimensionOptionsFromIDs method") +// }, // GetDimensionsFunc: func(datasetID string, versionID string) ([]bson.M, error) { // panic("mock out the GetDimensions method") // }, @@ -203,6 +212,9 @@ type MongoDBMock struct { // GetDimensionOptionsFunc mocks the GetDimensionOptions method. GetDimensionOptionsFunc func(version *models.Version, dimension string, offset int, limit int) (*models.DimensionOptionResults, error) + // GetDimensionOptionsFromIDsFunc mocks the GetDimensionOptionsFromIDs method. + GetDimensionOptionsFromIDsFunc func(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) + // GetDimensionsFunc mocks the GetDimensions method. GetDimensionsFunc func(datasetID string, versionID string) ([]bson.M, error) @@ -350,6 +362,15 @@ type MongoDBMock struct { // Limit is the limit argument value. Limit int } + // GetDimensionOptionsFromIDs holds details about calls to the GetDimensionOptionsFromIDs method. + GetDimensionOptionsFromIDs []struct { + // Version is the version argument value. + Version *models.Version + // Dimension is the dimension argument value. + Dimension string + // Ids is the ids argument value. + Ids []string + } // GetDimensions holds details about calls to the GetDimensions method. GetDimensions []struct { // DatasetID is the datasetID argument value. @@ -940,6 +961,45 @@ func (mock *MongoDBMock) GetDimensionOptionsCalls() []struct { return calls } +// GetDimensionOptionsFromIDs calls GetDimensionOptionsFromIDsFunc. +func (mock *MongoDBMock) GetDimensionOptionsFromIDs(version *models.Version, dimension string, ids []string) (*models.DimensionOptionResults, error) { + if mock.GetDimensionOptionsFromIDsFunc == nil { + panic("MongoDBMock.GetDimensionOptionsFromIDsFunc: method is nil but MongoDB.GetDimensionOptionsFromIDs was just called") + } + callInfo := struct { + Version *models.Version + Dimension string + Ids []string + }{ + Version: version, + Dimension: dimension, + Ids: ids, + } + lockMongoDBMockGetDimensionOptionsFromIDs.Lock() + mock.calls.GetDimensionOptionsFromIDs = append(mock.calls.GetDimensionOptionsFromIDs, callInfo) + lockMongoDBMockGetDimensionOptionsFromIDs.Unlock() + return mock.GetDimensionOptionsFromIDsFunc(version, dimension, ids) +} + +// GetDimensionOptionsFromIDsCalls gets all the calls that were made to GetDimensionOptionsFromIDs. +// Check the length with: +// len(mockedMongoDB.GetDimensionOptionsFromIDsCalls()) +func (mock *MongoDBMock) GetDimensionOptionsFromIDsCalls() []struct { + Version *models.Version + Dimension string + Ids []string +} { + var calls []struct { + Version *models.Version + Dimension string + Ids []string + } + lockMongoDBMockGetDimensionOptionsFromIDs.RLock() + calls = mock.calls.GetDimensionOptionsFromIDs + lockMongoDBMockGetDimensionOptionsFromIDs.RUnlock() + return calls +} + // GetDimensions calls GetDimensionsFunc. func (mock *MongoDBMock) GetDimensions(datasetID string, versionID string) ([]bson.M, error) { if mock.GetDimensionsFunc == nil { diff --git a/swagger.yaml b/swagger.yaml index f8c8cbb6..4f946fe0 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -162,7 +162,7 @@ parameters: $ref: '#/definitions/UpdateVersion' limit: name: limit - description: "Maximum number of items that will be returned. A value of zero (default) will return all available items, without limit." + description: "Maximum number of items that will be returned. A value of zero will return all available items, without limit. The default value is 100." in: query required: false type: integer @@ -172,6 +172,12 @@ parameters: in: query required: false type: integer + ids: + name: id + description: "List of ids, as comma separated values and/or as multiple query parameters with the same key (e.g. 'id=op1,op2&id=op3'). It defines the IDs that we want to retrieve. If provided, it takes precedence over offset and limit." + in: query + required: false + type: string securityDefinitions: FlorenceAPIKey: name: florence-token @@ -451,7 +457,7 @@ paths: tags: - "Public" summary: "Get a list of options from a dimension" - description: "Get a list of options which appear in this dimension and dataset. By default all options are returned, but a subset can be requested by providing offset and limit query paramters." + description: "Get a list of options which appear in this dimension and dataset. By default all options are returned, but a subset can be requested by providing offset and limit query parameters, or by providing the list of option IDs, only the IDs that are found will be returned." parameters: - $ref: '#/parameters/dimension' - $ref: '#/parameters/edition' @@ -459,6 +465,7 @@ paths: - $ref: '#/parameters/version' - $ref: '#/parameters/limit' - $ref: '#/parameters/offset' + - $ref: '#/parameters/ids' responses: 200: description: "Json object containing all options for a dimension"