Skip to content

Commit f7fea64

Browse files
committed
Add git token id + connectivity api
1 parent 2e02783 commit f7fea64

23 files changed

Lines changed: 430 additions & 54 deletions

internal/api/gittokens/delete.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func Delete(c web.Context) error {
2121
if errors.Is(err, gittokens.ErrNotFound) {
2222
return c.NotFound("git token not found")
2323
}
24+
if errors.Is(err, gittokens.ErrInUse) {
25+
return c.Conflict("git token is in use by one or more repositories")
26+
}
2427
c.L.Error("failed to delete git token", zap.Error(err), zap.Int64("id", id))
2528
return c.InternalError("failed to delete git token")
2629
}

internal/api/gittokens/delete_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12+
func createToken(t *testing.T, s *testutil.State) string {
13+
t.Helper()
14+
uid := testutil.UniqueID()
15+
body, err := s.Post("/v1/gittokens", map[string]any{
16+
"name": "Token " + uid,
17+
"provider": "github",
18+
"token": "ghp_test1234567890abcd",
19+
})
20+
require.NoError(t, err)
21+
return fmt.Sprintf("%.0f", body["id"].(float64))
22+
}
23+
1224
func TestDelete_success(t *testing.T) {
1325
t.Parallel()
1426
s := testutil.NewAuthState(t)
@@ -34,6 +46,40 @@ func TestDelete_notFound(t *testing.T) {
3446
testutil.RequireStatus(t, err, http.StatusNotFound)
3547
}
3648

49+
func TestDelete_blockedWhenInUse(t *testing.T) {
50+
t.Parallel()
51+
s := testutil.NewAuthState(t)
52+
53+
// Create a token
54+
uid := testutil.UniqueID()
55+
tokenBody, err := s.Post("/v1/gittokens", map[string]any{
56+
"name": "In-Use Token " + uid,
57+
"provider": "github",
58+
"token": "ghp_test1234567890abcd",
59+
})
60+
require.NoError(t, err)
61+
tokenIDFloat := tokenBody["id"].(float64)
62+
tokenID := fmt.Sprintf("%.0f", tokenIDFloat)
63+
64+
// Create a workspace and a private repo referencing the token
65+
wsBody, err := s.Post("/v1/workspaces", map[string]any{
66+
"name": "Token In Use WS " + uid,
67+
})
68+
require.NoError(t, err)
69+
wid := fmt.Sprintf("%.0f", wsBody["id"].(float64))
70+
71+
_, err = s.Post("/v1/workspaces/"+wid+"/repos", map[string]any{
72+
"url": "https://github.com/example/private-" + uid,
73+
"is_private": true,
74+
"git_token_id": tokenIDFloat,
75+
})
76+
require.NoError(t, err)
77+
78+
// Delete should be blocked
79+
deleteErr := s.DeleteStatus("/v1/gittokens/" + tokenID)
80+
testutil.RequireStatus(t, deleteErr, http.StatusConflict)
81+
}
82+
3783
func TestDelete_requiresAuth(t *testing.T) {
3884
t.Parallel()
3985
s := testutil.NewState(t)

internal/api/gittokens/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ import (
99
func Configure(e *echo.Echo, l *zap.Logger) {
1010
e.GET("/v1/gittokens", web.WrapAuth(List, l))
1111
e.POST("/v1/gittokens", web.WrapAuth(Create, l))
12+
e.PUT("/v1/gittokens/:id", web.WrapAuth(Update, l))
1213
e.DELETE("/v1/gittokens/:id", web.WrapAuth(Delete, l))
1314
}

internal/api/gittokens/update.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package gittokens
2+
3+
import (
4+
"errors"
5+
"strconv"
6+
7+
"github.com/gomantics/semantix/internal/api/web"
8+
"github.com/gomantics/semantix/internal/domains/gittokens"
9+
"go.uber.org/zap"
10+
)
11+
12+
type UpdateRequest struct {
13+
Name string `json:"name"`
14+
Token string `json:"token"`
15+
}
16+
17+
func Update(c web.Context) error {
18+
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
19+
if err != nil {
20+
return c.BadRequest("invalid id")
21+
}
22+
23+
var req UpdateRequest
24+
if err := c.Bind(&req); err != nil {
25+
return c.BadRequest("invalid request body")
26+
}
27+
28+
if req.Name == "" {
29+
return c.BadRequest("name is required")
30+
}
31+
if req.Token == "" {
32+
return c.BadRequest("token is required")
33+
}
34+
35+
ctx := c.Request().Context()
36+
37+
token, err := gittokens.Update(ctx, id, gittokens.UpdateParams{
38+
Name: req.Name,
39+
Token: req.Token,
40+
})
41+
if err != nil {
42+
if errors.Is(err, gittokens.ErrNotFound) {
43+
return c.NotFound("git token not found")
44+
}
45+
c.L.Error("failed to update git token", zap.Error(err), zap.Int64("id", id))
46+
return c.InternalError("failed to update git token")
47+
}
48+
49+
return c.OK(token)
50+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package repositories
2+
3+
import (
4+
"github.com/gomantics/semantix/internal/api/web"
5+
"github.com/gomantics/semantix/internal/domains/gittokens"
6+
"github.com/gomantics/semantix/internal/libs/gitrepo"
7+
"go.uber.org/zap"
8+
)
9+
10+
type ConnectivityRequest struct {
11+
URL string `json:"url"`
12+
GitTokenID *int64 `json:"git_token_id,omitempty"`
13+
}
14+
15+
type ConnectivityResponse struct {
16+
OK bool `json:"ok"`
17+
Message string `json:"message,omitempty"`
18+
}
19+
20+
func TestConnectivity(c web.Context) error {
21+
var req ConnectivityRequest
22+
if err := c.Bind(&req); err != nil {
23+
return c.BadRequest("invalid request body")
24+
}
25+
26+
if req.URL == "" {
27+
return c.BadRequest("url is required")
28+
}
29+
30+
ctx := c.Request().Context()
31+
provider := gitrepo.DetectProvider(req.URL)
32+
33+
var token string
34+
if req.GitTokenID != nil {
35+
gt, err := gittokens.GetByID(ctx, *req.GitTokenID)
36+
if err != nil {
37+
return c.BadRequest("git token not found")
38+
}
39+
token = gt.Token
40+
}
41+
42+
err := gitrepo.CheckConnectivity(ctx, gitrepo.CloneOptions{
43+
URL: req.URL,
44+
Provider: provider,
45+
Token: token,
46+
})
47+
if err != nil {
48+
c.L.Info("connectivity check failed", zap.String("url", req.URL), zap.Error(err))
49+
return c.JSON(200, ConnectivityResponse{OK: false, Message: "could not reach repository - check the URL and token"})
50+
}
51+
52+
return c.JSON(200, ConnectivityResponse{OK: true})
53+
}

internal/api/repositories/create.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package repositories
22

33
import (
4+
"errors"
45
"strconv"
56

67
"github.com/gomantics/semantix/internal/api/web"
@@ -9,8 +10,10 @@ import (
910
)
1011

1112
type CreateRequest struct {
12-
URL string `json:"url"`
13-
Branch string `json:"branch,omitempty"`
13+
URL string `json:"url"`
14+
Branch string `json:"branch,omitempty"`
15+
IsPrivate bool `json:"is_private,omitempty"`
16+
GitTokenID *int64 `json:"git_token_id,omitempty"`
1417
}
1518

1619
func Create(c web.Context) error {
@@ -32,10 +35,15 @@ func Create(c web.Context) error {
3235

3336
repo, err := repos.Create(ctx, repos.CreateParams{
3437
WorkspaceID: wid,
38+
GitTokenID: req.GitTokenID,
3539
URL: req.URL,
3640
Branch: req.Branch,
41+
IsPrivate: req.IsPrivate,
3742
})
3843
if err != nil {
44+
if errors.Is(err, repos.ErrTokenRequired) {
45+
return c.BadRequest("git_token_id is required for private repositories")
46+
}
3947
c.L.Error("failed to create repo", zap.Error(err), zap.Int64("wid", wid))
4048
return c.InternalError("failed to create repo")
4149
}

internal/api/repositories/create_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ func TestCreate_success(t *testing.T) {
2424
assert.Equal(t, "main", body["branch"])
2525
}
2626

27+
func TestCreate_privateWithoutToken(t *testing.T) {
28+
t.Parallel()
29+
s := testutil.NewAuthState(t)
30+
wid := createWorkspace(t, s)
31+
32+
err := s.PostStatus("/v1/workspaces/"+wid+"/repos", map[string]any{
33+
"url": "https://github.com/example/private-repo",
34+
"is_private": true,
35+
})
36+
testutil.RequireStatus(t, err, http.StatusBadRequest)
37+
}
38+
2739
func TestCreate_missingURL(t *testing.T) {
2840
t.Parallel()
2941
s := testutil.NewAuthState(t)

internal/api/repositories/router.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import (
77
)
88

99
func Configure(e *echo.Echo, l *zap.Logger) {
10-
g := e.Group("/v1/workspaces/:wid/repos")
11-
g.GET("", web.WrapAuth(List, l))
12-
g.POST("", web.WrapAuth(Create, l))
13-
g.GET("/:rid", web.WrapAuth(Get, l))
14-
g.DELETE("/:rid", web.WrapAuth(Delete, l))
15-
g.POST("/:rid/reindex", web.WrapAuth(Reindex, l))
10+
e.GET("/v1/workspaces/:wid/repos", web.WrapAuth(List, l))
11+
e.POST("/v1/workspaces/:wid/repos", web.WrapAuth(Create, l))
12+
e.GET("/v1/workspaces/:wid/repos/:rid", web.WrapAuth(Get, l))
13+
e.DELETE("/v1/workspaces/:wid/repos/:rid", web.WrapAuth(Delete, l))
14+
e.POST("/v1/workspaces/:wid/repos/:rid/reindex", web.WrapAuth(Reindex, l))
15+
e.POST("/v1/repos/connectivity", web.WrapAuth(TestConnectivity, l))
1616
}

internal/api/repositories/testdata/create_test.TestCreate_approvals.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"branch": "main",
33
"created": "[SCRUBBED]",
44
"id": "[SCRUBBED]",
5+
"is_private": false,
56
"status": "pending",
67
"updated": "[SCRUBBED]",
78
"url": "https://github.com/example/approvals-repo",

internal/api/web/context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ func (c Context) Created(data any) error {
8585
return c.JSON(http.StatusCreated, data)
8686
}
8787

88+
func (c Context) Conflict(message string) error {
89+
return c.Error(http.StatusConflict, message)
90+
}
91+
8892
func (c Context) NoContent() error {
8993
return c.Context.NoContent(http.StatusNoContent)
9094
}

0 commit comments

Comments
 (0)