diff --git a/api/v1/shortcut.go b/api/v1/shortcut.go index e67a938..a74ac83 100644 --- a/api/v1/shortcut.go +++ b/api/v1/shortcut.go @@ -90,7 +90,7 @@ func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted post shortcut request, err: %s", err)).SetInternal(err) } - shortcut, err := s.Store.CreateShortcut(ctx, &storepb.Shortcut{ + shortcut := &storepb.Shortcut{ CreatorId: userID, Name: strings.ToLower(create.Name), Link: create.Link, @@ -98,12 +98,16 @@ func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { Description: create.Description, Visibility: convertVisibilityToStorepb(create.Visibility), Tags: create.Tags, - OgMetadata: &storepb.OpenGraphMetadata{ + OgMetadata: &storepb.OpenGraphMetadata{}, + } + if create.OpenGraphMetadata != nil { + shortcut.OgMetadata = &storepb.OpenGraphMetadata{ Title: create.OpenGraphMetadata.Title, Description: create.OpenGraphMetadata.Description, Image: create.OpenGraphMetadata.Image, - }, - }) + } + } + shortcut, err := s.Store.CreateShortcut(ctx, shortcut) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create shortcut, err: %s", err)).SetInternal(err) } @@ -349,6 +353,8 @@ func convertVisibilityToStorepb(visibility Visibility) storepb.Visibility { switch visibility { case VisibilityPublic: return storepb.Visibility_PUBLIC + case VisibilityWorkspace: + return storepb.Visibility_WORKSPACE case VisibilityPrivate: return storepb.Visibility_PRIVATE default: diff --git a/server/server.go b/server/server.go index 5c46145..6172408 100644 --- a/server/server.go +++ b/server/server.go @@ -111,6 +111,10 @@ func (s *Server) Shutdown(ctx context.Context) { fmt.Printf("server stopped properly\n") } +func (s *Server) GetEcho() *echo.Echo { + return s.e +} + func (s *Server) getSystemSecretSessionName(ctx context.Context) (string, error) { secretSessionNameValue, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ Key: store.WorkspaceDisallowSignUp, diff --git a/test/server/auth_test.go b/test/server/auth_test.go new file mode 100644 index 0000000..ab60974 --- /dev/null +++ b/test/server/auth_test.go @@ -0,0 +1,93 @@ +package testserver + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + apiv1 "github.com/boojack/slash/api/v1" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestAuthServer(t *testing.T) { + ctx := context.Background() + s, err := NewTestingServer(ctx, t) + require.NoError(t, err) + defer s.Shutdown(ctx) + + signup := &apiv1.SignUpRequest{ + Email: "slash@yourselfhosted.com", + Password: "testpassword", + } + user, err := s.postAuthSignUp(signup) + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + + signin := &apiv1.SignInRequest{ + Email: "slash@yourselfhosted.com", + Password: "testpassword", + } + user, err = s.postAuthSignIn(signin) + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + err = s.postLogout() + require.NoError(t, err) +} + +func (s *TestingServer) postAuthSignUp(signup *apiv1.SignUpRequest) (*apiv1.User, error) { + rawData, err := json.Marshal(&signup) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal signup") + } + reader := bytes.NewReader(rawData) + body, err := s.post("/api/v1/auth/signup", reader, nil) + if err != nil { + return nil, errors.Wrap(err, "fail to post request") + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + user := &apiv1.User{} + if err = json.Unmarshal(buf.Bytes(), user); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal post signup response") + } + return user, nil +} + +func (s *TestingServer) postAuthSignIn(signip *apiv1.SignInRequest) (*apiv1.User, error) { + rawData, err := json.Marshal(&signip) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal signin") + } + reader := bytes.NewReader(rawData) + body, err := s.post("/api/v1/auth/signin", reader, nil) + if err != nil { + return nil, errors.Wrap(err, "fail to post request") + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + user := &apiv1.User{} + if err = json.Unmarshal(buf.Bytes(), user); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal post signin response") + } + return user, nil +} + +func (s *TestingServer) postLogout() error { + _, err := s.post("/api/v1/auth/logout", nil, nil) + if err != nil { + return errors.Wrap(err, "fail to post request") + } + return nil +} diff --git a/test/server/server.go b/test/server/server.go new file mode 100644 index 0000000..3539683 --- /dev/null +++ b/test/server/server.go @@ -0,0 +1,178 @@ +package testserver + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/boojack/slash/api/auth" + "github.com/boojack/slash/server" + "github.com/boojack/slash/server/profile" + "github.com/boojack/slash/store" + "github.com/boojack/slash/store/db" + "github.com/boojack/slash/test" + "github.com/pkg/errors" + + // sqlite driver. + _ "modernc.org/sqlite" +) + +type TestingServer struct { + server *server.Server + client *http.Client + profile *profile.Profile + cookie string +} + +func NewTestingServer(ctx context.Context, t *testing.T) (*TestingServer, error) { + profile := test.GetTestingProfile(t) + db := db.NewDB(profile) + if err := db.Open(ctx); err != nil { + return nil, errors.Wrap(err, "failed to open db") + } + + store := store.New(db.DBInstance, profile) + server, err := server.NewServer(ctx, profile, store) + if err != nil { + return nil, errors.Wrap(err, "failed to create server") + } + + s := &TestingServer{ + server: server, + client: &http.Client{}, + profile: profile, + cookie: "", + } + errChan := make(chan error, 1) + + go func() { + if err := s.server.Start(ctx); err != nil { + if err != http.ErrServerClosed { + errChan <- errors.Wrap(err, "failed to run main server") + } + } + }() + + if err := s.waitForServerStart(errChan); err != nil { + return nil, errors.Wrap(err, "failed to start server") + } + + return s, nil +} + +func (s *TestingServer) Shutdown(ctx context.Context) { + s.server.Shutdown(ctx) +} + +func (s *TestingServer) waitForServerStart(errChan <-chan error) error { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if s == nil { + continue + } + e := s.server.GetEcho() + if e == nil { + continue + } + addr := e.ListenerAddr() + if addr != nil && strings.Contains(addr.String(), ":") { + return nil // was started + } + case err := <-errChan: + if err == http.ErrServerClosed { + return nil + } + return err + } + } +} + +func (s *TestingServer) request(method, uri string, body io.Reader, params, header map[string]string) (io.ReadCloser, error) { + fullURL := fmt.Sprintf("http://localhost:%d%s", s.profile.Port, uri) + req, err := http.NewRequest(method, fullURL, body) + if err != nil { + return nil, errors.Wrapf(err, "fail to create a new %s request(%q)", method, fullURL) + } + + for k, v := range header { + req.Header.Set(k, v) + } + + q := url.Values{} + for k, v := range params { + q.Add(k, v) + } + if len(q) > 0 { + req.URL.RawQuery = q.Encode() + } + + resp, err := s.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "fail to send a %s request(%q)", method, fullURL) + } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read http response body") + } + return nil, errors.Errorf("http response error code %v body %q", resp.StatusCode, string(body)) + } + + if method == "POST" { + if strings.Contains(uri, "/api/v1/auth/signin") || strings.Contains(uri, "/api/v1/auth/signup") { + cookie := "" + h := resp.Header.Get("Set-Cookie") + parts := strings.Split(h, "; ") + for _, p := range parts { + if strings.HasPrefix(p, fmt.Sprintf("%s=", auth.AccessTokenCookieName)) { + cookie = p + break + } + } + if cookie == "" { + return nil, errors.Errorf("unable to find access token in the login response headers") + } + s.cookie = cookie + } else if strings.Contains(uri, "/api/v1/auth/logout") { + s.cookie = "" + } + } + return resp.Body, nil +} + +// get sends a GET client request. +func (s *TestingServer) get(url string, params map[string]string) (io.ReadCloser, error) { + return s.request("GET", url, nil, params, map[string]string{ + "Cookie": s.cookie, + }) +} + +// post sends a POST client request. +func (s *TestingServer) post(url string, body io.Reader, params map[string]string) (io.ReadCloser, error) { + return s.request("POST", url, body, params, map[string]string{ + "Cookie": s.cookie, + }) +} + +// patch sends a PATCH client request. +func (s *TestingServer) patch(url string, body io.Reader, params map[string]string) (io.ReadCloser, error) { + return s.request("PATCH", url, body, params, map[string]string{ + "Cookie": s.cookie, + }) +} + +// delete sends a DELETE client request. +func (s *TestingServer) delete(url string, params map[string]string) (io.ReadCloser, error) { + return s.request("DELETE", url, nil, params, map[string]string{ + "Cookie": s.cookie, + }) +} diff --git a/test/server/shortcut_test.go b/test/server/shortcut_test.go new file mode 100644 index 0000000..f001af1 --- /dev/null +++ b/test/server/shortcut_test.go @@ -0,0 +1,72 @@ +package testserver + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + apiv1 "github.com/boojack/slash/api/v1" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestShortcutServer(t *testing.T) { + ctx := context.Background() + s, err := NewTestingServer(ctx, t) + require.NoError(t, err) + defer s.Shutdown(ctx) + + signup := &apiv1.SignUpRequest{ + Email: "slash@yourselfhosted.com", + Password: "testpassword", + } + user, err := s.postAuthSignUp(signup) + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + user, err = s.getCurrentUser() + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + shortcutCreate := &apiv1.CreateShortcutRequest{ + Name: "test", + Link: "https://google.com", + Visibility: apiv1.VisibilityPublic, + Tags: []string{}, + } + shortcut, err := s.postShortcutCreate(shortcutCreate) + require.NoError(t, err) + require.Equal(t, shortcutCreate.Name, shortcut.Name) + require.Equal(t, shortcutCreate.Link, shortcut.Link) + err = s.deleteShortcut(shortcut.ID) + require.NoError(t, err) +} + +func (s *TestingServer) postShortcutCreate(request *apiv1.CreateShortcutRequest) (*apiv1.Shortcut, error) { + rawData, err := json.Marshal(&request) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal shortcut create") + } + reader := bytes.NewReader(rawData) + body, err := s.post("/api/v1/shortcut", reader, nil) + if err != nil { + return nil, errors.Wrap(err, "fail to post request") + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + shortcut := &apiv1.Shortcut{} + if err = json.Unmarshal(buf.Bytes(), &shortcut); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal post shortcut response") + } + return shortcut, nil +} + +func (s *TestingServer) deleteShortcut(shortcutID int32) error { + _, err := s.delete(fmt.Sprintf("/api/v1/shortcut/%d", shortcutID), nil) + return err +} diff --git a/test/server/user_test.go b/test/server/user_test.go new file mode 100644 index 0000000..ee5df02 --- /dev/null +++ b/test/server/user_test.go @@ -0,0 +1,103 @@ +package testserver + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + apiv1 "github.com/boojack/slash/api/v1" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestUserServer(t *testing.T) { + ctx := context.Background() + s, err := NewTestingServer(ctx, t) + require.NoError(t, err) + defer s.Shutdown(ctx) + + signup := &apiv1.SignUpRequest{ + Email: "slash@yourselfhosted.com", + Password: "testpassword", + } + user, err := s.postAuthSignUp(signup) + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + user, err = s.getCurrentUser() + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + user, err = s.getUserByID(user.ID) + require.NoError(t, err) + require.Equal(t, signup.Email, user.Email) + newEmail := "test@usermemos.com" + userPatch := &apiv1.PatchUserRequest{ + Email: &newEmail, + } + user, err = s.patchUser(user.ID, userPatch) + require.NoError(t, err) + require.Equal(t, newEmail, user.Email) +} + +func (s *TestingServer) getCurrentUser() (*apiv1.User, error) { + body, err := s.get("/api/v1/user/me", nil) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + user := &apiv1.User{} + if err = json.Unmarshal(buf.Bytes(), &user); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal get user response") + } + return user, nil +} + +func (s *TestingServer) getUserByID(userID int32) (*apiv1.User, error) { + body, err := s.get(fmt.Sprintf("/api/v1/user/%d", userID), nil) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + user := &apiv1.User{} + if err = json.Unmarshal(buf.Bytes(), &user); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal get user response") + } + return user, nil +} + +func (s *TestingServer) patchUser(userID int32, request *apiv1.PatchUserRequest) (*apiv1.User, error) { + rawData, err := json.Marshal(&request) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request") + } + reader := bytes.NewReader(rawData) + body, err := s.patch(fmt.Sprintf("/api/v1/user/%d", userID), reader, nil) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(body) + if err != nil { + return nil, errors.Wrap(err, "fail to read response body") + } + + user := &apiv1.User{} + if err = json.Unmarshal(buf.Bytes(), user); err != nil { + return nil, errors.Wrap(err, "fail to unmarshal patch user response") + } + return user, nil +} diff --git a/test/test.go b/test/test.go index bd2ae20..a3f3464 100644 --- a/test/test.go +++ b/test/test.go @@ -1,4 +1,4 @@ -package tests +package test import ( "fmt"