Skip to content
Merged
35 changes: 21 additions & 14 deletions pkg/api/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,38 +330,45 @@ query MyQuery {
return resp.Data.Products, nil
}

type getLatestProductVersionByIDParams struct {
type getActiveProductVersionByIDParams struct {
ID string `json:"id"`
}

type getLatestProductVersionByIDResponseData struct {
ProductVersions []ProductVersion `json:"ProductVersions"`
type getActiveProductVersionByIDProduct struct {
ActiveVersionID string `json:"active_version_id"`
}

type getLatestProductVersionByIDResponse struct {
Data getLatestProductVersionByIDResponseData `json:"data"`
type getActiveProductVersionByIDResponseData struct {
Product *getActiveProductVersionByIDProduct `json:"Products_by_pk"`
}

func (ds *Datastore) GetLatestProductVersionByID(ctx context.Context, id string) (ProductVersion, error) {
const query = `
type getActiveProductVersionByIDResponse struct {
Data getActiveProductVersionByIDResponseData `json:"data"`
}

// GetActiveProductVersionByID returns the active (live) product version for the given product ID.
// It reads the product's active_version_id directly, avoiding selection of draft or non-live versions.
func (ds *Datastore) GetActiveProductVersionByID(ctx context.Context, id string) (ProductVersion, error) {
const getProductQuery = `
query MyQuery ($id: uuid!) {
ProductVersions(where: {Product: {active_version_id: {_is_null: false},id: {_eq: $id}}}, order_by: {created_at: desc}) {
id
Products_by_pk(id: $id) {
active_version_id
}
}
`
req := GQLRequest{
Query: query,
Variables: getLatestProductVersionByIDParams{ID: id},
Query: getProductQuery,
Variables: getActiveProductVersionByIDParams{ID: id},
}
var resp getLatestProductVersionByIDResponse
var resp getActiveProductVersionByIDResponse
if err := ds.gqlClient.Do(ctx, req, &resp); err != nil {
return ProductVersion{}, err
}
if len(resp.Data.ProductVersions) == 0 {
if resp.Data.Product == nil || resp.Data.Product.ActiveVersionID == "" {
return ProductVersion{}, ErrNotFound
}
return resp.Data.ProductVersions[0], nil

return ProductVersion{ID: resp.Data.Product.ActiveVersionID}, nil
}

type getLogByIDParams struct {
Expand Down
84 changes: 64 additions & 20 deletions pkg/api/datastore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,14 +751,14 @@ func TestListProducts(t *testing.T) {
}
}

func TestGetLatestProductVersionByID(t *testing.T) {
func TestGetActiveProductVersionByID(t *testing.T) {
httpClient := resty.New()
httpmock.ActivateNonDefault(httpClient.GetClient())
defer httpmock.DeactivateAndReset()

type mock struct {
mockResponse getLatestProductVersionByIDResponse
status int
mockResponses []interface{}
status int
}

type want struct {
Expand All @@ -774,11 +774,11 @@ func TestGetLatestProductVersionByID(t *testing.T) {
{
name: "200-happy-path",
mock: mock{
mockResponse: getLatestProductVersionByIDResponse{
Data: getLatestProductVersionByIDResponseData{
ProductVersions: []ProductVersion{
{
ID: "ProductVersion1-id",
mockResponses: []interface{}{
getActiveProductVersionByIDResponse{
Data: getActiveProductVersionByIDResponseData{
Product: &getActiveProductVersionByIDProduct{
ActiveVersionID: "ProductVersion1-id",
},
},
},
Expand All @@ -796,9 +796,28 @@ func TestGetLatestProductVersionByID(t *testing.T) {
{
name: "404-error",
mock: mock{
mockResponse: getLatestProductVersionByIDResponse{
Data: getLatestProductVersionByIDResponseData{
ProductVersions: []ProductVersion{},
mockResponses: []interface{}{
getActiveProductVersionByIDResponse{
Data: getActiveProductVersionByIDResponseData{},
},
},
status: http.StatusOK,
},
want: want{
output: ProductVersion{},
err: ErrNotFound,
},
},
{
name: "404-error-empty-active-version-id",
mock: mock{
mockResponses: []interface{}{
getActiveProductVersionByIDResponse{
Data: getActiveProductVersionByIDResponseData{
Product: &getActiveProductVersionByIDProduct{
ActiveVersionID: "",
},
},
},
},
status: http.StatusOK,
Expand All @@ -808,19 +827,44 @@ func TestGetLatestProductVersionByID(t *testing.T) {
err: ErrNotFound,
},
},
// Regression: must return the product's active version, not the latest by creation order.
{
name: "200-active-version-returned-when-newer-draft-exists",
mock: mock{
mockResponses: []interface{}{
// Product lookup — active_version_id points to v1 (the live version),
// NOT v2 which was created more recently but is a draft without source code.
getActiveProductVersionByIDResponse{
Data: getActiveProductVersionByIDResponseData{
Product: &getActiveProductVersionByIDProduct{
ActiveVersionID: "v1-active-id",
},
},
},
},
status: http.StatusOK,
},
want: want{
output: ProductVersion{ID: "v1-active-id"},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

jsonData, err := json.Marshal(tt.mock.mockResponse)
if err != nil {
t.Fatalf("Error occurred during marshaling. Error: %s", err.Error())
}

mockResponse := string(jsonData)

responseCallCount := 0
httpmock.RegisterResponder("POST", "https://example.com",
func(_ *http.Request) (*http.Response, error) {
mockIndex := responseCallCount
if mockIndex >= len(tt.mock.mockResponses) {
mockIndex = len(tt.mock.mockResponses) - 1
}
jsonData, err := json.Marshal(tt.mock.mockResponses[mockIndex])
if err != nil {
t.Fatalf("Error occurred during marshaling. Error: %s", err.Error())
}
responseCallCount++
mockResponse := string(jsonData)
resp := httpmock.NewStringResponse(tt.mock.status, mockResponse)
resp.Header.Set("Content-Type", "application/json")
return resp, nil
Expand All @@ -829,7 +873,7 @@ func TestGetLatestProductVersionByID(t *testing.T) {
gqlClient := NewGraphQLClient("https://example.com", httpClient)
datastoreClient := NewDatastore(gqlClient)

output, err := datastoreClient.GetLatestProductVersionByID(t.Context(), "Product1-id")
output, err := datastoreClient.GetActiveProductVersionByID(t.Context(), "Product1-id")
if tt.want.err != nil {
require.EqualError(t, err, tt.want.err.Error())
httpmock.Reset()
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmdutil/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ type DatastoreInterface interface {
GetRuntimeByName(ctx context.Context, name string) (api.Runtime, error)
GetProject(ctx context.Context, accountID, name string) (api.Project, error)
ListProducts(ctx context.Context) ([]api.Product, error)
GetLatestProductVersionByID(ctx context.Context, id string) (api.ProductVersion, error)
GetActiveProductVersionByID(ctx context.Context, id string) (api.ProductVersion, error)
ListLogsByInstanceID(ctx context.Context, instanceID string, limit int, timestamp time.Time) ([]api.Log, error)
}

Expand Down
12 changes: 6 additions & 6 deletions testutil/mocks/factory.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions vcr/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,11 @@ func askTemplate(ctx context.Context, opts *Options) error {

selectedProductID := templateOptions.IDLookup[templateLabel]

spinner = cmdutil.DisplaySpinnerMessageWithHandle(" Retrieve the latest product template version... ")
selectedProductVersion, err := opts.Datastore().GetLatestProductVersionByID(ctx, selectedProductID)
spinner = cmdutil.DisplaySpinnerMessageWithHandle(" Retrieving active product template version... ")
selectedProductVersion, err := opts.Datastore().GetActiveProductVersionByID(ctx, selectedProductID)
spinner.Stop()
if err != nil {
return fmt.Errorf("failed to get the latest product template version: %w", err)
return fmt.Errorf("failed to get the active product template version: %w", err)
}
selectedProductVersionID := selectedProductVersion.ID

Expand Down
113 changes: 98 additions & 15 deletions vcr/init/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ func TestInit(t *testing.T) {
InitTemplateAskForUserChoiceTimes int
InitReturnTemplateLabel string
InitTemplateAskForUserChoiceErr error
InitGetLatestProductVersionByIDTimes int
InitGetLatestProductVersionByIDReturnTemplate api.ProductVersion
InitGetLatestProductVersionByIDReturnErr error
InitGetActiveProductVersionByIDTimes int
InitGetActiveProductVersionByIDReturnTemplate api.ProductVersion
InitGetActiveProductVersionByIDReturnErr error
InitGetTemplateTimes int
InitGetTemplateReturnTemplate []byte
InitGetTemplateReturnErr error
Expand Down Expand Up @@ -196,9 +196,9 @@ func TestInit(t *testing.T) {
InitReturnTemplateLabel: "template-label",
InitTemplateAskForUserChoiceErr: nil,

InitGetLatestProductVersionByIDTimes: 0,
InitGetLatestProductVersionByIDReturnTemplate: api.ProductVersion{},
InitGetLatestProductVersionByIDReturnErr: nil,
InitGetActiveProductVersionByIDTimes: 0,
InitGetActiveProductVersionByIDReturnTemplate: api.ProductVersion{},
InitGetActiveProductVersionByIDReturnErr: nil,
InitGetTemplateTimes: 0,
InitGetTemplateReturnTemplate: []byte{},
InitGetTemplateReturnErr: nil,
Expand Down Expand Up @@ -280,9 +280,9 @@ func TestInit(t *testing.T) {
InitReturnTemplateLabel: "template-label",
InitTemplateAskForUserChoiceErr: nil,

InitGetLatestProductVersionByIDTimes: 0,
InitGetLatestProductVersionByIDReturnTemplate: api.ProductVersion{},
InitGetLatestProductVersionByIDReturnErr: nil,
InitGetActiveProductVersionByIDTimes: 0,
InitGetActiveProductVersionByIDReturnTemplate: api.ProductVersion{},
InitGetActiveProductVersionByIDReturnErr: nil,
InitGetTemplateTimes: 0,
InitGetTemplateReturnTemplate: []byte{},
InitGetTemplateReturnErr: nil,
Expand Down Expand Up @@ -364,9 +364,9 @@ func TestInit(t *testing.T) {
InitReturnTemplateLabel: "product-name",
InitTemplateAskForUserChoiceErr: nil,

InitGetLatestProductVersionByIDTimes: 1,
InitGetLatestProductVersionByIDReturnTemplate: api.ProductVersion{ID: "product-version-id"},
InitGetLatestProductVersionByIDReturnErr: nil,
InitGetActiveProductVersionByIDTimes: 1,
InitGetActiveProductVersionByIDReturnTemplate: api.ProductVersion{ID: "product-version-id"},
InitGetActiveProductVersionByIDReturnErr: nil,
InitGetTemplateTimes: 1,
InitGetTemplateReturnTemplate: byteSlice,
InitGetTemplateReturnErr: nil,
Expand All @@ -375,6 +375,89 @@ func TestInit(t *testing.T) {
stdout: fmt.Sprintf("✓ %s/vcr.yml created\n", absPath),
},
},
{
name: "error-when-active-product-version-not-found",
cli: "testdata/",
mock: mock{
InitProjNameAskForUserInputQuestion: "Enter your project name:",
InitProjNameAskForUserInputTimes: 1,
InitReturnProjName: "project-name",
InitProjNameAskForUserInputErr: nil,

InitInstListVonageAppsFilter: "",
InitInstListVonageAppsTimes: 1,
InitReturnInstApps: api.ListVonageApplicationsOutput{Applications: []api.ApplicationListItem{{Name: "app-name", ID: "app-id"}}},
InitInstListVonageAppsReturnErr: nil,
InitInstAskForUserChoiceQuestion: "Select your Vonage application ID for deployment:",
InitInstAskForUserChoiceTimes: 1,
InitReturnInstAppLabel: "app-name - (app-id)",
InitInstAskForUserChoiceErr: nil,
InitInstAppNameAskForUserInputQuestion: "Enter your new Vonage application name for deployment:",
InitInstAppNameAskForUserInputTimes: 0,
InitReturnInstAppName: "app-name",
InitInstAppAskForUserInputErr: nil,
InitInstCreateTimes: 0,
InitInstCreateReturnApp: api.CreateVonageApplicationOutput{},
InitInstCreateReturnErr: nil,
InitInstCreateName: "app-name",

InitDebugListVonageAppsFilter: "",
InitDebugListVonageAppsTimes: 1,
InitReturnDebugApps: api.ListVonageApplicationsOutput{Applications: []api.ApplicationListItem{{Name: "app-name", ID: "app-id"}}},
InitDebugListVonageAppsReturnErr: nil,
InitDebugAskForUserChoiceQuestion: "Select your Vonage application ID for debug:",
InitDebugAskForUserChoiceTimes: 1,
InitReturnDebugAppLabel: "app-name - (app-id)",
InitDebugAskForUserChoiceErr: nil,
InitDebugAppNameAskForUserInputQuestion: "Enter your new Vonage application name for debug:",
InitDebugAppNameAskForUserInputTimes: 0,
InitReturnDebugAppName: "app-name",
InitDebugAppAskForUserInputErr: nil,
InitDebugCreateTimes: 0,
InitDebugCreateReturnApp: api.CreateVonageApplicationOutput{},
InitDebugCreateReturnErr: nil,
InitDebugCreateName: "app-name",

InitListRuntimesTimes: 1,
InitReturnRuntimes: []api.Runtime{{Name: "nodejs16", Comments: "", Language: "nodejs"}},
InitListRuntimesReturnErr: nil,
InitRuntimeAskForUserChoiceQuestion: "Select a runtime:",
InitRuntimeAskForUserChoiceTimes: 1,
InitReturnRuntimeLabel: "nodejs16",
InitRuntimeAskForUserChoiceErr: nil,

InitListRegionsTimes: 1,
InitReturnRegions: []api.Region{{Name: "AWS - Europe Ireland", Alias: "aws.euw1"}},
InitListRegionsReturnErr: nil,
InitRegionAskForUserChoiceQuestion: "Select a region:",
InitRegionAskForUserChoiceTimes: 1,
InitReturnRegionLabel: "AWS - Europe Ireland - (aws.euw1)",
InitRegionAskForUserChoiceErr: nil,

InitInstNameAskForUserInputQuestion: "Enter your Instance name:",
InitInstNameAskForUserInputTimes: 1,
InitReturnInstName: "instance-name",
InitInstNameAskForUserInputErr: nil,

InitListProductsTimes: 1,
InitReturnProducts: []api.Product{{ID: "product-id", Name: "product-name", ProgrammingLanguage: "NodeJS"}},
InitListProductsReturnErr: nil,
InitTemplateAskForUserChoiceQuestion: "Select a product template for runtime nodejs16: ",
InitTemplateAskForUserChoiceTimes: 1,
InitReturnTemplateLabel: "product-name",
InitTemplateAskForUserChoiceErr: nil,

InitGetActiveProductVersionByIDTimes: 1,
InitGetActiveProductVersionByIDReturnTemplate: api.ProductVersion{},
InitGetActiveProductVersionByIDReturnErr: api.ErrNotFound,
InitGetTemplateTimes: 0,
InitGetTemplateReturnTemplate: []byte{},
InitGetTemplateReturnErr: nil,
},
want: want{
errMsg: "failed to get the active product template version: not found",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -449,9 +532,9 @@ func TestInit(t *testing.T) {
Times(tt.mock.InitTemplateAskForUserChoiceTimes).
Return(tt.mock.InitReturnTemplateLabel, tt.mock.InitTemplateAskForUserChoiceErr)

datastoreMock.EXPECT().GetLatestProductVersionByID(gomock.Any(), gomock.Any()).
Times(tt.mock.InitGetLatestProductVersionByIDTimes).
Return(tt.mock.InitGetLatestProductVersionByIDReturnTemplate, tt.mock.InitGetLatestProductVersionByIDReturnErr)
datastoreMock.EXPECT().GetActiveProductVersionByID(gomock.Any(), gomock.Any()).
Times(tt.mock.InitGetActiveProductVersionByIDTimes).
Return(tt.mock.InitGetActiveProductVersionByIDReturnTemplate, tt.mock.InitGetActiveProductVersionByIDReturnErr)

marketplaceMock.EXPECT().GetTemplate(gomock.Any(), gomock.Any(), gomock.Any()).
Times(tt.mock.InitGetTemplateTimes).
Expand Down
Loading