Skip to content

Commit 49750fb

Browse files
committed
Add project URL parameter parse
1 parent 2d90bff commit 49750fb

File tree

5 files changed

+270
-37
lines changed

5 files changed

+270
-37
lines changed

coderd/coderd.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ func New(options *Options) http.Handler {
4444
r.Get("/{user}/organizations", users.userOrganizations)
4545
})
4646
})
47+
r.Route("/projects", func(r chi.Router) {
48+
r.Route("/{organization}", func(r chi.Router) {
49+
r.Use(httpmw.ExtractOrganizationParam(options.Database))
50+
r.Get("/", nil) // List projects
51+
r.Post("/", nil) // Create project
52+
r.Route("/{project}", func(r chi.Router) {
53+
r.Get("/", nil) // Get a single project
54+
r.Patch("/", nil) // Update a project
55+
})
56+
})
57+
})
4758
})
4859
r.NotFound(site.Handler().ServeHTTP)
4960
return r

database/databasefake/databasefake.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func (q *fakeQuerier) GetProjectByOrganizationAndName(_ context.Context, arg dat
105105
if project.OrganizationID != arg.OrganizationID {
106106
continue
107107
}
108-
if strings.EqualFold(project.Name, arg.Name) {
108+
if !strings.EqualFold(project.Name, arg.Name) {
109109
continue
110110
}
111111
return project, nil

httpmw/organizationparam_test.go

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import (
2222
func TestOrganizationParam(t *testing.T) {
2323
t.Parallel()
2424

25-
setupAuthentication := func(db database.Store, r *http.Request) database.User {
25+
setupAuthentication := func(db database.Store) (*http.Request, database.User) {
2626
var (
2727
id, secret = randomAPIKeyParts()
28+
r = httptest.NewRequest("GET", "/", nil)
2829
hashed = sha256.Sum256([]byte(secret))
2930
)
3031
r.AddCookie(&http.Cookie{
@@ -54,46 +55,53 @@ func TestOrganizationParam(t *testing.T) {
5455
ExpiresAt: database.Now().Add(time.Minute),
5556
})
5657
require.NoError(t, err)
57-
return user
58+
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
59+
return r, user
5860
}
5961

6062
t.Run("None", func(t *testing.T) {
6163
var (
62-
db = databasefake.New()
63-
r = httptest.NewRequest("GET", "/", nil)
64-
rw = httptest.NewRecorder()
65-
_ = setupAuthentication(db, r)
64+
db = databasefake.New()
65+
rw = httptest.NewRecorder()
66+
r, _ = setupAuthentication(db)
67+
rtr = chi.NewRouter()
6668
)
67-
httpmw.ExtractAPIKey(db, nil)(httpmw.ExtractOrganizationParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
68-
}))).ServeHTTP(rw, r)
69+
rtr.Use(
70+
httpmw.ExtractAPIKey(db, nil),
71+
httpmw.ExtractOrganizationParam(db),
72+
)
73+
rtr.Get("/", nil)
74+
rtr.ServeHTTP(rw, r)
6975
res := rw.Result()
7076
defer res.Body.Close()
7177
require.Equal(t, http.StatusBadRequest, res.StatusCode)
7278
})
7379

7480
t.Run("NotFound", func(t *testing.T) {
7581
var (
76-
db = databasefake.New()
77-
r = httptest.NewRequest("GET", "/", nil)
78-
rw = httptest.NewRecorder()
79-
_ = setupAuthentication(db, r)
82+
db = databasefake.New()
83+
rw = httptest.NewRecorder()
84+
r, _ = setupAuthentication(db)
85+
rtr = chi.NewRouter()
86+
)
87+
chi.RouteContext(r.Context()).URLParams.Add("organization", "nothin")
88+
rtr.Use(
89+
httpmw.ExtractAPIKey(db, nil),
90+
httpmw.ExtractOrganizationParam(db),
8091
)
81-
routeContext := chi.NewRouteContext()
82-
routeContext.URLParams.Add("organization", "example")
83-
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
84-
httpmw.ExtractAPIKey(db, nil)(httpmw.ExtractOrganizationParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
85-
}))).ServeHTTP(rw, r)
92+
rtr.Get("/", nil)
93+
rtr.ServeHTTP(rw, r)
8694
res := rw.Result()
8795
defer res.Body.Close()
8896
require.Equal(t, http.StatusNotFound, res.StatusCode)
8997
})
9098

9199
t.Run("NotInOrganization", func(t *testing.T) {
92100
var (
93-
db = databasefake.New()
94-
r = httptest.NewRequest("GET", "/", nil)
95-
rw = httptest.NewRecorder()
96-
_ = setupAuthentication(db, r)
101+
db = databasefake.New()
102+
rw = httptest.NewRecorder()
103+
r, _ = setupAuthentication(db)
104+
rtr = chi.NewRouter()
97105
)
98106
organization, err := db.InsertOrganization(r.Context(), database.InsertOrganizationParams{
99107
ID: uuid.NewString(),
@@ -102,22 +110,24 @@ func TestOrganizationParam(t *testing.T) {
102110
UpdatedAt: database.Now(),
103111
})
104112
require.NoError(t, err)
105-
routeContext := chi.NewRouteContext()
106-
routeContext.URLParams.Add("organization", organization.Name)
107-
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
108-
httpmw.ExtractAPIKey(db, nil)(httpmw.ExtractOrganizationParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
109-
}))).ServeHTTP(rw, r)
113+
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
114+
rtr.Use(
115+
httpmw.ExtractAPIKey(db, nil),
116+
httpmw.ExtractOrganizationParam(db),
117+
)
118+
rtr.Get("/", nil)
119+
rtr.ServeHTTP(rw, r)
110120
res := rw.Result()
111121
defer res.Body.Close()
112122
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
113123
})
114124

115125
t.Run("Success", func(t *testing.T) {
116126
var (
117-
db = databasefake.New()
118-
r = httptest.NewRequest("GET", "/", nil)
119-
rw = httptest.NewRecorder()
120-
user = setupAuthentication(db, r)
127+
db = databasefake.New()
128+
rw = httptest.NewRecorder()
129+
r, user = setupAuthentication(db)
130+
rtr = chi.NewRouter()
121131
)
122132
organization, err := db.InsertOrganization(r.Context(), database.InsertOrganizationParams{
123133
ID: uuid.NewString(),
@@ -133,15 +143,19 @@ func TestOrganizationParam(t *testing.T) {
133143
UpdatedAt: database.Now(),
134144
})
135145
require.NoError(t, err)
136-
routeContext := chi.NewRouteContext()
137-
routeContext.URLParams.Add("organization", organization.Name)
138-
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
139-
httpmw.ExtractAPIKey(db, nil)(httpmw.ExtractOrganizationParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
146+
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
147+
rtr.Use(
148+
httpmw.ExtractAPIKey(db, nil),
149+
httpmw.ExtractOrganizationParam(db),
150+
)
151+
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
140152
_ = httpmw.OrganizationParam(r)
141153
_ = httpmw.OrganizationMemberParam(r)
142-
}))).ServeHTTP(rw, r)
154+
rw.WriteHeader(http.StatusOK)
155+
})
156+
rtr.ServeHTTP(rw, r)
143157
res := rw.Result()
144158
defer res.Body.Close()
145-
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
159+
require.Equal(t, http.StatusOK, res.StatusCode)
146160
})
147161
}

httpmw/projectparam.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package httpmw
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
10+
"github.com/go-chi/chi"
11+
12+
"github.com/coder/coder/database"
13+
"github.com/coder/coder/httpapi"
14+
)
15+
16+
type projectParamContextKey struct{}
17+
18+
// ProjectParam returns the project from the ExtractProjectParameter handler.
19+
func ProjectParam(r *http.Request) database.Project {
20+
project, ok := r.Context().Value(projectParamContextKey{}).(database.Project)
21+
if !ok {
22+
panic("developer error: project param middleware not provided")
23+
}
24+
return project
25+
}
26+
27+
// ExtractProjectParameter grabs a project from the "project" URL parameter.
28+
func ExtractProjectParameter(db database.Store) func(http.Handler) http.Handler {
29+
return func(next http.Handler) http.Handler {
30+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
31+
organization := OrganizationParam(r)
32+
projectName := chi.URLParam(r, "project")
33+
if projectName == "" {
34+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
35+
Message: "project name must be provided",
36+
})
37+
return
38+
}
39+
project, err := db.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
40+
OrganizationID: organization.ID,
41+
Name: projectName,
42+
})
43+
if errors.Is(err, sql.ErrNoRows) {
44+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
45+
Message: fmt.Sprintf("project %q does not exist", projectName),
46+
})
47+
return
48+
}
49+
if err != nil {
50+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
51+
Message: fmt.Sprintf("get project: %s", err.Error()),
52+
})
53+
return
54+
}
55+
56+
ctx := context.WithValue(r.Context(), projectParamContextKey{}, project)
57+
next.ServeHTTP(rw, r.WithContext(ctx))
58+
})
59+
}
60+
}

httpmw/projectparam_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package httpmw_test
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/go-chi/chi"
13+
"github.com/google/uuid"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/coder/coder/cryptorand"
17+
"github.com/coder/coder/database"
18+
"github.com/coder/coder/database/databasefake"
19+
"github.com/coder/coder/httpmw"
20+
)
21+
22+
func TestProjectParam(t *testing.T) {
23+
t.Parallel()
24+
25+
setupAuthentication := func(db database.Store) (*http.Request, database.Organization) {
26+
var (
27+
id, secret = randomAPIKeyParts()
28+
hashed = sha256.Sum256([]byte(secret))
29+
)
30+
r := httptest.NewRequest("GET", "/", nil)
31+
r.AddCookie(&http.Cookie{
32+
Name: httpmw.AuthCookie,
33+
Value: fmt.Sprintf("%s-%s", id, secret),
34+
})
35+
userID, err := cryptorand.String(16)
36+
require.NoError(t, err)
37+
username, err := cryptorand.String(8)
38+
require.NoError(t, err)
39+
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
40+
ID: userID,
41+
Email: "testaccount@coder.com",
42+
Name: "example",
43+
LoginType: database.LoginTypeBuiltIn,
44+
HashedPassword: hashed[:],
45+
Username: username,
46+
CreatedAt: database.Now(),
47+
UpdatedAt: database.Now(),
48+
})
49+
require.NoError(t, err)
50+
_, err = db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
51+
ID: id,
52+
UserID: user.ID,
53+
HashedSecret: hashed[:],
54+
LastUsed: database.Now(),
55+
ExpiresAt: database.Now().Add(time.Minute),
56+
})
57+
require.NoError(t, err)
58+
orgID, err := cryptorand.String(16)
59+
require.NoError(t, err)
60+
organization, err := db.InsertOrganization(r.Context(), database.InsertOrganizationParams{
61+
ID: orgID,
62+
Name: "banana",
63+
Description: "wowie",
64+
CreatedAt: database.Now(),
65+
UpdatedAt: database.Now(),
66+
})
67+
require.NoError(t, err)
68+
_, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
69+
OrganizationID: orgID,
70+
UserID: user.ID,
71+
CreatedAt: database.Now(),
72+
UpdatedAt: database.Now(),
73+
})
74+
require.NoError(t, err)
75+
76+
ctx := chi.NewRouteContext()
77+
ctx.URLParams.Add("organization", organization.Name)
78+
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
79+
return r, organization
80+
}
81+
82+
t.Run("None", func(t *testing.T) {
83+
db := databasefake.New()
84+
rtr := chi.NewRouter()
85+
rtr.Use(
86+
httpmw.ExtractAPIKey(db, nil),
87+
httpmw.ExtractOrganizationParam(db),
88+
httpmw.ExtractProjectParameter(db),
89+
)
90+
rtr.Get("/", nil)
91+
r, _ := setupAuthentication(db)
92+
rw := httptest.NewRecorder()
93+
rtr.ServeHTTP(rw, r)
94+
95+
res := rw.Result()
96+
defer res.Body.Close()
97+
require.Equal(t, http.StatusBadRequest, res.StatusCode)
98+
})
99+
100+
t.Run("NotFound", func(t *testing.T) {
101+
db := databasefake.New()
102+
rtr := chi.NewRouter()
103+
rtr.Use(
104+
httpmw.ExtractAPIKey(db, nil),
105+
httpmw.ExtractOrganizationParam(db),
106+
httpmw.ExtractProjectParameter(db),
107+
)
108+
rtr.Get("/", nil)
109+
110+
r, _ := setupAuthentication(db)
111+
chi.RouteContext(r.Context()).URLParams.Add("project", "nothin")
112+
rw := httptest.NewRecorder()
113+
rtr.ServeHTTP(rw, r)
114+
115+
res := rw.Result()
116+
defer res.Body.Close()
117+
require.Equal(t, http.StatusNotFound, res.StatusCode)
118+
})
119+
120+
t.Run("Project", func(t *testing.T) {
121+
db := databasefake.New()
122+
rtr := chi.NewRouter()
123+
rtr.Use(
124+
httpmw.ExtractAPIKey(db, nil),
125+
httpmw.ExtractOrganizationParam(db),
126+
httpmw.ExtractProjectParameter(db),
127+
)
128+
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
129+
_ = httpmw.ProjectParam(r)
130+
rw.WriteHeader(http.StatusOK)
131+
})
132+
133+
r, org := setupAuthentication(db)
134+
project, err := db.InsertProject(context.Background(), database.InsertProjectParams{
135+
ID: uuid.New(),
136+
OrganizationID: org.ID,
137+
Name: "moo",
138+
})
139+
require.NoError(t, err)
140+
chi.RouteContext(r.Context()).URLParams.Add("project", project.Name)
141+
rw := httptest.NewRecorder()
142+
rtr.ServeHTTP(rw, r)
143+
144+
res := rw.Result()
145+
defer res.Body.Close()
146+
require.Equal(t, http.StatusOK, res.StatusCode)
147+
})
148+
}

0 commit comments

Comments
 (0)