Skip to content

Commit

Permalink
check if app exists before creating
Browse files Browse the repository at this point in the history
  • Loading branch information
jfeodor committed May 28, 2024
1 parent 08732ea commit 4f9aa18
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 11 deletions.
43 changes: 33 additions & 10 deletions cli/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

type AppService interface {
AppVersionUploadURL(ctx context.Context, input app.AppVersionUploadURLInput) (app.AppVersionUploadURLOutput, error)
ReadApp(ctx context.Context, input app.ReadAppInput) (app.ReadAppOutput, error)
Create(ctx context.Context, input app.CreateAppInput) (app.CreateAppOutput, error)
CreateVersion(ctx context.Context, input app.CreateAppVersionInput) (app.CreateAppVersionOutput, error)
AppVersionUploadURL(ctx context.Context, input app.AppVersionUploadURLInput) (app.AppVersionUploadURLOutput, error)
UploadAppSource(uploadURL string, archive io.Reader) error
DeployApp(ctx context.Context, input app.DeployAppInput) (app.DeployAppOutput, error)
DeployEvents(ctx context.Context, input app.DeployEventsInput) error
Expand Down Expand Up @@ -46,19 +47,12 @@ func Deploy(ctx context.Context, dir string, slug string, appName string, apps A
return err
}

appInput := app.CreateAppInput{
OrganizationSlug: slug,
Name: appName,
DisplayName: manifest.Name,
Description: manifest.Description,
}
appOutput, err := apps.Create(ctx, appInput)
appID, err := readOrCreateApp(ctx, apps, slug, appName, manifest)
if err != nil {
output.PrintErrorDetails("Error creating app remotely", err)
return err
}

appVersionInput := app.CreateAppVersionInput(appOutput)
appVersionInput := app.CreateAppVersionInput{AppID: appID}
appVersionOutput, err := apps.CreateVersion(ctx, appVersionInput)
if err != nil {
output.PrintErrorDetails("Error creating app version remotely", err)
Expand Down Expand Up @@ -112,3 +106,32 @@ func Deploy(ctx context.Context, dir string, slug string, appName string, apps A

return nil
}

func readOrCreateApp(ctx context.Context, apps AppService, slug string, appName string, manifest *manifest.Manifest) (string, error) {
appReadInput := app.ReadAppInput{
OrganizationSlug: slug,
Name: appName,
}
appReadOutput, err := apps.ReadApp(ctx, appReadInput)
switch {
case err == nil:
return appReadOutput.AppID, nil
case errors.Is(err, app.ErrAppNotFound):
appCreateInput := app.CreateAppInput{
OrganizationSlug: slug,
Name: appName,
DisplayName: manifest.Name,
Description: manifest.Description,
}
appCreateOutput, err := apps.Create(ctx, appCreateInput)
if err != nil {
output.PrintErrorDetails("Error creating app remotely", err)
return "", err
}

return appCreateOutput.AppID, nil
default:
output.PrintErrorDetails("Error reading remote app", err)
return "", err
}
}
20 changes: 19 additions & 1 deletion cli/cmd/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ func TestDeploy(t *testing.T) {
const uploadURL = "https://upload/url"
const deployVersionID = "deploy-version-id"

t.Run("happy path can run", func(t *testing.T) {
t.Run("give no existing app then happy path can run", func(t *testing.T) {
dir := t.TempDir()
copyTo(t, "../../testdata/streamlit_app", dir)

apps := &mockAppService{}
apps.On("ReadApp", mock.Anything, mock.Anything).Return(app.ReadAppOutput{}, app.ErrAppNotFound)
apps.On("Create", mock.Anything, mock.Anything).Return(app.CreateAppOutput{AppID: appID}, nil)
apps.On("CreateVersion", mock.Anything, mock.Anything).Return(app.CreateAppVersionOutput{AppVersionID: appVersionID}, nil)
apps.On("AppVersionUploadURL", mock.Anything, mock.Anything).Return(app.AppVersionUploadURLOutput{UploadURL: uploadURL}, nil)
Expand All @@ -41,6 +42,23 @@ func TestDeploy(t *testing.T) {
assert.NoError(t, err)
})

t.Run("give existing app then it does not create app", func(t *testing.T) {
dir := t.TempDir()
copyTo(t, "../../testdata/streamlit_app", dir)

apps := &mockAppService{}
apps.On("ReadApp", mock.Anything, mock.Anything).Return(app.ReadAppOutput{AppID: appID}, nil)
apps.On("CreateVersion", mock.Anything, mock.Anything).Return(app.CreateAppVersionOutput{AppVersionID: appVersionID}, nil)
apps.On("AppVersionUploadURL", mock.Anything, mock.Anything).Return(app.AppVersionUploadURLOutput{UploadURL: uploadURL}, nil)
apps.On("UploadAppSource", mock.Anything, mock.Anything).Return(nil)
apps.On("DeployApp", mock.Anything, mock.Anything).Return(app.DeployAppOutput{DeploymentVersionID: deployVersionID}, nil)
apps.On("DeployEvents", mock.Anything, mock.Anything).Return(nil)

err := Deploy(context.TODO(), dir, slug, appName, apps)

assert.NoError(t, err)
})

t.Run("given dir without numerous.toml then it returns error", func(t *testing.T) {
dir := t.TempDir()

Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/deploy/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type mockAppService struct {
mock.Mock
}

// ReadApp implements AppService.
func (m *mockAppService) ReadApp(ctx context.Context, input app.ReadAppInput) (app.ReadAppOutput, error) {
args := m.Called(ctx, input)
return args.Get(0).(app.ReadAppOutput), args.Error(1)
}

// DeployEvents implements AppService.
func (m *mockAppService) DeployEvents(ctx context.Context, input app.DeployEventsInput) error {
args := m.Called(ctx, input)
Expand Down
48 changes: 48 additions & 0 deletions cli/internal/app/app_read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package app

import (
"context"
"errors"
)

type ReadAppInput struct {
OrganizationSlug string
Name string
}

type ReadAppOutput struct {
AppID string
}

const queryAppText = `
query App($slug: String!, $name: String!) {
app(organizationSlug: $slug, appName: $name) {
id
}
}
`

type appResponse struct {
App struct {
ID string
}
}

var ErrAppNotFound = errors.New("app not found")

func (s *Service) ReadApp(ctx context.Context, input ReadAppInput) (ReadAppOutput, error) {
var resp appResponse

variables := map[string]any{"slug": input.OrganizationSlug, "name": input.Name}
err := s.client.Exec(ctx, queryAppText, &resp, variables)
if err != nil {
return ReadAppOutput{}, err
}

empty := appResponse{}
if resp == empty {
return ReadAppOutput{}, ErrAppNotFound
}

return ReadAppOutput{AppID: resp.App.ID}, nil
}
97 changes: 97 additions & 0 deletions cli/internal/app/app_read_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package app

import (
"context"
"testing"

"numerous/cli/test"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestAppRead(t *testing.T) {
t.Run("given app response it returns expected output", func(t *testing.T) {
doer := test.MockDoer{}
c := test.CreateTestGQLClient(t, &doer)
s := New(c, nil, nil)

respBody := `
{
"data": {
"app": {
"id": "some-app-id"
}
}
}
`
resp := test.JSONResponse(respBody)
doer.On("Do", mock.Anything).Return(resp, nil)

input := ReadAppInput{
OrganizationSlug: "organization-slug",
Name: "app-name",
}
output, err := s.ReadApp(context.TODO(), input)

expected := ReadAppOutput{
AppID: "some-app-id",
}
assert.NoError(t, err)
assert.Equal(t, expected, output)
})

t.Run("given empty response then it returns not found error", func(t *testing.T) {
doer := test.MockDoer{}
c := test.CreateTestGQLClient(t, &doer)
s := New(c, nil, nil)

respBody := `
{
"data": {
"app": null
}
}
`
resp := test.JSONResponse(respBody)
doer.On("Do", mock.Anything).Return(resp, nil)

input := ReadAppInput{
OrganizationSlug: "organization-slug",
Name: "app-name",
}
output, err := s.ReadApp(context.TODO(), input)

expected := ReadAppOutput{}
assert.ErrorIs(t, err, ErrAppNotFound)
assert.Equal(t, expected, output)
})

t.Run("given graphql error then it returns expected error", func(t *testing.T) {
doer := test.MockDoer{}
c := test.CreateTestGQLClient(t, &doer)
s := New(c, nil, nil)

respBody := `
{
"errors": [{
"message": "expected error message",
"location": [{"line": 1, "column": 1}],
"path": ["appCreate"]
}]
}
`
resp := test.JSONResponse(respBody)
doer.On("Do", mock.Anything).Return(resp, nil)

input := ReadAppInput{
OrganizationSlug: "organization-slug",
Name: "app-name",
}
output, err := s.ReadApp(context.TODO(), input)

expected := ReadAppOutput{}
assert.ErrorContains(t, err, "expected error message")
assert.Equal(t, expected, output)
})
}

0 comments on commit 4f9aa18

Please sign in to comment.