Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 08a0c3e

Browse files
author
Noah Hanjun Lee
authored
Fix lock POST API to handle the constraint error in the store (#229)
* Add a new method 'GetEnv' * Fix lock POST API to simplify
1 parent 688fd7f commit 08a0c3e

File tree

10 files changed

+170
-50
lines changed

10 files changed

+170
-50
lines changed

internal/interactor/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package interactor
2+
3+
import (
4+
"context"
5+
6+
"github.com/gitploy-io/gitploy/ent"
7+
"github.com/gitploy-io/gitploy/pkg/e"
8+
"github.com/gitploy-io/gitploy/vo"
9+
)
10+
11+
func (i *Interactor) GetEnv(ctx context.Context, u *ent.User, r *ent.Repo, env string) (*vo.Env, error) {
12+
config, err := i.SCM.GetConfig(ctx, u, r)
13+
if err != nil {
14+
return nil, err
15+
}
16+
17+
if !config.HasEnv(env) {
18+
return nil, e.NewError(e.ErrorCodeConfigUndefinedEnv, nil)
19+
}
20+
21+
return config.GetEnv(env), nil
22+
}

internal/interactor/config_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package interactor
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/golang/mock/gomock"
8+
9+
"github.com/gitploy-io/gitploy/ent"
10+
"github.com/gitploy-io/gitploy/internal/interactor/mock"
11+
"github.com/gitploy-io/gitploy/pkg/e"
12+
"github.com/gitploy-io/gitploy/vo"
13+
)
14+
15+
func TestInteractor_GetEnv(t *testing.T) {
16+
t.Run("Return an error when the environment is not defined.", func(t *testing.T) {
17+
ctrl := gomock.NewController(t)
18+
scm := mock.NewMockSCM(ctrl)
19+
20+
scm.
21+
EXPECT().
22+
GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})).
23+
Return(&vo.Config{
24+
Envs: []*vo.Env{},
25+
}, nil)
26+
27+
i := &Interactor{SCM: scm}
28+
29+
_, err := i.GetEnv(context.Background(), &ent.User{}, &ent.Repo{}, "production")
30+
if !e.HasErrorCode(err, e.ErrorCodeConfigUndefinedEnv) {
31+
t.Fatalf("GetEnv error = %v, wanted ErrorCodeConfigUndefinedEnv", err)
32+
}
33+
})
34+
35+
t.Run("Return the environment.", func(t *testing.T) {
36+
ctrl := gomock.NewController(t)
37+
scm := mock.NewMockSCM(ctrl)
38+
39+
scm.
40+
EXPECT().
41+
GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})).
42+
Return(&vo.Config{
43+
Envs: []*vo.Env{
44+
{
45+
Name: "production",
46+
},
47+
},
48+
}, nil)
49+
50+
i := &Interactor{SCM: scm}
51+
52+
_, err := i.GetEnv(context.Background(), &ent.User{}, &ent.Repo{}, "production")
53+
if err != nil {
54+
t.Fatalf("GetEnv returns an error: %v", err)
55+
}
56+
})
57+
}

internal/pkg/store/lock.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ func (s *Store) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error)
106106
e.ErrorCodeEntityUnprocessable,
107107
fmt.Sprintf("Failed to create a lock. The value of \"%s\" field is invalid.", err.(*ent.ValidationError).Name),
108108
err)
109+
} else if ent.IsConstraintError(err) {
110+
return nil, e.NewError(e.ErrorCodeLockAlreadyExist, err)
109111
} else if err != nil {
110112
return nil, e.NewError(e.ErrorCodeInternalError, err)
111113
}

internal/pkg/store/lock_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/gitploy-io/gitploy/ent"
89
"github.com/gitploy-io/gitploy/ent/enttest"
910
"github.com/gitploy-io/gitploy/ent/migrate"
11+
"github.com/gitploy-io/gitploy/pkg/e"
1012
)
1113

1214
func TestStore_ListExpiredLocksLessThanTime(t *testing.T) {
@@ -56,3 +58,53 @@ func TestStore_ListExpiredLocksLessThanTime(t *testing.T) {
5658
}
5759
})
5860
}
61+
62+
func TestStore_CreateLock(t *testing.T) {
63+
t.Run("Return an error when the lock is exist.", func(t *testing.T) {
64+
ctx := context.Background()
65+
66+
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",
67+
enttest.WithMigrateOptions(migrate.WithForeignKeys(false)),
68+
)
69+
defer client.Close()
70+
71+
t.Log("Lock the production environment.")
72+
client.Lock.
73+
Create().
74+
SetEnv("production").
75+
SetRepoID(1).
76+
SetUserID(1).
77+
SaveX(ctx)
78+
79+
s := NewStore(client)
80+
81+
t.Log("Lock the production environment again.")
82+
if _, err := s.CreateLock(ctx, &ent.Lock{
83+
Env: "production",
84+
RepoID: 1,
85+
UserID: 1,
86+
}); !e.HasErrorCode(err, e.ErrorCodeLockAlreadyExist) {
87+
t.Fatalf("Error = %v, wanted ErrorCodeEntityUnprocessable", err)
88+
}
89+
})
90+
91+
t.Run("Lock the environment.", func(t *testing.T) {
92+
ctx := context.Background()
93+
94+
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",
95+
enttest.WithMigrateOptions(migrate.WithForeignKeys(false)),
96+
)
97+
defer client.Close()
98+
99+
s := NewStore(client)
100+
101+
_, err := s.CreateLock(ctx, &ent.Lock{
102+
Env: "production",
103+
RepoID: 1,
104+
UserID: 1,
105+
})
106+
if err != nil {
107+
t.Fatalf("CreateLock retruns an error: %v", err)
108+
}
109+
})
110+
}

internal/server/api/v1/repos/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type (
3131
Deploy(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error)
3232
DeployToRemote(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error)
3333
GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.Config, error)
34+
GetEnv(ctx context.Context, u *ent.User, r *ent.Repo, env string) (*vo.Env, error)
3435

3536
ListReviews(ctx context.Context, d *ent.Deployment) ([]*ent.Review, error)
3637
FindReviewOfUser(ctx context.Context, u *ent.User, d *ent.Deployment) (*ent.Review, error)

internal/server/api/v1/repos/lock.go

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (r *Repo) CreateLock(c *gin.Context) {
8484
vu, _ := c.Get(global.KeyUser)
8585
u := vu.(*ent.User)
8686

87-
cfg, err := r.i.GetConfig(ctx, u, re)
87+
env, err := r.i.GetEnv(ctx, u, re, p.Env)
8888
if e.HasErrorCode(err, e.ErrorCodeEntityNotFound) {
8989
r.log.Check(gb.GetZapLogLevel(err), "The configuration file is not found.").Write(zap.Error(err))
9090
// To override the HTTP status 422.
@@ -96,32 +96,8 @@ func (r *Repo) CreateLock(c *gin.Context) {
9696
return
9797
}
9898

99-
if !cfg.HasEnv(p.Env) {
100-
r.log.Warn("The environment is not defined in the configuration.")
101-
gb.ResponseWithError(
102-
c,
103-
e.NewErrorWithMessage(e.ErrorCodeConfigParseError, "The environment is not defiend in the configuration.", nil),
104-
)
105-
return
106-
}
107-
108-
// TODO: migrate the business logic into the interactor.
109-
if ok, err := r.i.HasLockOfRepoForEnv(ctx, re, p.Env); ok {
110-
r.log.Warn("The lock already exist.", zap.String("env", p.Env))
111-
gb.ResponseWithError(
112-
c,
113-
e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, "The lock already exist.", err),
114-
)
115-
return
116-
} else if err != nil {
117-
r.log.Check(gb.GetZapLogLevel(err), "Failed to check the lock.").Write(zap.Error(err))
118-
gb.ResponseWithError(c, err)
119-
return
120-
}
121-
122-
// Lock the environment.
12399
l, err := r.i.CreateLock(ctx, &ent.Lock{
124-
Env: p.Env,
100+
Env: env.Name,
125101
ExpiredAt: expiredAt,
126102
UserID: u.ID,
127103
RepoID: re.ID,
@@ -136,7 +112,7 @@ func (r *Repo) CreateLock(c *gin.Context) {
136112
l = nl
137113
}
138114

139-
r.log.Debug("Lock the env.", zap.String("env", p.Env))
115+
r.log.Info("Lock the environment.", zap.String("repo", re.GetFullName()), zap.String("env", p.Env), zap.String("login", u.Login))
140116
gb.Response(c, http.StatusCreated, l)
141117
}
142118

@@ -201,6 +177,7 @@ func (r *Repo) UpdateLock(c *gin.Context) {
201177
l = nl
202178
}
203179

180+
r.log.Info("Patch the environment lock.", zap.Int("lock_id", l.ID))
204181
gb.Response(c, http.StatusOK, l)
205182
}
206183

internal/server/api/v1/repos/lock_test.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import (
1717
"github.com/gitploy-io/gitploy/ent"
1818
"github.com/gitploy-io/gitploy/internal/server/api/v1/repos/mock"
1919
"github.com/gitploy-io/gitploy/internal/server/global"
20+
"github.com/gitploy-io/gitploy/pkg/e"
2021
"github.com/gitploy-io/gitploy/vo"
2122
)
2223

2324
func TestRepo_CreateLock(t *testing.T) {
24-
t.Run("Return 422 when the env is not found", func(t *testing.T) {
25+
t.Run("Return 422 when the environment is undefined.", func(t *testing.T) {
2526
input := struct {
2627
payload *lockPostPayload
2728
}{
@@ -36,14 +37,8 @@ func TestRepo_CreateLock(t *testing.T) {
3637
t.Log("Read deploy.yml and check the env.")
3738
m.
3839
EXPECT().
39-
GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})).
40-
Return(&vo.Config{
41-
Envs: []*vo.Env{
42-
{
43-
Name: "dev",
44-
},
45-
},
46-
}, nil)
40+
GetEnv(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), gomock.Any()).
41+
Return(nil, e.NewError(e.ErrorCodeConfigUndefinedEnv, nil))
4742

4843
r := NewRepo(RepoConfig{}, m)
4944

@@ -61,7 +56,7 @@ func TestRepo_CreateLock(t *testing.T) {
6156

6257
router.ServeHTTP(w, req)
6358
if w.Code != http.StatusUnprocessableEntity {
64-
t.Fatalf("Code = %v, wanted %v. Body=%v", w.Code, http.StatusCreated, w.Body)
59+
t.Fatalf("Code = %v, wanted %v. Body=%v", w.Code, http.StatusUnprocessableEntity, w.Body)
6560
}
6661
})
6762

@@ -80,21 +75,11 @@ func TestRepo_CreateLock(t *testing.T) {
8075
t.Log("Read deploy.yml and check the env.")
8176
m.
8277
EXPECT().
83-
GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})).
84-
Return(&vo.Config{
85-
Envs: []*vo.Env{
86-
{
87-
Name: "production",
88-
},
89-
},
78+
GetEnv(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), gomock.Any()).
79+
Return(&vo.Env{
80+
Name: "production",
9081
}, nil)
9182

92-
t.Log("Check whether the env is locked or not.")
93-
m.
94-
EXPECT().
95-
HasLockOfRepoForEnv(gomock.Any(), gomock.AssignableToTypeOf(&ent.Repo{}), input.payload.Env).
96-
Return(false, nil)
97-
9883
t.Log("Lock the env.")
9984
m.
10085
EXPECT().

internal/server/api/v1/repos/mock/interactor.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/e/code.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
const (
99
// ErrorCodeConfigParseError is that an error occurs when it parse the file.
1010
ErrorCodeConfigParseError ErrorCode = "config_parse_error"
11+
// ErrorCodeConfigUndefinedEnv is that the environment is not defined in the configuration file.
12+
ErrorCodeConfigUndefinedEnv ErrorCode = "config_undefined_env"
1113
// ErrorCodeConfigRegexpError is the regexp(re2) is invalid.
1214
ErrorCodeConfigRegexpError ErrorCode = "config_regexp_error"
1315

@@ -22,6 +24,9 @@ const (
2224
// ErrorCodeDeploymentStatusNotWaiting is the status must be 'waiting' to create a remote deployment.
2325
ErrorCodeDeploymentStatusInvalid ErrorCode = "deployment_status_invalid"
2426

27+
// ErrorCodeLockAlreadyExist is that the environment is already locked.
28+
ErrorCodeLockAlreadyExist ErrorCode = "lock_already_exist"
29+
2530
// ErrorCodeLicenseDecode is the error when the license is decoded.
2631
ErrorCodeLicenseDecode ErrorCode = "license_decode"
2732
// ErrorCodeLicenseRequired is that the license is required.

pkg/e/trans.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import "net/http"
44

55
var messages = map[ErrorCode]string{
66
ErrorCodeConfigParseError: "The configuration is invalid.",
7+
ErrorCodeConfigUndefinedEnv: "The environment is not defined in the configuration.",
78
ErrorCodeConfigRegexpError: "The regexp is invalid.",
89
ErrorCodeDeploymentConflict: "The conflict occurs, please retry.",
910
ErrorCodeDeploymentInvalid: "The validation has failed.",
1011
ErrorCodeDeploymentLocked: "The environment is locked.",
1112
ErrorCodeDeploymentNotApproved: "The deployment is not approved",
1213
ErrorCodeDeploymentStatusInvalid: "The deployment status is invalid",
14+
ErrorCodeLockAlreadyExist: "The environment is already locked",
1315
ErrorCodeLicenseDecode: "Decoding the license is failed.",
1416
ErrorCodeLicenseRequired: "The license is required.",
1517
ErrorCodeEntityNotFound: "It is not found.",
@@ -30,12 +32,14 @@ func GetMessage(code ErrorCode) string {
3032

3133
var httpCodes = map[ErrorCode]int{
3234
ErrorCodeConfigParseError: http.StatusUnprocessableEntity,
35+
ErrorCodeConfigUndefinedEnv: http.StatusUnprocessableEntity,
3336
ErrorCodeConfigRegexpError: http.StatusUnprocessableEntity,
3437
ErrorCodeDeploymentConflict: http.StatusUnprocessableEntity,
3538
ErrorCodeDeploymentInvalid: http.StatusUnprocessableEntity,
3639
ErrorCodeDeploymentLocked: http.StatusUnprocessableEntity,
3740
ErrorCodeDeploymentNotApproved: http.StatusUnprocessableEntity,
3841
ErrorCodeDeploymentStatusInvalid: http.StatusUnprocessableEntity,
42+
ErrorCodeLockAlreadyExist: http.StatusUnprocessableEntity,
3943
ErrorCodeLicenseDecode: http.StatusUnprocessableEntity,
4044
ErrorCodeLicenseRequired: http.StatusPaymentRequired,
4145
ErrorCodeEntityNotFound: http.StatusNotFound,

0 commit comments

Comments
 (0)