diff --git a/api/app/account.go b/api/app/account.go index 8d5bf7d..6cecf56 100644 --- a/api/app/account.go +++ b/api/app/account.go @@ -19,32 +19,32 @@ package app import ( - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/render" - "github.com/spf13/viper" - null "gopkg.in/guregu/null.v3" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/render" + "github.com/spf13/viper" + null "gopkg.in/guregu/null.v3" ) // AccountResource specifies user management handler. type AccountResource struct { - Stores *Stores + Stores *Stores } // NewAccountResource create and returns a AccountResource. func NewAccountResource(stores *Stores) *AccountResource { - return &AccountResource{ - Stores: stores, - } + return &AccountResource{ + Stores: stores, + } } // CreateHandler is public endpoint for @@ -61,76 +61,76 @@ func NewAccountResource(stores *Stores) *AccountResource { // The account will be created and a confirmation email will be sent. // There is no way to set an avatar here and root will be false by default. func (rs *AccountResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &createUserAccountRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - user := &model.User{ - FirstName: data.User.FirstName, - LastName: data.User.LastName, - Email: data.User.Email, - StudentNumber: data.User.StudentNumber, - Semester: data.User.Semester, - Subject: data.User.Subject, - Language: data.User.Language, - ConfirmEmailToken: null.StringFrom(auth.GenerateToken(32)), // we will ask the user to confirm their email address - EncryptedPassword: data.Account.EncryptedPassword, - Root: false, - } - - // create user entry in database - newUser, err := rs.Stores.User.Create(user) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusCreated) - - // return user information of created entry - if err := render.Render(w, r, newUserResponse(newUser)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - err = sendConfirmEmailForUser(newUser) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // start from empty Request + data := &createUserAccountRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + user := &model.User{ + FirstName: data.User.FirstName, + LastName: data.User.LastName, + Email: data.User.Email, + StudentNumber: data.User.StudentNumber, + Semester: data.User.Semester, + Subject: data.User.Subject, + Language: data.User.Language, + ConfirmEmailToken: null.StringFrom(auth.GenerateToken(32)), // we will ask the user to confirm their email address + EncryptedPassword: data.Account.EncryptedPassword, + Root: false, + } + + // create user entry in database + newUser, err := rs.Stores.User.Create(user) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusCreated) + + // return user information of created entry + if err := render.Render(w, r, newUserResponse(newUser)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + err = sendConfirmEmailForUser(newUser) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } // sendConfirmEmailForUser will send the confirmation email to activate the account. func sendConfirmEmailForUser(user *model.User) error { - // send email - // Send Email to User - msg, err := email.NewEmailFromTemplate( - user.Email, - "Confirm Account Instructions", - "confirm_email.en.txt", - map[string]string{ - "first_name": user.FirstName, - "last_name": user.LastName, - "confirm_email_url": fmt.Sprintf("%s/#/confirmation", viper.GetString("url")), - "confirm_email_address": user.Email, - "confirm_email_token": user.ConfirmEmailToken.String, - }) - - if err != nil { - return err - } - err = email.DefaultMail.Send(msg) - if err != nil { - return err - } - - return nil + // send email + // Send Email to User + msg, err := email.NewEmailFromTemplate( + user.Email, + "Confirm Account Instructions", + "confirm_email.en.txt", + map[string]string{ + "first_name": user.FirstName, + "last_name": user.LastName, + "confirm_email_url": fmt.Sprintf("%s/#/confirmation", viper.GetString("url")), + "confirm_email_address": user.Email, + "confirm_email_token": user.ConfirmEmailToken.String, + }) + + if err != nil { + return err + } + err = email.DefaultMail.Send(msg) + if err != nil { + return err + } + + return nil } // EditHandler is public endpoint for @@ -149,75 +149,75 @@ func sendConfirmEmailForUser(user *model.User) error { // on the confirmation link is required to login again. func (rs *AccountResource) EditHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - // make a backup of old data - user, err := rs.Stores.User.Get(accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // start from database data - data := &accountRequest{} - - // update struct from JSON request - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // we require the account-part with at least one value - if data.OldPlainPassword == "" { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("old_plain_password in request is missing"))) - return - } - - // does the submitted old password match with the current active password? - if !auth.CheckPasswordHash(data.OldPlainPassword, user.EncryptedPassword) { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("credentials are wrong"))) - return - } - - // this is the ugly PATCH logic (instead of PUT) - emailHasChanged := false - if data.Account.Email != "" { - emailHasChanged = data.Account.Email != user.Email - } - - passwordHasChanged := data.Account.PlainPassword != "" - - // make sure email is valid - if emailHasChanged { - // we will ask the user to confirm their email address - user.ConfirmEmailToken = null.StringFrom(auth.GenerateToken(32)) - user.Email = data.Account.Email - } - - if passwordHasChanged { - user.EncryptedPassword = data.Account.EncryptedPassword - } - - if err := rs.Stores.User.Update(user); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // make sure email is valid - if emailHasChanged { - err = sendConfirmEmailForUser(user) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } - - render.Status(r, http.StatusNoContent) - - if err := render.Render(w, r, newUserResponse(user)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + // make a backup of old data + user, err := rs.Stores.User.Get(accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // start from database data + data := &accountRequest{} + + // update struct from JSON request + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // we require the account-part with at least one value + if data.OldPlainPassword == "" { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("old_plain_password in request is missing"))) + return + } + + // does the submitted old password match with the current active password? + if !auth.CheckPasswordHash(data.OldPlainPassword, user.EncryptedPassword) { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("credentials are wrong"))) + return + } + + // this is the ugly PATCH logic (instead of PUT) + emailHasChanged := false + if data.Account.Email != "" { + emailHasChanged = data.Account.Email != user.Email + } + + passwordHasChanged := data.Account.PlainPassword != "" + + // make sure email is valid + if emailHasChanged { + // we will ask the user to confirm their email address + user.ConfirmEmailToken = null.StringFrom(auth.GenerateToken(32)) + user.Email = data.Account.Email + } + + if passwordHasChanged { + user.EncryptedPassword = data.Account.EncryptedPassword + } + + if err := rs.Stores.User.Update(user); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // make sure email is valid + if emailHasChanged { + err = sendConfirmEmailForUser(user) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } + + render.Status(r, http.StatusNoContent) + + if err := render.Render(w, r, newUserResponse(user)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -232,17 +232,17 @@ func (rs *AccountResource) EditHandler(w http.ResponseWriter, r *http.Request) { // DESCRIPTION: // It will contain all information as this can only query the own account func (rs *AccountResource) GetHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - user, err := rs.Stores.User.Get(accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - if err := render.Render(w, r, newUserResponse(user)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + user, err := rs.Stores.User.Get(accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + if err := render.Render(w, r, newUserResponse(user)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -259,17 +259,17 @@ func (rs *AccountResource) GetHandler(w http.ResponseWriter, r *http.Request) { // otherwise it will use a default image. We currently support only jpg images. func (rs *AccountResource) GetAvatarHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - file := helper.NewAvatarFileHandle(accessClaims.LoginID) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + file := helper.NewAvatarFileHandle(accessClaims.LoginID) - if !file.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !file.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := file.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err := file.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } @@ -286,25 +286,25 @@ func (rs *AccountResource) GetAvatarHandler(w http.ResponseWriter, r *http.Reque // We currently support only jpg, jpeg,png images. func (rs *AccountResource) ChangeAvatarHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - // get current user - user, err := rs.Stores.User.Get(accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } + // get current user + user, err := rs.Stores.User.Get(accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } - if _, err := helper.NewAvatarFileHandle(user.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - } + if _, err := helper.NewAvatarFileHandle(user.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + } - user.AvatarURL = null.StringFrom(fmt.Sprintf("/api/v1/users/%s/avatar", strconv.FormatInt(user.ID, 10))) - if err := rs.Stores.User.Update(user); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + user.AvatarURL = null.StringFrom(fmt.Sprintf("/api/v1/users/%s/avatar", strconv.FormatInt(user.ID, 10))) + if err := rs.Stores.User.Update(user); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // DeleteAvatarHandler is public endpoint for @@ -318,20 +318,20 @@ func (rs *AccountResource) ChangeAvatarHandler(w http.ResponseWriter, r *http.Re // DESCRIPTION: // This is necessary, when a user wants to switch back to a default avatar. func (rs *AccountResource) DeleteAvatarHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - // get current user - user, err := rs.Stores.User.Get(accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } + // get current user + user, err := rs.Stores.User.Get(accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } - if err = helper.NewAvatarFileHandle(user.ID).Delete(); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err = helper.NewAvatarFileHandle(user.ID).Delete(); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // GetEnrollmentsHandler is public endpoint for @@ -344,14 +344,14 @@ func (rs *AccountResource) DeleteAvatarHandler(w http.ResponseWriter, r *http.Re // SUMMARY: Retrieve the specific account avatar from the request identity // This lists all course enrollments of the request identity including role. func (rs *AccountResource) GetEnrollmentsHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - // get enrollments - enrollments, err := rs.Stores.User.GetEnrollments(accessClaims.LoginID) + // get enrollments + enrollments, err := rs.Stores.User.GetEnrollments(accessClaims.LoginID) - // render JSON reponse - if err = render.RenderList(w, r, rs.newUserEnrollmentsResponse(enrollments)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, rs.newUserEnrollmentsResponse(enrollments)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } diff --git a/api/app/account_test.go b/api/app/account_test.go index 76325af..e18dba4 100644 --- a/api/app/account_test.go +++ b/api/app/account_test.go @@ -19,405 +19,405 @@ package app import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "testing" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "testing" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + "github.com/spf13/viper" ) func TestAccount(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - - tape := &Tape{} - - var stores *Stores - - g.Describe("Account", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - }) - - g.It("Query should require valid claims", func() { - w := tape.Get("/api/v1/account") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - }) - - g.Xit("Query should not return info when claims are invalid", func() { - // we removed that endpoint - w := tape.GetWithClaims("/api/v1/account", 0, true) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Should get all enrollments", func() { - enrollmentsExpected, err := stores.User.GetEnrollments(1) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/account/enrollments", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - enrollmentsActual := []userEnrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(len(enrollmentsExpected)) - - for j := 0; j < len(enrollmentsExpected); j++ { - g.Assert(enrollmentsActual[j].Role).Equal(enrollmentsExpected[j].Role) - g.Assert(enrollmentsActual[j].CourseID).Equal(enrollmentsExpected[j].CourseID) - g.Assert(enrollmentsActual[j].ID).Equal(int64(0)) - } - }) - - g.It("Should not create invalid accounts (missing user data)", func() { - w := tape.Post("/api/v1/account", - H{ - "account": H{ - "email": "foo@test.com", - "plain_password": "bar", - }, - "user": H{ - "first_name": "", - }, - }) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should not create accounts with too short password", func() { - - minLen := viper.GetInt("min_password_length") - tooShortPassword := auth.GenerateToken(minLen - 1) - - w := tape.Post("/api/v1/account", - H{ - "account": H{ - "email": "foo@test.com", - "plain_password": tooShortPassword, - }, - "user": H{ - "first_name": "Data", - }, - }) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should create valid accounts", func() { - - minLen := viper.GetInt("min_password_length") - validPassword := auth.GenerateToken(minLen) - - request := H{ - "user": H{ - "first_name": "Max ", - "last_name": " Mustermensch", // contains whitespaces - "email": "max@Mensch.com ", // contains uppercase - "student_number": "0815", - "semester": 2, - "subject": "bio2", - "language": "de", - }, - "account": H{ - "email": "max@Mensch.com ", - "plain_password": validPassword, - }, - } - - w := tape.Post("/api/v1/account", request) - g.Assert(w.Code).Equal(http.StatusCreated) - - userAfter, err := stores.User.FindByEmail("max@mensch.com") - g.Assert(err).Equal(nil) - - g.Assert(userAfter.FirstName).Equal("Max") - g.Assert(userAfter.LastName).Equal("Mustermensch") - g.Assert(userAfter.Email).Equal("max@mensch.com") - g.Assert(userAfter.StudentNumber).Equal("0815") - g.Assert(userAfter.Semester).Equal(2) - g.Assert(userAfter.Subject).Equal("bio2") - g.Assert(userAfter.Language).Equal("de") - g.Assert(userAfter.Root).Equal(false) - - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) - g.Assert(userAfter.ResetPasswordToken.Valid).Equal(false) - g.Assert(userAfter.AvatarURL.Valid).Equal(false) - - g.Assert(auth.CheckPasswordHash(validPassword, userAfter.EncryptedPassword)).Equal(true) - }) - - g.It("Changes should require valid access-claims", func() { - - data := H{ - "account": H{ - "email": "foo@uni-tuebingen.de", - "plain_password": "new_pass", - }, - "old_plain_password": "test", - } - - w := tape.Patch("/api/v1/account", data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusNoContent) - }) - - g.It("Changes should require valid credentials", func() { - - data := H{ - "account": H{ - "email": "foo@uni-tuebingen.de", - "plain_password": "new_pass", - }, - "old_plain_password": "test_false", - } - - w := tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should change email and password when correct password ", func() { - - data := H{ - "account": H{ - "email": "foo@uni-tuebingen.de", - "plain_password": "new_pass", - }, - "old_plain_password": "test", - } - - w := tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusNoContent) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.Email).Equal("foo@uni-tuebingen.de") - - isPasswordValid := auth.CheckPasswordHash("new_pass", userAfter.EncryptedPassword) - g.Assert(isPasswordValid).Equal(true) - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) - }) - - g.It("Should only change email when correct old password ", func() { - - data := H{ - "account": H{ - "email": "foo@uni-tuebingen.de", - }, - "old_plain_password": "test", - } - - w := tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusNoContent) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.Email).Equal("foo@uni-tuebingen.de") - - isPasswordValid := auth.CheckPasswordHash("test", userAfter.EncryptedPassword) - g.Assert(isPasswordValid).Equal(true) - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) - }) - - g.It("Should only require valid email when correct old password ", func() { - - data := H{ - "account": H{ - "email": "foo@uni-tuebingen", - }, - "old_plain_password": "test", - } - - w := tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should only change password when correct old password ", func() { - - data := H{ - "account": H{ - "plain_password": "fooerrr", - }, - "old_plain_password": "test", - } - - w := tape.PatchWithClaims("/api/v1/account", data, 1, true) - g.Assert(w.Code).Equal(http.StatusNoContent) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") - - isPasswordValid := auth.CheckPasswordHash("fooerrr", userAfter.EncryptedPassword) - g.Assert(isPasswordValid).Equal(true) - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(false) - }) - - g.It("should change avatar (jpg)", func() { - defer helper.NewAvatarFileHandle(1).Delete() - - // no file so far - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - // no avatar by default - w := tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - userReturned := &userResponse{} - err := json.NewDecoder(w.Body).Decode(userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(false) - - // upload avatar - avatarFilename := fmt.Sprintf("%s/default-avatar.jpg", viper.GetString("fixtures_dir")) - w, err = tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/jpg", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) - - user, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(user.AvatarURL.Valid).Equal(true) - - // there should be now an avatar - w = tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(true) - - w = tape.GetWithClaims("/api/v1/account/avatar", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - if !strings.HasSuffix(w.HeaderMap["Content-Type"][0], "jpeg") { - g.Assert(strings.HasSuffix(w.HeaderMap["Content-Type"][0], "jpg")).Equal(true) - } - - }) - - g.It("should change avatar (png)", func() { - defer helper.NewAvatarFileHandle(1).Delete() - - // no file so far - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - // no avatar by default - w := tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - userReturned := &userResponse{} - err := json.NewDecoder(w.Body).Decode(userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(false) - - // upload avatar - avatarFilename := fmt.Sprintf("%s/default-avatar.png", viper.GetString("fixtures_dir")) - w, err = tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/png", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) - - user, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(user.AvatarURL.Valid).Equal(true) - - // there should be now an avatar - w = tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(true) - - w = tape.GetWithClaims("/api/v1/account/avatar", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(strings.HasSuffix(w.HeaderMap["Content-Type"][0], "png")).Equal(true) - - }) - - g.It("reject to large avatars (jpg)", func() { - defer helper.NewAvatarFileHandle(1).Delete() - - // create 10MB file > 5kb - f, err := os.Create("/tmp/foo.jpg") - if err != nil { - log.Fatal(err) - } - if err := f.Truncate(1e7); err != nil { - log.Fatal(err) - } - defer f.Close() - - defer func() { - os.Remove("/tmp/foo.jpg") - }() - - // no file so far - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - // no avatar by default - w := tape.GetWithClaims("/api/v1/account", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - userReturned := &userResponse{} - err = json.NewDecoder(w.Body).Decode(userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(false) - - // upload avatar - w, err = tape.UploadWithClaims("/api/v1/account/avatar", "/tmp/foo.jpg", "image/jpg", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusBadRequest) - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - }) - - g.It("Should have a way to delete own avatar", func() { - defer helper.NewAvatarFileHandle(1).Delete() - - // no file so far - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - // upload avatar - avatarFilename := fmt.Sprintf("%s/default-avatar.jpg", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/jpg", 1, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) - - // there should be now an avatar - w = tape.GetWithClaims("/api/v1/account", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - userReturned := &userResponse{} - err = json.NewDecoder(w.Body).Decode(userReturned) - g.Assert(err).Equal(nil) - g.Assert(userReturned.AvatarURL.Valid).Equal(true) - - // delete - w = tape.DeleteWithClaims("/api/v1/account/avatar", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) - - }) - - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + + tape := &Tape{} + + var stores *Stores + + g.Describe("Account", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + }) + + g.It("Query should require valid claims", func() { + w := tape.Get("/api/v1/account") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + }) + + g.Xit("Query should not return info when claims are invalid", func() { + // we removed that endpoint + w := tape.GetWithClaims("/api/v1/account", 0, true) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Should get all enrollments", func() { + enrollmentsExpected, err := stores.User.GetEnrollments(1) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/account/enrollments", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + enrollmentsActual := []userEnrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(len(enrollmentsExpected)) + + for j := 0; j < len(enrollmentsExpected); j++ { + g.Assert(enrollmentsActual[j].Role).Equal(enrollmentsExpected[j].Role) + g.Assert(enrollmentsActual[j].CourseID).Equal(enrollmentsExpected[j].CourseID) + g.Assert(enrollmentsActual[j].ID).Equal(int64(0)) + } + }) + + g.It("Should not create invalid accounts (missing user data)", func() { + w := tape.Post("/api/v1/account", + H{ + "account": H{ + "email": "foo@test.com", + "plain_password": "bar", + }, + "user": H{ + "first_name": "", + }, + }) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should not create accounts with too short password", func() { + + minLen := viper.GetInt("min_password_length") + tooShortPassword := auth.GenerateToken(minLen - 1) + + w := tape.Post("/api/v1/account", + H{ + "account": H{ + "email": "foo@test.com", + "plain_password": tooShortPassword, + }, + "user": H{ + "first_name": "Data", + }, + }) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should create valid accounts", func() { + + minLen := viper.GetInt("min_password_length") + validPassword := auth.GenerateToken(minLen) + + request := H{ + "user": H{ + "first_name": "Max ", + "last_name": " Mustermensch", // contains whitespaces + "email": "max@Mensch.com ", // contains uppercase + "student_number": "0815", + "semester": 2, + "subject": "bio2", + "language": "de", + }, + "account": H{ + "email": "max@Mensch.com ", + "plain_password": validPassword, + }, + } + + w := tape.Post("/api/v1/account", request) + g.Assert(w.Code).Equal(http.StatusCreated) + + userAfter, err := stores.User.FindByEmail("max@mensch.com") + g.Assert(err).Equal(nil) + + g.Assert(userAfter.FirstName).Equal("Max") + g.Assert(userAfter.LastName).Equal("Mustermensch") + g.Assert(userAfter.Email).Equal("max@mensch.com") + g.Assert(userAfter.StudentNumber).Equal("0815") + g.Assert(userAfter.Semester).Equal(2) + g.Assert(userAfter.Subject).Equal("bio2") + g.Assert(userAfter.Language).Equal("de") + g.Assert(userAfter.Root).Equal(false) + + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) + g.Assert(userAfter.ResetPasswordToken.Valid).Equal(false) + g.Assert(userAfter.AvatarURL.Valid).Equal(false) + + g.Assert(auth.CheckPasswordHash(validPassword, userAfter.EncryptedPassword)).Equal(true) + }) + + g.It("Changes should require valid access-claims", func() { + + data := H{ + "account": H{ + "email": "foo@uni-tuebingen.de", + "plain_password": "new_pass", + }, + "old_plain_password": "test", + } + + w := tape.Patch("/api/v1/account", data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusNoContent) + }) + + g.It("Changes should require valid credentials", func() { + + data := H{ + "account": H{ + "email": "foo@uni-tuebingen.de", + "plain_password": "new_pass", + }, + "old_plain_password": "test_false", + } + + w := tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should change email and password when correct password ", func() { + + data := H{ + "account": H{ + "email": "foo@uni-tuebingen.de", + "plain_password": "new_pass", + }, + "old_plain_password": "test", + } + + w := tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusNoContent) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.Email).Equal("foo@uni-tuebingen.de") + + isPasswordValid := auth.CheckPasswordHash("new_pass", userAfter.EncryptedPassword) + g.Assert(isPasswordValid).Equal(true) + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) + }) + + g.It("Should only change email when correct old password ", func() { + + data := H{ + "account": H{ + "email": "foo@uni-tuebingen.de", + }, + "old_plain_password": "test", + } + + w := tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusNoContent) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.Email).Equal("foo@uni-tuebingen.de") + + isPasswordValid := auth.CheckPasswordHash("test", userAfter.EncryptedPassword) + g.Assert(isPasswordValid).Equal(true) + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) + }) + + g.It("Should only require valid email when correct old password ", func() { + + data := H{ + "account": H{ + "email": "foo@uni-tuebingen", + }, + "old_plain_password": "test", + } + + w := tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should only change password when correct old password ", func() { + + data := H{ + "account": H{ + "plain_password": "fooerrr", + }, + "old_plain_password": "test", + } + + w := tape.PatchWithClaims("/api/v1/account", data, 1, true) + g.Assert(w.Code).Equal(http.StatusNoContent) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") + + isPasswordValid := auth.CheckPasswordHash("fooerrr", userAfter.EncryptedPassword) + g.Assert(isPasswordValid).Equal(true) + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(false) + }) + + g.It("should change avatar (jpg)", func() { + defer helper.NewAvatarFileHandle(1).Delete() + + // no file so far + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + // no avatar by default + w := tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + userReturned := &userResponse{} + err := json.NewDecoder(w.Body).Decode(userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(false) + + // upload avatar + avatarFilename := fmt.Sprintf("%s/default-avatar.jpg", viper.GetString("fixtures_dir")) + w, err = tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/jpg", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) + + user, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(user.AvatarURL.Valid).Equal(true) + + // there should be now an avatar + w = tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(true) + + w = tape.GetWithClaims("/api/v1/account/avatar", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + if !strings.HasSuffix(w.HeaderMap["Content-Type"][0], "jpeg") { + g.Assert(strings.HasSuffix(w.HeaderMap["Content-Type"][0], "jpg")).Equal(true) + } + + }) + + g.It("should change avatar (png)", func() { + defer helper.NewAvatarFileHandle(1).Delete() + + // no file so far + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + // no avatar by default + w := tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + userReturned := &userResponse{} + err := json.NewDecoder(w.Body).Decode(userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(false) + + // upload avatar + avatarFilename := fmt.Sprintf("%s/default-avatar.png", viper.GetString("fixtures_dir")) + w, err = tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/png", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) + + user, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(user.AvatarURL.Valid).Equal(true) + + // there should be now an avatar + w = tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(true) + + w = tape.GetWithClaims("/api/v1/account/avatar", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(strings.HasSuffix(w.HeaderMap["Content-Type"][0], "png")).Equal(true) + + }) + + g.It("reject to large avatars (jpg)", func() { + defer helper.NewAvatarFileHandle(1).Delete() + + // create 10MB file > 5kb + f, err := os.Create("/tmp/foo.jpg") + if err != nil { + log.Fatal(err) + } + if err := f.Truncate(1e7); err != nil { + log.Fatal(err) + } + defer f.Close() + + defer func() { + os.Remove("/tmp/foo.jpg") + }() + + // no file so far + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + // no avatar by default + w := tape.GetWithClaims("/api/v1/account", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + userReturned := &userResponse{} + err = json.NewDecoder(w.Body).Decode(userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(false) + + // upload avatar + w, err = tape.UploadWithClaims("/api/v1/account/avatar", "/tmp/foo.jpg", "image/jpg", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusBadRequest) + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + }) + + g.It("Should have a way to delete own avatar", func() { + defer helper.NewAvatarFileHandle(1).Delete() + + // no file so far + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + // upload avatar + avatarFilename := fmt.Sprintf("%s/default-avatar.jpg", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/account/avatar", avatarFilename, "image/jpg", 1, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(true) + + // there should be now an avatar + w = tape.GetWithClaims("/api/v1/account", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + userReturned := &userResponse{} + err = json.NewDecoder(w.Body).Decode(userReturned) + g.Assert(err).Equal(nil) + g.Assert(userReturned.AvatarURL.Valid).Equal(true) + + // delete + w = tape.DeleteWithClaims("/api/v1/account/avatar", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + g.Assert(helper.NewAvatarFileHandle(1).Exists()).Equal(false) + + }) + + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/api.go b/api/app/api.go index 65b8ac2..51f04af 100644 --- a/api/app/api.go +++ b/api/app/api.go @@ -19,227 +19,227 @@ package app import ( - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/database" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/database" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type ctxKey int const ( - ctxAccount ctxKey = iota - ctxProfile + ctxAccount ctxKey = iota + ctxProfile ) // UserStore defines user related database queries type UserStore interface { - Get(userID int64) (*model.User, error) - Update(p *model.User) error - GetAll() ([]model.User, error) - Create(p *model.User) (*model.User, error) - Delete(userID int64) error - FindByEmail(email string) (*model.User, error) - GetEnrollments(userID int64) ([]model.Enrollment, error) + Get(userID int64) (*model.User, error) + Update(p *model.User) error + GetAll() ([]model.User, error) + Create(p *model.User) (*model.User, error) + Delete(userID int64) error + FindByEmail(email string) (*model.User, error) + GetEnrollments(userID int64) ([]model.Enrollment, error) } // CourseStore defines course related database queries type CourseStore interface { - Get(courseID int64) (*model.Course, error) - Update(p *model.Course) error - GetAll() ([]model.Course, error) - Create(p *model.Course) (*model.Course, error) - Delete(courseID int64) error - Enroll(courseID int64, userID int64, role int64) error - Disenroll(courseID int64, userID int64) error - EnrolledUsers( - courseID int64, - roleFilter []string, - filterFirstName string, - filterLastName string, - filterEmail string, - filterSubject string, - filterLanguage string) ([]model.UserCourse, error) - FindEnrolledUsers( - courseID int64, - roleFilter []string, - filterQuery string, - ) ([]model.UserCourse, error) - GetUserEnrollment(courseID int64, userID int64) (*model.UserCourse, error) - PointsForUser(userID int64, courseID int64) ([]model.SheetPoints, error) - RoleInCourse(userID int64, courseID int64) (authorize.CourseRole, error) - UpdateRole(courseID, userID int64, role int) error + Get(courseID int64) (*model.Course, error) + Update(p *model.Course) error + GetAll() ([]model.Course, error) + Create(p *model.Course) (*model.Course, error) + Delete(courseID int64) error + Enroll(courseID int64, userID int64, role int64) error + Disenroll(courseID int64, userID int64) error + EnrolledUsers( + courseID int64, + roleFilter []string, + filterFirstName string, + filterLastName string, + filterEmail string, + filterSubject string, + filterLanguage string) ([]model.UserCourse, error) + FindEnrolledUsers( + courseID int64, + roleFilter []string, + filterQuery string, + ) ([]model.UserCourse, error) + GetUserEnrollment(courseID int64, userID int64) (*model.UserCourse, error) + PointsForUser(userID int64, courseID int64) ([]model.SheetPoints, error) + RoleInCourse(userID int64, courseID int64) (authorize.CourseRole, error) + UpdateRole(courseID, userID int64, role int) error } // SheetStore specifies required database queries for Sheet management. type SheetStore interface { - Get(SheetID int64) (*model.Sheet, error) - Update(p *model.Sheet) error - GetAll() ([]model.Sheet, error) - Create(p *model.Sheet, courseID int64) (*model.Sheet, error) - Delete(SheetID int64) error - SheetsOfCourse(courseID int64) ([]model.Sheet, error) - IdentifyCourseOfSheet(sheetID int64) (*model.Course, error) - PointsForUser(userID int64, sheetID int64) ([]model.TaskPoints, error) + Get(SheetID int64) (*model.Sheet, error) + Update(p *model.Sheet) error + GetAll() ([]model.Sheet, error) + Create(p *model.Sheet, courseID int64) (*model.Sheet, error) + Delete(SheetID int64) error + SheetsOfCourse(courseID int64) ([]model.Sheet, error) + IdentifyCourseOfSheet(sheetID int64) (*model.Course, error) + PointsForUser(userID int64, sheetID int64) ([]model.TaskPoints, error) } // TaskStore specifies required database queries for Task management. type TaskStore interface { - Get(TaskID int64) (*model.Task, error) - Update(p *model.Task) error - GetAll() ([]model.Task, error) - Create(p *model.Task, sheetID int64) (*model.Task, error) - Delete(TaskID int64) error - TasksOfSheet(sheetID int64) ([]model.Task, error) - IdentifyCourseOfTask(taskID int64) (*model.Course, error) - IdentifySheetOfTask(taskID int64) (*model.Sheet, error) - - GetAverageRating(taskID int64) (float32, error) - GetRatingOfTaskByUser(taskID int64, userID int64) (*model.TaskRating, error) - GetRating(taskRatingID int64) (*model.TaskRating, error) - CreateRating(p *model.TaskRating) (*model.TaskRating, error) - UpdateRating(p *model.TaskRating) error - GetAllMissingTasksForUser(userID int64) ([]model.MissingTask, error) + Get(TaskID int64) (*model.Task, error) + Update(p *model.Task) error + GetAll() ([]model.Task, error) + Create(p *model.Task, sheetID int64) (*model.Task, error) + Delete(TaskID int64) error + TasksOfSheet(sheetID int64) ([]model.Task, error) + IdentifyCourseOfTask(taskID int64) (*model.Course, error) + IdentifySheetOfTask(taskID int64) (*model.Sheet, error) + + GetAverageRating(taskID int64) (float32, error) + GetRatingOfTaskByUser(taskID int64, userID int64) (*model.TaskRating, error) + GetRating(taskRatingID int64) (*model.TaskRating, error) + CreateRating(p *model.TaskRating) (*model.TaskRating, error) + UpdateRating(p *model.TaskRating) error + GetAllMissingTasksForUser(userID int64) ([]model.MissingTask, error) } // GroupStore specifies required database queries for Task management. type GroupStore interface { - Get(groupID int64) (*model.Group, error) - GetAll() ([]model.Group, error) - Create(p *model.Group) (*model.Group, error) - Update(p *model.Group) error - Delete(taskID int64) error - // GroupsOfCourse(courseID int64) ([]model.Group, error) - GroupsOfCourse(courseID int64) ([]model.GroupWithTutor, error) - GetInCourseWithUser(userID int64, courseID int64) ([]model.GroupWithTutor, error) - GetMembers(groupID int64) ([]model.User, error) - GetOfTutor(tutorID int64, courseID int64) ([]model.GroupWithTutor, error) - IdentifyCourseOfGroup(groupID int64) (*model.Course, error) - - GetBidOfUserForGroup(userID int64, groupID int64) (bid int, err error) - InsertBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) - UpdateBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) - - GetBidsForCourseForUser(courseID int64, userID int64) ([]model.GroupBid, error) - GetBidsForCourse(courseID int64) ([]model.GroupBid, error) - - GetGroupEnrollmentOfUserInCourse(userID int64, courseID int64) (*model.GroupEnrollment, error) - CreateGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) (*model.GroupEnrollment, error) - ChangeGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) error - - EnrolledUsers(courseID int64, groupID int64, roleFilter []string, - filterFirstName string, filterLastName string, filterEmail string, filterSubject string, - filterLanguage string) ([]model.UserCourse, error) + Get(groupID int64) (*model.Group, error) + GetAll() ([]model.Group, error) + Create(p *model.Group) (*model.Group, error) + Update(p *model.Group) error + Delete(taskID int64) error + // GroupsOfCourse(courseID int64) ([]model.Group, error) + GroupsOfCourse(courseID int64) ([]model.GroupWithTutor, error) + GetInCourseWithUser(userID int64, courseID int64) ([]model.GroupWithTutor, error) + GetMembers(groupID int64) ([]model.User, error) + GetOfTutor(tutorID int64, courseID int64) ([]model.GroupWithTutor, error) + IdentifyCourseOfGroup(groupID int64) (*model.Course, error) + + GetBidOfUserForGroup(userID int64, groupID int64) (bid int, err error) + InsertBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) + UpdateBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) + + GetBidsForCourseForUser(courseID int64, userID int64) ([]model.GroupBid, error) + GetBidsForCourse(courseID int64) ([]model.GroupBid, error) + + GetGroupEnrollmentOfUserInCourse(userID int64, courseID int64) (*model.GroupEnrollment, error) + CreateGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) (*model.GroupEnrollment, error) + ChangeGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) error + + EnrolledUsers(courseID int64, groupID int64, roleFilter []string, + filterFirstName string, filterLastName string, filterEmail string, filterSubject string, + filterLanguage string) ([]model.UserCourse, error) } // MaterialStore defines material related database queries type MaterialStore interface { - Get(sheetID int64) (*model.Material, error) - Create(p *model.Material, courseID int64) (*model.Material, error) - Update(p *model.Material) error - Delete(sheetID int64) error - MaterialsOfCourse(courseID int64, requiredRole int) ([]model.Material, error) - IdentifyCourseOfMaterial(sheetID int64) (*model.Course, error) - GetAll() ([]model.Material, error) + Get(sheetID int64) (*model.Material, error) + Create(p *model.Material, courseID int64) (*model.Material, error) + Update(p *model.Material) error + Delete(sheetID int64) error + MaterialsOfCourse(courseID int64, requiredRole int) ([]model.Material, error) + IdentifyCourseOfMaterial(sheetID int64) (*model.Course, error) + GetAll() ([]model.Material, error) } // SubmissionStore defines submission related database queries type SubmissionStore interface { - Get(submissionID int64) (*model.Submission, error) - GetByUserAndTask(userID int64, taskID int64) (*model.Submission, error) - Create(p *model.Submission) (*model.Submission, error) - GetFiltered(filterCourseID, filterGroupID, filterUserID, filterSheetID, filterTaskID int64) ([]model.Submission, error) + Get(submissionID int64) (*model.Submission, error) + GetByUserAndTask(userID int64, taskID int64) (*model.Submission, error) + Create(p *model.Submission) (*model.Submission, error) + GetFiltered(filterCourseID, filterGroupID, filterUserID, filterSheetID, filterTaskID int64) ([]model.Submission, error) } // GradeStore defines grades related database queries type GradeStore interface { - GetFiltered( - courseID int64, - sheetID int64, - taskID int64, - groupID int64, - userID int64, - tutorID int64, - feedback string, - acquiredPoints int, - publicTestStatus int, - privateTestStatus int, - publicExecutationState int, - privateExecutationState int, - ) ([]model.Grade, error) - Get(id int64) (*model.Grade, error) - GetForSubmission(id int64) (*model.Grade, error) - Update(p *model.Grade) error - IdentifyCourseOfGrade(gradeID int64) (*model.Course, error) - GetAllMissingGrades(courseID int64, tutorID int64, groupID int64) ([]model.MissingGrade, error) - Create(p *model.Grade) (*model.Grade, error) - - UpdatePrivateTestInfo(gradeID int64, log string, status int) error - UpdatePublicTestInfo(gradeID int64, log string, status int) error - IdentifyTaskOfGrade(gradeID int64) (*model.Task, error) - GetOverviewGrades(courseID int64, groupID int64) ([]model.OverviewGrade, error) + GetFiltered( + courseID int64, + sheetID int64, + taskID int64, + groupID int64, + userID int64, + tutorID int64, + feedback string, + acquiredPoints int, + publicTestStatus int, + privateTestStatus int, + publicExecutationState int, + privateExecutationState int, + ) ([]model.Grade, error) + Get(id int64) (*model.Grade, error) + GetForSubmission(id int64) (*model.Grade, error) + Update(p *model.Grade) error + IdentifyCourseOfGrade(gradeID int64) (*model.Course, error) + GetAllMissingGrades(courseID int64, tutorID int64, groupID int64) ([]model.MissingGrade, error) + Create(p *model.Grade) (*model.Grade, error) + + UpdatePrivateTestInfo(gradeID int64, log string, status int) error + UpdatePublicTestInfo(gradeID int64, log string, status int) error + IdentifyTaskOfGrade(gradeID int64) (*model.Task, error) + GetOverviewGrades(courseID int64, groupID int64) ([]model.OverviewGrade, error) } // API provides application resources and handlers. type API struct { - User *UserResource - Account *AccountResource - Auth *AuthResource - Course *CourseResource - Sheet *SheetResource - Task *TaskResource - Group *GroupResource - TaskRating *TaskRatingResource - Submission *SubmissionResource - Material *MaterialResource - Grade *GradeResource - Common *CommonResource + User *UserResource + Account *AccountResource + Auth *AuthResource + Course *CourseResource + Sheet *SheetResource + Task *TaskResource + Group *GroupResource + TaskRating *TaskRatingResource + Submission *SubmissionResource + Material *MaterialResource + Grade *GradeResource + Common *CommonResource } // Stores is the collection of stores. We use this struct to express a kind of // hierarchy of database queries, e.g. stores.User.Get(1) type Stores struct { - Course CourseStore - User UserStore - Sheet SheetStore - Task TaskStore - Group GroupStore - Submission SubmissionStore - Material MaterialStore - Grade GradeStore + Course CourseStore + User UserStore + Sheet SheetStore + Task TaskStore + Group GroupStore + Submission SubmissionStore + Material MaterialStore + Grade GradeStore } // NewStores build all stores and connect them to a database. func NewStores(db *sqlx.DB) *Stores { - return &Stores{ - Course: database.NewCourseStore(db), - User: database.NewUserStore(db), - Sheet: database.NewSheetStore(db), - Task: database.NewTaskStore(db), - Group: database.NewGroupStore(db), - Submission: database.NewSubmissionStore(db), - Material: database.NewMaterialStore(db), - Grade: database.NewGradeStore(db), - } + return &Stores{ + Course: database.NewCourseStore(db), + User: database.NewUserStore(db), + Sheet: database.NewSheetStore(db), + Task: database.NewTaskStore(db), + Group: database.NewGroupStore(db), + Submission: database.NewSubmissionStore(db), + Material: database.NewMaterialStore(db), + Grade: database.NewGradeStore(db), + } } // NewAPI configures and returns application API. func NewAPI(db *sqlx.DB) (*API, error) { - stores := NewStores(db) - - api := &API{ - Account: NewAccountResource(stores), - Auth: NewAuthResource(stores), - User: NewUserResource(stores), - Course: NewCourseResource(stores), - Sheet: NewSheetResource(stores), - Task: NewTaskResource(stores), - Group: NewGroupResource(stores), - TaskRating: NewTaskRatingResource(stores), - Submission: NewSubmissionResource(stores), - Material: NewMaterialResource(stores), - Grade: NewGradeResource(stores), - Common: NewCommonResource(stores), - } - return api, nil + stores := NewStores(db) + + api := &API{ + Account: NewAccountResource(stores), + Auth: NewAuthResource(stores), + User: NewUserResource(stores), + Course: NewCourseResource(stores), + Sheet: NewSheetResource(stores), + Task: NewTaskResource(stores), + Group: NewGroupResource(stores), + TaskRating: NewTaskRatingResource(stores), + Submission: NewSubmissionResource(stores), + Material: NewMaterialResource(stores), + Grade: NewGradeResource(stores), + Common: NewCommonResource(stores), + } + return api, nil } diff --git a/api/app/auth.go b/api/app/auth.go index 446f2ad..b0ff539 100644 --- a/api/app/auth.go +++ b/api/app/auth.go @@ -19,30 +19,30 @@ package app import ( - "errors" - "fmt" - "net/http" - - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/go-chi/jwtauth" - "github.com/go-chi/render" - "github.com/spf13/viper" - null "gopkg.in/guregu/null.v3" + "errors" + "fmt" + "net/http" + + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/go-chi/jwtauth" + "github.com/go-chi/render" + "github.com/spf13/viper" + null "gopkg.in/guregu/null.v3" ) // AuthResource specifies user management handler. type AuthResource struct { - Stores *Stores + Stores *Stores } // NewAuthResource create and returns a AuthResource. func NewAuthResource(stores *Stores) *AuthResource { - return &AuthResource{ - Stores: stores, - } + return &AuthResource{ + Stores: stores, + } } // RefreshAccessTokenHandler is public endpoint for @@ -59,112 +59,112 @@ func NewAuthResource(stores *Stores) *AuthResource { // This endpoint will generate the access token without login credentials // if the refresh token is given. func (rs *AuthResource) RefreshAccessTokenHandler(w http.ResponseWriter, r *http.Request) { - // Login with your username and password to get the generated JWT refresh and - // access tokens. Alternatively, if the refresh token is already present in - // the header the access token is returned. - // This is a corner case, so we do not rely on middleware here - - // access the underlying JWT functions - tokenManager, err := authenticate.NewTokenAuth() - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // we test wether there is already a JWT Token - if authenticate.HasHeaderToken(r) { - - // parse string from header - tokenStr := jwtauth.TokenFromHeader(r) - - // ok, there is a token in the header - refreshClaims := &authenticate.RefreshClaims{} - err := refreshClaims.ParseRefreshClaimsFromToken(tokenStr) - - if err != nil { - // something went wrong during getting the claims - fmt.Println(err) - render.Render(w, r, ErrUnauthorized) - return - } - - fmt.Println("refreshClaims.LoginID", refreshClaims.LoginID) - fmt.Println("refreshClaims.AccessNotRefresh", refreshClaims.AccessNotRefresh) - - // everything ok - targetUser, err := rs.Stores.User.Get(refreshClaims.LoginID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // we just need to return an access-token - accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(targetUser.ID, targetUser.Root)) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - resp := &authResponse{ - AccessToken: accessToken, - } - - // return access token only - if err := render.Render(w, r, resp); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - } else { - - // we are given email-password credentials - data := &loginRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // does such a user exists with request email adress? - potentialUser, err := rs.Stores.User.FindByEmail(data.Email) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // does the password match? - if !auth.CheckPasswordHash(data.PlainPassword, potentialUser.EncryptedPassword) { - render.Render(w, r, ErrNotFound) - return - } - - refreshToken, err := tokenManager.CreateRefreshJWT(authenticate.NewRefreshClaims(potentialUser.ID)) - - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(potentialUser.ID, potentialUser.Root)) - - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - resp := &authResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, - } - - // return user information of created entry - if err := render.Render(w, r, resp); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - } + // Login with your username and password to get the generated JWT refresh and + // access tokens. Alternatively, if the refresh token is already present in + // the header the access token is returned. + // This is a corner case, so we do not rely on middleware here + + // access the underlying JWT functions + tokenManager, err := authenticate.NewTokenAuth() + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // we test wether there is already a JWT Token + if authenticate.HasHeaderToken(r) { + + // parse string from header + tokenStr := jwtauth.TokenFromHeader(r) + + // ok, there is a token in the header + refreshClaims := &authenticate.RefreshClaims{} + err := refreshClaims.ParseRefreshClaimsFromToken(tokenStr) + + if err != nil { + // something went wrong during getting the claims + fmt.Println(err) + render.Render(w, r, ErrUnauthorized) + return + } + + fmt.Println("refreshClaims.LoginID", refreshClaims.LoginID) + fmt.Println("refreshClaims.AccessNotRefresh", refreshClaims.AccessNotRefresh) + + // everything ok + targetUser, err := rs.Stores.User.Get(refreshClaims.LoginID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // we just need to return an access-token + accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(targetUser.ID, targetUser.Root)) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + resp := &authResponse{ + AccessToken: accessToken, + } + + // return access token only + if err := render.Render(w, r, resp); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + } else { + + // we are given email-password credentials + data := &loginRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // does such a user exists with request email adress? + potentialUser, err := rs.Stores.User.FindByEmail(data.Email) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // does the password match? + if !auth.CheckPasswordHash(data.PlainPassword, potentialUser.EncryptedPassword) { + render.Render(w, r, ErrNotFound) + return + } + + refreshToken, err := tokenManager.CreateRefreshJWT(authenticate.NewRefreshClaims(potentialUser.ID)) + + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(potentialUser.ID, potentialUser.Root)) + + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + resp := &authResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + } + + // return user information of created entry + if err := render.Render(w, r, resp); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + } } @@ -180,56 +180,56 @@ func (rs *AuthResource) RefreshAccessTokenHandler(w http.ResponseWriter, r *http // This endpoint will generate the access token without login credentials // if the refresh token is given. func (rs *AuthResource) LoginHandler(w http.ResponseWriter, r *http.Request) { - // we are given email-password credentials - - data := &loginRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // does such a user exists with request email adress? - potentialUser, err := rs.Stores.User.FindByEmail(data.Email) - if err != nil { - render.Render(w, r, ErrBadRequest) - return - } - - // does the password match? - if !auth.CheckPasswordHash(data.PlainPassword, potentialUser.EncryptedPassword) { - totalFailedLoginsVec.WithLabelValues().Inc() - render.Render(w, r, ErrBadRequestWithDetails(errors.New("credentials are wrong"))) - return - } - - // fmt.Println(potentialUser.ConfirmEmailToken) - // is the email address confirmed? - if potentialUser.ConfirmEmailToken.Valid { - // Valid is true if String is not NULL - // confirm token `potentialUser.ConfirmEmailToken.String` exists - render.Render(w, r, ErrBadRequestWithDetails(errors.New("email not confirmed"))) - return - } - - // user passed all tests - accessClaims := &authenticate.AccessClaims{ - LoginID: potentialUser.ID, - Root: potentialUser.Root, - } - - // fmt.Println("WRITE accessClaims.LoginID", accessClaims.LoginID) - // fmt.Println("WRITE accessClaims.Root", accessClaims.Root) - - w = accessClaims.WriteToSession(w, r) - - resp := &loginResponse{Root: potentialUser.Root} - // return access token only - if err := render.Render(w, r, resp); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // we are given email-password credentials + + data := &loginRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // does such a user exists with request email adress? + potentialUser, err := rs.Stores.User.FindByEmail(data.Email) + if err != nil { + render.Render(w, r, ErrBadRequest) + return + } + + // does the password match? + if !auth.CheckPasswordHash(data.PlainPassword, potentialUser.EncryptedPassword) { + totalFailedLoginsVec.WithLabelValues().Inc() + render.Render(w, r, ErrBadRequestWithDetails(errors.New("credentials are wrong"))) + return + } + + // fmt.Println(potentialUser.ConfirmEmailToken) + // is the email address confirmed? + if potentialUser.ConfirmEmailToken.Valid { + // Valid is true if String is not NULL + // confirm token `potentialUser.ConfirmEmailToken.String` exists + render.Render(w, r, ErrBadRequestWithDetails(errors.New("email not confirmed"))) + return + } + + // user passed all tests + accessClaims := &authenticate.AccessClaims{ + LoginID: potentialUser.ID, + Root: potentialUser.Root, + } + + // fmt.Println("WRITE accessClaims.LoginID", accessClaims.LoginID) + // fmt.Println("WRITE accessClaims.Root", accessClaims.Root) + + w = accessClaims.WriteToSession(w, r) + + resp := &loginResponse{Root: potentialUser.Root} + // return access token only + if err := render.Render(w, r, resp); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -242,8 +242,8 @@ func (rs *AuthResource) LoginHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 401,Unauthenticated // SUMMARY: Destroy a session func (rs *AuthResource) LogoutHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - accessClaims.DestroyInSession(w, r) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims.DestroyInSession(w, r) } // RequestPasswordResetHandler is public endpoint for @@ -255,47 +255,47 @@ func (rs *AuthResource) LogoutHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 400,BadRequest // SUMMARY: will send an email with password reset link func (rs *AuthResource) RequestPasswordResetHandler(w http.ResponseWriter, r *http.Request) { - data := &resetPasswordRequest{} - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // does such a user exists with request email adress? - user, err := rs.Stores.User.FindByEmail(data.Email) - if err != nil { - render.Render(w, r, ErrBadRequest) - return - } - - user.ResetPasswordToken = null.StringFrom(auth.GenerateToken(32)) - rs.Stores.User.Update(user) - - // Send Email to User - // https://infomark-staging.informatik.uni-tuebingen.de/#/password_reset/example@uni-tuebingen.de/af1ecf6f - msg, err := email.NewEmailFromTemplate( - user.Email, - "Password Reset Instructions", - "request_password_token.en.txt", - map[string]string{ - "first_name": user.FirstName, - "last_name": user.LastName, - "email_address": user.Email, - "reset_password_url": fmt.Sprintf("%s/#/password_reset", viper.GetString("url")), - "reset_password_token": user.ResetPasswordToken.String, - }) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - err = email.DefaultMail.Send(msg) - // err = email.Send() - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusOK) + data := &resetPasswordRequest{} + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // does such a user exists with request email adress? + user, err := rs.Stores.User.FindByEmail(data.Email) + if err != nil { + render.Render(w, r, ErrBadRequest) + return + } + + user.ResetPasswordToken = null.StringFrom(auth.GenerateToken(32)) + rs.Stores.User.Update(user) + + // Send Email to User + // https://infomark-staging.informatik.uni-tuebingen.de/#/password_reset/example@uni-tuebingen.de/af1ecf6f + msg, err := email.NewEmailFromTemplate( + user.Email, + "Password Reset Instructions", + "request_password_token.en.txt", + map[string]string{ + "first_name": user.FirstName, + "last_name": user.LastName, + "email_address": user.Email, + "reset_password_url": fmt.Sprintf("%s/#/password_reset", viper.GetString("url")), + "reset_password_token": user.ResetPasswordToken.String, + }) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + err = email.DefaultMail.Send(msg) + // err = email.Send() + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusOK) } // UpdatePasswordHandler is public endpoint for @@ -307,41 +307,41 @@ func (rs *AuthResource) RequestPasswordResetHandler(w http.ResponseWriter, r *ht // RESPONSE: 400,BadRequest // SUMMARY: sets a new password func (rs *AuthResource) UpdatePasswordHandler(w http.ResponseWriter, r *http.Request) { - data := &updatePasswordRequest{} - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // does such a user exists with request email adress? - user, err := rs.Stores.User.FindByEmail(data.Email) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // compare token - if user.ResetPasswordToken.String != data.ResetPasswordToken { - render.Render(w, r, ErrBadRequest) - return - } - - // token is ok, remove token and set new password - user.ResetPasswordToken = null.String{} - user.EncryptedPassword, err = auth.HashPassword(data.PlainPassword) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // fmt.Println(user) - if err := rs.Stores.User.Update(user); err != nil { - // fmt.Println(err) - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusOK) + data := &updatePasswordRequest{} + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // does such a user exists with request email adress? + user, err := rs.Stores.User.FindByEmail(data.Email) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // compare token + if user.ResetPasswordToken.String != data.ResetPasswordToken { + render.Render(w, r, ErrBadRequest) + return + } + + // token is ok, remove token and set new password + user.ResetPasswordToken = null.String{} + user.EncryptedPassword, err = auth.HashPassword(data.PlainPassword) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // fmt.Println(user) + if err := rs.Stores.User.Update(user); err != nil { + // fmt.Println(err) + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusOK) } // ConfirmEmailHandler is public endpoint for @@ -353,32 +353,32 @@ func (rs *AuthResource) UpdatePasswordHandler(w http.ResponseWriter, r *http.Req // RESPONSE: 400,BadRequest // SUMMARY: handles the confirmation link and activate an account func (rs *AuthResource) ConfirmEmailHandler(w http.ResponseWriter, r *http.Request) { - data := &confirmEmailRequest{} - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // does such a user exists with request email adress? - user, err := rs.Stores.User.FindByEmail(data.Email) - if err != nil { - render.Render(w, r, ErrBadRequest) - return - } - - // compare token - if user.ConfirmEmailToken.String != data.ConfirmEmailToken { - render.Render(w, r, ErrBadRequest) - return - } - - // token is ok - user.ConfirmEmailToken = null.String{} - if err := rs.Stores.User.Update(user); err != nil { - fmt.Println(err) - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusOK) + data := &confirmEmailRequest{} + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // does such a user exists with request email adress? + user, err := rs.Stores.User.FindByEmail(data.Email) + if err != nil { + render.Render(w, r, ErrBadRequest) + return + } + + // compare token + if user.ConfirmEmailToken.String != data.ConfirmEmailToken { + render.Render(w, r, ErrBadRequest) + return + } + + // token is ok + user.ConfirmEmailToken = null.String{} + if err := rs.Stores.User.Update(user); err != nil { + fmt.Println(err) + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusOK) } diff --git a/api/app/auth_test.go b/api/app/auth_test.go index b79c252..a5ae5f6 100644 --- a/api/app/auth_test.go +++ b/api/app/auth_test.go @@ -19,228 +19,228 @@ package app import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - redis "github.com/go-redis/redis" - "github.com/spf13/viper" - null "gopkg.in/guregu/null.v3" + "net/http" + "net/http/httptest" + "testing" + + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + redis "github.com/go-redis/redis" + "github.com/spf13/viper" + null "gopkg.in/guregu/null.v3" ) func TestAuth(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - - tape := &Tape{} - - var w *httptest.ResponseRecorder - var stores *Stores - - option, err := redis.ParseURL(viper.GetString("redis_url")) - if err != nil { - panic(err) - } - redisClient := redis.NewClient(option) - - g.Describe("Auth", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - tape.Router, _ = New(tape.DB, false) - stores = NewStores(tape.DB) - _ = stores - }) - - g.It("Not existent user cannot log in", func() { - - w = tape.Post("/api/v1/auth/sessions", - H{ - "email": "peter.zwegat@uni-tuebingen.de", - "plain_password": "", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Wrong credentials should fail", func() { - - w = tape.Post("/api/v1/auth/sessions", - H{ - "email": "test@uni-tuebingen.de", - "plain_password": "testOops", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should not login when confirm email token is set", func() { - - // tamper confirmation token reset token - userBefore, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") - g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(false) - userBefore.ConfirmEmailToken = null.StringFrom("testtoken") - stores.User.Update(userBefore) - - w = tape.Post("/api/v1/auth/sessions", - H{ - "email": "test@uni-tuebingen.de", - "plain_password": "test", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Correct credentials should log in", func() { - - w = tape.Post("/api/v1/auth/sessions", - H{ - "email": "test@uni-tuebingen.de", - "plain_password": "test", - }, - ) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Password-Reset will fail if email invalid", func() { - - w = tape.Post("/api/v1/auth/request_password_reset", - H{ - "email": "test2@uni-tuebingen.de", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Correct Password-Reset-Token will change password", func() { - - // state before - userBefore, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") - g.Assert(userBefore.ResetPasswordToken.Valid).Equal(false) - - w = tape.Post("/api/v1/auth/request_password_reset", - H{ - "email": "test@uni-tuebingen.de", - }, - ) - g.Assert(w.Code).Equal(http.StatusOK) - - // state after request - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") - g.Assert(userAfter.ResetPasswordToken.Valid).Equal(true) - - // use token to reset password - w = tape.Post("/api/v1/auth/update_password", - H{ - "reset_password_token": userAfter.ResetPasswordToken.String, - "plain_password": "new_password", - "email": "test@uni-tuebingen.de", - }, - ) - g.Assert(w.Code).Equal(http.StatusOK) - - userAfter2, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter2.Email).Equal("test@uni-tuebingen.de") - g.Assert(userAfter2.ResetPasswordToken.Valid).Equal(false) - - isPasswordValid := auth.CheckPasswordHash("new_password", userAfter2.EncryptedPassword) - g.Assert(isPasswordValid).Equal(true) - - isPasswordValid = auth.CheckPasswordHash("test", userAfter2.EncryptedPassword) - g.Assert(isPasswordValid).Equal(false) - }) - - g.It("Invalid Password-Reset-Token is denied", func() { - - w = tape.Post("/api/v1/auth/update_password", - H{ - "reset_password_token": "invalid_string", - "plain_password": "new_password", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") - }) - - g.It("Invalid Email-Confirmation-Token is denied", func() { - - // setup confirmation token - userBefore, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") - userBefore.ConfirmEmailToken = null.StringFrom("testtoken") - stores.User.Update(userBefore) - g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(true) - - w = tape.Post("/api/v1/auth/confirm_email", - H{ - "email": "test@uni-tuebingen.de", - "confirmation_token": "testtoken_wrong", - }, - ) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) - }) - - g.It("Correct Email-Confirmation-Token will confirm email", func() { - - // setup confirmation token - userBefore, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") - userBefore.ConfirmEmailToken = null.StringFrom("testtoken") - stores.User.Update(userBefore) - g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(true) - - w = tape.Post("/api/v1/auth/confirm_email", - H{ - "email": "test@uni-tuebingen.de", - "confirmation_token": "testtoken", - }, - ) - g.Assert(w.Code).Equal(http.StatusOK) - - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(false) - }) - - g.It("Should limit requests per minute to do an login", func() { - payload := H{ - "email": "test@uni-tuebingen.de", - "plain_password": "test", - } - - for i := 0; i < 10; i++ { - w = tape.Post("/api/v1/auth/sessions", payload) - } - g.Assert(w.Code).Equal(http.StatusOK) - w = tape.Post("/api/v1/auth/sessions", payload) - g.Assert(w.Code).Equal(http.StatusTooManyRequests) - }) - - g.AfterEach(func() { - tape.AfterEach() - err := redisClient.Set("infomark-logins:1.2.3.4-infomark-logins", "0", 0).Err() - g.Assert(err).Equal(nil) - }) - - }) + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + + tape := &Tape{} + + var w *httptest.ResponseRecorder + var stores *Stores + + option, err := redis.ParseURL(viper.GetString("redis_url")) + if err != nil { + panic(err) + } + redisClient := redis.NewClient(option) + + g.Describe("Auth", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + tape.Router, _ = New(tape.DB, false) + stores = NewStores(tape.DB) + _ = stores + }) + + g.It("Not existent user cannot log in", func() { + + w = tape.Post("/api/v1/auth/sessions", + H{ + "email": "peter.zwegat@uni-tuebingen.de", + "plain_password": "", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Wrong credentials should fail", func() { + + w = tape.Post("/api/v1/auth/sessions", + H{ + "email": "test@uni-tuebingen.de", + "plain_password": "testOops", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should not login when confirm email token is set", func() { + + // tamper confirmation token reset token + userBefore, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") + g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(false) + userBefore.ConfirmEmailToken = null.StringFrom("testtoken") + stores.User.Update(userBefore) + + w = tape.Post("/api/v1/auth/sessions", + H{ + "email": "test@uni-tuebingen.de", + "plain_password": "test", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Correct credentials should log in", func() { + + w = tape.Post("/api/v1/auth/sessions", + H{ + "email": "test@uni-tuebingen.de", + "plain_password": "test", + }, + ) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Password-Reset will fail if email invalid", func() { + + w = tape.Post("/api/v1/auth/request_password_reset", + H{ + "email": "test2@uni-tuebingen.de", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Correct Password-Reset-Token will change password", func() { + + // state before + userBefore, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") + g.Assert(userBefore.ResetPasswordToken.Valid).Equal(false) + + w = tape.Post("/api/v1/auth/request_password_reset", + H{ + "email": "test@uni-tuebingen.de", + }, + ) + g.Assert(w.Code).Equal(http.StatusOK) + + // state after request + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") + g.Assert(userAfter.ResetPasswordToken.Valid).Equal(true) + + // use token to reset password + w = tape.Post("/api/v1/auth/update_password", + H{ + "reset_password_token": userAfter.ResetPasswordToken.String, + "plain_password": "new_password", + "email": "test@uni-tuebingen.de", + }, + ) + g.Assert(w.Code).Equal(http.StatusOK) + + userAfter2, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter2.Email).Equal("test@uni-tuebingen.de") + g.Assert(userAfter2.ResetPasswordToken.Valid).Equal(false) + + isPasswordValid := auth.CheckPasswordHash("new_password", userAfter2.EncryptedPassword) + g.Assert(isPasswordValid).Equal(true) + + isPasswordValid = auth.CheckPasswordHash("test", userAfter2.EncryptedPassword) + g.Assert(isPasswordValid).Equal(false) + }) + + g.It("Invalid Password-Reset-Token is denied", func() { + + w = tape.Post("/api/v1/auth/update_password", + H{ + "reset_password_token": "invalid_string", + "plain_password": "new_password", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.Email).Equal("test@uni-tuebingen.de") + }) + + g.It("Invalid Email-Confirmation-Token is denied", func() { + + // setup confirmation token + userBefore, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") + userBefore.ConfirmEmailToken = null.StringFrom("testtoken") + stores.User.Update(userBefore) + g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(true) + + w = tape.Post("/api/v1/auth/confirm_email", + H{ + "email": "test@uni-tuebingen.de", + "confirmation_token": "testtoken_wrong", + }, + ) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(true) + }) + + g.It("Correct Email-Confirmation-Token will confirm email", func() { + + // setup confirmation token + userBefore, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userBefore.Email).Equal("test@uni-tuebingen.de") + userBefore.ConfirmEmailToken = null.StringFrom("testtoken") + stores.User.Update(userBefore) + g.Assert(userBefore.ConfirmEmailToken.Valid).Equal(true) + + w = tape.Post("/api/v1/auth/confirm_email", + H{ + "email": "test@uni-tuebingen.de", + "confirmation_token": "testtoken", + }, + ) + g.Assert(w.Code).Equal(http.StatusOK) + + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(userAfter.ConfirmEmailToken.Valid).Equal(false) + }) + + g.It("Should limit requests per minute to do an login", func() { + payload := H{ + "email": "test@uni-tuebingen.de", + "plain_password": "test", + } + + for i := 0; i < 10; i++ { + w = tape.Post("/api/v1/auth/sessions", payload) + } + g.Assert(w.Code).Equal(http.StatusOK) + w = tape.Post("/api/v1/auth/sessions", payload) + g.Assert(w.Code).Equal(http.StatusTooManyRequests) + }) + + g.AfterEach(func() { + tape.AfterEach() + err := redisClient.Set("infomark-logins:1.2.3.4-infomark-logins", "0", 0).Err() + g.Assert(err).Equal(nil) + }) + + }) } diff --git a/api/app/common_test.go b/api/app/common_test.go index d7ebd58..f2f4fb4 100644 --- a/api/app/common_test.go +++ b/api/app/common_test.go @@ -19,49 +19,49 @@ package app import ( - "net/http" - "testing" - "time" + "net/http" + "testing" + "time" - "github.com/franela/goblin" + "github.com/franela/goblin" ) func TestCommon(t *testing.T) { - g := goblin.Goblin(t) + g := goblin.Goblin(t) - tape := NewTape() + tape := NewTape() - g.Describe("Common", func() { + g.Describe("Common", func() { - g.BeforeEach(func() { - tape.BeforeEach() - }) + g.BeforeEach(func() { + tape.BeforeEach() + }) - g.It("Should pong", func() { - w := tape.Get("/api/v1/ping") - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(w.Body.String()).Equal("pong") + g.It("Should pong", func() { + w := tape.Get("/api/v1/ping") + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(w.Body.String()).Equal("pong") - }) + }) - g.It("Too late is too late", func() { + g.It("Too late is too late", func() { - now := NowUTC() - before := now.Add(-time.Hour) - after := now.Add(time.Hour) + now := NowUTC() + before := now.Add(-time.Hour) + after := now.Add(time.Hour) - g.Assert(OverTime(before)).Equal(true) // is over time - g.Assert(OverTime(after)).Equal(false) // is ok + g.Assert(OverTime(before)).Equal(true) // is over time + g.Assert(OverTime(after)).Equal(false) // is ok - // is public - g.Assert(PublicYet(after)).Equal(false) - g.Assert(PublicYet(before)).Equal(true) + // is public + g.Assert(PublicYet(after)).Equal(false) + g.Assert(PublicYet(before)).Equal(true) - }) + }) - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/course.go b/api/app/course.go index 6fb42e5..0e98a77 100644 --- a/api/app/course.go +++ b/api/app/course.go @@ -19,32 +19,32 @@ package app import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // CourseResource specifies course management handler. type CourseResource struct { - Stores *Stores + Stores *Stores } // NewCourseResource create and returns a CourseResource. func NewCourseResource(stores *Stores) *CourseResource { - return &CourseResource{ - Stores: stores, - } + return &CourseResource{ + Stores: stores, + } } // IndexHandler is public endpoint for @@ -57,14 +57,14 @@ func NewCourseResource(stores *Stores) *CourseResource { // RESPONSE: 403,Unauthorized // SUMMARY: list all courses func (rs *CourseResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - // fetch collection of courses from database - courses, err := rs.Stores.Course.GetAll() - - // render JSON reponse - if err = render.RenderList(w, r, rs.newCourseListResponse(courses)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // fetch collection of courses from database + courses, err := rs.Stores.Course.GetAll() + + // render JSON reponse + if err = render.RenderList(w, r, rs.newCourseListResponse(courses)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // CreateHandler is public endpoint for @@ -78,36 +78,36 @@ func (rs *CourseResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: create a new course func (rs *CourseResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &courseRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - course := &model.Course{} - course.Name = data.Name - course.Description = data.Description - course.BeginsAt = data.BeginsAt - course.EndsAt = data.EndsAt - course.RequiredPercentage = data.RequiredPercentage - - // create course entry in database - newCourse, err := rs.Stores.Course.Create(course) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusCreated) - - // return course information of created entry - if err := render.Render(w, r, rs.newCourseResponse(newCourse)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // start from empty Request + data := &courseRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + course := &model.Course{} + course.Name = data.Name + course.Description = data.Description + course.BeginsAt = data.BeginsAt + course.EndsAt = data.EndsAt + course.RequiredPercentage = data.RequiredPercentage + + // create course entry in database + newCourse, err := rs.Stores.Course.Create(course) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusCreated) + + // return course information of created entry + if err := render.Render(w, r, rs.newCourseResponse(newCourse)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -122,20 +122,20 @@ func (rs *CourseResource) CreateHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: get a specific course func (rs *CourseResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `course` is retrieved via middle-ware - course, ok := r.Context().Value(common.CtxKeyCourse).(*model.Course) - if !ok { - render.Render(w, r, ErrInternalServerErrorWithDetails(errors.New("course context is missing"))) - return - } - - // render JSON reponse - if err := render.Render(w, r, rs.newCourseResponse(course)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // `course` is retrieved via middle-ware + course, ok := r.Context().Value(common.CtxKeyCourse).(*model.Course) + if !ok { + render.Render(w, r, ErrInternalServerErrorWithDetails(errors.New("course context is missing"))) + return + } + + // render JSON reponse + if err := render.Render(w, r, rs.newCourseResponse(course)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // EditHandler is public endpoint for @@ -150,30 +150,30 @@ func (rs *CourseResource) GetHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: update a specific course func (rs *CourseResource) EditHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &courseRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - course.Name = data.Name - course.Description = data.Description - course.BeginsAt = data.BeginsAt - course.EndsAt = data.EndsAt - course.RequiredPercentage = data.RequiredPercentage - - // update database entry - if err := rs.Stores.Course.Update(course); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // TODO(patwie): change StatusNoContent - render.Status(r, http.StatusNoContent) + // start from empty Request + data := &courseRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + course.Name = data.Name + course.Description = data.Description + course.BeginsAt = data.BeginsAt + course.EndsAt = data.EndsAt + course.RequiredPercentage = data.RequiredPercentage + + // update database entry + if err := rs.Stores.Course.Update(course); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // TODO(patwie): change StatusNoContent + render.Status(r, http.StatusNoContent) } // DeleteHandler is public endpoint for @@ -187,19 +187,19 @@ func (rs *CourseResource) EditHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: delete a specific course func (rs *CourseResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - // Warning: There is more to do! Currently we just dis-enroll all students, - // remove all sheets and delete the course it self FROM THE DATABASE. - // This does not remove gradings and the sheets or touches any file! + // Warning: There is more to do! Currently we just dis-enroll all students, + // remove all sheets and delete the course it self FROM THE DATABASE. + // This does not remove gradings and the sheets or touches any file! - // update database entry - if err := rs.Stores.Course.Delete(course.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Course.Delete(course.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // IndexEnrollmentsHandler is public endpoint for @@ -224,58 +224,58 @@ func (rs *CourseResource) DeleteHandler(w http.ResponseWriter, r *http.Request) // by first_name, last_name or email. The 'q' does not need be wrapped by '%'. But all other query strings // do need to be wrapped by '%' to indicated end and start of a string. func (rs *CourseResource) IndexEnrollmentsHandler(w http.ResponseWriter, r *http.Request) { - // /courses/1/enrollments?roles=0,1 - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - filterQuery := helper.StringFromURL(r, "q", "") - - // extract filters - filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) - filterFirstName := helper.StringFromURL(r, "first_name", "%%") - filterLastName := helper.StringFromURL(r, "last_name", "%%") - filterEmail := helper.StringFromURL(r, "email", "%%") - filterSubject := helper.StringFromURL(r, "subject", "%%") - filterLanguage := helper.StringFromURL(r, "language", "%%") - - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - if givenRole == authorize.STUDENT { - // students cannot query other students - filterRoles = []string{"1", "2"} - } - - var ( - enrolledUsers []model.UserCourse - err error - ) - - if filterQuery != "" { - filterQuery = fmt.Sprintf("%%%s%%", filterQuery) - enrolledUsers, err = rs.Stores.Course.FindEnrolledUsers(course.ID, - filterRoles, filterQuery, - ) - } else { - enrolledUsers, err = rs.Stores.Course.EnrolledUsers(course.ID, - filterRoles, filterFirstName, filterLastName, filterEmail, - filterSubject, filterLanguage, - ) - - } - - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - enrolledUsers = EnsurePrivacyInEnrollments(enrolledUsers, givenRole) - - // render JSON reponse - if err = render.RenderList(w, r, newEnrollmentListResponse(enrolledUsers)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // /courses/1/enrollments?roles=0,1 + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + filterQuery := helper.StringFromURL(r, "q", "") + + // extract filters + filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) + filterFirstName := helper.StringFromURL(r, "first_name", "%%") + filterLastName := helper.StringFromURL(r, "last_name", "%%") + filterEmail := helper.StringFromURL(r, "email", "%%") + filterSubject := helper.StringFromURL(r, "subject", "%%") + filterLanguage := helper.StringFromURL(r, "language", "%%") + + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + if givenRole == authorize.STUDENT { + // students cannot query other students + filterRoles = []string{"1", "2"} + } + + var ( + enrolledUsers []model.UserCourse + err error + ) + + if filterQuery != "" { + filterQuery = fmt.Sprintf("%%%s%%", filterQuery) + enrolledUsers, err = rs.Stores.Course.FindEnrolledUsers(course.ID, + filterRoles, filterQuery, + ) + } else { + enrolledUsers, err = rs.Stores.Course.EnrolledUsers(course.ID, + filterRoles, filterFirstName, filterLastName, filterEmail, + filterSubject, filterLanguage, + ) + + } + + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + enrolledUsers = EnsurePrivacyInEnrollments(enrolledUsers, givenRole) + + // render JSON reponse + if err = render.RenderList(w, r, newEnrollmentListResponse(enrolledUsers)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // GetUserEnrollmentHandler is public endpoint for @@ -290,27 +290,27 @@ func (rs *CourseResource) IndexEnrollmentsHandler(w http.ResponseWriter, r *http // RESPONSE: 403,Unauthorized // SUMMARY: give enrollment of a specific user in a specific course func (rs *CourseResource) GetUserEnrollmentHandler(w http.ResponseWriter, r *http.Request) { - // /courses/1/enrollments?roles=0,1 - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - user := r.Context().Value(common.CtxKeyUser).(*model.User) + // /courses/1/enrollments?roles=0,1 + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + user := r.Context().Value(common.CtxKeyUser).(*model.User) - // find role in the course + // find role in the course - userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, user.ID) - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, user.ID) + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - resp := newEnrollmentResponse(userEnrollment) + resp := newEnrollmentResponse(userEnrollment) - // render JSON reponse - if err = render.Render(w, r, resp); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.Render(w, r, resp); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // DeleteUserEnrollmentHandler is public endpoint for @@ -325,29 +325,29 @@ func (rs *CourseResource) GetUserEnrollmentHandler(w http.ResponseWriter, r *htt // RESPONSE: 403,Unauthorized // SUMMARY: give enrollment of a specific user in a specific course func (rs *CourseResource) DeleteUserEnrollmentHandler(w http.ResponseWriter, r *http.Request) { - // /courses/1/enrollments?roles=0,1 - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - user := r.Context().Value(common.CtxKeyUser).(*model.User) + // /courses/1/enrollments?roles=0,1 + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + user := r.Context().Value(common.CtxKeyUser).(*model.User) - // find role in the course + // find role in the course - userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, user.ID) - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, user.ID) + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - if int64(userEnrollment.Role) > int64(authorize.STUDENT) { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("Cannot disenroll tutors"))) - return - } + if int64(userEnrollment.Role) > int64(authorize.STUDENT) { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("Cannot disenroll tutors"))) + return + } - if err := rs.Stores.Course.Disenroll(course.ID, user.ID); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + if err := rs.Stores.Course.Disenroll(course.ID, user.ID); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // ChangeRole is public endpoint for @@ -363,25 +363,25 @@ func (rs *CourseResource) DeleteUserEnrollmentHandler(w http.ResponseWriter, r * // RESPONSE: 403,Unauthorized // SUMMARY: change role of specific user func (rs *CourseResource) ChangeRole(w http.ResponseWriter, r *http.Request) { - // /courses/1/enrollments?roles=0,1 + // /courses/1/enrollments?roles=0,1 - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - user := r.Context().Value(common.CtxKeyUser).(*model.User) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + user := r.Context().Value(common.CtxKeyUser).(*model.User) - data := &changeRoleInCourseRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + data := &changeRoleInCourseRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - // update database entry - if err := rs.Stores.Course.UpdateRole(course.ID, user.ID, data.Role); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Course.UpdateRole(course.ID, user.ID, data.Role); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // EnrollHandler is public endpoint for @@ -396,32 +396,32 @@ func (rs *CourseResource) ChangeRole(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: enroll a user into a course func (rs *CourseResource) EnrollHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - role := int64(0) - if accessClaims.Root { - role = int64(2) - } + role := int64(0) + if accessClaims.Root { + role = int64(2) + } - // update database entry - if err := rs.Stores.Course.Enroll(course.ID, accessClaims.LoginID, role); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Course.Enroll(course.ID, accessClaims.LoginID, role); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + userEnrollment, err := rs.Stores.Course.GetUserEnrollment(course.ID, accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - render.Status(r, http.StatusCreated) + render.Status(r, http.StatusCreated) - if err := render.Render(w, r, newEnrollmentResponse(userEnrollment)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + if err := render.Render(w, r, newEnrollmentResponse(userEnrollment)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -436,23 +436,23 @@ func (rs *CourseResource) EnrollHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: disenroll a user from a course func (rs *CourseResource) DisenrollHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - if givenRole == authorize.TUTOR { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("tutors cannot disenroll from a course"))) - return - } + if givenRole == authorize.TUTOR { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("tutors cannot disenroll from a course"))) + return + } - // update database entry - if err := rs.Stores.Course.Disenroll(course.ID, accessClaims.LoginID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Course.Disenroll(course.ID, accessClaims.LoginID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // SendEmailHandler is public endpoint for @@ -475,48 +475,48 @@ func (rs *CourseResource) DisenrollHandler(w http.ResponseWriter, r *http.Reques // SUMMARY: send email to entire course filtered func (rs *CourseResource) SendEmailHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) - - data := &EmailRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // extract filters - filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) - filterFirstName := "%%" - filterLastName := "%%" - filterEmail := "%%" - filterSubject := "%%" - filterLanguage := "%%" - - recipients, err := rs.Stores.Course.EnrolledUsers(course.ID, - filterRoles, filterFirstName, filterLastName, filterEmail, - filterSubject, filterLanguage, - ) - - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - for _, recipient := range recipients { - // add sender identity - msg := email.NewEmailFromUser( - recipient.Email, - data.Subject, - data.Body, - accessUser, - ) - - email.OutgoingEmailsChannel <- msg - } + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) + + data := &EmailRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // extract filters + filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) + filterFirstName := "%%" + filterLastName := "%%" + filterEmail := "%%" + filterSubject := "%%" + filterLanguage := "%%" + + recipients, err := rs.Stores.Course.EnrolledUsers(course.ID, + filterRoles, filterFirstName, filterLastName, filterEmail, + filterSubject, filterLanguage, + ) + + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + for _, recipient := range recipients { + // add sender identity + msg := email.NewEmailFromUser( + recipient.Email, + data.Subject, + data.Body, + accessUser, + ) + + email.OutgoingEmailsChannel <- msg + } } @@ -530,23 +530,23 @@ func (rs *CourseResource) SendEmailHandler(w http.ResponseWriter, r *http.Reques // RESPONSE: 403,Unauthorized // SUMMARY: get all points for the request identity func (rs *CourseResource) PointsHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - sheetPoints, err := rs.Stores.Course.PointsForUser(accessClaims.LoginID, course.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + sheetPoints, err := rs.Stores.Course.PointsForUser(accessClaims.LoginID, course.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - // resp := &SheetPointsResponse{SheetPoints: sheetPoints} + // resp := &SheetPointsResponse{SheetPoints: sheetPoints} - if err := render.RenderList(w, r, newSheetPointsListResponse(sheetPoints)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + if err := render.RenderList(w, r, newSheetPointsListResponse(sheetPoints)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // BidsHandler is public endpoint for @@ -559,40 +559,40 @@ func (rs *CourseResource) PointsHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: get all bids for the request identity in a course func (rs *CourseResource) BidsHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - var bids []model.GroupBid - var err error + var bids []model.GroupBid + var err error - if givenRole == authorize.TUTOR { - // tutors see nothing - render.Render(w, r, ErrBadRequestWithDetails(errors.New("tutors cannot have bids for a group in a course"))) - return + if givenRole == authorize.TUTOR { + // tutors see nothing + render.Render(w, r, ErrBadRequestWithDetails(errors.New("tutors cannot have bids for a group in a course"))) + return - } + } - if givenRole == authorize.STUDENT { - // students only see their own bids - bids, err = rs.Stores.Group.GetBidsForCourseForUser(course.ID, accessClaims.LoginID) - } else { - // admins see all (to later setup the bid) - bids, err = rs.Stores.Group.GetBidsForCourse(course.ID) - } + if givenRole == authorize.STUDENT { + // students only see their own bids + bids, err = rs.Stores.Group.GetBidsForCourseForUser(course.ID, accessClaims.LoginID) + } else { + // admins see all (to later setup the bid) + bids, err = rs.Stores.Group.GetBidsForCourse(course.ID) + } - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - if err := render.RenderList(w, r, newGroupBidsListResponse(bids)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + if err := render.RenderList(w, r, newGroupBidsListResponse(bids)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // ............................................................................. @@ -602,50 +602,50 @@ func (rs *CourseResource) BidsHandler(w http.ResponseWriter, r *http.Request) { // the Course could not be found, we stop here and return a 404. // We do NOT check whether the course is authorized to get this course. func (rs *CourseResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO: check permission if inquirer of request is allowed to access this course - // Should be done via another middleware - var courseID int64 - var err error - - // try to get id from URL - if courseID, err = strconv.ParseInt(chi.URLParam(r, "course_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific course in database - course, err := rs.Stores.Course.Get(courseID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // serve next - ctx := context.WithValue(r.Context(), common.CtxKeyCourse, course) - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO: check permission if inquirer of request is allowed to access this course + // Should be done via another middleware + var courseID int64 + var err error + + // try to get id from URL + if courseID, err = strconv.ParseInt(chi.URLParam(r, "course_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific course in database + course, err := rs.Stores.Course.Get(courseID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // serve next + ctx := context.WithValue(r.Context(), common.CtxKeyCourse, course) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } // RoleContext middleware extracts the role of an identity in a given course func (rs *CourseResource) RoleContext(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - // find role in the course - courseRole, err := rs.Stores.Course.RoleInCourse(accessClaims.LoginID, course.ID) - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - if accessClaims.Root { - courseRole = authorize.ADMIN - } - - // serve next - ctx := context.WithValue(r.Context(), common.CtxKeyCourseRole, courseRole) - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + // find role in the course + courseRole, err := rs.Stores.Course.RoleInCourse(accessClaims.LoginID, course.ID) + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + if accessClaims.Root { + courseRole = authorize.ADMIN + } + + // serve next + ctx := context.WithValue(r.Context(), common.CtxKeyCourseRole, courseRole) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/course_test.go b/api/app/course_test.go index ac61a29..056e5e4 100644 --- a/api/app/course_test.go +++ b/api/app/course_test.go @@ -19,572 +19,572 @@ package app import ( - "encoding/json" - "net/http" - "testing" - "time" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/franela/goblin" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/franela/goblin" ) func DBGetInt(tape *Tape, stmt string, param1 int64) (int, error) { - var rsl int - err := tape.DB.Get(&rsl, stmt, param1) - return rsl, err + var rsl int + err := tape.DB.Get(&rsl, stmt, param1) + return rsl, err } func DBGetInt2(tape *Tape, stmt string, param1 int64, param2 int64) (int, error) { - var rsl int - err := tape.DB.Get(&rsl, stmt, param1, param2) - return rsl, err + var rsl int + err := tape.DB.Get(&rsl, stmt, param1, param2) + return rsl, err } func TestCourse(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - // email.DefaultMail = email.TerminalMail - go email.BackgroundSend(email.OutgoingEmailsChannel) - - tape := &Tape{} - - var stores *Stores - - g.Describe("Course", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) - - g.It("Query should require claims", func() { - - w := tape.Get("/api/v1/courses") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims("/api/v1/courses", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - }) - - g.It("Should list all courses", func() { - w := tape.GetWithClaims("/api/v1/courses", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - coursesActual := []model.Course{} - err := json.NewDecoder(w.Body).Decode(&coursesActual) - g.Assert(err).Equal(nil) - g.Assert(len(coursesActual)).Equal(2) - }) - - g.It("Should get a specific course", func() { - - w := tape.GetWithClaims("/api/v1/courses/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - courseActual := &courseResponse{} - err := json.NewDecoder(w.Body).Decode(courseActual) - g.Assert(err).Equal(nil) - - courseExpected, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(courseActual.ID).Equal(courseExpected.ID) - g.Assert(courseActual.Name).Equal(courseExpected.Name) - g.Assert(courseActual.Description).Equal(courseExpected.Description) - g.Assert(courseActual.BeginsAt.Equal(courseExpected.BeginsAt)).Equal(true) - g.Assert(courseActual.EndsAt.Equal(courseExpected.EndsAt)).Equal(true) - g.Assert(courseActual.RequiredPercentage).Equal(courseExpected.RequiredPercentage) - }) - - g.It("Should be able to filter enrollments (all)", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1", - courseActive.ID, - ) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/enrollments", 1, true) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) - - g.It("Should be able to filter enrollments (students only)", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseActive.ID, - ) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", 1, true) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) - - g.It("Should be able to query enrollments (tutor+admin only)", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - enrollmentsExpected, err := stores.Course.FindEnrolledUsers(courseActive.ID, - []string{"0", "1", "2"}, "%chi%", - ) - - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?q=chi", 1, false) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(len(enrollmentsExpected)) - }) - - g.It("Should be able to filter enrollments (tutors only)", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 1", - courseActive.ID, - ) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=1", 1, false) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) - - g.It("Should be able to filter enrollments (students+tutors only)", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role IN (0,1)", - courseActive.ID, - ) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0,1", 1, false) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) - - g.It("Should be able to filter enrollments (but receive only tutors + admins), when role=student", func() { - courseActive, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role IN (1, 2)", - courseActive.ID, - ) - g.Assert(err).Equal(nil) - - // 112 is a student - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", 112, false) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) - - g.It("Should be able to filter enrollments (but not see field protected by privacy), when role=tutor,student", func() { - // 112 is a student - userID := int64(112) - w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", userID, false) - enrollmentsActual := []enrollmentResponse{} - err := json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - - for _, el := range enrollmentsActual { - g.Assert(el.User.StudentNumber).Equal("") - } - }) - - g.It("Creating course should require claims", func() { - w := tape.Post("/api/v1/courses", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Creating course should require body", func() { - w := tape.PlayWithClaims("POST", "/api/v1/courses", 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should create valid course", func() { - - coursesBefore, err := stores.Course.GetAll() - g.Assert(err).Equal(nil) - - entrySent := courseRequest{ - Name: "Info2_new", - Description: "Lorem Ipsum_new", - BeginsAt: helper.Time(time.Now()), - EndsAt: helper.Time(time.Now().Add(time.Hour * 1)), - RequiredPercentage: 43, - } - - g.Assert(entrySent.Validate()).Equal(nil) - - // students - w := tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin in course (cannot be admin, course does not exists yet) - w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 1, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 1, true) - g.Assert(w.Code).Equal(http.StatusCreated) - - // verify body - courseReturn := &courseResponse{} - err = json.NewDecoder(w.Body).Decode(&courseReturn) - g.Assert(courseReturn.Name).Equal(entrySent.Name) - g.Assert(courseReturn.Description).Equal(entrySent.Description) - g.Assert(courseReturn.BeginsAt.Equal(entrySent.BeginsAt)).Equal(true) - g.Assert(courseReturn.EndsAt.Equal(entrySent.EndsAt)).Equal(true) - g.Assert(courseReturn.RequiredPercentage).Equal(entrySent.RequiredPercentage) - - // verify database - courseNew, err := stores.Course.Get(courseReturn.ID) - g.Assert(err).Equal(nil) - g.Assert(courseReturn.Name).Equal(courseNew.Name) - g.Assert(courseReturn.Description).Equal(courseNew.Description) - g.Assert(courseReturn.BeginsAt.Equal(courseNew.BeginsAt)).Equal(true) - g.Assert(courseReturn.EndsAt.Equal(courseNew.EndsAt)).Equal(true) - g.Assert(courseReturn.RequiredPercentage).Equal(courseNew.RequiredPercentage) - - coursesAfter, err := stores.Course.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(coursesAfter)).Equal(len(coursesBefore) + 1) - }) - - g.It("Should send email to all enrolled users", func() { - w := tape.PostWithClaims("/api/v1/courses/1/emails", H{ - "subject": "subj", - "body": "text", - }, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Changes should require access claims", func() { - w := tape.Put("/api/v1/courses/1", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Should perform updates", func() { - - entrySent := courseRequest{ - Name: "Info2_update", - Description: "Lorem Ipsum_update", - BeginsAt: helper.Time(time.Now()), - EndsAt: helper.Time(time.Now()), - RequiredPercentage: 99, - } - - g.Assert(entrySent.Validate()).Equal(nil) - - // students - w := tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.Name).Equal(entrySent.Name) - g.Assert(entryAfter.Description).Equal(entrySent.Description) - g.Assert(entryAfter.BeginsAt.Equal(entrySent.BeginsAt)).Equal(true) - g.Assert(entryAfter.EndsAt.Equal(entrySent.EndsAt)).Equal(true) - g.Assert(entryAfter.RequiredPercentage).Equal(entrySent.RequiredPercentage) - }) - - g.It("Should delete when valid access claims", func() { - entriesBefore, err := stores.Course.GetAll() - g.Assert(err).Equal(nil) - - w := tape.Delete("/api/v1/courses/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // verify nothing has changes - entriesAfter, err := stores.Course.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) - - // students - w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // verify a course less exists - entriesAfter, err = stores.Course.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) - - g.It("Non-Global root enroll as students", func() { - - courseID := int64(1) - userID := int64(112) - - w := tape.PostWithClaims("/api/v1/courses/1/enrollments", helper.H{}, userID, false) - g.Assert(w.Code).Equal(http.StatusCreated) - - role, err := DBGetInt2( - tape, - "SELECT role FROM user_course WHERE course_id = $1 and user_id = $2", - courseID, userID, - ) - g.Assert(err).Equal(nil) - g.Assert(role).Equal(0) - - }) - - g.It("Global root enroll as admins", func() { - - courseID := int64(1) - userID := int64(112) - - w := tape.PostWithClaims("/api/v1/courses/1/enrollments", helper.H{}, userID, true) - g.Assert(w.Code).Equal(http.StatusCreated) - - role, err := DBGetInt2( - tape, - "SELECT role FROM user_course WHERE course_id = $1 and user_id = $2", - courseID, userID, - ) - g.Assert(err).Equal(nil) - g.Assert(role).Equal(2) - - }) - - g.It("Can disenroll from course", func() { - - courseID := int64(1) - - numberEnrollmentsBefore, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - - w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) - - numberEnrollmentsAfter, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore - 1) + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + // email.DefaultMail = email.TerminalMail + go email.BackgroundSend(email.OutgoingEmailsChannel) + + tape := &Tape{} + + var stores *Stores + + g.Describe("Course", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) + + g.It("Query should require claims", func() { + + w := tape.Get("/api/v1/courses") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims("/api/v1/courses", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + }) + + g.It("Should list all courses", func() { + w := tape.GetWithClaims("/api/v1/courses", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + coursesActual := []model.Course{} + err := json.NewDecoder(w.Body).Decode(&coursesActual) + g.Assert(err).Equal(nil) + g.Assert(len(coursesActual)).Equal(2) + }) + + g.It("Should get a specific course", func() { + + w := tape.GetWithClaims("/api/v1/courses/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + courseActual := &courseResponse{} + err := json.NewDecoder(w.Body).Decode(courseActual) + g.Assert(err).Equal(nil) + + courseExpected, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(courseActual.ID).Equal(courseExpected.ID) + g.Assert(courseActual.Name).Equal(courseExpected.Name) + g.Assert(courseActual.Description).Equal(courseExpected.Description) + g.Assert(courseActual.BeginsAt.Equal(courseExpected.BeginsAt)).Equal(true) + g.Assert(courseActual.EndsAt.Equal(courseExpected.EndsAt)).Equal(true) + g.Assert(courseActual.RequiredPercentage).Equal(courseExpected.RequiredPercentage) + }) + + g.It("Should be able to filter enrollments (all)", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1", + courseActive.ID, + ) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/enrollments", 1, true) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) + + g.It("Should be able to filter enrollments (students only)", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseActive.ID, + ) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", 1, true) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) + + g.It("Should be able to query enrollments (tutor+admin only)", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + enrollmentsExpected, err := stores.Course.FindEnrolledUsers(courseActive.ID, + []string{"0", "1", "2"}, "%chi%", + ) + + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?q=chi", 1, false) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(len(enrollmentsExpected)) + }) + + g.It("Should be able to filter enrollments (tutors only)", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 1", + courseActive.ID, + ) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=1", 1, false) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) + + g.It("Should be able to filter enrollments (students+tutors only)", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role IN (0,1)", + courseActive.ID, + ) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0,1", 1, false) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) + + g.It("Should be able to filter enrollments (but receive only tutors + admins), when role=student", func() { + courseActive, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role IN (1, 2)", + courseActive.ID, + ) + g.Assert(err).Equal(nil) + + // 112 is a student + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", 112, false) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) + + g.It("Should be able to filter enrollments (but not see field protected by privacy), when role=tutor,student", func() { + // 112 is a student + userID := int64(112) + w := tape.GetWithClaims("/api/v1/courses/1/enrollments?roles=0", userID, false) + enrollmentsActual := []enrollmentResponse{} + err := json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + + for _, el := range enrollmentsActual { + g.Assert(el.User.StudentNumber).Equal("") + } + }) + + g.It("Creating course should require claims", func() { + w := tape.Post("/api/v1/courses", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Creating course should require body", func() { + w := tape.PlayWithClaims("POST", "/api/v1/courses", 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should create valid course", func() { + + coursesBefore, err := stores.Course.GetAll() + g.Assert(err).Equal(nil) + + entrySent := courseRequest{ + Name: "Info2_new", + Description: "Lorem Ipsum_new", + BeginsAt: helper.Time(time.Now()), + EndsAt: helper.Time(time.Now().Add(time.Hour * 1)), + RequiredPercentage: 43, + } + + g.Assert(entrySent.Validate()).Equal(nil) + + // students + w := tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin in course (cannot be admin, course does not exists yet) + w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 1, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PlayDataWithClaims("POST", "/api/v1/courses", tape.ToH(entrySent), 1, true) + g.Assert(w.Code).Equal(http.StatusCreated) + + // verify body + courseReturn := &courseResponse{} + err = json.NewDecoder(w.Body).Decode(&courseReturn) + g.Assert(courseReturn.Name).Equal(entrySent.Name) + g.Assert(courseReturn.Description).Equal(entrySent.Description) + g.Assert(courseReturn.BeginsAt.Equal(entrySent.BeginsAt)).Equal(true) + g.Assert(courseReturn.EndsAt.Equal(entrySent.EndsAt)).Equal(true) + g.Assert(courseReturn.RequiredPercentage).Equal(entrySent.RequiredPercentage) + + // verify database + courseNew, err := stores.Course.Get(courseReturn.ID) + g.Assert(err).Equal(nil) + g.Assert(courseReturn.Name).Equal(courseNew.Name) + g.Assert(courseReturn.Description).Equal(courseNew.Description) + g.Assert(courseReturn.BeginsAt.Equal(courseNew.BeginsAt)).Equal(true) + g.Assert(courseReturn.EndsAt.Equal(courseNew.EndsAt)).Equal(true) + g.Assert(courseReturn.RequiredPercentage).Equal(courseNew.RequiredPercentage) + + coursesAfter, err := stores.Course.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(coursesAfter)).Equal(len(coursesBefore) + 1) + }) + + g.It("Should send email to all enrolled users", func() { + w := tape.PostWithClaims("/api/v1/courses/1/emails", H{ + "subject": "subj", + "body": "text", + }, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Changes should require access claims", func() { + w := tape.Put("/api/v1/courses/1", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Should perform updates", func() { + + entrySent := courseRequest{ + Name: "Info2_update", + Description: "Lorem Ipsum_update", + BeginsAt: helper.Time(time.Now()), + EndsAt: helper.Time(time.Now()), + RequiredPercentage: 99, + } + + g.Assert(entrySent.Validate()).Equal(nil) + + // students + w := tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1", tape.ToH(entrySent), 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.Name).Equal(entrySent.Name) + g.Assert(entryAfter.Description).Equal(entrySent.Description) + g.Assert(entryAfter.BeginsAt.Equal(entrySent.BeginsAt)).Equal(true) + g.Assert(entryAfter.EndsAt.Equal(entrySent.EndsAt)).Equal(true) + g.Assert(entryAfter.RequiredPercentage).Equal(entrySent.RequiredPercentage) + }) + + g.It("Should delete when valid access claims", func() { + entriesBefore, err := stores.Course.GetAll() + g.Assert(err).Equal(nil) + + w := tape.Delete("/api/v1/courses/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // verify nothing has changes + entriesAfter, err := stores.Course.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) + + // students + w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PlayWithClaims("DELETE", "/api/v1/courses/1", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // verify a course less exists + entriesAfter, err = stores.Course.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + }) + + g.It("Non-Global root enroll as students", func() { + + courseID := int64(1) + userID := int64(112) + + w := tape.PostWithClaims("/api/v1/courses/1/enrollments", helper.H{}, userID, false) + g.Assert(w.Code).Equal(http.StatusCreated) + + role, err := DBGetInt2( + tape, + "SELECT role FROM user_course WHERE course_id = $1 and user_id = $2", + courseID, userID, + ) + g.Assert(err).Equal(nil) + g.Assert(role).Equal(0) + + }) + + g.It("Global root enroll as admins", func() { + + courseID := int64(1) + userID := int64(112) + + w := tape.PostWithClaims("/api/v1/courses/1/enrollments", helper.H{}, userID, true) + g.Assert(w.Code).Equal(http.StatusCreated) + + role, err := DBGetInt2( + tape, + "SELECT role FROM user_course WHERE course_id = $1 and user_id = $2", + courseID, userID, + ) + g.Assert(err).Equal(nil) + g.Assert(role).Equal(2) + + }) + + g.It("Can disenroll from course", func() { + + courseID := int64(1) + + numberEnrollmentsBefore, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + + w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) + + numberEnrollmentsAfter, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore - 1) - }) + }) - g.It("Can disenroll a specific user from course", func() { + g.It("Can disenroll a specific user from course", func() { - courseID := int64(1) - - numberEnrollmentsBefore, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - - // admin - w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments/113", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + courseID := int64(1) + + numberEnrollmentsBefore, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + + // admin + w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments/113", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - numberEnrollmentsAfter, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore - 1) + numberEnrollmentsAfter, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore - 1) - }) + }) - g.It("Cannot disenroll a specific user from course if user is tutor", func() { + g.It("Cannot disenroll a specific user from course if user is tutor", func() { - courseID := int64(1) + courseID := int64(1) - numberEnrollmentsBefore, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) + numberEnrollmentsBefore, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) - // admin - w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments/2", 1, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) + // admin + w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments/2", 1, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) - numberEnrollmentsAfter, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore) + numberEnrollmentsAfter, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore) - }) + }) - g.It("Cannot disenroll as a tutor from course", func() { - courseID := int64(1) - userID := int64(2) + g.It("Cannot disenroll as a tutor from course", func() { + courseID := int64(1) + userID := int64(2) - numberEnrollmentsBefore, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) + numberEnrollmentsBefore, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) - w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments", userID, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) + w := tape.DeleteWithClaims("/api/v1/courses/1/enrollments", userID, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) - numberEnrollmentsAfter, err := DBGetInt( - tape, - "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", - courseID, - ) - g.Assert(err).Equal(nil) - g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore) - }) + numberEnrollmentsAfter, err := DBGetInt( + tape, + "SELECT count(*) FROM user_course WHERE course_id = $1 and role = 0", + courseID, + ) + g.Assert(err).Equal(nil) + g.Assert(numberEnrollmentsAfter).Equal(numberEnrollmentsBefore) + }) - g.It("should see bids in course", func() { + g.It("should see bids in course", func() { - // tutors cannot use this - w := tape.GetWithClaims("/api/v1/courses/1/bids", 2, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) + // tutors cannot use this + w := tape.GetWithClaims("/api/v1/courses/1/bids", 2, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) - // admins will see all - w = tape.GetWithClaims("/api/v1/courses/1/bids", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + // admins will see all + w = tape.GetWithClaims("/api/v1/courses/1/bids", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - // students will see their own - w = tape.GetWithClaims("/api/v1/courses/1/bids", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // students will see their own + w = tape.GetWithClaims("/api/v1/courses/1/bids", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Show user enrollement info", func() { + g.It("Show user enrollement info", func() { - w := tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 122, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w := tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 122, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 3, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 3, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - result := enrollmentResponse{} + result := enrollmentResponse{} - w = tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - err := json.NewDecoder(w.Body).Decode(&result) - g.Assert(err).Equal(nil) - g.Assert(result.User.ID).Equal(int64(2)) - g.Assert(result.Role).Equal(int64(1)) - - w = tape.GetWithClaims("/api/v1/courses/1/enrollments/112", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&result) - g.Assert(err).Equal(nil) - g.Assert(result.User.ID).Equal(int64(112)) - g.Assert(result.Role).Equal(int64(0)) + w = tape.GetWithClaims("/api/v1/courses/1/enrollments/2", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + err := json.NewDecoder(w.Body).Decode(&result) + g.Assert(err).Equal(nil) + g.Assert(result.User.ID).Equal(int64(2)) + g.Assert(result.Role).Equal(int64(1)) + + w = tape.GetWithClaims("/api/v1/courses/1/enrollments/112", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&result) + g.Assert(err).Equal(nil) + g.Assert(result.User.ID).Equal(int64(112)) + g.Assert(result.Role).Equal(int64(0)) - }) + }) - g.It("Should update role", func() { + g.It("Should update role", func() { - w := tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w := tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 3, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 3, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.PutWithClaims("/api/v1/courses/1/enrollments/112", H{"role": 1}, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - w = tape.GetWithClaims("/api/v1/courses/1/enrollments/112", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/enrollments/112", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - result := enrollmentResponse{} - err := json.NewDecoder(w.Body).Decode(&result) - g.Assert(err).Equal(nil) - g.Assert(result.User.ID).Equal(int64(112)) - g.Assert(result.Role).Equal(int64(1)) + result := enrollmentResponse{} + err := json.NewDecoder(w.Body).Decode(&result) + g.Assert(err).Equal(nil) + g.Assert(result.User.ID).Equal(int64(112)) + g.Assert(result.Role).Equal(int64(1)) - }) + }) - g.It("Permission test", func() { - url := "/api/v1/courses/1" + g.It("Permission test", func() { + url := "/api/v1/courses/1" - // global root can do whatever they want - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + // global root can do whatever they want + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled tutors can access - w = tape.GetWithClaims(url, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled tutors can access + w = tape.GetWithClaims(url, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled students can access - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled students can access + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // disenroll student - w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // disenroll student + w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // cannot access anymore - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) + // cannot access anymore + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/errors.go b/api/app/errors.go index c2c276e..7e36aeb 100644 --- a/api/app/errors.go +++ b/api/app/errors.go @@ -19,98 +19,98 @@ package app import ( - "net/http" + "net/http" - "github.com/go-chi/render" - validation "github.com/go-ozzo/ozzo-validation" + "github.com/go-chi/render" + validation "github.com/go-ozzo/ozzo-validation" ) // ErrResponse renderer type for handling all sorts of errors. type ErrResponse struct { - Err error `json:"-"` // low-level runtime error - HTTPStatusCode int `json:"-"` // http response status code + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code - StatusText string `json:"status"` // user-level status message - AppCode int64 `json:"code,omitempty"` // application-specific error code - ErrorText string `json:"error,omitempty"` // application-level error message, for debugging - ValidationErrors validation.Errors `json:"errors,omitempty"` // user level model validation errors + StatusText string `json:"status"` // user-level status message + AppCode int64 `json:"code,omitempty"` // application-specific error code + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging + ValidationErrors validation.Errors `json:"errors,omitempty"` // user level model validation errors } // Render sets the application-specific error code in AppCode. func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { - render.Status(r, e.HTTPStatusCode) - return nil + render.Status(r, e.HTTPStatusCode) + return nil } // ErrRender returns status 422 Unprocessable Entity rendering response error. func ErrRender(err error) render.Renderer { - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusUnprocessableEntity, - StatusText: http.StatusText(http.StatusUnprocessableEntity), - ErrorText: err.Error(), - } + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusUnprocessableEntity, + StatusText: http.StatusText(http.StatusUnprocessableEntity), + ErrorText: err.Error(), + } } // ErrBadRequestWithDetails returns status 400 with a text func ErrBadRequestWithDetails(err error) *ErrResponse { - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusBadRequest, - StatusText: http.StatusText(http.StatusBadRequest), - ErrorText: err.Error(), - } + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusBadRequest, + StatusText: http.StatusText(http.StatusBadRequest), + ErrorText: err.Error(), + } } // ErrInternalServerErrorWithDetails returns status 500 with a text func ErrInternalServerErrorWithDetails(err error) *ErrResponse { - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusInternalServerError, - StatusText: http.StatusText(http.StatusInternalServerError), - ErrorText: err.Error(), - } + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusInternalServerError, + StatusText: http.StatusText(http.StatusInternalServerError), + ErrorText: err.Error(), + } } // ErrTimeoutWithDetails returns status 504 with a text func ErrTimeoutWithDetails(err error) *ErrResponse { - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusGatewayTimeout, - StatusText: http.StatusText(http.StatusGatewayTimeout), - ErrorText: err.Error(), - } + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusGatewayTimeout, + StatusText: http.StatusText(http.StatusGatewayTimeout), + ErrorText: err.Error(), + } } // ErrUnauthorizedWithDetails returns status 403 with a text // e.g. "User doesn't have enough privilege" func ErrUnauthorizedWithDetails(err error) *ErrResponse { - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusForbidden, - StatusText: http.StatusText(http.StatusForbidden), - ErrorText: err.Error(), - } + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusForbidden, + StatusText: http.StatusText(http.StatusForbidden), + ErrorText: err.Error(), + } } // see https://stackoverflow.com/a/50143519/7443104 var ( - // ErrBadRequest returns status 400 Bad Request for malformed request body. - ErrBadRequest = &ErrResponse{HTTPStatusCode: http.StatusBadRequest, StatusText: http.StatusText(http.StatusBadRequest)} + // ErrBadRequest returns status 400 Bad Request for malformed request body. + ErrBadRequest = &ErrResponse{HTTPStatusCode: http.StatusBadRequest, StatusText: http.StatusText(http.StatusBadRequest)} - // ErrUnauthorized returns 401 Unauthorized. - // e.g. "User has not logged-in" - ErrUnauthenticated = &ErrResponse{HTTPStatusCode: http.StatusUnauthorized, StatusText: http.StatusText(http.StatusUnauthorized)} + // ErrUnauthorized returns 401 Unauthorized. + // e.g. "User has not logged-in" + ErrUnauthenticated = &ErrResponse{HTTPStatusCode: http.StatusUnauthorized, StatusText: http.StatusText(http.StatusUnauthorized)} - // ErrForbidden returns status 403 Forbidden for unauthorized request. - // e.g. "User doesn't have enough privilege" - ErrUnauthorized = &ErrResponse{HTTPStatusCode: http.StatusForbidden, StatusText: http.StatusText(http.StatusForbidden)} + // ErrForbidden returns status 403 Forbidden for unauthorized request. + // e.g. "User doesn't have enough privilege" + ErrUnauthorized = &ErrResponse{HTTPStatusCode: http.StatusForbidden, StatusText: http.StatusText(http.StatusForbidden)} - // ErrNotFound returns status 404 Not Found for invalid resource request. - ErrNotFound = &ErrResponse{HTTPStatusCode: http.StatusNotFound, StatusText: http.StatusText(http.StatusNotFound)} + // ErrNotFound returns status 404 Not Found for invalid resource request. + ErrNotFound = &ErrResponse{HTTPStatusCode: http.StatusNotFound, StatusText: http.StatusText(http.StatusNotFound)} - // ErrInternalServerError returns status 500 Internal Server Error. - ErrInternalServerError = &ErrResponse{HTTPStatusCode: http.StatusInternalServerError, StatusText: http.StatusText(http.StatusInternalServerError)} + // ErrInternalServerError returns status 500 Internal Server Error. + ErrInternalServerError = &ErrResponse{HTTPStatusCode: http.StatusInternalServerError, StatusText: http.StatusText(http.StatusInternalServerError)} ) // StatusContinue = 100 // RFC 7231, 6.2.1 diff --git a/api/app/grade.go b/api/app/grade.go index 49382a9..ec3cea8 100644 --- a/api/app/grade.go +++ b/api/app/grade.go @@ -19,31 +19,31 @@ package app import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // GradeResource specifies Grade management handler. type GradeResource struct { - Stores *Stores + Stores *Stores } // NewGradeResource create and returns a GradeResource. func NewGradeResource(stores *Stores) *GradeResource { - return &GradeResource{ - Stores: stores, - } + return &GradeResource{ + Stores: stores, + } } // EditHandler is public endpoint for @@ -59,39 +59,39 @@ func NewGradeResource(stores *Stores) *GradeResource { // RESPONSE: 403,Unauthorized // SUMMARY: edit a grade func (rs *GradeResource) EditHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) - data := &GradeRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - task, err := rs.Stores.Grade.IdentifyTaskOfGrade(currentGrade.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if data.AcquiredPoints > task.MaxPoints { - render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("aquired points is larger than max-points %v is more than %v", data.AcquiredPoints, task.MaxPoints))) - return - } - - currentGrade.Feedback = data.Feedback - currentGrade.AcquiredPoints = data.AcquiredPoints - - currentGrade.TutorID = accessClaims.LoginID - - // update database entry - if err := rs.Stores.Grade.Update(currentGrade); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) + data := &GradeRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + task, err := rs.Stores.Grade.IdentifyTaskOfGrade(currentGrade.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if data.AcquiredPoints > task.MaxPoints { + render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("aquired points is larger than max-points %v is more than %v", data.AcquiredPoints, task.MaxPoints))) + return + } + + currentGrade.Feedback = data.Feedback + currentGrade.AcquiredPoints = data.AcquiredPoints + + currentGrade.TutorID = accessClaims.LoginID + + // update database entry + if err := rs.Stores.Grade.Update(currentGrade); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // GetByIDHandler is public endpoint for @@ -106,16 +106,16 @@ func (rs *GradeResource) EditHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: get a grade func (rs *GradeResource) GetByIDHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) - // return Material information of created entry - if err := render.Render(w, r, newGradeResponse(currentGrade, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // return Material information of created entry + if err := render.Render(w, r, newGradeResponse(currentGrade, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // PublicResultEditHandler is public endpoint for @@ -132,45 +132,45 @@ func (rs *GradeResource) GetByIDHandler(w http.ResponseWriter, r *http.Request) // SUMMARY: update information for grade from background worker func (rs *GradeResource) PublicResultEditHandler(w http.ResponseWriter, r *http.Request) { - data := &GradeFromWorkerRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) - - submission, err := rs.Stores.Submission.Get(currentGrade.SubmissionID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if data.Status != 0 { - totalDockerFailExitCounterVec.WithLabelValues( - fmt.Sprintf("%d", submission.TaskID), - "public", - ).Inc() - - } else { - totalDockerSuccessExitCounterVec.WithLabelValues( - fmt.Sprintf("%d", submission.TaskID), - "public", - ).Inc() - } - - // currentGrade.PublicTestLog = data.Log - // currentGrade.PublicTestStatus = data.Status - // currentGrade.PublicExecutionState = 2 - - render.Status(r, http.StatusNoContent) - - // update database entry - if err := rs.Stores.Grade.UpdatePublicTestInfo(currentGrade.ID, data.Log, data.Status); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + data := &GradeFromWorkerRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) + + submission, err := rs.Stores.Submission.Get(currentGrade.SubmissionID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if data.Status != 0 { + totalDockerFailExitCounterVec.WithLabelValues( + fmt.Sprintf("%d", submission.TaskID), + "public", + ).Inc() + + } else { + totalDockerSuccessExitCounterVec.WithLabelValues( + fmt.Sprintf("%d", submission.TaskID), + "public", + ).Inc() + } + + // currentGrade.PublicTestLog = data.Log + // currentGrade.PublicTestStatus = data.Status + // currentGrade.PublicExecutionState = 2 + + render.Status(r, http.StatusNoContent) + + // update database entry + if err := rs.Stores.Grade.UpdatePublicTestInfo(currentGrade.ID, data.Log, data.Status); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } @@ -188,49 +188,49 @@ func (rs *GradeResource) PublicResultEditHandler(w http.ResponseWriter, r *http. // SUMMARY: update information for grade from background worker func (rs *GradeResource) PrivateResultEditHandler(w http.ResponseWriter, r *http.Request) { - data := &GradeFromWorkerRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) - - submission, err := rs.Stores.Submission.Get(currentGrade.SubmissionID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if data.Status != 0 { - totalDockerFailExitCounterVec.WithLabelValues( - fmt.Sprintf("%d", submission.TaskID), - "private", - ).Inc() - - } else { - totalDockerSuccessExitCounterVec.WithLabelValues( - fmt.Sprintf("%d", submission.TaskID), - "private", - ).Inc() - } - - // currentGrade.PrivateTestLog = data.Log - // currentGrade.PrivateTestStatus = data.Status - // currentGrade.PrivateExecutionState = 2 - - // fmt.Println(currentGrade.ID) - // fmt.Println(currentGrade.PrivateTestLog) - // fmt.Println(currentGrade.PrivateTestStatus) - - render.Status(r, http.StatusNoContent) - - // update database entry - if err := rs.Stores.Grade.UpdatePrivateTestInfo(currentGrade.ID, data.Log, data.Status); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + data := &GradeFromWorkerRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + currentGrade := r.Context().Value(common.CtxKeyGrade).(*model.Grade) + + submission, err := rs.Stores.Submission.Get(currentGrade.SubmissionID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if data.Status != 0 { + totalDockerFailExitCounterVec.WithLabelValues( + fmt.Sprintf("%d", submission.TaskID), + "private", + ).Inc() + + } else { + totalDockerSuccessExitCounterVec.WithLabelValues( + fmt.Sprintf("%d", submission.TaskID), + "private", + ).Inc() + } + + // currentGrade.PrivateTestLog = data.Log + // currentGrade.PrivateTestStatus = data.Status + // currentGrade.PrivateExecutionState = 2 + + // fmt.Println(currentGrade.ID) + // fmt.Println(currentGrade.PrivateTestLog) + // fmt.Println(currentGrade.PrivateTestStatus) + + render.Status(r, http.StatusNoContent) + + // update database entry + if err := rs.Stores.Grade.UpdatePrivateTestInfo(currentGrade.ID, data.Log, data.Status); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } @@ -256,52 +256,52 @@ func (rs *GradeResource) PrivateResultEditHandler(w http.ResponseWriter, r *http // RESPONSE: 403,Unauthorized // SUMMARY: Query grades in a course func (rs *GradeResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - filterSheetID := helper.Int64FromURL(r, "sheet_id", 0) - filterTaskID := helper.Int64FromURL(r, "task_id", 0) - filterGroupID := helper.Int64FromURL(r, "group_id", 0) - - if filterGroupID == 0 { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("group_id is missing"))) - return - } - - filterUserID := helper.Int64FromURL(r, "user_id", 0) - filterTutorID := helper.Int64FromURL(r, "tutor_id", 0) - filterFeedback := helper.StringFromURL(r, "feedback", "%%") - filterAcquiredPoints := helper.IntFromURL(r, "acquired_points", -1) - filterPublicTestStatus := helper.IntFromURL(r, "public_test_status", -1) - filterPrivateTestStatus := helper.IntFromURL(r, "private_test_status", -1) - filterPublicExecutationState := helper.IntFromURL(r, "public_execution_state", -1) - filterPrivateExecutationState := helper.IntFromURL(r, "private_execution_state", -1) - - submissions, err := rs.Stores.Grade.GetFiltered( - course.ID, - filterSheetID, - filterTaskID, - filterGroupID, - filterUserID, - filterTutorID, - filterFeedback, - filterAcquiredPoints, - filterPublicTestStatus, - filterPrivateTestStatus, - filterPublicExecutationState, - filterPrivateExecutationState, - ) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // render JSON reponse - if err = render.RenderList(w, r, newGradeListResponse(submissions, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + filterSheetID := helper.Int64FromURL(r, "sheet_id", 0) + filterTaskID := helper.Int64FromURL(r, "task_id", 0) + filterGroupID := helper.Int64FromURL(r, "group_id", 0) + + if filterGroupID == 0 { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("group_id is missing"))) + return + } + + filterUserID := helper.Int64FromURL(r, "user_id", 0) + filterTutorID := helper.Int64FromURL(r, "tutor_id", 0) + filterFeedback := helper.StringFromURL(r, "feedback", "%%") + filterAcquiredPoints := helper.IntFromURL(r, "acquired_points", -1) + filterPublicTestStatus := helper.IntFromURL(r, "public_test_status", -1) + filterPrivateTestStatus := helper.IntFromURL(r, "private_test_status", -1) + filterPublicExecutationState := helper.IntFromURL(r, "public_execution_state", -1) + filterPrivateExecutationState := helper.IntFromURL(r, "private_execution_state", -1) + + submissions, err := rs.Stores.Grade.GetFiltered( + course.ID, + filterSheetID, + filterTaskID, + filterGroupID, + filterUserID, + filterTutorID, + filterFeedback, + filterAcquiredPoints, + filterPublicTestStatus, + filterPrivateTestStatus, + filterPublicExecutationState, + filterPrivateExecutationState, + ) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // render JSON reponse + if err = render.RenderList(w, r, newGradeListResponse(submissions, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } @@ -319,30 +319,30 @@ func (rs *GradeResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // DESCRIPTION: // {"sheets":[{"id":179,"name":"1"},{"id":180,"name":"2"}],"achievements":[{"user_info":{"id":42,"first_name":"Sören","last_name":"Haase","student_number":"1161"},"points":[5,0]},{"user_info":{"id":43,"first_name":"Resi","last_name":"Naser","student_number":"1000"},"points":[8,7]}]} func (rs *GradeResource) IndexSummaryHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - filterGroupID := helper.Int64FromURL(r, "group_id", 0) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + filterGroupID := helper.Int64FromURL(r, "group_id", 0) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - grades, err := rs.Stores.Grade.GetOverviewGrades(course.ID, filterGroupID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + grades, err := rs.Stores.Grade.GetOverviewGrades(course.ID, filterGroupID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - sheets, err := rs.Stores.Sheet.SheetsOfCourse(course.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + sheets, err := rs.Stores.Sheet.SheetsOfCourse(course.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - // render JSON reponse - if err = render.Render(w, r, newGradeOverviewResponse(grades, sheets, givenRole)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.Render(w, r, newGradeOverviewResponse(grades, sheets, givenRole)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } @@ -358,24 +358,24 @@ func (rs *GradeResource) IndexSummaryHandler(w http.ResponseWriter, r *http.Requ // RESPONSE: 403,Unauthorized // SUMMARY: the missing grades for the request identity func (rs *GradeResource) IndexMissingHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - filterGroupID := helper.Int64FromURL(r, "group_id", 0) + filterGroupID := helper.Int64FromURL(r, "group_id", 0) - grades, err := rs.Stores.Grade.GetAllMissingGrades(course.ID, accessClaims.LoginID, filterGroupID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + grades, err := rs.Stores.Grade.GetAllMissingGrades(course.ID, accessClaims.LoginID, filterGroupID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - // render JSON reponse - if err = render.RenderList(w, r, newMissingGradeListResponse(grades)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, newMissingGradeListResponse(grades)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } @@ -386,45 +386,45 @@ func (rs *GradeResource) IndexMissingHandler(w http.ResponseWriter, r *http.Requ // the Grade could not be found, we stop here and return a 404. // We do NOT check whether the identity is authorized to get this Grade. func (rs *GradeResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) + courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) - var gradeID int64 - var err error + var gradeID int64 + var err error - // try to get id from URL - if gradeID, err = strconv.ParseInt(chi.URLParam(r, "grade_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } + // try to get id from URL + if gradeID, err = strconv.ParseInt(chi.URLParam(r, "grade_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } - // find specific course in database - grade, err := rs.Stores.Grade.Get(gradeID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } + // find specific course in database + grade, err := rs.Stores.Grade.Get(gradeID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } - // serve next - ctx := context.WithValue(r.Context(), common.CtxKeyGrade, grade) + // serve next + ctx := context.WithValue(r.Context(), common.CtxKeyGrade, grade) - // when there is a gradeID in the url, there is NOT a courseID in the url, - // BUT: when there is a grade, there is a course + // when there is a gradeID in the url, there is NOT a courseID in the url, + // BUT: when there is a grade, there is a course - course, err := rs.Stores.Grade.IdentifyCourseOfGrade(grade.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + course, err := rs.Stores.Grade.IdentifyCourseOfGrade(grade.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - if courseFromURL.ID != course.ID { - render.Render(w, r, ErrNotFound) - return - } + if courseFromURL.ID != course.ID { + render.Render(w, r, ErrNotFound) + return + } - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - next.ServeHTTP(w, r.WithContext(ctx)) - }) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/grade_responses.go b/api/app/grade_responses.go index c3e17bc..cfb0ddc 100644 --- a/api/app/grade_responses.go +++ b/api/app/grade_responses.go @@ -19,293 +19,293 @@ package app import ( - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/render" - "github.com/spf13/viper" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/render" + "github.com/spf13/viper" ) // ............................................................................. // GradeResponse is the response payload for Grade management. type GradeResponse struct { - ID int64 `json:"id" example:"1"` - PublicExecutionState int `json:"public_execution_state" example:"1"` - PrivateExecutionState int `json:"private_execution_state" example:"1"` - PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` - PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` - PublicTestStatus int `json:"public_test_status" example:"1"` - PrivateTestStatus int `json:"private_test_status" example:"0"` - AcquiredPoints int `json:"acquired_points" example:"19"` - Feedback string `json:"feedback" example:"Some feedback"` - TutorID int64 `json:"tutor_id" example:"2"` - SubmissionID int64 `json:"submission_id" example:"31"` - FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` - User *struct { - ID int64 `json:"id" example:"1"` - FirstName string `json:"first_name" example:"Max"` - LastName string `json:"last_name" example:"Mustermensch"` - Email string `json:"email" example:"test@unit-tuebingen.de"` - } `json:"user"` + ID int64 `json:"id" example:"1"` + PublicExecutionState int `json:"public_execution_state" example:"1"` + PrivateExecutionState int `json:"private_execution_state" example:"1"` + PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` + PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` + PublicTestStatus int `json:"public_test_status" example:"1"` + PrivateTestStatus int `json:"private_test_status" example:"0"` + AcquiredPoints int `json:"acquired_points" example:"19"` + Feedback string `json:"feedback" example:"Some feedback"` + TutorID int64 `json:"tutor_id" example:"2"` + SubmissionID int64 `json:"submission_id" example:"31"` + FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` + User *struct { + ID int64 `json:"id" example:"1"` + FirstName string `json:"first_name" example:"Max"` + LastName string `json:"last_name" example:"Mustermensch"` + Email string `json:"email" example:"test@unit-tuebingen.de"` + } `json:"user"` } // Render post-processes a GradeResponse. func (body *GradeResponse) Render(w http.ResponseWriter, r *http.Request) error { - return nil + return nil } // newGradeResponse creates a response from a Grade model. func newGradeResponse(p *model.Grade, courseID int64) *GradeResponse { - fileURL := "" - if helper.NewSubmissionFileHandle(p.ID).Exists() { - fileURL = fmt.Sprintf("%s/api/v1/courses/%d/submissions/%d/file", - viper.GetString("url"), - courseID, - p.SubmissionID, - ) - } - - user := &struct { - ID int64 `json:"id" example:"1"` - FirstName string `json:"first_name" example:"Max"` - LastName string `json:"last_name" example:"Mustermensch"` - Email string `json:"email" example:"test@unit-tuebingen.de"` - }{ - ID: p.UserID, - FirstName: p.UserFirstName, - LastName: p.UserLastName, - Email: p.UserEmail, - } - - return &GradeResponse{ - ID: p.ID, - PublicExecutionState: p.PublicExecutionState, - PrivateExecutionState: p.PrivateExecutionState, - PublicTestLog: p.PublicTestLog, - PrivateTestLog: p.PrivateTestLog, - PublicTestStatus: p.PublicTestStatus, - PrivateTestStatus: p.PrivateTestStatus, - AcquiredPoints: p.AcquiredPoints, - Feedback: p.Feedback, - TutorID: p.TutorID, - User: user, - SubmissionID: p.SubmissionID, - FileURL: fileURL, - } + fileURL := "" + if helper.NewSubmissionFileHandle(p.ID).Exists() { + fileURL = fmt.Sprintf("%s/api/v1/courses/%d/submissions/%d/file", + viper.GetString("url"), + courseID, + p.SubmissionID, + ) + } + + user := &struct { + ID int64 `json:"id" example:"1"` + FirstName string `json:"first_name" example:"Max"` + LastName string `json:"last_name" example:"Mustermensch"` + Email string `json:"email" example:"test@unit-tuebingen.de"` + }{ + ID: p.UserID, + FirstName: p.UserFirstName, + LastName: p.UserLastName, + Email: p.UserEmail, + } + + return &GradeResponse{ + ID: p.ID, + PublicExecutionState: p.PublicExecutionState, + PrivateExecutionState: p.PrivateExecutionState, + PublicTestLog: p.PublicTestLog, + PrivateTestLog: p.PrivateTestLog, + PublicTestStatus: p.PublicTestStatus, + PrivateTestStatus: p.PrivateTestStatus, + AcquiredPoints: p.AcquiredPoints, + Feedback: p.Feedback, + TutorID: p.TutorID, + User: user, + SubmissionID: p.SubmissionID, + FileURL: fileURL, + } } // newGradeListResponse creates a response from a list of Grade models. func newGradeListResponse(Grades []model.Grade, courseID int64) []render.Renderer { - list := []render.Renderer{} - for k := range Grades { - list = append(list, newGradeResponse(&Grades[k], courseID)) - } - return list + list := []render.Renderer{} + for k := range Grades { + list = append(list, newGradeResponse(&Grades[k], courseID)) + } + return list } // MissingGradeResponse is the response payload for showing tutors // which grades are still in the loop. We expect them to write a feedback // for all submissions. type MissingGradeResponse struct { - Grade *struct { - ID int64 `json:"id" example:"1"` - PublicExecutionState int `json:"public_execution_state" example:"1"` - PrivateExecutionState int `json:"private_execution_state" example:"1"` - PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` - PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` - PublicTestStatus int `json:"public_test_status" example:"1"` - PrivateTestStatus int `json:"private_test_status" example:"0"` - AcquiredPoints int `json:"acquired_points" example:"19"` - Feedback string `json:"feedback" example:"Some feedback"` - TutorID int64 `json:"tutor_id" example:"2"` - SubmissionID int64 `json:"submission_id" example:"31"` - FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` - User *struct { - ID int64 `json:"id" example:"1"` - FirstName string `json:"first_name" example:"Max"` - LastName string `json:"last_name" example:"Mustermensch"` - Email string `json:"email" example:"test@unit-tuebingen.de"` - } `json:"user"` - } `json:"grade"` - CourseID int64 `json:"course_id" example:"1"` - SheetID int64 `json:"sheet_id" example:"10"` - TaskID int64 `json:"task_id" example:"2"` + Grade *struct { + ID int64 `json:"id" example:"1"` + PublicExecutionState int `json:"public_execution_state" example:"1"` + PrivateExecutionState int `json:"private_execution_state" example:"1"` + PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` + PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` + PublicTestStatus int `json:"public_test_status" example:"1"` + PrivateTestStatus int `json:"private_test_status" example:"0"` + AcquiredPoints int `json:"acquired_points" example:"19"` + Feedback string `json:"feedback" example:"Some feedback"` + TutorID int64 `json:"tutor_id" example:"2"` + SubmissionID int64 `json:"submission_id" example:"31"` + FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` + User *struct { + ID int64 `json:"id" example:"1"` + FirstName string `json:"first_name" example:"Max"` + LastName string `json:"last_name" example:"Mustermensch"` + Email string `json:"email" example:"test@unit-tuebingen.de"` + } `json:"user"` + } `json:"grade"` + CourseID int64 `json:"course_id" example:"1"` + SheetID int64 `json:"sheet_id" example:"10"` + TaskID int64 `json:"task_id" example:"2"` } // Render post-processes a MissingGradeResponse. func (body *MissingGradeResponse) Render(w http.ResponseWriter, r *http.Request) error { - return nil + return nil } // newMissingGradeResponse creates a response from a Grade model. func newMissingGradeResponse(p *model.MissingGrade) *MissingGradeResponse { - fileURL := "" - if helper.NewSubmissionFileHandle(p.SubmissionID).Exists() { - fileURL = fmt.Sprintf("/api/v1/submissions/%s/file", strconv.FormatInt(p.SubmissionID, 10)) - } - - user := &struct { - ID int64 `json:"id" example:"1"` - FirstName string `json:"first_name" example:"Max"` - LastName string `json:"last_name" example:"Mustermensch"` - Email string `json:"email" example:"test@unit-tuebingen.de"` - }{ - ID: p.UserID, - FirstName: p.UserFirstName, - LastName: p.UserLastName, - Email: p.UserEmail, - } - - grade := &struct { - ID int64 `json:"id" example:"1"` - PublicExecutionState int `json:"public_execution_state" example:"1"` - PrivateExecutionState int `json:"private_execution_state" example:"1"` - PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` - PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` - PublicTestStatus int `json:"public_test_status" example:"1"` - PrivateTestStatus int `json:"private_test_status" example:"0"` - AcquiredPoints int `json:"acquired_points" example:"19"` - Feedback string `json:"feedback" example:"Some feedback"` - TutorID int64 `json:"tutor_id" example:"2"` - SubmissionID int64 `json:"submission_id" example:"31"` - FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` - User *struct { - ID int64 `json:"id" example:"1"` - FirstName string `json:"first_name" example:"Max"` - LastName string `json:"last_name" example:"Mustermensch"` - Email string `json:"email" example:"test@unit-tuebingen.de"` - } `json:"user"` - }{ - ID: p.ID, - PublicExecutionState: p.PublicExecutionState, - PrivateExecutionState: p.PrivateExecutionState, - PublicTestLog: p.PublicTestLog, - PrivateTestLog: p.PrivateTestLog, - PublicTestStatus: p.PublicTestStatus, - PrivateTestStatus: p.PrivateTestStatus, - AcquiredPoints: p.AcquiredPoints, - Feedback: p.Feedback, - TutorID: p.TutorID, - User: user, - SubmissionID: p.SubmissionID, - FileURL: fileURL, - } - - r := &MissingGradeResponse{ - Grade: grade, - CourseID: p.CourseID, - SheetID: p.SheetID, - TaskID: p.TaskID, - } - - return r + fileURL := "" + if helper.NewSubmissionFileHandle(p.SubmissionID).Exists() { + fileURL = fmt.Sprintf("/api/v1/submissions/%s/file", strconv.FormatInt(p.SubmissionID, 10)) + } + + user := &struct { + ID int64 `json:"id" example:"1"` + FirstName string `json:"first_name" example:"Max"` + LastName string `json:"last_name" example:"Mustermensch"` + Email string `json:"email" example:"test@unit-tuebingen.de"` + }{ + ID: p.UserID, + FirstName: p.UserFirstName, + LastName: p.UserLastName, + Email: p.UserEmail, + } + + grade := &struct { + ID int64 `json:"id" example:"1"` + PublicExecutionState int `json:"public_execution_state" example:"1"` + PrivateExecutionState int `json:"private_execution_state" example:"1"` + PublicTestLog string `json:"public_test_log" example:"Lorem Ipsum"` + PrivateTestLog string `json:"private_test_log" example:"Lorem Ipsum"` + PublicTestStatus int `json:"public_test_status" example:"1"` + PrivateTestStatus int `json:"private_test_status" example:"0"` + AcquiredPoints int `json:"acquired_points" example:"19"` + Feedback string `json:"feedback" example:"Some feedback"` + TutorID int64 `json:"tutor_id" example:"2"` + SubmissionID int64 `json:"submission_id" example:"31"` + FileURL string `json:"file_url" example:"/api/v1/submissions/61/file"` + User *struct { + ID int64 `json:"id" example:"1"` + FirstName string `json:"first_name" example:"Max"` + LastName string `json:"last_name" example:"Mustermensch"` + Email string `json:"email" example:"test@unit-tuebingen.de"` + } `json:"user"` + }{ + ID: p.ID, + PublicExecutionState: p.PublicExecutionState, + PrivateExecutionState: p.PrivateExecutionState, + PublicTestLog: p.PublicTestLog, + PrivateTestLog: p.PrivateTestLog, + PublicTestStatus: p.PublicTestStatus, + PrivateTestStatus: p.PrivateTestStatus, + AcquiredPoints: p.AcquiredPoints, + Feedback: p.Feedback, + TutorID: p.TutorID, + User: user, + SubmissionID: p.SubmissionID, + FileURL: fileURL, + } + + r := &MissingGradeResponse{ + Grade: grade, + CourseID: p.CourseID, + SheetID: p.SheetID, + TaskID: p.TaskID, + } + + return r } // newMissingGradeListResponse creates a response from a list of Grade models. func newMissingGradeListResponse(Grades []model.MissingGrade) []render.Renderer { - list := []render.Renderer{} - for k := range Grades { - list = append(list, newMissingGradeResponse(&Grades[k])) - } - return list + list := []render.Renderer{} + for k := range Grades { + list = append(list, newMissingGradeResponse(&Grades[k])) + } + return list } // for the swagger build relying on go.ast we need to duplicate code here type sheetInfo struct { - ID int64 `json:"id" example:"42"` - Name string `json:"name" example:"sheet 0"` + ID int64 `json:"id" example:"42"` + Name string `json:"name" example:"sheet 0"` } type userInfo struct { - ID int64 `json:"id" example:"42"` - FirstName string `json:"first_name" example:"max"` - LastName string `json:"last_name" example:"mustermensch"` - StudentNumber string `json:"student_number" example:"0815"` - Email string `json:"email" example:"user@example.com"` + ID int64 `json:"id" example:"42"` + FirstName string `json:"first_name" example:"max"` + LastName string `json:"last_name" example:"mustermensch"` + StudentNumber string `json:"student_number" example:"0815"` + Email string `json:"email" example:"user@example.com"` } type achievementInfo struct { - User userInfo `json:"user_info" example:""` - Points []int `json:"points" example:"4"` + User userInfo `json:"user_info" example:""` + Points []int `json:"points" example:"4"` } // GradeOverviewResponse captures the summary for all grades over all sheets // for a subset of users. type GradeOverviewResponse struct { - Sheets []sheetInfo `json:"sheets" example:""` - Achievements []achievementInfo `json:"achievements" example:""` + Sheets []sheetInfo `json:"sheets" example:""` + Achievements []achievementInfo `json:"achievements" example:""` } // newGradeOverviewResponse creates a response from a Material model. func newGradeOverviewResponse(collection []model.OverviewGrade, sheets []model.Sheet, role authorize.CourseRole) *GradeOverviewResponse { - obj := &GradeOverviewResponse{} - // collection is sorted by user_id - - // only do this once - sheet2pos := make(map[int64]int) - for k, s := range sheets { - obj.Sheets = append(obj.Sheets, sheetInfo{s.ID, s.Name}) - sheet2pos[s.ID] = k - } - - if len(collection) > 0 { - oldUser := userInfo{ - ID: collection[0].UserID, - FirstName: collection[0].UserFirstName, - LastName: collection[0].UserLastName, - StudentNumber: collection[0].UserStudentNumber, - Email: collection[0].UserEmail, - } - currentPoints := make([]int, len(sheets)) - - // iterate collection of users - // {user, sheet, points} - // {user, sheet, points} - // This is sparse: Students without submissions for one sheet are not listed. - // We need to explicitly list them here with "0" points. - for _, entry := range collection { - - // other student - if entry.UserID != oldUser.ID { - - if role == authorize.TUTOR { - oldUser.StudentNumber = "" - } - - obj.Achievements = append(obj.Achievements, achievementInfo{oldUser, currentPoints}) - - // reset points - currentPoints = make([]int, len(sheets)) - - oldUser = userInfo{ - ID: entry.UserID, - FirstName: entry.UserFirstName, - LastName: entry.UserLastName, - StudentNumber: entry.UserStudentNumber, - Email: entry.UserEmail, - } - } - - currentPoints[sheet2pos[entry.SheetID]] = entry.Points - } - - // add the last student - if len(collection) > 0 { - obj.Achievements = append(obj.Achievements, achievementInfo{oldUser, currentPoints}) - } - } - - return obj + obj := &GradeOverviewResponse{} + // collection is sorted by user_id + + // only do this once + sheet2pos := make(map[int64]int) + for k, s := range sheets { + obj.Sheets = append(obj.Sheets, sheetInfo{s.ID, s.Name}) + sheet2pos[s.ID] = k + } + + if len(collection) > 0 { + oldUser := userInfo{ + ID: collection[0].UserID, + FirstName: collection[0].UserFirstName, + LastName: collection[0].UserLastName, + StudentNumber: collection[0].UserStudentNumber, + Email: collection[0].UserEmail, + } + currentPoints := make([]int, len(sheets)) + + // iterate collection of users + // {user, sheet, points} + // {user, sheet, points} + // This is sparse: Students without submissions for one sheet are not listed. + // We need to explicitly list them here with "0" points. + for _, entry := range collection { + + // other student + if entry.UserID != oldUser.ID { + + if role == authorize.TUTOR { + oldUser.StudentNumber = "" + } + + obj.Achievements = append(obj.Achievements, achievementInfo{oldUser, currentPoints}) + + // reset points + currentPoints = make([]int, len(sheets)) + + oldUser = userInfo{ + ID: entry.UserID, + FirstName: entry.UserFirstName, + LastName: entry.UserLastName, + StudentNumber: entry.UserStudentNumber, + Email: entry.UserEmail, + } + } + + currentPoints[sheet2pos[entry.SheetID]] = entry.Points + } + + // add the last student + if len(collection) > 0 { + obj.Achievements = append(obj.Achievements, achievementInfo{oldUser, currentPoints}) + } + } + + return obj } // Render post-processes a GradeOverviewResponse. func (body *GradeOverviewResponse) Render(w http.ResponseWriter, r *http.Request) error { - return nil + return nil } diff --git a/api/app/grade_test.go b/api/app/grade_test.go index 075485c..e4912a2 100644 --- a/api/app/grade_test.go +++ b/api/app/grade_test.go @@ -19,643 +19,643 @@ package app import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strconv" - "testing" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/franela/goblin" - "github.com/spf13/viper" - null "gopkg.in/guregu/null.v3" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "testing" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/franela/goblin" + "github.com/spf13/viper" + null "gopkg.in/guregu/null.v3" ) func copyFile(src, dst string) (int64, error) { - sourceFileStat, err := os.Stat(src) - if err != nil { - return 0, err - } - - if !sourceFileStat.Mode().IsRegular() { - return 0, fmt.Errorf("%s is not a regular file", src) - } - - source, err := os.Open(src) - if err != nil { - return 0, err - } - defer source.Close() - - destination, err := os.Create(dst) - if err != nil { - return 0, err - } - defer destination.Close() - nBytes, err := io.Copy(destination, source) - return nBytes, err + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + nBytes, err := io.Copy(destination, source) + return nBytes, err } func TestGrade(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - - tape := &Tape{} - - var stores *Stores - - g.Describe("Grade", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) - - g.It("Query should require access claims", func() { - url := "/api/v1/courses/1/grades?group_id=1" - w := tape.Get(url) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Should get a specific grade", func() { - - w := tape.GetWithClaims("/api/v1/courses/1/grades/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - gradeActual := &GradeResponse{} - err := json.NewDecoder(w.Body).Decode(gradeActual) - g.Assert(err).Equal(nil) - - hnd := helper.NewSubmissionFileHandle(gradeActual.SubmissionID) - g.Assert(hnd.Exists()).Equal(false) - gradeExpected, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(gradeActual.ID).Equal(gradeExpected.ID) - g.Assert(gradeActual.PublicExecutionState).Equal(gradeExpected.PublicExecutionState) - g.Assert(gradeActual.PrivateExecutionState).Equal(gradeExpected.PrivateExecutionState) - g.Assert(gradeActual.PublicTestLog).Equal(gradeExpected.PublicTestLog) - g.Assert(gradeActual.PrivateTestLog).Equal(gradeExpected.PrivateTestLog) - g.Assert(gradeActual.PublicTestStatus).Equal(gradeExpected.PublicTestStatus) - g.Assert(gradeActual.PrivateTestStatus).Equal(gradeExpected.PrivateTestStatus) - g.Assert(gradeActual.AcquiredPoints).Equal(gradeExpected.AcquiredPoints) - g.Assert(gradeActual.Feedback).Equal(gradeExpected.Feedback) - g.Assert(gradeActual.TutorID).Equal(gradeExpected.TutorID) - g.Assert(gradeActual.User.ID).Equal(gradeExpected.UserID) - g.Assert(gradeActual.User.FirstName).Equal(gradeExpected.UserFirstName) - g.Assert(gradeActual.User.LastName).Equal(gradeExpected.UserLastName) - g.Assert(gradeActual.User.Email).Equal(gradeExpected.UserEmail) - g.Assert(gradeActual.SubmissionID).Equal(gradeExpected.SubmissionID) - g.Assert(gradeActual.FileURL).Equal("") - - defer hnd.Delete() - // now file exists - src := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - dest := fmt.Sprintf("%s/submissions/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(gradeActual.SubmissionID, 10)) - copyFile(src, dest) - - g.Assert(hnd.Exists()).Equal(true) - - w = tape.GetWithClaims("/api/v1/courses/1/grades/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - err = json.NewDecoder(w.Body).Decode(gradeActual) - g.Assert(err).Equal(nil) - - g.Assert(gradeActual.ID).Equal(gradeExpected.ID) - g.Assert(gradeActual.PublicExecutionState).Equal(gradeExpected.PublicExecutionState) - g.Assert(gradeActual.PrivateExecutionState).Equal(gradeExpected.PrivateExecutionState) - g.Assert(gradeActual.PublicTestLog).Equal(gradeExpected.PublicTestLog) - g.Assert(gradeActual.PrivateTestLog).Equal(gradeExpected.PrivateTestLog) - g.Assert(gradeActual.PublicTestStatus).Equal(gradeExpected.PublicTestStatus) - g.Assert(gradeActual.PrivateTestStatus).Equal(gradeExpected.PrivateTestStatus) - g.Assert(gradeActual.AcquiredPoints).Equal(gradeExpected.AcquiredPoints) - g.Assert(gradeActual.Feedback).Equal(gradeExpected.Feedback) - g.Assert(gradeActual.TutorID).Equal(gradeExpected.TutorID) - g.Assert(gradeActual.User.ID).Equal(gradeExpected.UserID) - g.Assert(gradeActual.User.FirstName).Equal(gradeExpected.UserFirstName) - g.Assert(gradeActual.User.LastName).Equal(gradeExpected.UserLastName) - g.Assert(gradeActual.User.Email).Equal(gradeExpected.UserEmail) - g.Assert(gradeActual.SubmissionID).Equal(gradeExpected.SubmissionID) - - url := viper.GetString("url") - - g.Assert(gradeActual.FileURL).Equal(fmt.Sprintf("%s/api/v1/courses/1/submissions/1/file", url)) - - }) - - g.It("Should list all grades of a group", func() { - url := "/api/v1/courses/1/grades?group_id=1" - - gradesExpected, err := stores.Grade.GetFiltered(1, 0, 0, 1, 0, 0, "%%", -1, -1, -1, -1, -1) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - gradesActual := []GradeResponse{} - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - g.Assert(len(gradesActual)).Equal(len(gradesExpected)) - }) - - g.It("Should list all grades of a group with some filters", func() { - - w := tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&public_test_status=0", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - gradesActual := []GradeResponse{} - err := json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - for _, el := range gradesActual { - g.Assert(el.PublicTestStatus).Equal(0) - } - - w = tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&private_test_status=0", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - for _, el := range gradesActual { - g.Assert(el.PrivateTestStatus).Equal(0) - } - - w = tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&tutor_id=3", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - for _, el := range gradesActual { - g.Assert(el.TutorID).Equal(int64(3)) - } - }) - - g.It("Should perform updates", func() { - - data := H{ - "acquired_points": 3, - "feedback": "Lorem Ipsum_update", - } - - w := tape.Put("/api/v1/courses/1/grades/1", data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.Feedback).Equal("Lorem Ipsum_update") - g.Assert(entryAfter.AcquiredPoints).Equal(3) - g.Assert(entryAfter.TutorID).Equal(int64(3)) - }) - - g.It("Should perform updates when zero points", func() { - - data := H{ - "acquired_points": 0, - "feedback": "Lorem Ipsum_update", - } - - w := tape.Put("/api/v1/courses/1/grades/1", data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.Feedback).Equal("Lorem Ipsum_update") - g.Assert(entryAfter.AcquiredPoints).Equal(0) - g.Assert(entryAfter.TutorID).Equal(int64(3)) - }) - - g.Xit("Should not perform updates when missing points", func() { - // todo difference between "0" and None - data := H{ - // "acquired_points": 0, - "feedback": "Lorem Ipsum_update", - } - - w := tape.Put("/api/v1/courses/1/grades/1", data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - }) - - g.It("Should not perform updates (too many points)", func() { - - task, err := stores.Grade.IdentifyTaskOfGrade(1) - g.Assert(err).Equal(nil) - - entryBefore, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - data := H{ - "acquired_points": task.MaxPoints + 10, - "feedback": "Lorem Ipsum_update", - } - - w := tape.Put("/api/v1/courses/1/grades/1", data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - entryAfter, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.Feedback).Equal(entryBefore.Feedback) - g.Assert(entryAfter.AcquiredPoints).Equal(entryBefore.AcquiredPoints) - g.Assert(entryAfter.TutorID).Equal(entryBefore.TutorID) - }) - - g.It("Should list missing grades", func() { - gradesActual := []MissingGradeResponse{} - // students have no missing data - // but we do not know if a user is student in a course - w := tape.GetWithClaims("/api/v1/courses/1/grades/missing", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) - err := json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - g.Assert(len(gradesActual)).Equal(0) - - // admin (mock creates feed back for all submissions) - w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - - gradesExpected, err := stores.Grade.GetAllMissingGrades(1, 1, 0) - g.Assert(err).Equal(nil) - g.Assert(len(gradesActual)).Equal(len(gradesExpected)) - - // tutors (mock creates feed back for all submissions) - w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 3, false) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) - - gradesExpected, err = stores.Grade.GetAllMissingGrades(1, 3, 0) - g.Assert(err).Equal(nil) - g.Assert(len(gradesActual)).Equal(len(gradesExpected)) - - _, err = tape.DB.Exec("UPDATE grades SET feedback='' WHERE tutor_id = 3 ") - g.Assert(err).Equal(nil) - - // tutors (mock creates feed back for all submissions) - w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 3, false) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&gradesActual) - g.Assert(err).Equal(nil) + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + + tape := &Tape{} + + var stores *Stores + + g.Describe("Grade", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) + + g.It("Query should require access claims", func() { + url := "/api/v1/courses/1/grades?group_id=1" + w := tape.Get(url) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Should get a specific grade", func() { + + w := tape.GetWithClaims("/api/v1/courses/1/grades/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + gradeActual := &GradeResponse{} + err := json.NewDecoder(w.Body).Decode(gradeActual) + g.Assert(err).Equal(nil) + + hnd := helper.NewSubmissionFileHandle(gradeActual.SubmissionID) + g.Assert(hnd.Exists()).Equal(false) + gradeExpected, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(gradeActual.ID).Equal(gradeExpected.ID) + g.Assert(gradeActual.PublicExecutionState).Equal(gradeExpected.PublicExecutionState) + g.Assert(gradeActual.PrivateExecutionState).Equal(gradeExpected.PrivateExecutionState) + g.Assert(gradeActual.PublicTestLog).Equal(gradeExpected.PublicTestLog) + g.Assert(gradeActual.PrivateTestLog).Equal(gradeExpected.PrivateTestLog) + g.Assert(gradeActual.PublicTestStatus).Equal(gradeExpected.PublicTestStatus) + g.Assert(gradeActual.PrivateTestStatus).Equal(gradeExpected.PrivateTestStatus) + g.Assert(gradeActual.AcquiredPoints).Equal(gradeExpected.AcquiredPoints) + g.Assert(gradeActual.Feedback).Equal(gradeExpected.Feedback) + g.Assert(gradeActual.TutorID).Equal(gradeExpected.TutorID) + g.Assert(gradeActual.User.ID).Equal(gradeExpected.UserID) + g.Assert(gradeActual.User.FirstName).Equal(gradeExpected.UserFirstName) + g.Assert(gradeActual.User.LastName).Equal(gradeExpected.UserLastName) + g.Assert(gradeActual.User.Email).Equal(gradeExpected.UserEmail) + g.Assert(gradeActual.SubmissionID).Equal(gradeExpected.SubmissionID) + g.Assert(gradeActual.FileURL).Equal("") + + defer hnd.Delete() + // now file exists + src := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + dest := fmt.Sprintf("%s/submissions/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(gradeActual.SubmissionID, 10)) + copyFile(src, dest) + + g.Assert(hnd.Exists()).Equal(true) + + w = tape.GetWithClaims("/api/v1/courses/1/grades/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + err = json.NewDecoder(w.Body).Decode(gradeActual) + g.Assert(err).Equal(nil) + + g.Assert(gradeActual.ID).Equal(gradeExpected.ID) + g.Assert(gradeActual.PublicExecutionState).Equal(gradeExpected.PublicExecutionState) + g.Assert(gradeActual.PrivateExecutionState).Equal(gradeExpected.PrivateExecutionState) + g.Assert(gradeActual.PublicTestLog).Equal(gradeExpected.PublicTestLog) + g.Assert(gradeActual.PrivateTestLog).Equal(gradeExpected.PrivateTestLog) + g.Assert(gradeActual.PublicTestStatus).Equal(gradeExpected.PublicTestStatus) + g.Assert(gradeActual.PrivateTestStatus).Equal(gradeExpected.PrivateTestStatus) + g.Assert(gradeActual.AcquiredPoints).Equal(gradeExpected.AcquiredPoints) + g.Assert(gradeActual.Feedback).Equal(gradeExpected.Feedback) + g.Assert(gradeActual.TutorID).Equal(gradeExpected.TutorID) + g.Assert(gradeActual.User.ID).Equal(gradeExpected.UserID) + g.Assert(gradeActual.User.FirstName).Equal(gradeExpected.UserFirstName) + g.Assert(gradeActual.User.LastName).Equal(gradeExpected.UserLastName) + g.Assert(gradeActual.User.Email).Equal(gradeExpected.UserEmail) + g.Assert(gradeActual.SubmissionID).Equal(gradeExpected.SubmissionID) + + url := viper.GetString("url") + + g.Assert(gradeActual.FileURL).Equal(fmt.Sprintf("%s/api/v1/courses/1/submissions/1/file", url)) + + }) + + g.It("Should list all grades of a group", func() { + url := "/api/v1/courses/1/grades?group_id=1" + + gradesExpected, err := stores.Grade.GetFiltered(1, 0, 0, 1, 0, 0, "%%", -1, -1, -1, -1, -1) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + gradesActual := []GradeResponse{} + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + g.Assert(len(gradesActual)).Equal(len(gradesExpected)) + }) + + g.It("Should list all grades of a group with some filters", func() { + + w := tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&public_test_status=0", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + gradesActual := []GradeResponse{} + err := json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + for _, el := range gradesActual { + g.Assert(el.PublicTestStatus).Equal(0) + } + + w = tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&private_test_status=0", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + for _, el := range gradesActual { + g.Assert(el.PrivateTestStatus).Equal(0) + } + + w = tape.GetWithClaims("/api/v1/courses/1/grades?group_id=1&tutor_id=3", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + for _, el := range gradesActual { + g.Assert(el.TutorID).Equal(int64(3)) + } + }) + + g.It("Should perform updates", func() { + + data := H{ + "acquired_points": 3, + "feedback": "Lorem Ipsum_update", + } + + w := tape.Put("/api/v1/courses/1/grades/1", data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.Feedback).Equal("Lorem Ipsum_update") + g.Assert(entryAfter.AcquiredPoints).Equal(3) + g.Assert(entryAfter.TutorID).Equal(int64(3)) + }) + + g.It("Should perform updates when zero points", func() { + + data := H{ + "acquired_points": 0, + "feedback": "Lorem Ipsum_update", + } + + w := tape.Put("/api/v1/courses/1/grades/1", data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.Feedback).Equal("Lorem Ipsum_update") + g.Assert(entryAfter.AcquiredPoints).Equal(0) + g.Assert(entryAfter.TutorID).Equal(int64(3)) + }) + + g.Xit("Should not perform updates when missing points", func() { + // todo difference between "0" and None + data := H{ + // "acquired_points": 0, + "feedback": "Lorem Ipsum_update", + } + + w := tape.Put("/api/v1/courses/1/grades/1", data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + }) + + g.It("Should not perform updates (too many points)", func() { + + task, err := stores.Grade.IdentifyTaskOfGrade(1) + g.Assert(err).Equal(nil) + + entryBefore, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + data := H{ + "acquired_points": task.MaxPoints + 10, + "feedback": "Lorem Ipsum_update", + } + + w := tape.Put("/api/v1/courses/1/grades/1", data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 1, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/grades/1", data, 3, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + entryAfter, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.Feedback).Equal(entryBefore.Feedback) + g.Assert(entryAfter.AcquiredPoints).Equal(entryBefore.AcquiredPoints) + g.Assert(entryAfter.TutorID).Equal(entryBefore.TutorID) + }) + + g.It("Should list missing grades", func() { + gradesActual := []MissingGradeResponse{} + // students have no missing data + // but we do not know if a user is student in a course + w := tape.GetWithClaims("/api/v1/courses/1/grades/missing", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) + err := json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + g.Assert(len(gradesActual)).Equal(0) + + // admin (mock creates feed back for all submissions) + w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + + gradesExpected, err := stores.Grade.GetAllMissingGrades(1, 1, 0) + g.Assert(err).Equal(nil) + g.Assert(len(gradesActual)).Equal(len(gradesExpected)) + + // tutors (mock creates feed back for all submissions) + w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 3, false) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) + + gradesExpected, err = stores.Grade.GetAllMissingGrades(1, 3, 0) + g.Assert(err).Equal(nil) + g.Assert(len(gradesActual)).Equal(len(gradesExpected)) + + _, err = tape.DB.Exec("UPDATE grades SET feedback='' WHERE tutor_id = 3 ") + g.Assert(err).Equal(nil) + + // tutors (mock creates feed back for all submissions) + w = tape.GetWithClaims("/api/v1/courses/1/grades/missing", 3, false) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&gradesActual) + g.Assert(err).Equal(nil) - gradesExpected, err = stores.Grade.GetAllMissingGrades(1, 3, 0) - g.Assert(err).Equal(nil) + gradesExpected, err = stores.Grade.GetAllMissingGrades(1, 3, 0) + g.Assert(err).Equal(nil) - // see mock.py - g.Assert(len(gradesActual)).Equal(len(gradesExpected)) - for k, el := range gradesActual { - g.Assert(el.Grade.ID).Equal(gradesExpected[k].Grade.ID) - g.Assert(el.Grade.PublicExecutionState).Equal(gradesExpected[k].Grade.PublicExecutionState) - g.Assert(el.Grade.PrivateExecutionState).Equal(gradesExpected[k].Grade.PrivateExecutionState) - g.Assert(el.Grade.PublicTestLog).Equal(gradesExpected[k].Grade.PublicTestLog) - g.Assert(el.Grade.PrivateTestLog).Equal(gradesExpected[k].Grade.PrivateTestLog) - g.Assert(el.Grade.PublicTestStatus).Equal(gradesExpected[k].Grade.PublicTestStatus) - g.Assert(el.Grade.PrivateTestStatus).Equal(gradesExpected[k].Grade.PrivateTestStatus) - g.Assert(el.Grade.AcquiredPoints).Equal(gradesExpected[k].Grade.AcquiredPoints) - g.Assert(el.Grade.PrivateTestLog).Equal(gradesExpected[k].Grade.PrivateTestLog) - g.Assert(el.Grade.Feedback).Equal("") - g.Assert(el.Grade.TutorID).Equal(int64(3)) - g.Assert(el.Grade.SubmissionID).Equal(gradesExpected[k].Grade.SubmissionID) + // see mock.py + g.Assert(len(gradesActual)).Equal(len(gradesExpected)) + for k, el := range gradesActual { + g.Assert(el.Grade.ID).Equal(gradesExpected[k].Grade.ID) + g.Assert(el.Grade.PublicExecutionState).Equal(gradesExpected[k].Grade.PublicExecutionState) + g.Assert(el.Grade.PrivateExecutionState).Equal(gradesExpected[k].Grade.PrivateExecutionState) + g.Assert(el.Grade.PublicTestLog).Equal(gradesExpected[k].Grade.PublicTestLog) + g.Assert(el.Grade.PrivateTestLog).Equal(gradesExpected[k].Grade.PrivateTestLog) + g.Assert(el.Grade.PublicTestStatus).Equal(gradesExpected[k].Grade.PublicTestStatus) + g.Assert(el.Grade.PrivateTestStatus).Equal(gradesExpected[k].Grade.PrivateTestStatus) + g.Assert(el.Grade.AcquiredPoints).Equal(gradesExpected[k].Grade.AcquiredPoints) + g.Assert(el.Grade.PrivateTestLog).Equal(gradesExpected[k].Grade.PrivateTestLog) + g.Assert(el.Grade.Feedback).Equal("") + g.Assert(el.Grade.TutorID).Equal(int64(3)) + g.Assert(el.Grade.SubmissionID).Equal(gradesExpected[k].Grade.SubmissionID) - g.Assert(el.Grade.User.ID).Equal(gradesExpected[k].Grade.UserID) - g.Assert(el.Grade.User.FirstName).Equal(gradesExpected[k].Grade.UserFirstName) - g.Assert(el.Grade.User.LastName).Equal(gradesExpected[k].Grade.UserLastName) - g.Assert(el.Grade.User.Email).Equal(gradesExpected[k].Grade.UserEmail) - } - }) + g.Assert(el.Grade.User.ID).Equal(gradesExpected[k].Grade.UserID) + g.Assert(el.Grade.User.FirstName).Equal(gradesExpected[k].Grade.UserFirstName) + g.Assert(el.Grade.User.LastName).Equal(gradesExpected[k].Grade.UserLastName) + g.Assert(el.Grade.User.Email).Equal(gradesExpected[k].Grade.UserEmail) + } + }) - g.It("Should handle feedback from public tests", func() { - - url := "/api/v1/courses/1/grades/1/public_result" - - data := H{ - "log": "some new logs", - "status": 2, - } - - w := tape.Post(url, data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PostWithClaims(url, data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PostWithClaims(url, data, 3, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PostWithClaims(url, data, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.PublicTestLog).Equal("some new logs") - g.Assert(entryAfter.PublicTestStatus).Equal(2) - - }) - - g.It("Should handle feedback from private tests", func() { - - url := "/api/v1/courses/1/grades/1/private_result" - - data := H{ - "log": "some new logs", - "status": 2, - } - - w := tape.Post(url, data) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.PostWithClaims(url, data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PostWithClaims(url, data, 3, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PostWithClaims(url, data, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Grade.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.PrivateTestLog).Equal("some new logs") - g.Assert(entryAfter.PrivateTestStatus).Equal(2) - - }) - - g.It("Should show correct overview", func() { - - course, err := stores.Course.Get(1) - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE tasks CASCADE;") - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE sheets CASCADE;") - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE task_sheet CASCADE;") - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE sheet_course CASCADE;") - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE grades CASCADE;") - g.Assert(err).Equal(nil) - - _, err = tape.DB.Exec("TRUNCATE TABLE submissions CASCADE;") - g.Assert(err).Equal(nil) - - tasks, err := stores.Task.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(tasks)).Equal(0) - - // create Sheets in database - sheet1, err := stores.Sheet.Create(&model.Sheet{ - Name: "1", - PublishAt: NowUTC(), - DueAt: NowUTC(), - }, course.ID) - g.Assert(err).Equal(nil) - - sheet2, err := stores.Sheet.Create(&model.Sheet{ - Name: "2", - PublishAt: NowUTC(), - DueAt: NowUTC(), - }, course.ID) - g.Assert(err).Equal(nil) - - // fmt.Println("sheet 1", sheet1.ID) - // fmt.Println("sheet 2", sheet2.ID) - - // create tasks - task1, err := stores.Task.Create(&model.Task{ - Name: "1", - MaxPoints: 30, - PublicDockerImage: null.StringFrom("ff"), - PrivateDockerImage: null.StringFrom("ff"), - }, sheet1.ID) - - task2, err := stores.Task.Create(&model.Task{ - Name: "2", - MaxPoints: 31, - PublicDockerImage: null.StringFrom("ff"), - PrivateDockerImage: null.StringFrom("ff"), - }, sheet2.ID) - - _ = task1 - _ = task2 - - uid1 := int64(42) - uid2 := int64(43) - - user1, err := stores.User.Get(uid1) - g.Assert(err).Equal(nil) - user2, err := stores.User.Get(uid2) - g.Assert(err).Equal(nil) - - sub1, err := stores.Submission.Create(&model.Submission{UserID: uid1, TaskID: task1.ID}) - g.Assert(err).Equal(nil) - - // test empty grades - response := GradeOverviewResponse{} - w := tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) - // fmt.Println(w.Body) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&response) - g.Assert(err).Equal(nil) - g.Assert(len(response.Sheets)).Equal(2) - g.Assert(len(response.Achievements)).Equal(0) - - grade1 := &model.Grade{ - PublicExecutionState: 0, - PrivateExecutionState: 0, - PublicTestLog: "empty", - PrivateTestLog: "empty", - PublicTestStatus: 0, - PrivateTestStatus: 0, - AcquiredPoints: 5, - Feedback: "", - TutorID: 1, - SubmissionID: sub1.ID, - } - - _, err = stores.Grade.Create(grade1) - g.Assert(err).Equal(nil) - - p := []model.Grade{} - err = tape.DB.Select(&p, "SELECT * FROM grades;") - g.Assert(err).Equal(nil) - g.Assert(len(p)).Equal(1) - - response = GradeOverviewResponse{} - w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) - // fmt.Println(w.Body) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&response) - g.Assert(err).Equal(nil) - g.Assert(len(response.Sheets)).Equal(2) - g.Assert(len(response.Achievements)).Equal(1) - g.Assert(len(response.Achievements[0].Points)).Equal(2) - g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) - g.Assert(response.Achievements[0].Points[1]).Equal(0) - g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) - g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) - - // --------------------- - sub2, err := stores.Submission.Create(&model.Submission{UserID: uid2, TaskID: task2.ID}) - g.Assert(err).Equal(nil) - grade2 := &model.Grade{ - PublicExecutionState: 0, - PrivateExecutionState: 0, - PublicTestLog: "empty", - PrivateTestLog: "empty", - PublicTestStatus: 0, - PrivateTestStatus: 0, - AcquiredPoints: 7, - Feedback: "", - TutorID: 1, - SubmissionID: sub2.ID, - } - - _, err = stores.Grade.Create(grade2) - g.Assert(err).Equal(nil) - - p = []model.Grade{} - err = tape.DB.Select(&p, "SELECT * FROM grades;") - g.Assert(err).Equal(nil) - g.Assert(len(p)).Equal(2) - - response = GradeOverviewResponse{} - w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) - // fmt.Println(w.Body) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&response) - g.Assert(err).Equal(nil) - g.Assert(len(response.Sheets)).Equal(2) - g.Assert(len(response.Achievements)).Equal(2) - g.Assert(len(response.Achievements[0].Points)).Equal(2) - g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) - g.Assert(response.Achievements[0].Points[1]).Equal(0) - g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) - g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) - - g.Assert(len(response.Achievements[1].Points)).Equal(2) - g.Assert(response.Achievements[1].Points[0]).Equal(0) - g.Assert(response.Achievements[1].Points[1]).Equal(grade2.AcquiredPoints) - g.Assert(response.Achievements[1].User.ID).Equal(user2.ID) - g.Assert(response.Achievements[1].User.Email).Equal(user2.Email) - - // --------------------- - sub3, err := stores.Submission.Create(&model.Submission{UserID: uid2, TaskID: task1.ID}) - g.Assert(err).Equal(nil) - grade3 := &model.Grade{ - PublicExecutionState: 0, - PrivateExecutionState: 0, - PublicTestLog: "empty", - PrivateTestLog: "empty", - PublicTestStatus: 0, - PrivateTestStatus: 0, - AcquiredPoints: 8, - Feedback: "", - TutorID: 1, - SubmissionID: sub3.ID, - } - - _, err = stores.Grade.Create(grade3) - g.Assert(err).Equal(nil) - - p = []model.Grade{} - err = tape.DB.Select(&p, "SELECT * FROM grades;") - g.Assert(err).Equal(nil) - g.Assert(len(p)).Equal(3) - - response = GradeOverviewResponse{} - w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) - // fmt.Println(w.Body) - g.Assert(w.Code).Equal(http.StatusOK) - err = json.NewDecoder(w.Body).Decode(&response) - g.Assert(err).Equal(nil) - g.Assert(len(response.Sheets)).Equal(2) - g.Assert(len(response.Achievements)).Equal(2) - g.Assert(len(response.Achievements[0].Points)).Equal(2) - g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) - g.Assert(response.Achievements[0].Points[1]).Equal(0) - g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) - g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) - - g.Assert(len(response.Achievements[1].Points)).Equal(2) - g.Assert(response.Achievements[1].Points[0]).Equal(grade3.AcquiredPoints) - g.Assert(response.Achievements[1].Points[1]).Equal(grade2.AcquiredPoints) - g.Assert(response.Achievements[1].User.ID).Equal(user2.ID) - g.Assert(response.Achievements[1].User.Email).Equal(user2.Email) - - }) - - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g.It("Should handle feedback from public tests", func() { + + url := "/api/v1/courses/1/grades/1/public_result" + + data := H{ + "log": "some new logs", + "status": 2, + } + + w := tape.Post(url, data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PostWithClaims(url, data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PostWithClaims(url, data, 3, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PostWithClaims(url, data, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.PublicTestLog).Equal("some new logs") + g.Assert(entryAfter.PublicTestStatus).Equal(2) + + }) + + g.It("Should handle feedback from private tests", func() { + + url := "/api/v1/courses/1/grades/1/private_result" + + data := H{ + "log": "some new logs", + "status": 2, + } + + w := tape.Post(url, data) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.PostWithClaims(url, data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PostWithClaims(url, data, 3, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PostWithClaims(url, data, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Grade.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.PrivateTestLog).Equal("some new logs") + g.Assert(entryAfter.PrivateTestStatus).Equal(2) + + }) + + g.It("Should show correct overview", func() { + + course, err := stores.Course.Get(1) + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE tasks CASCADE;") + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE sheets CASCADE;") + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE task_sheet CASCADE;") + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE sheet_course CASCADE;") + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE grades CASCADE;") + g.Assert(err).Equal(nil) + + _, err = tape.DB.Exec("TRUNCATE TABLE submissions CASCADE;") + g.Assert(err).Equal(nil) + + tasks, err := stores.Task.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(tasks)).Equal(0) + + // create Sheets in database + sheet1, err := stores.Sheet.Create(&model.Sheet{ + Name: "1", + PublishAt: NowUTC(), + DueAt: NowUTC(), + }, course.ID) + g.Assert(err).Equal(nil) + + sheet2, err := stores.Sheet.Create(&model.Sheet{ + Name: "2", + PublishAt: NowUTC(), + DueAt: NowUTC(), + }, course.ID) + g.Assert(err).Equal(nil) + + // fmt.Println("sheet 1", sheet1.ID) + // fmt.Println("sheet 2", sheet2.ID) + + // create tasks + task1, err := stores.Task.Create(&model.Task{ + Name: "1", + MaxPoints: 30, + PublicDockerImage: null.StringFrom("ff"), + PrivateDockerImage: null.StringFrom("ff"), + }, sheet1.ID) + + task2, err := stores.Task.Create(&model.Task{ + Name: "2", + MaxPoints: 31, + PublicDockerImage: null.StringFrom("ff"), + PrivateDockerImage: null.StringFrom("ff"), + }, sheet2.ID) + + _ = task1 + _ = task2 + + uid1 := int64(42) + uid2 := int64(43) + + user1, err := stores.User.Get(uid1) + g.Assert(err).Equal(nil) + user2, err := stores.User.Get(uid2) + g.Assert(err).Equal(nil) + + sub1, err := stores.Submission.Create(&model.Submission{UserID: uid1, TaskID: task1.ID}) + g.Assert(err).Equal(nil) + + // test empty grades + response := GradeOverviewResponse{} + w := tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) + // fmt.Println(w.Body) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&response) + g.Assert(err).Equal(nil) + g.Assert(len(response.Sheets)).Equal(2) + g.Assert(len(response.Achievements)).Equal(0) + + grade1 := &model.Grade{ + PublicExecutionState: 0, + PrivateExecutionState: 0, + PublicTestLog: "empty", + PrivateTestLog: "empty", + PublicTestStatus: 0, + PrivateTestStatus: 0, + AcquiredPoints: 5, + Feedback: "", + TutorID: 1, + SubmissionID: sub1.ID, + } + + _, err = stores.Grade.Create(grade1) + g.Assert(err).Equal(nil) + + p := []model.Grade{} + err = tape.DB.Select(&p, "SELECT * FROM grades;") + g.Assert(err).Equal(nil) + g.Assert(len(p)).Equal(1) + + response = GradeOverviewResponse{} + w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) + // fmt.Println(w.Body) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&response) + g.Assert(err).Equal(nil) + g.Assert(len(response.Sheets)).Equal(2) + g.Assert(len(response.Achievements)).Equal(1) + g.Assert(len(response.Achievements[0].Points)).Equal(2) + g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) + g.Assert(response.Achievements[0].Points[1]).Equal(0) + g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) + g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) + + // --------------------- + sub2, err := stores.Submission.Create(&model.Submission{UserID: uid2, TaskID: task2.ID}) + g.Assert(err).Equal(nil) + grade2 := &model.Grade{ + PublicExecutionState: 0, + PrivateExecutionState: 0, + PublicTestLog: "empty", + PrivateTestLog: "empty", + PublicTestStatus: 0, + PrivateTestStatus: 0, + AcquiredPoints: 7, + Feedback: "", + TutorID: 1, + SubmissionID: sub2.ID, + } + + _, err = stores.Grade.Create(grade2) + g.Assert(err).Equal(nil) + + p = []model.Grade{} + err = tape.DB.Select(&p, "SELECT * FROM grades;") + g.Assert(err).Equal(nil) + g.Assert(len(p)).Equal(2) + + response = GradeOverviewResponse{} + w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) + // fmt.Println(w.Body) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&response) + g.Assert(err).Equal(nil) + g.Assert(len(response.Sheets)).Equal(2) + g.Assert(len(response.Achievements)).Equal(2) + g.Assert(len(response.Achievements[0].Points)).Equal(2) + g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) + g.Assert(response.Achievements[0].Points[1]).Equal(0) + g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) + g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) + + g.Assert(len(response.Achievements[1].Points)).Equal(2) + g.Assert(response.Achievements[1].Points[0]).Equal(0) + g.Assert(response.Achievements[1].Points[1]).Equal(grade2.AcquiredPoints) + g.Assert(response.Achievements[1].User.ID).Equal(user2.ID) + g.Assert(response.Achievements[1].User.Email).Equal(user2.Email) + + // --------------------- + sub3, err := stores.Submission.Create(&model.Submission{UserID: uid2, TaskID: task1.ID}) + g.Assert(err).Equal(nil) + grade3 := &model.Grade{ + PublicExecutionState: 0, + PrivateExecutionState: 0, + PublicTestLog: "empty", + PrivateTestLog: "empty", + PublicTestStatus: 0, + PrivateTestStatus: 0, + AcquiredPoints: 8, + Feedback: "", + TutorID: 1, + SubmissionID: sub3.ID, + } + + _, err = stores.Grade.Create(grade3) + g.Assert(err).Equal(nil) + + p = []model.Grade{} + err = tape.DB.Select(&p, "SELECT * FROM grades;") + g.Assert(err).Equal(nil) + g.Assert(len(p)).Equal(3) + + response = GradeOverviewResponse{} + w = tape.GetWithClaims("/api/v1/courses/1/grades/summary", 1, true) + // fmt.Println(w.Body) + g.Assert(w.Code).Equal(http.StatusOK) + err = json.NewDecoder(w.Body).Decode(&response) + g.Assert(err).Equal(nil) + g.Assert(len(response.Sheets)).Equal(2) + g.Assert(len(response.Achievements)).Equal(2) + g.Assert(len(response.Achievements[0].Points)).Equal(2) + g.Assert(response.Achievements[0].Points[0]).Equal(grade1.AcquiredPoints) + g.Assert(response.Achievements[0].Points[1]).Equal(0) + g.Assert(response.Achievements[0].User.ID).Equal(user1.ID) + g.Assert(response.Achievements[0].User.Email).Equal(user1.Email) + + g.Assert(len(response.Achievements[1].Points)).Equal(2) + g.Assert(response.Achievements[1].Points[0]).Equal(grade3.AcquiredPoints) + g.Assert(response.Achievements[1].Points[1]).Equal(grade2.AcquiredPoints) + g.Assert(response.Achievements[1].User.ID).Equal(user2.ID) + g.Assert(response.Achievements[1].User.Email).Equal(user2.Email) + + }) + + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/group.go b/api/app/group.go index 8f5e400..3fe05ac 100644 --- a/api/app/group.go +++ b/api/app/group.go @@ -19,32 +19,32 @@ package app import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // GroupResource specifies Group management handler. type GroupResource struct { - Stores *Stores + Stores *Stores } // NewGroupResource create and returns a GroupResource. func NewGroupResource(stores *Stores) *GroupResource { - return &GroupResource{ - Stores: stores, - } + return &GroupResource{ + Stores: stores, + } } // ............................................................................. @@ -63,17 +63,17 @@ func NewGroupResource(stores *Stores) *GroupResource { // The ordering is abitary func (rs *GroupResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - var groups []model.GroupWithTutor - var err error + var groups []model.GroupWithTutor + var err error - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - groups, err = rs.Stores.Group.GroupsOfCourse(course.ID) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + groups, err = rs.Stores.Group.GroupsOfCourse(course.ID) - // render JSON reponse - if err = render.RenderList(w, r, rs.newGroupListResponse(groups)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, rs.newGroupListResponse(groups)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // CreateHandler is public endpoint for @@ -89,41 +89,41 @@ func (rs *GroupResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: create a new group func (rs *GroupResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &groupRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - group := &model.Group{} - group.TutorID = data.Tutor.ID - group.CourseID = course.ID - group.Description = data.Description - - tutor, err := rs.Stores.User.Get(group.TutorID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // create Group entry in database - newGroup, err := rs.Stores.Group.Create(group) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusCreated) - - // return Group information of created entry - if err := render.Render(w, r, rs.newGroupResponse(newGroup, tutor)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // start from empty Request + data := &groupRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + group := &model.Group{} + group.TutorID = data.Tutor.ID + group.CourseID = course.ID + group.Description = data.Description + + tutor, err := rs.Stores.User.Get(group.TutorID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // create Group entry in database + newGroup, err := rs.Stores.Group.Create(group) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusCreated) + + // return Group information of created entry + if err := render.Render(w, r, rs.newGroupResponse(newGroup, tutor)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -139,22 +139,22 @@ func (rs *GroupResource) CreateHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: get a specific group func (rs *GroupResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `Task` is retrieved via middle-ware - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - - tutor, err := rs.Stores.User.Get(group.TutorID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // render JSON reponse - if err := render.Render(w, r, rs.newGroupResponse(group, tutor)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // `Task` is retrieved via middle-ware + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + + tutor, err := rs.Stores.User.Get(group.TutorID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // render JSON reponse + if err := render.Render(w, r, rs.newGroupResponse(group, tutor)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // GetMineHandler is public endpoint for @@ -169,53 +169,53 @@ func (rs *GroupResource) GetHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: get the group the request identity is enrolled in func (rs *GroupResource) GetMineHandler(w http.ResponseWriter, r *http.Request) { - // TODO(patwie): handle case when user is tutor in group - - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - courseRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - var ( - groups []model.GroupWithTutor - err error - ) - - if courseRole == authorize.STUDENT { - // here catch on the cases, when user is a student and enrolled in a group - groups, err = rs.Stores.Group.GetInCourseWithUser(accessClaims.LoginID, course.ID) - } else { - // must be tutor - groups, err = rs.Stores.Group.GetOfTutor(accessClaims.LoginID, course.ID) - - } - - // if we cannot find such an entry, this means the user have not been assigned to a group - if err != nil { - fmt.Println(err) - render.Render(w, r, ErrNotFound) - return - } - - // // students can be only within one group -> [0] is ok - // // tutors can be in multiple groups -> [0] is ok (they are the same person) - // tutor, err := rs.Stores.User.Get(groups[0].TutorID) - // if err != nil { - // render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - // return - // } - - if len(groups) == 0 { - render.Render(w, r, ErrNotFound) - return - } - - // render JSON reponse - if err := render.RenderList(w, r, rs.newGroupListResponse(groups)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // TODO(patwie): handle case when user is tutor in group + + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + courseRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + var ( + groups []model.GroupWithTutor + err error + ) + + if courseRole == authorize.STUDENT { + // here catch on the cases, when user is a student and enrolled in a group + groups, err = rs.Stores.Group.GetInCourseWithUser(accessClaims.LoginID, course.ID) + } else { + // must be tutor + groups, err = rs.Stores.Group.GetOfTutor(accessClaims.LoginID, course.ID) + + } + + // if we cannot find such an entry, this means the user have not been assigned to a group + if err != nil { + fmt.Println(err) + render.Render(w, r, ErrNotFound) + return + } + + // // students can be only within one group -> [0] is ok + // // tutors can be in multiple groups -> [0] is ok (they are the same person) + // tutor, err := rs.Stores.User.Get(groups[0].TutorID) + // if err != nil { + // render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + // return + // } + + if len(groups) == 0 { + render.Render(w, r, ErrNotFound) + return + } + + // render JSON reponse + if err := render.RenderList(w, r, rs.newGroupListResponse(groups)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } @@ -232,26 +232,26 @@ func (rs *GroupResource) GetMineHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: update a specific group func (rs *GroupResource) EditHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &groupRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - group.TutorID = data.Tutor.ID - group.Description = data.Description - - // update database entry - if err := rs.Stores.Group.Update(group); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + // start from empty Request + data := &groupRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + group.TutorID = data.Tutor.ID + group.Description = data.Description + + // update database entry + if err := rs.Stores.Group.Update(group); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // DeleteHandler is public endpoint for @@ -266,15 +266,15 @@ func (rs *GroupResource) EditHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: delete a specific group func (rs *GroupResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - // update database entry - if err := rs.Stores.Group.Delete(group.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Group.Delete(group.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // IndexEnrollmentsHandler is public endpoint for @@ -295,43 +295,43 @@ func (rs *GroupResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: list all courses func (rs *GroupResource) IndexEnrollmentsHandler(w http.ResponseWriter, r *http.Request) { - // /courses/1/enrollments?roles=0,1 - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - // extract filters - filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) - filterFirstName := helper.StringFromURL(r, "first_name", "%%") - filterLastName := helper.StringFromURL(r, "last_name", "%%") - filterEmail := helper.StringFromURL(r, "email", "%%") - filterSubject := helper.StringFromURL(r, "subject", "%%") - filterLanguage := helper.StringFromURL(r, "language", "%%") - - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - if givenRole == authorize.STUDENT { - // students cannot query other students - filterRoles = []string{"1", "2"} - } - - enrolledUsers, err := rs.Stores.Group.EnrolledUsers(course.ID, group.ID, - filterRoles, filterFirstName, filterLastName, filterEmail, - filterSubject, filterLanguage, - ) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - enrolledUsers = EnsurePrivacyInEnrollments(enrolledUsers, givenRole) - - // render JSON reponse - if err = render.RenderList(w, r, newEnrollmentListResponse(enrolledUsers)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // /courses/1/enrollments?roles=0,1 + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + // extract filters + filterRoles := helper.StringArrayFromURL(r, "roles", []string{"0", "1", "2"}) + filterFirstName := helper.StringFromURL(r, "first_name", "%%") + filterLastName := helper.StringFromURL(r, "last_name", "%%") + filterEmail := helper.StringFromURL(r, "email", "%%") + filterSubject := helper.StringFromURL(r, "subject", "%%") + filterLanguage := helper.StringFromURL(r, "language", "%%") + + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + if givenRole == authorize.STUDENT { + // students cannot query other students + filterRoles = []string{"1", "2"} + } + + enrolledUsers, err := rs.Stores.Group.EnrolledUsers(course.ID, group.ID, + filterRoles, filterFirstName, filterLastName, filterEmail, + filterSubject, filterLanguage, + ) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + enrolledUsers = EnsurePrivacyInEnrollments(enrolledUsers, givenRole) + + // render JSON reponse + if err = render.RenderList(w, r, newEnrollmentListResponse(enrolledUsers)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // EditGroupEnrollmentHandler is public endpoint for @@ -347,44 +347,44 @@ func (rs *GroupResource) IndexEnrollmentsHandler(w http.ResponseWriter, r *http. // RESPONSE: 403,Unauthorized // SUMMARY: will assign a given user to a group or change the group assignment func (rs *GroupResource) EditGroupEnrollmentHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &groupEnrollmentRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - enrollment, err := rs.Stores.Group.GetGroupEnrollmentOfUserInCourse(data.UserID, course.ID) - - if err != nil { - // does not exists yet - - enrollment := &model.GroupEnrollment{ - UserID: data.UserID, - GroupID: group.ID, - } - - _, err := rs.Stores.Group.CreateGroupEnrollmentOfUserInCourse(enrollment) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - } else { - // does exists --> simply change it - enrollment.GroupID = group.ID - if err := rs.Stores.Group.ChangeGroupEnrollmentOfUserInCourse(enrollment); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - } - - render.Status(r, http.StatusNoContent) + // start from empty Request + data := &groupEnrollmentRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + enrollment, err := rs.Stores.Group.GetGroupEnrollmentOfUserInCourse(data.UserID, course.ID) + + if err != nil { + // does not exists yet + + enrollment := &model.GroupEnrollment{ + UserID: data.UserID, + GroupID: group.ID, + } + + _, err := rs.Stores.Group.CreateGroupEnrollmentOfUserInCourse(enrollment) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + } else { + // does exists --> simply change it + enrollment.GroupID = group.ID + if err := rs.Stores.Group.ChangeGroupEnrollmentOfUserInCourse(enrollment); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + } + + render.Status(r, http.StatusNoContent) } // ChangeBidHandler is public endpoint for @@ -401,53 +401,53 @@ func (rs *GroupResource) EditGroupEnrollmentHandler(w http.ResponseWriter, r *ht // SUMMARY: change or add the bid for enrolling in a group func (rs *GroupResource) ChangeBidHandler(w http.ResponseWriter, r *http.Request) { - courseRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - if courseRole != authorize.STUDENT { - render.Render(w, r, ErrBadRequestWithDetails(errors.New("Only students in a course can bid for a group"))) - return - } - - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - // start from empty Request - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - - data := &groupBidRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - _, existsErr := rs.Stores.Group.GetBidOfUserForGroup(accessClaims.LoginID, group.ID) - if existsErr == nil { - // exists - // update database entry - if _, err := rs.Stores.Group.UpdateBidOfUserForGroup(accessClaims.LoginID, group.ID, data.Bid); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - render.Status(r, http.StatusNoContent) - } else { - // insert - // insert database entry - if _, err := rs.Stores.Group.InsertBidOfUserForGroup(accessClaims.LoginID, group.ID, data.Bid); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - render.Status(r, http.StatusCreated) - - resp := &GroupBidResponse{Bid: data.Bid} - - if err := render.Render(w, r, resp); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - } - - render.Status(r, http.StatusNoContent) + courseRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + if courseRole != authorize.STUDENT { + render.Render(w, r, ErrBadRequestWithDetails(errors.New("Only students in a course can bid for a group"))) + return + } + + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + // start from empty Request + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + + data := &groupBidRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + _, existsErr := rs.Stores.Group.GetBidOfUserForGroup(accessClaims.LoginID, group.ID) + if existsErr == nil { + // exists + // update database entry + if _, err := rs.Stores.Group.UpdateBidOfUserForGroup(accessClaims.LoginID, group.ID, data.Bid); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + render.Status(r, http.StatusNoContent) + } else { + // insert + // insert database entry + if _, err := rs.Stores.Group.InsertBidOfUserForGroup(accessClaims.LoginID, group.ID, data.Bid); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + render.Status(r, http.StatusCreated) + + resp := &GroupBidResponse{Bid: data.Bid} + + if err := render.Render(w, r, resp); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + } + + render.Status(r, http.StatusNoContent) } // SendEmailHandler is public endpoint for @@ -465,35 +465,35 @@ func (rs *GroupResource) ChangeBidHandler(w http.ResponseWriter, r *http.Request // SUMMARY: send email to entire group func (rs *GroupResource) SendEmailHandler(w http.ResponseWriter, r *http.Request) { - group := r.Context().Value(common.CtxKeyGroup).(*model.Group) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) + group := r.Context().Value(common.CtxKeyGroup).(*model.Group) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) - data := &EmailRequest{} + data := &EmailRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - recipients, err := rs.Stores.Group.GetMembers(group.ID) + recipients, err := rs.Stores.Group.GetMembers(group.ID) - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - for _, recipient := range recipients { - msg := email.NewEmailFromUser( - recipient.Email, - data.Subject, - data.Body, - accessUser, - ) + for _, recipient := range recipients { + msg := email.NewEmailFromUser( + recipient.Email, + data.Subject, + data.Body, + accessUser, + ) - email.OutgoingEmailsChannel <- msg - } + email.OutgoingEmailsChannel <- msg + } } @@ -504,44 +504,44 @@ func (rs *GroupResource) SendEmailHandler(w http.ResponseWriter, r *http.Request // the group could not be found, we stop here and return a 404. // We do NOT check whether the identity is authorized to get this group. func (rs *GroupResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - var groupID int64 - var err error - - // try to get id from URL - if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific group in database - group, err := rs.Stores.Group.Get(groupID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - ctx := context.WithValue(r.Context(), common.CtxKeyGroup, group) - - // when there is a groupID in the url, there is NOT a courseID in the url, - // BUT: when there is a group, there is a course - - course, err := rs.Stores.Group.IdentifyCourseOfGroup(group.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if courseFromURL.ID != course.ID { - render.Render(w, r, ErrNotFound) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - - // serve next - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + var groupID int64 + var err error + + // try to get id from URL + if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific group in database + group, err := rs.Stores.Group.Get(groupID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), common.CtxKeyGroup, group) + + // when there is a groupID in the url, there is NOT a courseID in the url, + // BUT: when there is a group, there is a course + + course, err := rs.Stores.Group.IdentifyCourseOfGroup(group.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if courseFromURL.ID != course.ID { + render.Render(w, r, ErrNotFound) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + + // serve next + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/group_test.go b/api/app/group_test.go index f0f566c..c60cf6f 100644 --- a/api/app/group_test.go +++ b/api/app/group_test.go @@ -19,392 +19,392 @@ package app import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" ) func TestGroup(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - - tape := &Tape{} - - var stores *Stores - - g.Describe("Group", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) - - g.It("Query should require access claims", func() { - - w := tape.Get("/api/v1/courses/1/groups") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims("/api/v1/courses/1/groups", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Should list all groups from a course", func() { - w := tape.GetWithClaims("/api/v1/courses/1/groups", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - groupsActual := []GroupResponse{} - err := json.NewDecoder(w.Body).Decode(&groupsActual) - g.Assert(err).Equal(nil) - g.Assert(len(groupsActual)).Equal(10) - }) - - g.It("Should get a specific group", func() { - entryExpected, err := stores.Group.Get(1) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/groups/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - entryActual := &GroupResponse{} - err = json.NewDecoder(w.Body).Decode(entryActual) - g.Assert(err).Equal(nil) - - g.Assert(entryActual.ID).Equal(entryExpected.ID) - g.Assert(entryActual.Tutor.ID).Equal(entryExpected.TutorID) - g.Assert(entryActual.CourseID).Equal(entryExpected.CourseID) - g.Assert(entryActual.Description).Equal(entryExpected.Description) - - t, err := stores.User.Get(entryExpected.TutorID) - g.Assert(err).Equal(nil) - g.Assert(entryActual.Tutor.FirstName).Equal(t.FirstName) - g.Assert(entryActual.Tutor.LastName).Equal(t.LastName) - g.Assert(entryActual.Tutor.AvatarURL).Equal(t.AvatarURL) - g.Assert(entryActual.Tutor.Email).Equal(t.Email) - g.Assert(entryActual.Tutor.Language).Equal(t.Language) - }) - - g.It("Creating should require claims", func() { - w := tape.Post("/api/v1/courses/1/groups", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.Xit("Creating should require body", func() { - // TODO empty request with claims - }) - - g.It("Should create valid group", func() { - entriesBefore, err := stores.Group.GroupsOfCourse(1) - g.Assert(err).Equal(nil) - - tutorID := int64(1) - - entrySent := helper.H{ - "tutor": helper.H{ - "id": tutorID, - }, - "description": "blah blahe", - } - - // err = entrySent.Validate() - // g.Assert(err).Equal(nil) - - w := tape.PostWithClaims("/api/v1/courses/1/groups", entrySent, 1, true) - g.Assert(w.Code).Equal(http.StatusCreated) - - entryReturn := &GroupResponse{} - err = json.NewDecoder(w.Body).Decode(&entryReturn) - g.Assert(entryReturn.Tutor.ID).Equal(tutorID) - g.Assert(entryReturn.CourseID).Equal(int64(1)) - g.Assert(entryReturn.Description).Equal("blah blahe") - - t, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - g.Assert(entryReturn.Tutor.FirstName).Equal(t.FirstName) - g.Assert(entryReturn.Tutor.LastName).Equal(t.LastName) - g.Assert(entryReturn.Tutor.AvatarURL).Equal(t.AvatarURL) - g.Assert(entryReturn.Tutor.Email).Equal(t.Email) - g.Assert(entryReturn.Tutor.Language).Equal(t.Language) - - entriesAfter, err := stores.Group.GroupsOfCourse(1) - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) + 1) - }) - - g.It("Should update a group", func() { - // group (id=1) belongs to course(id=1) - tutorID := int64(9) - entrySent := helper.H{ - "tutor": helper.H{ - "id": tutorID, - }, - "description": "new descr", - } - - // students - w := tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryAfter, err := stores.Group.Get(1) - g.Assert(err).Equal(nil) - - g.Assert(entryAfter.TutorID).Equal(tutorID) - g.Assert(entryAfter.CourseID).Equal(int64(1)) - }) - - g.It("Should delete when valid access claims", func() { - entriesBefore, err := stores.Group.GetAll() - g.Assert(err).Equal(nil) - - w := tape.Delete("/api/v1/courses/1/groups/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // verify nothing has changes - entriesAfter, err := stores.Group.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) - - // students - w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // verify a sheet less exists - entriesAfter, err = stores.Group.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) - - g.It("Should change a bid to a group", func() { - userID := int64(112) - - // admins are not allowed - w := tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, 1, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - // tutors are not allowed - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, 2, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) - - // students - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, userID, false) - g.Assert(w.Code).Equal(http.StatusOK) - // no content - - // delete to test insert - tape.DB.Exec(`DELETE FROM group_bids where user_id = $1`, userID) - - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, userID, false) - g.Assert(w.Code).Equal(http.StatusCreated) - entryReturn := &GroupBidResponse{} - err := json.NewDecoder(w.Body).Decode(&entryReturn) - g.Assert(err).Equal(nil) - g.Assert(entryReturn.Bid).Equal(4) - - }) - - g.It("Find my group when being a student", func() { - // a random student (checked via pgweb) - loginID := int64(112) - - w := tape.Get("/api/v1/courses/1/groups/own") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + + tape := &Tape{} + + var stores *Stores + + g.Describe("Group", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) + + g.It("Query should require access claims", func() { + + w := tape.Get("/api/v1/courses/1/groups") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims("/api/v1/courses/1/groups", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Should list all groups from a course", func() { + w := tape.GetWithClaims("/api/v1/courses/1/groups", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + groupsActual := []GroupResponse{} + err := json.NewDecoder(w.Body).Decode(&groupsActual) + g.Assert(err).Equal(nil) + g.Assert(len(groupsActual)).Equal(10) + }) + + g.It("Should get a specific group", func() { + entryExpected, err := stores.Group.Get(1) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/groups/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + entryActual := &GroupResponse{} + err = json.NewDecoder(w.Body).Decode(entryActual) + g.Assert(err).Equal(nil) + + g.Assert(entryActual.ID).Equal(entryExpected.ID) + g.Assert(entryActual.Tutor.ID).Equal(entryExpected.TutorID) + g.Assert(entryActual.CourseID).Equal(entryExpected.CourseID) + g.Assert(entryActual.Description).Equal(entryExpected.Description) + + t, err := stores.User.Get(entryExpected.TutorID) + g.Assert(err).Equal(nil) + g.Assert(entryActual.Tutor.FirstName).Equal(t.FirstName) + g.Assert(entryActual.Tutor.LastName).Equal(t.LastName) + g.Assert(entryActual.Tutor.AvatarURL).Equal(t.AvatarURL) + g.Assert(entryActual.Tutor.Email).Equal(t.Email) + g.Assert(entryActual.Tutor.Language).Equal(t.Language) + }) + + g.It("Creating should require claims", func() { + w := tape.Post("/api/v1/courses/1/groups", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.Xit("Creating should require body", func() { + // TODO empty request with claims + }) + + g.It("Should create valid group", func() { + entriesBefore, err := stores.Group.GroupsOfCourse(1) + g.Assert(err).Equal(nil) + + tutorID := int64(1) + + entrySent := helper.H{ + "tutor": helper.H{ + "id": tutorID, + }, + "description": "blah blahe", + } + + // err = entrySent.Validate() + // g.Assert(err).Equal(nil) + + w := tape.PostWithClaims("/api/v1/courses/1/groups", entrySent, 1, true) + g.Assert(w.Code).Equal(http.StatusCreated) + + entryReturn := &GroupResponse{} + err = json.NewDecoder(w.Body).Decode(&entryReturn) + g.Assert(entryReturn.Tutor.ID).Equal(tutorID) + g.Assert(entryReturn.CourseID).Equal(int64(1)) + g.Assert(entryReturn.Description).Equal("blah blahe") + + t, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + g.Assert(entryReturn.Tutor.FirstName).Equal(t.FirstName) + g.Assert(entryReturn.Tutor.LastName).Equal(t.LastName) + g.Assert(entryReturn.Tutor.AvatarURL).Equal(t.AvatarURL) + g.Assert(entryReturn.Tutor.Email).Equal(t.Email) + g.Assert(entryReturn.Tutor.Language).Equal(t.Language) + + entriesAfter, err := stores.Group.GroupsOfCourse(1) + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) + 1) + }) + + g.It("Should update a group", func() { + // group (id=1) belongs to course(id=1) + tutorID := int64(9) + entrySent := helper.H{ + "tutor": helper.H{ + "id": tutorID, + }, + "description": "new descr", + } + + // students + w := tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PlayDataWithClaims("PUT", "/api/v1/courses/1/groups/1", entrySent, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryAfter, err := stores.Group.Get(1) + g.Assert(err).Equal(nil) + + g.Assert(entryAfter.TutorID).Equal(tutorID) + g.Assert(entryAfter.CourseID).Equal(int64(1)) + }) + + g.It("Should delete when valid access claims", func() { + entriesBefore, err := stores.Group.GetAll() + g.Assert(err).Equal(nil) + + w := tape.Delete("/api/v1/courses/1/groups/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // verify nothing has changes + entriesAfter, err := stores.Group.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) + + // students + w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.DeleteWithClaims("/api/v1/courses/1/groups/1", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // verify a sheet less exists + entriesAfter, err = stores.Group.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + }) + + g.It("Should change a bid to a group", func() { + userID := int64(112) + + // admins are not allowed + w := tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, 1, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + // tutors are not allowed + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, 2, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) + + // students + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, userID, false) + g.Assert(w.Code).Equal(http.StatusOK) + // no content + + // delete to test insert + tape.DB.Exec(`DELETE FROM group_bids where user_id = $1`, userID) + + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/bids", H{"bid": 4}, userID, false) + g.Assert(w.Code).Equal(http.StatusCreated) + entryReturn := &GroupBidResponse{} + err := json.NewDecoder(w.Body).Decode(&entryReturn) + g.Assert(err).Equal(nil) + g.Assert(entryReturn.Bid).Equal(4) + + }) + + g.It("Find my group when being a student", func() { + // a random student (checked via pgweb) + loginID := int64(112) + + w := tape.Get("/api/v1/courses/1/groups/own") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/groups/own", loginID, false) - g.Assert(w.Code).Equal(http.StatusOK) - - entryReturn := []GroupResponse{} - err := json.NewDecoder(w.Body).Decode(&entryReturn) - g.Assert(err).Equal(nil) - - // we cannot check the other entries - g.Assert(entryReturn[0].CourseID).Equal(int64(1)) - }) + w = tape.GetWithClaims("/api/v1/courses/1/groups/own", loginID, false) + g.Assert(w.Code).Equal(http.StatusOK) + + entryReturn := []GroupResponse{} + err := json.NewDecoder(w.Body).Decode(&entryReturn) + g.Assert(err).Equal(nil) + + // we cannot check the other entries + g.Assert(entryReturn[0].CourseID).Equal(int64(1)) + }) - g.It("Find my group when being a tutor", func() { - // a random student (checked via pgweb) - loginID := int64(2) + g.It("Find my group when being a tutor", func() { + // a random student (checked via pgweb) + loginID := int64(2) - w := tape.Get("/api/v1/courses/1/groups/own") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/groups/own") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/groups/own", loginID, true) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/groups/own", loginID, true) + g.Assert(w.Code).Equal(http.StatusOK) - entryReturn := []GroupResponse{} - err := json.NewDecoder(w.Body).Decode(&entryReturn) - g.Assert(err).Equal(nil) + entryReturn := []GroupResponse{} + err := json.NewDecoder(w.Body).Decode(&entryReturn) + g.Assert(err).Equal(nil) - // we cannot check the other entries - g.Assert(entryReturn[0].CourseID).Equal(int64(1)) - g.Assert(entryReturn[0].Tutor.ID).Equal(loginID) + // we cannot check the other entries + g.Assert(entryReturn[0].CourseID).Equal(int64(1)) + g.Assert(entryReturn[0].Tutor.ID).Equal(loginID) - t, err := stores.User.Get(loginID) - g.Assert(err).Equal(nil) - g.Assert(entryReturn[0].Tutor.FirstName).Equal(t.FirstName) - g.Assert(entryReturn[0].Tutor.LastName).Equal(t.LastName) - g.Assert(entryReturn[0].Tutor.AvatarURL).Equal(t.AvatarURL) - g.Assert(entryReturn[0].Tutor.Email).Equal(t.Email) - g.Assert(entryReturn[0].Tutor.Language).Equal(t.Language) + t, err := stores.User.Get(loginID) + g.Assert(err).Equal(nil) + g.Assert(entryReturn[0].Tutor.FirstName).Equal(t.FirstName) + g.Assert(entryReturn[0].Tutor.LastName).Equal(t.LastName) + g.Assert(entryReturn[0].Tutor.AvatarURL).Equal(t.AvatarURL) + g.Assert(entryReturn[0].Tutor.Email).Equal(t.Email) + g.Assert(entryReturn[0].Tutor.Language).Equal(t.Language) - }) + }) - g.It("Only tutors and admins can send emails to a group", func() { - w := tape.Post("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) + g.It("Only tutors and admins can send emails to a group", func() { + w := tape.Post("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) - // student - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // student + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // tutor - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + // tutor + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - // admin - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + // admin + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/emails", H{"subject": "subj", "body": "body"}, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Permission test", func() { - url := "/api/v1/courses/1/groups" + g.It("Permission test", func() { + url := "/api/v1/courses/1/groups" - // global root can do whatever they want - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + // global root can do whatever they want + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled tutors can access - w = tape.GetWithClaims(url, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled tutors can access + w = tape.GetWithClaims(url, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled students can access - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled students can access + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // disenroll student - w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // disenroll student + w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // cannot access anymore - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) + // cannot access anymore + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) - g.It("should manually add user to group (update)", func() { - url := "/api/v1/courses/1/groups/1/enrollments" + g.It("should manually add user to group (update)", func() { + url := "/api/v1/courses/1/groups/1/enrollments" - w := tape.Post(url, H{"user_id": 112}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Post(url, H{"user_id": 112}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) - // student - w = tape.PostWithClaims(url, H{"user_id": 112}, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // student + w = tape.PostWithClaims(url, H{"user_id": 112}, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // tutor - w = tape.PostWithClaims(url, H{"user_id": 112}, 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // tutor + w = tape.PostWithClaims(url, H{"user_id": 112}, 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // admin - w = tape.PostWithClaims(url, H{"user_id": 112}, 1, false) - fmt.Println(w.Body) - g.Assert(w.Code).Equal(http.StatusOK) + // admin + w = tape.PostWithClaims(url, H{"user_id": 112}, 1, false) + fmt.Println(w.Body) + g.Assert(w.Code).Equal(http.StatusOK) - enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) - g.Assert(err).Equal(nil) - g.Assert(enrollment.UserID).Equal(int64(112)) - g.Assert(enrollment.GroupID).Equal(int64(1)) + enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) + g.Assert(err).Equal(nil) + g.Assert(enrollment.UserID).Equal(int64(112)) + g.Assert(enrollment.GroupID).Equal(int64(1)) - // admin - w = tape.PostWithClaims("/api/v1/courses/1/groups/2/enrollments", H{"user_id": 112}, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + // admin + w = tape.PostWithClaims("/api/v1/courses/1/groups/2/enrollments", H{"user_id": 112}, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - enrollment, err = stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) - g.Assert(err).Equal(nil) - g.Assert(enrollment.UserID).Equal(int64(112)) - g.Assert(enrollment.GroupID).Equal(int64(2)) + enrollment, err = stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) + g.Assert(err).Equal(nil) + g.Assert(enrollment.UserID).Equal(int64(112)) + g.Assert(enrollment.GroupID).Equal(int64(2)) - }) + }) - g.It("should manually add user to group (create)", func() { + g.It("should manually add user to group (create)", func() { - // remove all user_group from student - _, err := tape.DB.Exec("DELETE FROM user_group WHERE user_id = 112;") - g.Assert(err).Equal(nil) + // remove all user_group from student + _, err := tape.DB.Exec("DELETE FROM user_group WHERE user_id = 112;") + g.Assert(err).Equal(nil) - w := tape.Post("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Post("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) - // student - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // student + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // tutor - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // tutor + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // admin - w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + // admin + w = tape.PostWithClaims("/api/v1/courses/1/groups/1/enrollments", H{"user_id": 112}, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) - g.Assert(err).Equal(nil) - g.Assert(enrollment.UserID).Equal(int64(112)) - g.Assert(enrollment.GroupID).Equal(int64(1)) + enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) + g.Assert(err).Equal(nil) + g.Assert(enrollment.UserID).Equal(int64(112)) + g.Assert(enrollment.GroupID).Equal(int64(1)) - // admin - w = tape.PostWithClaims("/api/v1/courses/1/groups/2/enrollments", H{"user_id": 112}, 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + // admin + w = tape.PostWithClaims("/api/v1/courses/1/groups/2/enrollments", H{"user_id": 112}, 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - enrollment, err = stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) - g.Assert(err).Equal(nil) - g.Assert(enrollment.UserID).Equal(int64(112)) - g.Assert(enrollment.GroupID).Equal(int64(2)) + enrollment, err = stores.Group.GetGroupEnrollmentOfUserInCourse(112, 1) + g.Assert(err).Equal(nil) + g.Assert(enrollment.UserID).Equal(int64(112)) + g.Assert(enrollment.GroupID).Equal(int64(2)) - }) + }) - g.It("Should be able to filter enrollments (all)", func() { - groupActive, err := stores.Group.Get(1) - g.Assert(err).Equal(nil) + g.It("Should be able to filter enrollments (all)", func() { + groupActive, err := stores.Group.Get(1) + g.Assert(err).Equal(nil) - numberEnrollmentsExpected, err := DBGetInt( - tape, - "SELECT count(*) FROM user_group WHERE group_id = $1", - groupActive.ID, - ) - g.Assert(err).Equal(nil) + numberEnrollmentsExpected, err := DBGetInt( + tape, + "SELECT count(*) FROM user_group WHERE group_id = $1", + groupActive.ID, + ) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/groups/1/enrollments", 1, true) - enrollmentsActual := []enrollmentResponse{} - err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) - g.Assert(err).Equal(nil) - g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) - }) + w := tape.GetWithClaims("/api/v1/courses/1/groups/1/enrollments", 1, true) + enrollmentsActual := []enrollmentResponse{} + err = json.NewDecoder(w.Body).Decode(&enrollmentsActual) + g.Assert(err).Equal(nil) + g.Assert(len(enrollmentsActual)).Equal(numberEnrollmentsExpected) + }) - g.AfterEach(func() { - tape.AfterEach() - }) + g.AfterEach(func() { + tape.AfterEach() + }) - }) + }) } diff --git a/api/app/material.go b/api/app/material.go index cb07048..674b84a 100644 --- a/api/app/material.go +++ b/api/app/material.go @@ -19,29 +19,29 @@ package app import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // MaterialResource specifies Material management handler. type MaterialResource struct { - Stores *Stores + Stores *Stores } // NewMaterialResource create and returns a MaterialResource. func NewMaterialResource(stores *Stores) *MaterialResource { - return &MaterialResource{ - Stores: stores, - } + return &MaterialResource{ + Stores: stores, + } } // IndexHandler is public endpoint for @@ -59,23 +59,23 @@ func NewMaterialResource(stores *Stores) *MaterialResource { // Kind means 0: slide, 1: supplementary func (rs *MaterialResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - var materials []model.Material - var err error - // we use middle to detect whether there is a course given - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - materials, err = rs.Stores.Material.MaterialsOfCourse(course.ID, givenRole.ToInt()) - - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // render JSON reponse - if err = render.RenderList(w, r, rs.newMaterialListResponse(givenRole, course.ID, materials)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + var materials []model.Material + var err error + // we use middle to detect whether there is a course given + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + materials, err = rs.Stores.Material.MaterialsOfCourse(course.ID, givenRole.ToInt()) + + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // render JSON reponse + if err = render.RenderList(w, r, rs.newMaterialListResponse(givenRole, course.ID, materials)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // CreateHandler is public endpoint for @@ -93,39 +93,39 @@ func (rs *MaterialResource) IndexHandler(w http.ResponseWriter, r *http.Request) // Kind means 0: slide, 1: supplementary func (rs *MaterialResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - // start from empty Request - data := &MaterialRequest{} + // start from empty Request + data := &MaterialRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - material := &model.Material{ - Name: data.Name, - Kind: data.Kind, - PublishAt: data.PublishAt, - LectureAt: data.LectureAt, - RequiredRole: data.RequiredRole, - } + material := &model.Material{ + Name: data.Name, + Kind: data.Kind, + PublishAt: data.PublishAt, + LectureAt: data.LectureAt, + RequiredRole: data.RequiredRole, + } - // create Material entry in database - newMaterial, err := rs.Stores.Material.Create(material, course.ID) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // create Material entry in database + newMaterial, err := rs.Stores.Material.Create(material, course.ID) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusCreated) + render.Status(r, http.StatusCreated) - // return Material information of created entry - if err := render.Render(w, r, rs.newMaterialResponse(newMaterial, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // return Material information of created entry + if err := render.Render(w, r, rs.newMaterialResponse(newMaterial, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -143,28 +143,28 @@ func (rs *MaterialResource) CreateHandler(w http.ResponseWriter, r *http.Request // DESCRIPTION: // Kind means 0: slide, 1: supplementary func (rs *MaterialResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `Material` is retrieved via middle-ware - material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - if givenRole == authorize.STUDENT && !PublicYet(material.PublishAt) { - render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("Not public yet"))) - return - } - - if material.RequiredRole > givenRole.ToInt() { - render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("no access allowed with your role"))) - return - } - - // render JSON reponse - if err := render.Render(w, r, rs.newMaterialResponse(material, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // `Material` is retrieved via middle-ware + material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + if givenRole == authorize.STUDENT && !PublicYet(material.PublishAt) { + render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("Not public yet"))) + return + } + + if material.RequiredRole > givenRole.ToInt() { + render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("no access allowed with your role"))) + return + } + + // render JSON reponse + if err := render.Render(w, r, rs.newMaterialResponse(material, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // EditHandler is public endpoint for @@ -182,30 +182,30 @@ func (rs *MaterialResource) GetHandler(w http.ResponseWriter, r *http.Request) { // DESCRIPTION: // Kind means 0: slide, 1: supplementary func (rs *MaterialResource) EditHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &MaterialRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) - - material.Name = data.Name - material.Kind = data.Kind - material.PublishAt = data.PublishAt - material.LectureAt = data.LectureAt - material.RequiredRole = data.RequiredRole - - // update database entry - if err := rs.Stores.Material.Update(material); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + // start from empty Request + data := &MaterialRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) + + material.Name = data.Name + material.Kind = data.Kind + material.PublishAt = data.PublishAt + material.LectureAt = data.LectureAt + material.RequiredRole = data.RequiredRole + + // update database entry + if err := rs.Stores.Material.Update(material); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // DeleteHandler is public endpoint for @@ -220,15 +220,15 @@ func (rs *MaterialResource) EditHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: delete a specific material func (rs *MaterialResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) + material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) - // update database entry - if err := rs.Stores.Material.Delete(material.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Material.Delete(material.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // GetFileHandler is public endpoint for @@ -244,23 +244,23 @@ func (rs *MaterialResource) DeleteHandler(w http.ResponseWriter, r *http.Request // SUMMARY: get the zip file of a material func (rs *MaterialResource) GetFileHandler(w http.ResponseWriter, r *http.Request) { - material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - hnd := helper.NewMaterialFileHandle(material.ID) - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } - - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - if givenRole == authorize.STUDENT && !PublicYet(material.PublishAt) { - render.Render(w, r, ErrNotFound) - return - } - - if err := hnd.WriteToBodyWithName(fmt.Sprintf("%s-%s", course.Name, material.Filename), w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + hnd := helper.NewMaterialFileHandle(material.ID) + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } + + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + if givenRole == authorize.STUDENT && !PublicYet(material.PublishAt) { + render.Render(w, r, ErrNotFound) + return + } + + if err := hnd.WriteToBodyWithName(fmt.Sprintf("%s-%s", course.Name, material.Filename), w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } // ChangeFileHandler is public endpoint for @@ -278,27 +278,27 @@ func (rs *MaterialResource) GetFileHandler(w http.ResponseWriter, r *http.Reques // DESCRIPTION: // This endpoint will only support pdf or zip files. func (rs *MaterialResource) ChangeFileHandler(w http.ResponseWriter, r *http.Request) { - // will always be a POST - material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) + // will always be a POST + material := r.Context().Value(common.CtxKeyMaterial).(*model.Material) - var ( - err error - filename string - ) + var ( + err error + filename string + ) - // the file will be located - if filename, err = helper.NewMaterialFileHandle(material.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + // the file will be located + if filename, err = helper.NewMaterialFileHandle(material.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } - material.Filename = filename + material.Filename = filename - if err := rs.Stores.Material.Update(material); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + if err := rs.Stores.Material.Update(material); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // ............................................................................. @@ -308,50 +308,50 @@ func (rs *MaterialResource) ChangeFileHandler(w http.ResponseWriter, r *http.Req // the Material could not be found, we stop here and return a 404. // We do NOT check whether the Material is authorized to get this Material. func (rs *MaterialResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) - // Should be done via another middleware - var materialID int64 - var err error - - // try to get id from URL - if materialID, err = strconv.ParseInt(chi.URLParam(r, "material_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific Material in database - material, err := rs.Stores.Material.Get(materialID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // public yet? - if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(material.PublishAt) { - render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("material not published yet"))) - return - } - - ctx := context.WithValue(r.Context(), common.CtxKeyMaterial, material) - - // when there is a sheetID in the url, there is NOT a courseID in the url, - // BUT: when there is a material, there is a course - - course, err := rs.Stores.Material.IdentifyCourseOfMaterial(material.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if courseFromURL.ID != course.ID { - render.Render(w, r, ErrNotFound) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - - // serve next - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) + // Should be done via another middleware + var materialID int64 + var err error + + // try to get id from URL + if materialID, err = strconv.ParseInt(chi.URLParam(r, "material_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific Material in database + material, err := rs.Stores.Material.Get(materialID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // public yet? + if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(material.PublishAt) { + render.Render(w, r, ErrUnauthorizedWithDetails(fmt.Errorf("material not published yet"))) + return + } + + ctx := context.WithValue(r.Context(), common.CtxKeyMaterial, material) + + // when there is a sheetID in the url, there is NOT a courseID in the url, + // BUT: when there is a material, there is a course + + course, err := rs.Stores.Material.IdentifyCourseOfMaterial(material.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if courseFromURL.ID != course.ID { + render.Render(w, r, ErrNotFound) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + + // serve next + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/material_test.go b/api/app/material_test.go index a53c517..2841dd3 100644 --- a/api/app/material_test.go +++ b/api/app/material_test.go @@ -19,490 +19,490 @@ package app import ( - "encoding/json" - "fmt" - "mime" - "net/http" - "testing" - "time" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "mime" + "net/http" + "testing" + "time" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + "github.com/spf13/viper" ) func TestMaterial(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail - tape := &Tape{} + tape := &Tape{} - var stores *Stores + var stores *Stores - g.Describe("Material", func() { + g.Describe("Material", func() { - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - }) + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + }) - g.It("Query should require access claims", func() { + g.It("Query should require access claims", func() { - w := tape.Get("/api/v1/courses/1/materials") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/materials") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/materials", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) + w = tape.GetWithClaims("/api/v1/courses/1/materials", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) - g.It("Should list all materials a course (student)", func() { - materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) - g.Assert(err).Equal(nil) + g.It("Should list all materials a course (student)", func() { + materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) + g.Assert(err).Equal(nil) - for _, mat := range materialsExpected { - mat.PublishAt = NowUTC().Add(-time.Hour) - stores.Material.Update(&mat) - } + for _, mat := range materialsExpected { + mat.PublishAt = NowUTC().Add(-time.Hour) + stores.Material.Update(&mat) + } - user, err := stores.Course.GetUserEnrollment(1, 112) - g.Assert(err).Equal(nil) - g.Assert(user.Role).Equal(int64(0)) + user, err := stores.Course.GetUserEnrollment(1, 112) + g.Assert(err).Equal(nil) + g.Assert(user.Role).Equal(int64(0)) - w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) + g.Assert(w.Code).Equal(http.StatusOK) - materialsActual := []MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(&materialsActual) - g.Assert(err).Equal(nil) + materialsActual := []MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(&materialsActual) + g.Assert(err).Equal(nil) - g.Assert(len(materialsActual)).Equal(len(materialsExpected)) - }) + g.Assert(len(materialsActual)).Equal(len(materialsExpected)) + }) - g.It("Should list all materials a course (tutor)", func() { + g.It("Should list all materials a course (tutor)", func() { - user, err := stores.Course.GetUserEnrollment(1, 2) - g.Assert(err).Equal(nil) - g.Assert(user.Role).Equal(int64(1)) + user, err := stores.Course.GetUserEnrollment(1, 2) + g.Assert(err).Equal(nil) + g.Assert(user.Role).Equal(int64(1)) - w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) + g.Assert(w.Code).Equal(http.StatusOK) - materialsActual := []MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(&materialsActual) - g.Assert(err).Equal(nil) + materialsActual := []MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(&materialsActual) + g.Assert(err).Equal(nil) - materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) - g.Assert(err).Equal(nil) - g.Assert(len(materialsActual)).Equal(len(materialsExpected)) - }) + materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) + g.Assert(err).Equal(nil) + g.Assert(len(materialsActual)).Equal(len(materialsExpected)) + }) - g.It("Should list all materials a course (admin)", func() { + g.It("Should list all materials a course (admin)", func() { - user, err := stores.Course.GetUserEnrollment(1, 1) - g.Assert(err).Equal(nil) - g.Assert(user.Role).Equal(int64(2)) + user, err := stores.Course.GetUserEnrollment(1, 1) + g.Assert(err).Equal(nil) + g.Assert(user.Role).Equal(int64(2)) - w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/materials", user.ID, false) + g.Assert(w.Code).Equal(http.StatusOK) - materialsActual := []MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(&materialsActual) - g.Assert(err).Equal(nil) + materialsActual := []MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(&materialsActual) + g.Assert(err).Equal(nil) - materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) - g.Assert(err).Equal(nil) - g.Assert(len(materialsActual)).Equal(len(materialsExpected)) - }) + materialsExpected, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) + g.Assert(err).Equal(nil) + g.Assert(len(materialsActual)).Equal(len(materialsExpected)) + }) - g.It("Should get a specific material", func() { - materialExpected, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) + g.It("Should get a specific material", func() { + materialExpected, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/materials/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - materialActual := &MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(materialActual) - g.Assert(err).Equal(nil) + w := tape.GetWithClaims("/api/v1/courses/1/materials/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + materialActual := &MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(materialActual) + g.Assert(err).Equal(nil) - g.Assert(materialActual.ID).Equal(materialExpected.ID) - g.Assert(materialActual.Name).Equal(materialExpected.Name) - g.Assert(materialActual.PublishAt.Equal(materialExpected.PublishAt)).Equal(true) - g.Assert(materialActual.LectureAt.Equal(materialExpected.LectureAt)).Equal(true) - }) + g.Assert(materialActual.ID).Equal(materialExpected.ID) + g.Assert(materialActual.Name).Equal(materialExpected.Name) + g.Assert(materialActual.PublishAt.Equal(materialExpected.PublishAt)).Equal(true) + g.Assert(materialActual.LectureAt.Equal(materialExpected.LectureAt)).Equal(true) + }) - g.It("Should not get a specific material (unpublish)", func() { - materialExpected, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - materialExpected.RequiredRole = 0 - stores.Material.Update(materialExpected) + g.It("Should not get a specific material (unpublish)", func() { + materialExpected, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + materialExpected.RequiredRole = 0 + stores.Material.Update(materialExpected) - materialExpected.PublishAt = NowUTC().Add(time.Hour) - err = stores.Material.Update(materialExpected) - g.Assert(err).Equal(nil) + materialExpected.PublishAt = NowUTC().Add(time.Hour) + err = stores.Material.Update(materialExpected) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/materials/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w := tape.GetWithClaims("/api/v1/courses/1/materials/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - materialExpected.PublishAt = NowUTC().Add(-time.Hour) - err = stores.Material.Update(materialExpected) - g.Assert(err).Equal(nil) + materialExpected.PublishAt = NowUTC().Add(-time.Hour) + err = stores.Material.Update(materialExpected) + g.Assert(err).Equal(nil) - w = tape.GetWithClaims("/api/v1/courses/1/materials/1", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/materials/1", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Should create valid material for tutors", func() { + g.It("Should create valid material for tutors", func() { - materialsBeforeStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) - g.Assert(err).Equal(nil) + materialsBeforeStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) + g.Assert(err).Equal(nil) - materialsBeforeTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) - g.Assert(err).Equal(nil) + materialsBeforeTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) + g.Assert(err).Equal(nil) - materialsBeforeAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) - g.Assert(err).Equal(nil) + materialsBeforeAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) + g.Assert(err).Equal(nil) - materialSent := MaterialRequest{ - Name: "Material_new", - Kind: 0, - RequiredRole: authorize.TUTOR.ToInt(), - PublishAt: helper.Time(time.Now()), - LectureAt: helper.Time(time.Now()), - } - - g.Assert(materialSent.Validate()).Equal(nil) - - w := tape.Post("/api/v1/courses/1/materials", helper.ToH(materialSent)) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 1, false) - g.Assert(w.Code).Equal(http.StatusCreated) - - materialsAfterStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) - g.Assert(err).Equal(nil) - materialsAfterTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) - g.Assert(err).Equal(nil) - materialsAfterAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) - g.Assert(err).Equal(nil) - - materialReturn := &MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(&materialReturn) - g.Assert(err).Equal(nil) - g.Assert(materialReturn.Name).Equal(materialSent.Name) - g.Assert(materialReturn.Kind).Equal(materialSent.Kind) - g.Assert(materialReturn.RequiredRole).Equal(materialSent.RequiredRole) - g.Assert(materialReturn.PublishAt.Equal(materialSent.PublishAt)).Equal(true) - g.Assert(materialReturn.LectureAt.Equal(materialSent.LectureAt)).Equal(true) - - g.Assert(len(materialsAfterStudent)).Equal(len(materialsBeforeStudent)) - g.Assert(len(materialsAfterTutor)).Equal(len(materialsBeforeTutor) + 1) - g.Assert(len(materialsAfterAdmin)).Equal(len(materialsBeforeAdmin) + 1) - }) - - g.It("Should create valid material for admins", func() { - - materialsBeforeStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) - g.Assert(err).Equal(nil) - - materialsBeforeTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) - g.Assert(err).Equal(nil) - - materialsBeforeAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) - g.Assert(err).Equal(nil) - - materialSent := MaterialRequest{ - Name: "Material_new", - Kind: 0, - RequiredRole: authorize.ADMIN.ToInt(), - PublishAt: helper.Time(time.Now()), - LectureAt: helper.Time(time.Now()), - } - - g.Assert(materialSent.Validate()).Equal(nil) - - w := tape.Post("/api/v1/courses/1/materials", helper.ToH(materialSent)) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 1, false) - g.Assert(w.Code).Equal(http.StatusCreated) - - materialsAfterStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) - g.Assert(err).Equal(nil) - materialsAfterTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) - g.Assert(err).Equal(nil) - materialsAfterAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) - g.Assert(err).Equal(nil) - - materialReturn := &MaterialResponse{} - err = json.NewDecoder(w.Body).Decode(&materialReturn) - g.Assert(err).Equal(nil) - g.Assert(materialReturn.Name).Equal(materialSent.Name) - g.Assert(materialReturn.Kind).Equal(materialSent.Kind) - g.Assert(materialReturn.RequiredRole).Equal(materialSent.RequiredRole) - g.Assert(materialReturn.PublishAt.Equal(materialSent.PublishAt)).Equal(true) - g.Assert(materialReturn.LectureAt.Equal(materialSent.LectureAt)).Equal(true) - - g.Assert(len(materialsAfterStudent)).Equal(len(materialsBeforeStudent)) - g.Assert(len(materialsAfterTutor)).Equal(len(materialsBeforeTutor)) - g.Assert(len(materialsAfterAdmin)).Equal(len(materialsBeforeAdmin) + 1) - }) - - g.It("Creating a material should require body", func() { - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/materials", H{}, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should not create material with missing data", func() { - data := H{ - "name": "Sheet_new", - "publish_at": "2019-02-01T01:02:03Z", - // "lecture_at" is be missing - } - - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/materials", data, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should skip non-existent material file", func() { - - hnd := helper.NewMaterialFileHandle(1) - g.Assert(hnd.Exists()).Equal(false) - g.Assert(hnd.Exists()).Equal(false) - - w := tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - }) - - g.It("Should upload material file", func() { - defer helper.NewMaterialFileHandle(1).Delete() - - // set to publish - material, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - material.PublishAt = NowUTC().Add(-2 * time.Hour) - err = stores.Material.Update(material) - g.Assert(err).Equal(nil) - - // no file so far - g.Assert(helper.NewMaterialFileHandle(1).Exists()).Equal(false) - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - - // students - w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w, err = tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w, err = tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 1, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - // check disk - hnd := helper.NewMaterialFileHandle(1) - g.Assert(hnd.Exists()).Equal(true) - - // a file should be now served - w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Should upload material file (zip)", func() { - defer helper.NewMaterialFileHandle(1).Delete() - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - // admin - w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 1, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - // check disk - hnd := helper.NewMaterialFileHandle(1) - g.Assert(hnd.Exists()).Equal(true) - - // a file should be now served - w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) - g.Assert(w.HeaderMap["Content-Type"][0]).Equal("application/zip") - g.Assert(w.Code).Equal(http.StatusOK) - - course, err := stores.Material.IdentifyCourseOfMaterial(1) - g.Assert(err).Equal(nil) - - _, params, err := mime.ParseMediaType(w.HeaderMap["Content-Disposition"][0]) - g.Assert(err).Equal(nil) - g.Assert(params["filename"]).Equal(fmt.Sprintf("%s-empty.zip", course.Name)) - }) - - g.It("Should upload material file (pdf)", func() { - defer helper.NewMaterialFileHandle(1).Delete() - filename := fmt.Sprintf("%s/empty.pdf", viper.GetString("fixtures_dir")) - // admin - w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/pdf", 1, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - // check disk - hnd := helper.NewMaterialFileHandle(1) - g.Assert(hnd.Exists()).Equal(true) - - // a file should be now served - w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) - g.Assert(w.HeaderMap["Content-Type"][0]).Equal("application/pdf") - g.Assert(w.Code).Equal(http.StatusOK) - - course, err := stores.Material.IdentifyCourseOfMaterial(1) - g.Assert(err).Equal(nil) - - _, params, err := mime.ParseMediaType(w.HeaderMap["Content-Disposition"][0]) - g.Assert(err).Equal(nil) - g.Assert(params["filename"]).Equal(fmt.Sprintf("%s-empty.pdf", course.Name)) - }) - - g.It("Changes should require claims", func() { - w := tape.Put("/api/v1/courses/1/materials", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Should perform updates", func() { - - // set to publish - material, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - material.PublishAt = NowUTC().Add(-2 * time.Hour) - err = stores.Material.Update(material) - g.Assert(err).Equal(nil) - - materialSent := MaterialRequest{ - Name: "Material_new", - Kind: 0, - PublishAt: helper.Time(time.Now()), - LectureAt: helper.Time(time.Now()), - } - - // students - w := tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 122, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - materialAfter, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - g.Assert(materialAfter.Name).Equal(materialSent.Name) - g.Assert(materialAfter.Kind).Equal(materialSent.Kind) - g.Assert(materialAfter.PublishAt.Equal(materialSent.PublishAt)).Equal(true) - g.Assert(materialAfter.LectureAt.Equal(materialSent.LectureAt)).Equal(true) - }) - - g.It("Should delete when valid access claims", func() { - - // set to publish - material, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - material.PublishAt = NowUTC().Add(-2 * time.Hour) - err = stores.Material.Update(material) - g.Assert(err).Equal(nil) - - entriesBefore, err := stores.Material.GetAll() - g.Assert(err).Equal(nil) - - w := tape.Delete("/api/v1/courses/1/materials/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // verify nothing has changes - entriesAfter, err := stores.Material.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) - - // admin - w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // verify a sheet less exists - entriesAfter, err = stores.Material.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) - - g.It("Should delete when valid access claims and not published", func() { - - // set to publish - material, err := stores.Material.Get(1) - g.Assert(err).Equal(nil) - material.PublishAt = NowUTC().Add(2 * time.Hour) - err = stores.Material.Update(material) - g.Assert(err).Equal(nil) - - entriesBefore, err := stores.Material.GetAll() - g.Assert(err).Equal(nil) - - // admin - w := tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // verify a sheet less exists - entriesAfter, err := stores.Material.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) - - g.It("Permission test", func() { - url := "/api/v1/courses/1/materials" - - // global root can do whatever they want - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - // enrolled tutors can access - w = tape.GetWithClaims(url, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // enrolled students can access - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // disenroll student - w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // cannot access anymore - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) - - g.AfterEach(func() { - tape.AfterEach() - }) - }) + materialSent := MaterialRequest{ + Name: "Material_new", + Kind: 0, + RequiredRole: authorize.TUTOR.ToInt(), + PublishAt: helper.Time(time.Now()), + LectureAt: helper.Time(time.Now()), + } + + g.Assert(materialSent.Validate()).Equal(nil) + + w := tape.Post("/api/v1/courses/1/materials", helper.ToH(materialSent)) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 1, false) + g.Assert(w.Code).Equal(http.StatusCreated) + + materialsAfterStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) + g.Assert(err).Equal(nil) + materialsAfterTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) + g.Assert(err).Equal(nil) + materialsAfterAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) + g.Assert(err).Equal(nil) + + materialReturn := &MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(&materialReturn) + g.Assert(err).Equal(nil) + g.Assert(materialReturn.Name).Equal(materialSent.Name) + g.Assert(materialReturn.Kind).Equal(materialSent.Kind) + g.Assert(materialReturn.RequiredRole).Equal(materialSent.RequiredRole) + g.Assert(materialReturn.PublishAt.Equal(materialSent.PublishAt)).Equal(true) + g.Assert(materialReturn.LectureAt.Equal(materialSent.LectureAt)).Equal(true) + + g.Assert(len(materialsAfterStudent)).Equal(len(materialsBeforeStudent)) + g.Assert(len(materialsAfterTutor)).Equal(len(materialsBeforeTutor) + 1) + g.Assert(len(materialsAfterAdmin)).Equal(len(materialsBeforeAdmin) + 1) + }) + + g.It("Should create valid material for admins", func() { + + materialsBeforeStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) + g.Assert(err).Equal(nil) + + materialsBeforeTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) + g.Assert(err).Equal(nil) + + materialsBeforeAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) + g.Assert(err).Equal(nil) + + materialSent := MaterialRequest{ + Name: "Material_new", + Kind: 0, + RequiredRole: authorize.ADMIN.ToInt(), + PublishAt: helper.Time(time.Now()), + LectureAt: helper.Time(time.Now()), + } + + g.Assert(materialSent.Validate()).Equal(nil) + + w := tape.Post("/api/v1/courses/1/materials", helper.ToH(materialSent)) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + w = tape.PostWithClaims("/api/v1/courses/1/materials", helper.ToH(materialSent), 1, false) + g.Assert(w.Code).Equal(http.StatusCreated) + + materialsAfterStudent, err := stores.Material.MaterialsOfCourse(1, authorize.STUDENT.ToInt()) + g.Assert(err).Equal(nil) + materialsAfterTutor, err := stores.Material.MaterialsOfCourse(1, authorize.TUTOR.ToInt()) + g.Assert(err).Equal(nil) + materialsAfterAdmin, err := stores.Material.MaterialsOfCourse(1, authorize.ADMIN.ToInt()) + g.Assert(err).Equal(nil) + + materialReturn := &MaterialResponse{} + err = json.NewDecoder(w.Body).Decode(&materialReturn) + g.Assert(err).Equal(nil) + g.Assert(materialReturn.Name).Equal(materialSent.Name) + g.Assert(materialReturn.Kind).Equal(materialSent.Kind) + g.Assert(materialReturn.RequiredRole).Equal(materialSent.RequiredRole) + g.Assert(materialReturn.PublishAt.Equal(materialSent.PublishAt)).Equal(true) + g.Assert(materialReturn.LectureAt.Equal(materialSent.LectureAt)).Equal(true) + + g.Assert(len(materialsAfterStudent)).Equal(len(materialsBeforeStudent)) + g.Assert(len(materialsAfterTutor)).Equal(len(materialsBeforeTutor)) + g.Assert(len(materialsAfterAdmin)).Equal(len(materialsBeforeAdmin) + 1) + }) + + g.It("Creating a material should require body", func() { + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/materials", H{}, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should not create material with missing data", func() { + data := H{ + "name": "Sheet_new", + "publish_at": "2019-02-01T01:02:03Z", + // "lecture_at" is be missing + } + + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/materials", data, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should skip non-existent material file", func() { + + hnd := helper.NewMaterialFileHandle(1) + g.Assert(hnd.Exists()).Equal(false) + g.Assert(hnd.Exists()).Equal(false) + + w := tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + }) + + g.It("Should upload material file", func() { + defer helper.NewMaterialFileHandle(1).Delete() + + // set to publish + material, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + material.PublishAt = NowUTC().Add(-2 * time.Hour) + err = stores.Material.Update(material) + g.Assert(err).Equal(nil) + + // no file so far + g.Assert(helper.NewMaterialFileHandle(1).Exists()).Equal(false) + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + + // students + w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w, err = tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w, err = tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 1, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + // check disk + hnd := helper.NewMaterialFileHandle(1) + g.Assert(hnd.Exists()).Equal(true) + + // a file should be now served + w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Should upload material file (zip)", func() { + defer helper.NewMaterialFileHandle(1).Delete() + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + // admin + w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/zip", 1, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + // check disk + hnd := helper.NewMaterialFileHandle(1) + g.Assert(hnd.Exists()).Equal(true) + + // a file should be now served + w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) + g.Assert(w.HeaderMap["Content-Type"][0]).Equal("application/zip") + g.Assert(w.Code).Equal(http.StatusOK) + + course, err := stores.Material.IdentifyCourseOfMaterial(1) + g.Assert(err).Equal(nil) + + _, params, err := mime.ParseMediaType(w.HeaderMap["Content-Disposition"][0]) + g.Assert(err).Equal(nil) + g.Assert(params["filename"]).Equal(fmt.Sprintf("%s-empty.zip", course.Name)) + }) + + g.It("Should upload material file (pdf)", func() { + defer helper.NewMaterialFileHandle(1).Delete() + filename := fmt.Sprintf("%s/empty.pdf", viper.GetString("fixtures_dir")) + // admin + w, err := tape.UploadWithClaims("/api/v1/courses/1/materials/1/file", filename, "application/pdf", 1, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + // check disk + hnd := helper.NewMaterialFileHandle(1) + g.Assert(hnd.Exists()).Equal(true) + + // a file should be now served + w = tape.GetWithClaims("/api/v1/courses/1/materials/1/file", 1, true) + g.Assert(w.HeaderMap["Content-Type"][0]).Equal("application/pdf") + g.Assert(w.Code).Equal(http.StatusOK) + + course, err := stores.Material.IdentifyCourseOfMaterial(1) + g.Assert(err).Equal(nil) + + _, params, err := mime.ParseMediaType(w.HeaderMap["Content-Disposition"][0]) + g.Assert(err).Equal(nil) + g.Assert(params["filename"]).Equal(fmt.Sprintf("%s-empty.pdf", course.Name)) + }) + + g.It("Changes should require claims", func() { + w := tape.Put("/api/v1/courses/1/materials", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Should perform updates", func() { + + // set to publish + material, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + material.PublishAt = NowUTC().Add(-2 * time.Hour) + err = stores.Material.Update(material) + g.Assert(err).Equal(nil) + + materialSent := MaterialRequest{ + Name: "Material_new", + Kind: 0, + PublishAt: helper.Time(time.Now()), + LectureAt: helper.Time(time.Now()), + } + + // students + w := tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 122, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/materials/1", tape.ToH(materialSent), 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + materialAfter, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + g.Assert(materialAfter.Name).Equal(materialSent.Name) + g.Assert(materialAfter.Kind).Equal(materialSent.Kind) + g.Assert(materialAfter.PublishAt.Equal(materialSent.PublishAt)).Equal(true) + g.Assert(materialAfter.LectureAt.Equal(materialSent.LectureAt)).Equal(true) + }) + + g.It("Should delete when valid access claims", func() { + + // set to publish + material, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + material.PublishAt = NowUTC().Add(-2 * time.Hour) + err = stores.Material.Update(material) + g.Assert(err).Equal(nil) + + entriesBefore, err := stores.Material.GetAll() + g.Assert(err).Equal(nil) + + w := tape.Delete("/api/v1/courses/1/materials/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // verify nothing has changes + entriesAfter, err := stores.Material.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) + + // admin + w = tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // verify a sheet less exists + entriesAfter, err = stores.Material.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + }) + + g.It("Should delete when valid access claims and not published", func() { + + // set to publish + material, err := stores.Material.Get(1) + g.Assert(err).Equal(nil) + material.PublishAt = NowUTC().Add(2 * time.Hour) + err = stores.Material.Update(material) + g.Assert(err).Equal(nil) + + entriesBefore, err := stores.Material.GetAll() + g.Assert(err).Equal(nil) + + // admin + w := tape.DeleteWithClaims("/api/v1/courses/1/materials/1", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // verify a sheet less exists + entriesAfter, err := stores.Material.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + }) + + g.It("Permission test", func() { + url := "/api/v1/courses/1/materials" + + // global root can do whatever they want + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + // enrolled tutors can access + w = tape.GetWithClaims(url, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // enrolled students can access + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // disenroll student + w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // cannot access anymore + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) + + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/prometheus.go b/api/app/prometheus.go index 174a102..7b37dd1 100644 --- a/api/app/prometheus.go +++ b/api/app/prometheus.go @@ -19,59 +19,59 @@ package app import ( - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus" ) var ( - totalFailedLoginsVec = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "auth", - Subsystem: "logins", - Name: "failed_logins", - Help: "Total number of failed logins", - }, - // - []string{}, - ) + totalFailedLoginsVec = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "auth", + Subsystem: "logins", + Name: "failed_logins", + Help: "Total number of failed logins", + }, + // + []string{}, + ) - totalSubmissionCounterVec = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "worker", - Subsystem: "submissions", - Name: "pushed_total", - Help: "Total number of submissions pushed to the server", - }, - // - []string{"task_id"}, - ) + totalSubmissionCounterVec = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "worker", + Subsystem: "submissions", + Name: "pushed_total", + Help: "Total number of submissions pushed to the server", + }, + // + []string{"task_id"}, + ) - totalDockerFailExitCounterVec = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "worker", - Subsystem: "submissions", - Name: "failed_total", - Help: "Total number of submissions where docker has unsuccessful exit status", - }, - // - []string{"task_id", "kind"}, - ) + totalDockerFailExitCounterVec = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "worker", + Subsystem: "submissions", + Name: "failed_total", + Help: "Total number of submissions where docker has unsuccessful exit status", + }, + // + []string{"task_id", "kind"}, + ) - totalDockerSuccessExitCounterVec = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "worker", - Subsystem: "submissions", - Name: "success_total", - Help: "Total number of submissions where docker has successful exit status", - }, - // - []string{"task_id", "kind"}, - ) + totalDockerSuccessExitCounterVec = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "worker", + Subsystem: "submissions", + Name: "success_total", + Help: "Total number of submissions where docker has successful exit status", + }, + // + []string{"task_id", "kind"}, + ) ) func init() { - // register with the prometheus collector - prometheus.MustRegister(totalSubmissionCounterVec) - prometheus.MustRegister(totalDockerFailExitCounterVec) - prometheus.MustRegister(totalDockerSuccessExitCounterVec) - prometheus.MustRegister(totalFailedLoginsVec) + // register with the prometheus collector + prometheus.MustRegister(totalSubmissionCounterVec) + prometheus.MustRegister(totalDockerFailExitCounterVec) + prometheus.MustRegister(totalDockerSuccessExitCounterVec) + prometheus.MustRegister(totalFailedLoginsVec) } diff --git a/api/app/router.go b/api/app/router.go index 7ae4436..9f57422 100644 --- a/api/app/router.go +++ b/api/app/router.go @@ -19,24 +19,24 @@ package app import ( - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/go-chi/cors" - "github.com/go-chi/render" - "github.com/jmoiron/sqlx" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" + "github.com/go-chi/render" + "github.com/jmoiron/sqlx" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) // LimitedDecoder limits the amount of data a client can send in a JSON data request. @@ -44,401 +44,401 @@ import ( // Therefore we limit the amount of data which is read by the server whenever // we need to parse a JSON request. func LimitedDecoder(r *http.Request, v interface{}) error { - var err error - - switch render.GetRequestContentType(r) { - case render.ContentTypeJSON: - body := io.LimitReader(r.Body, viper.GetInt64("max_request_json_bytes")) - err = render.DecodeJSON(body, v) - default: - err = errors.New("render: unable to automatically decode the request content type") - } - // - return err + var err error + + switch render.GetRequestContentType(r) { + case render.ContentTypeJSON: + body := io.LimitReader(r.Body, viper.GetInt64("max_request_json_bytes")) + err = render.DecodeJSON(body, v) + default: + err = errors.New("render: unable to automatically decode the request content type") + } + // + return err } var log = logrus.New() func init() { - render.Decode = LimitedDecoder + render.Decode = LimitedDecoder - log.SetFormatter(&logrus.TextFormatter{ - DisableColors: false, - FullTimestamp: true, - }) - log.Out = os.Stdout + log.SetFormatter(&logrus.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + }) + log.Out = os.Stdout } func LoggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - next.ServeHTTP(w, r) - end := time.Now() - log.WithFields(logrus.Fields{ - "method": r.Method, - // "proto": r.Proto, - "agent": r.UserAgent(), - "remote": r.RemoteAddr, - "latency": end.Sub(start), - "time": end.Format(time.RFC3339), - }).Info(r.RequestURI) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + end := time.Now() + log.WithFields(logrus.Fields{ + "method": r.Method, + // "proto": r.Proto, + "agent": r.UserAgent(), + "remote": r.RemoteAddr, + "latency": end.Sub(start), + "time": end.Format(time.RFC3339), + }).Info(r.RequestURI) + }) } // VersionMiddleware writes the current API version to the headers. func VersionMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-INFOMARK-VERSION", "0.0.1") - next.ServeHTTP(w, r) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-INFOMARK-VERSION", "0.0.1") + next.ServeHTTP(w, r) + }) } // SecureMiddleware writes required access headers to all requests. func SecureMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-XSS-Protection", "1; mode=block") - - if r.UserAgent() == "" { - render.Render(w, r, ErrBadRequestWithDetails( - fmt.Errorf(`Request forbidden by administrative rules. + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1; mode=block") + + if r.UserAgent() == "" { + render.Render(w, r, ErrBadRequestWithDetails( + fmt.Errorf(`Request forbidden by administrative rules. Please make sure your request has a User-Agent header`))) - return - } + return + } - next.ServeHTTP(w, r) - }) + next.ServeHTTP(w, r) + }) } // NoCache writes required cache headers to all requests. func NoCache(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") - w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") - w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") + w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") + w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) - next.ServeHTTP(w, r) - }) + next.ServeHTTP(w, r) + }) } // New configures application resources and routes. func New(db *sqlx.DB, log bool) (*chi.Mux, error) { - logger := logrus.StandardLogger() - - if err := db.Ping(); err != nil { - logger.WithField("module", "database").Error(err) - return nil, err - } - - appAPI, err := NewAPI(db) - if err != nil { - logger.WithField("module", "app").Error(err) - return nil, err - } - - r := chi.NewRouter() - r.Use(VersionMiddleware) - r.Use(SecureMiddleware) - r.Use(NoCache) - r.Use(middleware.Recoverer) - r.Use(middleware.RequestID) - if log { - r.Use(LoggingMiddleware) - } - r.Use(render.SetContentType(render.ContentTypeJSON)) - r.Use(corsConfig().Handler) - - // r.Use(authenticate.AuthenticateAccessJWT) - r.Route("/api", func(r chi.Router) { - - r.Route("/v1", func(r chi.Router) { - - // open routes - r.Group(func(r chi.Router) { - - // we assume 600 students - // so we reset to 600 request per minute here - r.Use(authenticate.RateLimitMiddleware("infomark-logins", - fmt.Sprintf("%d-M", viper.GetInt64("auth_total_requests_per_minute")), - viper.GetString("redis_url"), - )) - - r.Post("/auth/token", appAPI.Auth.RefreshAccessTokenHandler) - r.Post("/auth/sessions", appAPI.Auth.LoginHandler) - r.Post("/auth/request_password_reset", appAPI.Auth.RequestPasswordResetHandler) - r.Post("/auth/update_password", appAPI.Auth.UpdatePasswordHandler) - r.Post("/auth/confirm_email", appAPI.Auth.ConfirmEmailHandler) - r.Post("/account", appAPI.Account.CreateHandler) - r.Get("/ping", appAPI.Common.PingHandler) - r.Get("/privacy_statement", appAPI.Common.PrivacyStatementHandler) - }) - - // protected routes - r.Group(func(r chi.Router) { - r.Use(authenticate.RequiredValidAccessClaims) - - r.Get("/me", appAPI.User.GetMeHandler) - r.Put("/me", appAPI.User.EditMeHandler) - - r.Route("/users", func(r chi.Router) { - r.Get("/", appAPI.User.IndexHandler) - r.Route("/{user_id}", func(r chi.Router) { - r.Use(appAPI.User.Context) - r.Get("/", appAPI.User.GetHandler) - r.Get("/avatar", appAPI.User.GetAvatarHandler) - r.Put("/", appAPI.User.EditHandler) - r.Delete("/", appAPI.User.DeleteHandler) - r.Post("/emails", appAPI.User.SendEmailHandler) - }) - }) - - r.Route("/courses", func(r chi.Router) { - r.Get("/", appAPI.Course.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Course.CreateHandler) - - r.Route("/{course_id}", func(r chi.Router) { - r.Use(appAPI.Course.Context) - r.Use(appAPI.Course.RoleContext) - r.Post("/enrollments", appAPI.Course.EnrollHandler) - - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.STUDENT)) - - r.Get("/", appAPI.Course.GetHandler) - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - - r.Post("/emails", appAPI.Course.SendEmailHandler) - - r.Put("/", appAPI.Course.EditHandler) - r.Delete("/", appAPI.Course.DeleteHandler) - }) - - r.Get("/enrollments", appAPI.Course.IndexEnrollmentsHandler) - r.Delete("/enrollments", appAPI.Course.DisenrollHandler) - - r.Get("/points", appAPI.Course.PointsHandler) - r.Get("/bids", appAPI.Course.BidsHandler) - - r.Route("/enrollments/{user_id}", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - - r.Use(appAPI.User.Context) - r.Get("/", appAPI.Course.GetUserEnrollmentHandler) - r.Delete("/", appAPI.Course.DeleteUserEnrollmentHandler) - r.Put("/", appAPI.Course.ChangeRole) - - }) - - r.Route("/sheets", func(r chi.Router) { - r.Get("/", appAPI.Sheet.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Sheet.CreateHandler) - - r.Route("/{sheet_id}", func(r chi.Router) { - r.Use(appAPI.Sheet.Context) - - // ensures user is enrolled in the associated course - - r.Get("/", appAPI.Sheet.GetHandler) - - r.Route("/tasks", func(r chi.Router) { - r.Get("/", appAPI.Task.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Task.CreateHandler) + logger := logrus.StandardLogger() + + if err := db.Ping(); err != nil { + logger.WithField("module", "database").Error(err) + return nil, err + } + + appAPI, err := NewAPI(db) + if err != nil { + logger.WithField("module", "app").Error(err) + return nil, err + } + + r := chi.NewRouter() + r.Use(VersionMiddleware) + r.Use(SecureMiddleware) + r.Use(NoCache) + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + if log { + r.Use(LoggingMiddleware) + } + r.Use(render.SetContentType(render.ContentTypeJSON)) + r.Use(corsConfig().Handler) + + // r.Use(authenticate.AuthenticateAccessJWT) + r.Route("/api", func(r chi.Router) { + + r.Route("/v1", func(r chi.Router) { + + // open routes + r.Group(func(r chi.Router) { + + // we assume 600 students + // so we reset to 600 request per minute here + r.Use(authenticate.RateLimitMiddleware("infomark-logins", + fmt.Sprintf("%d-M", viper.GetInt64("auth_total_requests_per_minute")), + viper.GetString("redis_url"), + )) + + r.Post("/auth/token", appAPI.Auth.RefreshAccessTokenHandler) + r.Post("/auth/sessions", appAPI.Auth.LoginHandler) + r.Post("/auth/request_password_reset", appAPI.Auth.RequestPasswordResetHandler) + r.Post("/auth/update_password", appAPI.Auth.UpdatePasswordHandler) + r.Post("/auth/confirm_email", appAPI.Auth.ConfirmEmailHandler) + r.Post("/account", appAPI.Account.CreateHandler) + r.Get("/ping", appAPI.Common.PingHandler) + r.Get("/privacy_statement", appAPI.Common.PrivacyStatementHandler) + }) + + // protected routes + r.Group(func(r chi.Router) { + r.Use(authenticate.RequiredValidAccessClaims) + + r.Get("/me", appAPI.User.GetMeHandler) + r.Put("/me", appAPI.User.EditMeHandler) + + r.Route("/users", func(r chi.Router) { + r.Get("/", appAPI.User.IndexHandler) + r.Route("/{user_id}", func(r chi.Router) { + r.Use(appAPI.User.Context) + r.Get("/", appAPI.User.GetHandler) + r.Get("/avatar", appAPI.User.GetAvatarHandler) + r.Put("/", appAPI.User.EditHandler) + r.Delete("/", appAPI.User.DeleteHandler) + r.Post("/emails", appAPI.User.SendEmailHandler) + }) + }) + + r.Route("/courses", func(r chi.Router) { + r.Get("/", appAPI.Course.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Course.CreateHandler) + + r.Route("/{course_id}", func(r chi.Router) { + r.Use(appAPI.Course.Context) + r.Use(appAPI.Course.RoleContext) + r.Post("/enrollments", appAPI.Course.EnrollHandler) + + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.STUDENT)) + + r.Get("/", appAPI.Course.GetHandler) + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + + r.Post("/emails", appAPI.Course.SendEmailHandler) + + r.Put("/", appAPI.Course.EditHandler) + r.Delete("/", appAPI.Course.DeleteHandler) + }) + + r.Get("/enrollments", appAPI.Course.IndexEnrollmentsHandler) + r.Delete("/enrollments", appAPI.Course.DisenrollHandler) + + r.Get("/points", appAPI.Course.PointsHandler) + r.Get("/bids", appAPI.Course.BidsHandler) + + r.Route("/enrollments/{user_id}", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + + r.Use(appAPI.User.Context) + r.Get("/", appAPI.Course.GetUserEnrollmentHandler) + r.Delete("/", appAPI.Course.DeleteUserEnrollmentHandler) + r.Put("/", appAPI.Course.ChangeRole) + + }) + + r.Route("/sheets", func(r chi.Router) { + r.Get("/", appAPI.Sheet.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Sheet.CreateHandler) + + r.Route("/{sheet_id}", func(r chi.Router) { + r.Use(appAPI.Sheet.Context) + + // ensures user is enrolled in the associated course + + r.Get("/", appAPI.Sheet.GetHandler) + + r.Route("/tasks", func(r chi.Router) { + r.Get("/", appAPI.Task.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Task.CreateHandler) - }) + }) - r.Get("/file", appAPI.Sheet.GetFileHandler) - r.Get("/points", appAPI.Sheet.PointsHandler) + r.Get("/file", appAPI.Sheet.GetFileHandler) + r.Get("/points", appAPI.Sheet.PointsHandler) - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - r.Put("/", appAPI.Sheet.EditHandler) - r.Delete("/", appAPI.Sheet.DeleteHandler) + r.Put("/", appAPI.Sheet.EditHandler) + r.Delete("/", appAPI.Sheet.DeleteHandler) - r.Post("/file", appAPI.Sheet.ChangeFileHandler) - }) + r.Post("/file", appAPI.Sheet.ChangeFileHandler) + }) - }) // sheet_id - }) + }) // sheet_id + }) - r.Route("/groups", func(r chi.Router) { - r.Get("/own", appAPI.Group.GetMineHandler) - r.Get("/", appAPI.Group.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Group.CreateHandler) - r.Route("/{group_id}", func(r chi.Router) { - r.Use(appAPI.Group.Context) - r.Use(appAPI.Course.RoleContext) + r.Route("/groups", func(r chi.Router) { + r.Get("/own", appAPI.Group.GetMineHandler) + r.Get("/", appAPI.Group.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Group.CreateHandler) + r.Route("/{group_id}", func(r chi.Router) { + r.Use(appAPI.Group.Context) + r.Use(appAPI.Course.RoleContext) - // ensures user is enrolled in the associated course + // ensures user is enrolled in the associated course - r.Post("/bids", appAPI.Group.ChangeBidHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Post("/emails", appAPI.Group.SendEmailHandler) - r.Get("/enrollments", appAPI.Group.IndexEnrollmentsHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/enrollments", appAPI.Group.EditGroupEnrollmentHandler) + r.Post("/bids", appAPI.Group.ChangeBidHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Post("/emails", appAPI.Group.SendEmailHandler) + r.Get("/enrollments", appAPI.Group.IndexEnrollmentsHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/enrollments", appAPI.Group.EditGroupEnrollmentHandler) - r.Get("/", appAPI.Group.GetHandler) + r.Get("/", appAPI.Group.GetHandler) - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - r.Put("/", appAPI.Group.EditHandler) - r.Delete("/", appAPI.Group.DeleteHandler) - }) - }) - }) + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + r.Put("/", appAPI.Group.EditHandler) + r.Delete("/", appAPI.Group.DeleteHandler) + }) + }) + }) - r.Route("/grades", func(r chi.Router) { + r.Route("/grades", func(r chi.Router) { - r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/", appAPI.Grade.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/summary", appAPI.Grade.IndexSummaryHandler) - // does not require a role - r.Get("/missing", appAPI.Grade.IndexMissingHandler) - r.Route("/{grade_id}", func(r chi.Router) { - r.Use(appAPI.Grade.Context) - r.Use(appAPI.Course.RoleContext) + r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/", appAPI.Grade.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/summary", appAPI.Grade.IndexSummaryHandler) + // does not require a role + r.Get("/missing", appAPI.Grade.IndexMissingHandler) + r.Route("/{grade_id}", func(r chi.Router) { + r.Use(appAPI.Grade.Context) + r.Use(appAPI.Course.RoleContext) - // ensures user is enrolled in the associated course - r.Use(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)) + // ensures user is enrolled in the associated course + r.Use(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)) - r.Put("/", appAPI.Grade.EditHandler) - r.Get("/", appAPI.Grade.GetByIDHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/public_result", appAPI.Grade.PublicResultEditHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/private_result", appAPI.Grade.PrivateResultEditHandler) + r.Put("/", appAPI.Grade.EditHandler) + r.Get("/", appAPI.Grade.GetByIDHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/public_result", appAPI.Grade.PublicResultEditHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/private_result", appAPI.Grade.PrivateResultEditHandler) - }) - }) + }) + }) - r.Route("/materials", func(r chi.Router) { + r.Route("/materials", func(r chi.Router) { - r.Get("/", appAPI.Material.IndexHandler) - r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Material.CreateHandler) + r.Get("/", appAPI.Material.IndexHandler) + r.With(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)).Post("/", appAPI.Material.CreateHandler) - r.Route("/{material_id}", func(r chi.Router) { - r.Use(appAPI.Material.Context) - r.Use(appAPI.Course.RoleContext) + r.Route("/{material_id}", func(r chi.Router) { + r.Use(appAPI.Material.Context) + r.Use(appAPI.Course.RoleContext) - // ensures user is enrolled in the associated course - r.Get("/", appAPI.Material.GetHandler) - r.Get("/file", appAPI.Material.GetFileHandler) + // ensures user is enrolled in the associated course + r.Get("/", appAPI.Material.GetHandler) + r.Get("/file", appAPI.Material.GetFileHandler) - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - r.Put("/", appAPI.Material.EditHandler) - r.Delete("/", appAPI.Material.DeleteHandler) - r.Post("/file", appAPI.Material.ChangeFileHandler) - }) - }) - }) + r.Put("/", appAPI.Material.EditHandler) + r.Delete("/", appAPI.Material.DeleteHandler) + r.Post("/file", appAPI.Material.ChangeFileHandler) + }) + }) + }) - r.Route("/submissions", func(r chi.Router) { - r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/", appAPI.Submission.IndexHandler) + r.Route("/submissions", func(r chi.Router) { + r.With(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)).Get("/", appAPI.Submission.IndexHandler) - r.Route("/{submission_id}", func(r chi.Router) { - r.Use(appAPI.Submission.Context) - r.Use(appAPI.Course.RoleContext) - r.Get("/file", appAPI.Submission.GetFileByIDHandler) - }) - }) + r.Route("/{submission_id}", func(r chi.Router) { + r.Use(appAPI.Submission.Context) + r.Use(appAPI.Course.RoleContext) + r.Get("/file", appAPI.Submission.GetFileByIDHandler) + }) + }) - r.Route("/tasks", func(r chi.Router) { + r.Route("/tasks", func(r chi.Router) { - r.Get("/missing", appAPI.Task.MissingIndexHandler) - r.Route("/{task_id}", func(r chi.Router) { - r.Use(appAPI.Task.Context) - r.Use(appAPI.Course.RoleContext) + r.Get("/missing", appAPI.Task.MissingIndexHandler) + r.Route("/{task_id}", func(r chi.Router) { + r.Use(appAPI.Task.Context) + r.Use(appAPI.Course.RoleContext) - // ensures user is enrolled in the associated course + // ensures user is enrolled in the associated course - r.Get("/", appAPI.Task.GetHandler) - r.Get("/ratings", appAPI.TaskRating.GetHandler) - r.Post("/ratings", appAPI.TaskRating.ChangeHandler) + r.Get("/", appAPI.Task.GetHandler) + r.Get("/ratings", appAPI.TaskRating.GetHandler) + r.Post("/ratings", appAPI.TaskRating.ChangeHandler) - r.Get("/submission", appAPI.Submission.GetFileHandler) - r.Post("/submission", appAPI.Submission.UploadFileHandler) + r.Get("/submission", appAPI.Submission.GetFileHandler) + r.Post("/submission", appAPI.Submission.UploadFileHandler) - r.Get("/result", appAPI.Task.GetSubmissionResultHandler) + r.Get("/result", appAPI.Task.GetSubmissionResultHandler) - r.Route("/", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) + r.Route("/", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.ADMIN)) - r.Put("/", appAPI.Task.EditHandler) - r.Delete("/", appAPI.Task.DeleteHandler) + r.Put("/", appAPI.Task.EditHandler) + r.Delete("/", appAPI.Task.DeleteHandler) - r.Get("/public_file", appAPI.Task.GetPublicTestFileHandler) - r.Get("/private_file", appAPI.Task.GetPrivateTestFileHandler) + r.Get("/public_file", appAPI.Task.GetPublicTestFileHandler) + r.Get("/private_file", appAPI.Task.GetPrivateTestFileHandler) - r.Post("/public_file", appAPI.Task.ChangePublicTestFileHandler) - r.Post("/private_file", appAPI.Task.ChangePrivateTestFileHandler) - }) + r.Post("/public_file", appAPI.Task.ChangePublicTestFileHandler) + r.Post("/private_file", appAPI.Task.ChangePrivateTestFileHandler) + }) - r.Route("/groups/{group_id}", func(r chi.Router) { - r.Use(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)) - r.Use(appAPI.Group.Context) - r.Get("/file", appAPI.Submission.GetCollectionFileHandler) - r.Get("/", appAPI.Submission.GetCollectionHandler) + r.Route("/groups/{group_id}", func(r chi.Router) { + r.Use(authorize.RequiresAtLeastCourseRole(authorize.TUTOR)) + r.Use(appAPI.Group.Context) + r.Get("/file", appAPI.Submission.GetCollectionFileHandler) + r.Get("/", appAPI.Submission.GetCollectionHandler) - }) + }) - }) + }) - }) // tasks + }) // tasks - }) + }) - }) // course_id - }) // course + }) // course_id + }) // course - r.Get("/account", appAPI.Account.GetHandler) - r.Get("/account/enrollments", appAPI.Account.GetEnrollmentsHandler) - r.Get("/account/avatar", appAPI.Account.GetAvatarHandler) - r.Post("/account/avatar", appAPI.Account.ChangeAvatarHandler) - r.Delete("/account/avatar", appAPI.Account.DeleteAvatarHandler) - r.Patch("/account", appAPI.Account.EditHandler) - r.Delete("/auth/sessions", appAPI.Auth.LogoutHandler) + r.Get("/account", appAPI.Account.GetHandler) + r.Get("/account/enrollments", appAPI.Account.GetEnrollmentsHandler) + r.Get("/account/avatar", appAPI.Account.GetAvatarHandler) + r.Post("/account/avatar", appAPI.Account.ChangeAvatarHandler) + r.Delete("/account/avatar", appAPI.Account.DeleteAvatarHandler) + r.Patch("/account", appAPI.Account.EditHandler) + r.Delete("/auth/sessions", appAPI.Auth.LogoutHandler) - }) + }) - }) - }) + }) + }) - workDir, _ := os.Getwd() - filesDir := filepath.Join(workDir, "static") - FileServer(r, "/", http.Dir(filesDir)) + workDir, _ := os.Getwd() + filesDir := filepath.Join(workDir, "static") + FileServer(r, "/", http.Dir(filesDir)) - return r, nil + return r, nil } // FileServer conveniently sets up a http.FileServer handler to serve // static files from a http.FileSystem. func FileServer(r chi.Router, path string, root http.FileSystem) { - if strings.ContainsAny(path, "{}*") { - panic("FileServer does not permit URL parameters.") - } + if strings.ContainsAny(path, "{}*") { + panic("FileServer does not permit URL parameters.") + } - fs := http.StripPrefix(path, http.FileServer(root)) + fs := http.StripPrefix(path, http.FileServer(root)) - if path != "/" && path[len(path)-1] != '/' { - r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) - path += "/" - } - path += "*" + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) + path += "/" + } + path += "*" - r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fs.ServeHTTP(w, r) - })) + r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + })) } func corsConfig() *cors.Cors { - // Basic CORS - // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing - return cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - MaxAge: 86400, // Maximum value not ignored by any of major browsers - }) + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + return cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 86400, // Maximum value not ignored by any of major browsers + }) } diff --git a/api/app/sheet.go b/api/app/sheet.go index d7fd077..485da24 100644 --- a/api/app/sheet.go +++ b/api/app/sheet.go @@ -19,30 +19,30 @@ package app import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // SheetResource specifies Sheet management handler. type SheetResource struct { - Stores *Stores + Stores *Stores } // NewSheetResource create and returns a SheetResource. func NewSheetResource(stores *Stores) *SheetResource { - return &SheetResource{ - Stores: stores, - } + return &SheetResource{ + Stores: stores, + } } // IndexHandler is public endpoint for @@ -59,20 +59,20 @@ func NewSheetResource(stores *Stores) *SheetResource { // The sheets are ordered by their names func (rs *SheetResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - var sheets []model.Sheet - var err error - // we use middle to detect whether there is a course given + var sheets []model.Sheet + var err error + // we use middle to detect whether there is a course given - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - sheets, err = rs.Stores.Sheet.SheetsOfCourse(course.ID) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + sheets, err = rs.Stores.Sheet.SheetsOfCourse(course.ID) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - // render JSON reponse - if err = render.RenderList(w, r, rs.newSheetListResponse(givenRole, sheets)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, rs.newSheetListResponse(givenRole, sheets)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // CreateHandler is public endpoint for @@ -88,37 +88,37 @@ func (rs *SheetResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: create a new sheet func (rs *SheetResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - // start from empty Request - data := &SheetRequest{} + // start from empty Request + data := &SheetRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - sheet := &model.Sheet{ - Name: data.Name, - PublishAt: data.PublishAt, - DueAt: data.DueAt, - } + sheet := &model.Sheet{ + Name: data.Name, + PublishAt: data.PublishAt, + DueAt: data.DueAt, + } - // create Sheet entry in database - newSheet, err := rs.Stores.Sheet.Create(sheet, course.ID) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // create Sheet entry in database + newSheet, err := rs.Stores.Sheet.Create(sheet, course.ID) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusCreated) + render.Status(r, http.StatusCreated) - // return Sheet information of created entry - if err := render.Render(w, r, rs.newSheetResponse(newSheet)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // return Sheet information of created entry + if err := render.Render(w, r, rs.newSheetResponse(newSheet)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -134,16 +134,16 @@ func (rs *SheetResource) CreateHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: get a specific sheet func (rs *SheetResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `Sheet` is retrieved via middle-ware - Sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + // `Sheet` is retrieved via middle-ware + Sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - // render JSON reponse - if err := render.Render(w, r, rs.newSheetResponse(Sheet)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err := render.Render(w, r, rs.newSheetResponse(Sheet)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // EditHandler is public endpoint for @@ -159,28 +159,28 @@ func (rs *SheetResource) GetHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: update a specific sheet func (rs *SheetResource) EditHandler(w http.ResponseWriter, r *http.Request) { - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - // start from empty Request - data := &SheetRequest{} + // start from empty Request + data := &SheetRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - sheet.Name = data.Name - sheet.PublishAt = data.PublishAt - sheet.DueAt = data.DueAt + sheet.Name = data.Name + sheet.PublishAt = data.PublishAt + sheet.DueAt = data.DueAt - // update database entry - if err := rs.Stores.Sheet.Update(sheet); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Sheet.Update(sheet); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // DeleteHandler is public endpoint for @@ -195,15 +195,15 @@ func (rs *SheetResource) EditHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: delete a specific sheet func (rs *SheetResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - Sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + Sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - // update database entry - if err := rs.Stores.Sheet.Delete(Sheet.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Sheet.Delete(Sheet.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // GetFileHandler is public endpoint for @@ -219,18 +219,18 @@ func (rs *SheetResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: get the zip file of a sheet func (rs *SheetResource) GetFileHandler(w http.ResponseWriter, r *http.Request) { - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - hnd := helper.NewSheetFileHandle(sheet.ID) + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + hnd := helper.NewSheetFileHandle(sheet.ID) - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := hnd.WriteToBodyWithName(fmt.Sprintf("%s-%s.zip", course.Name, sheet.Name), w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err := hnd.WriteToBodyWithName(fmt.Sprintf("%s-%s.zip", course.Name, sheet.Name), w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } @@ -247,14 +247,14 @@ func (rs *SheetResource) GetFileHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: change the zip file of a sheet func (rs *SheetResource) ChangeFileHandler(w http.ResponseWriter, r *http.Request) { - // will always be a POST - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - - // the file will be located - if _, err := helper.NewSheetFileHandle(sheet.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } - render.Status(r, http.StatusOK) + // will always be a POST + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + + // the file will be located + if _, err := helper.NewSheetFileHandle(sheet.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } + render.Status(r, http.StatusOK) } // PointsHandler is public endpoint for @@ -269,22 +269,22 @@ func (rs *SheetResource) ChangeFileHandler(w http.ResponseWriter, r *http.Reques // RESPONSE: 403,Unauthorized // SUMMARY: return all points from a sheet for the request identity func (rs *SheetResource) PointsHandler(w http.ResponseWriter, r *http.Request) { - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - taskPoints, err := rs.Stores.Sheet.PointsForUser(accessClaims.LoginID, sheet.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // resp := &SheetPointsResponse{SheetPoints: taskPoints} - if err := render.RenderList(w, r, newTaskPointsListResponse(taskPoints)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + taskPoints, err := rs.Stores.Sheet.PointsForUser(accessClaims.LoginID, sheet.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // resp := &SheetPointsResponse{SheetPoints: taskPoints} + if err := render.RenderList(w, r, newTaskPointsListResponse(taskPoints)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // ............................................................................. @@ -294,50 +294,50 @@ func (rs *SheetResource) PointsHandler(w http.ResponseWriter, r *http.Request) { // the Sheet could not be found, we stop here and return a 404. // We do NOT check whether the Sheet is authorized to get this Sheet. func (rs *SheetResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - var sheetID int64 - var err error - - // try to get id from URL - if sheetID, err = strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific Sheet in database - sheet, err := rs.Stores.Sheet.Get(sheetID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // public yet? - if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { - render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) - return - } - - ctx := context.WithValue(r.Context(), common.CtxKeySheet, sheet) - - // when there is a sheetID in the url, there is NOT a courseID in the url, - // BUT: when there is a sheet, there is a course - - course, err := rs.Stores.Sheet.IdentifyCourseOfSheet(sheet.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if courseFromURL.ID != course.ID { - render.Render(w, r, ErrNotFound) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - - // serve next - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + var sheetID int64 + var err error + + // try to get id from URL + if sheetID, err = strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific Sheet in database + sheet, err := rs.Stores.Sheet.Get(sheetID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // public yet? + if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { + render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) + return + } + + ctx := context.WithValue(r.Context(), common.CtxKeySheet, sheet) + + // when there is a sheetID in the url, there is NOT a courseID in the url, + // BUT: when there is a sheet, there is a course + + course, err := rs.Stores.Sheet.IdentifyCourseOfSheet(sheet.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if courseFromURL.ID != course.ID { + render.Render(w, r, ErrNotFound) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + + // serve next + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/sheet_test.go b/api/app/sheet_test.go index c75eca1..df2ee51 100644 --- a/api/app/sheet_test.go +++ b/api/app/sheet_test.go @@ -19,272 +19,272 @@ package app import ( - "encoding/json" - "fmt" - "net/http" - "testing" - "time" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + "github.com/spf13/viper" ) func TestSheet(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail - tape := &Tape{} - - var stores *Stores + tape := &Tape{} + + var stores *Stores - g.Describe("Sheet", func() { - - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) - - g.It("Query should require access claims", func() { - - w := tape.Get("/api/v1/courses/1/sheets") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims("/api/v1/courses/1/sheets", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Should list all sheets a course", func() { - - w := tape.GetWithClaims("/api/v1/courses/1/sheets", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - sheetsActual := []SheetResponse{} - err := json.NewDecoder(w.Body).Decode(&sheetsActual) - g.Assert(err).Equal(nil) - g.Assert(len(sheetsActual)).Equal(10) - }) - - g.It("Should get a specific sheet", func() { - sheetExpected, err := stores.Sheet.Get(1) - g.Assert(err).Equal(nil) - - w := tape.GetWithClaims("/api/v1/courses/1/sheets/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - sheetActual := &SheetResponse{} - err = json.NewDecoder(w.Body).Decode(sheetActual) - g.Assert(err).Equal(nil) - - g.Assert(sheetActual.ID).Equal(sheetExpected.ID) - g.Assert(sheetActual.Name).Equal(sheetExpected.Name) - g.Assert(sheetActual.PublishAt.Equal(sheetExpected.PublishAt)).Equal(true) - g.Assert(sheetActual.DueAt.Equal(sheetExpected.DueAt)).Equal(true) - }) - - g.It("Creating a sheet should require access claims", func() { - w := tape.Post("/api/v1/courses/1/sheets", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Creating a sheet should require access body", func() { - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", H{}, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should not create sheet with missing data", func() { - data := H{ - "name": "Sheet_new", - "publish_at": "2019-02-01T01:02:03Z", - // "due_at" is be missing - } - - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", data, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should not create sheet with wrong times", func() { - data := H{ - "name": "Sheet_new", - "publish_at": "2019-02-01T01:02:03Z", - "due_at": "2018-02-01T01:02:03Z", // time before publish - } - - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", data, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) - - g.It("Should create valid sheet", func() { - sheetsBefore, err := stores.Sheet.SheetsOfCourse(1) - g.Assert(err).Equal(nil) - - sheetSent := SheetRequest{ - Name: "Sheet_new", - PublishAt: helper.Time(time.Now()), - DueAt: helper.Time(time.Now()), - } - - // students - w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 1, false) - g.Assert(w.Code).Equal(http.StatusCreated) - - sheetReturn := &SheetResponse{} - err = json.NewDecoder(w.Body).Decode(&sheetReturn) - g.Assert(err).Equal(nil) - g.Assert(sheetReturn.Name).Equal("Sheet_new") - g.Assert(sheetReturn.PublishAt.Equal(sheetSent.PublishAt)).Equal(true) - g.Assert(sheetReturn.DueAt.Equal(sheetSent.DueAt)).Equal(true) - - sheetsAfter, err := stores.Sheet.SheetsOfCourse(1) - g.Assert(err).Equal(nil) - g.Assert(len(sheetsAfter)).Equal(len(sheetsBefore) + 1) - }) - - g.It("Should skip non-existent sheet file", func() { - w := tape.GetWithClaims("/api/v1/courses/1/sheets/1/file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - }) - - g.It("Should upload sheet file", func() { - defer helper.NewSheetFileHandle(1).Delete() - - // no file so far - g.Assert(helper.NewSheetFileHandle(1).Exists()).Equal(false) - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - - // students - w, err := tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w, err = tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w, err = tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 1, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - // check disk - g.Assert(helper.NewSheetFileHandle(1).Exists()).Equal(true) - - // a file should be now served - w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/file", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Changes should require claims", func() { - w := tape.Put("/api/v1/courses/1/sheets", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Should perform updates", func() { - - sheetSent := SheetRequest{ - Name: "Sheet_update", - PublishAt: helper.Time(time.Now()), - DueAt: helper.Time(time.Now()), - } - - // students - w := tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 122, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // admin - w = tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - sheetAfter, err := stores.Sheet.Get(1) - g.Assert(err).Equal(nil) - g.Assert(sheetAfter.Name).Equal("Sheet_update") - g.Assert(sheetAfter.PublishAt.Equal(sheetSent.PublishAt)).Equal(true) - g.Assert(sheetAfter.DueAt.Equal(sheetSent.DueAt)).Equal(true) - }) - - g.It("Should delete when valid access claims", func() { - entriesBefore, err := stores.Sheet.GetAll() - g.Assert(err).Equal(nil) - - w := tape.Delete("/api/v1/courses/1/sheets/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - // students - w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // tutors - w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - // verify nothing has changes - entriesAfter, err := stores.Sheet.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) - - // admin - w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // verify a sheet less exists - entriesAfter, err = stores.Sheet.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) - - g.It("Should see points for a sheet", func() { - userID := int64(112) - - w := tape.Get("/api/v1/courses/1/sheets/1/points") - g.Assert(w.Code).Equal(http.StatusUnauthorized) - - w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/points", userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + g.Describe("Sheet", func() { + + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) + + g.It("Query should require access claims", func() { + + w := tape.Get("/api/v1/courses/1/sheets") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims("/api/v1/courses/1/sheets", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Should list all sheets a course", func() { + + w := tape.GetWithClaims("/api/v1/courses/1/sheets", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + sheetsActual := []SheetResponse{} + err := json.NewDecoder(w.Body).Decode(&sheetsActual) + g.Assert(err).Equal(nil) + g.Assert(len(sheetsActual)).Equal(10) + }) + + g.It("Should get a specific sheet", func() { + sheetExpected, err := stores.Sheet.Get(1) + g.Assert(err).Equal(nil) + + w := tape.GetWithClaims("/api/v1/courses/1/sheets/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + sheetActual := &SheetResponse{} + err = json.NewDecoder(w.Body).Decode(sheetActual) + g.Assert(err).Equal(nil) + + g.Assert(sheetActual.ID).Equal(sheetExpected.ID) + g.Assert(sheetActual.Name).Equal(sheetExpected.Name) + g.Assert(sheetActual.PublishAt.Equal(sheetExpected.PublishAt)).Equal(true) + g.Assert(sheetActual.DueAt.Equal(sheetExpected.DueAt)).Equal(true) + }) + + g.It("Creating a sheet should require access claims", func() { + w := tape.Post("/api/v1/courses/1/sheets", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Creating a sheet should require access body", func() { + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", H{}, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should not create sheet with missing data", func() { + data := H{ + "name": "Sheet_new", + "publish_at": "2019-02-01T01:02:03Z", + // "due_at" is be missing + } + + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", data, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should not create sheet with wrong times", func() { + data := H{ + "name": "Sheet_new", + "publish_at": "2019-02-01T01:02:03Z", + "due_at": "2018-02-01T01:02:03Z", // time before publish + } + + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", data, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) + + g.It("Should create valid sheet", func() { + sheetsBefore, err := stores.Sheet.SheetsOfCourse(1) + g.Assert(err).Equal(nil) + + sheetSent := SheetRequest{ + Name: "Sheet_new", + PublishAt: helper.Time(time.Now()), + DueAt: helper.Time(time.Now()), + } + + // students + w := tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PlayDataWithClaims("POST", "/api/v1/courses/1/sheets", tape.ToH(sheetSent), 1, false) + g.Assert(w.Code).Equal(http.StatusCreated) + + sheetReturn := &SheetResponse{} + err = json.NewDecoder(w.Body).Decode(&sheetReturn) + g.Assert(err).Equal(nil) + g.Assert(sheetReturn.Name).Equal("Sheet_new") + g.Assert(sheetReturn.PublishAt.Equal(sheetSent.PublishAt)).Equal(true) + g.Assert(sheetReturn.DueAt.Equal(sheetSent.DueAt)).Equal(true) + + sheetsAfter, err := stores.Sheet.SheetsOfCourse(1) + g.Assert(err).Equal(nil) + g.Assert(len(sheetsAfter)).Equal(len(sheetsBefore) + 1) + }) + + g.It("Should skip non-existent sheet file", func() { + w := tape.GetWithClaims("/api/v1/courses/1/sheets/1/file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + }) + + g.It("Should upload sheet file", func() { + defer helper.NewSheetFileHandle(1).Delete() + + // no file so far + g.Assert(helper.NewSheetFileHandle(1).Exists()).Equal(false) + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + + // students + w, err := tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w, err = tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w, err = tape.UploadWithClaims("/api/v1/courses/1/sheets/1/file", filename, "application/zip", 1, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + // check disk + g.Assert(helper.NewSheetFileHandle(1).Exists()).Equal(true) + + // a file should be now served + w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/file", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Changes should require claims", func() { + w := tape.Put("/api/v1/courses/1/sheets", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Should perform updates", func() { + + sheetSent := SheetRequest{ + Name: "Sheet_update", + PublishAt: helper.Time(time.Now()), + DueAt: helper.Time(time.Now()), + } + + // students + w := tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 122, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // admin + w = tape.PutWithClaims("/api/v1/courses/1/sheets/1", tape.ToH(sheetSent), 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + sheetAfter, err := stores.Sheet.Get(1) + g.Assert(err).Equal(nil) + g.Assert(sheetAfter.Name).Equal("Sheet_update") + g.Assert(sheetAfter.PublishAt.Equal(sheetSent.PublishAt)).Equal(true) + g.Assert(sheetAfter.DueAt.Equal(sheetSent.DueAt)).Equal(true) + }) + + g.It("Should delete when valid access claims", func() { + entriesBefore, err := stores.Sheet.GetAll() + g.Assert(err).Equal(nil) + + w := tape.Delete("/api/v1/courses/1/sheets/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + // students + w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // tutors + w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + // verify nothing has changes + entriesAfter, err := stores.Sheet.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) + + // admin + w = tape.DeleteWithClaims("/api/v1/courses/1/sheets/1", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // verify a sheet less exists + entriesAfter, err = stores.Sheet.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + }) + + g.It("Should see points for a sheet", func() { + userID := int64(112) + + w := tape.Get("/api/v1/courses/1/sheets/1/points") + g.Assert(w.Code).Equal(http.StatusUnauthorized) + + w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/points", userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) - - g.It("Permission test", func() { - url := "/api/v1/courses/1/sheets" - - // global root can do whatever they want - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - // enrolled tutors can access - w = tape.GetWithClaims(url, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // enrolled students can access - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusOK) - - // disenroll student - w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + }) + + g.It("Permission test", func() { + url := "/api/v1/courses/1/sheets" + + // global root can do whatever they want + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + // enrolled tutors can access + w = tape.GetWithClaims(url, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // enrolled students can access + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusOK) + + // disenroll student + w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // cannot access anymore - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) - - g.AfterEach(func() { - tape.AfterEach() - }) - }) + // cannot access anymore + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) + + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/submission.go b/api/app/submission.go index 97b968e..42b80da 100644 --- a/api/app/submission.go +++ b/api/app/submission.go @@ -19,33 +19,33 @@ package app import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/api/shared" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" - "github.com/spf13/viper" + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/api/shared" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/spf13/viper" ) // SubmissionResource specifies Submission management handler. type SubmissionResource struct { - Stores *Stores + Stores *Stores } // NewSubmissionResource create and returns a SubmissionResource. func NewSubmissionResource(stores *Stores) *SubmissionResource { - return &SubmissionResource{ - Stores: stores, - } + return &SubmissionResource{ + Stores: stores, + } } // GetFileHandler is public endpoint for @@ -60,36 +60,36 @@ func NewSubmissionResource(stores *Stores) *SubmissionResource { // RESPONSE: 403,Unauthorized // SUMMARY: get the zip file containing the submission of the request identity for a given task func (rs *SubmissionResource) GetFileHandler(w http.ResponseWriter, r *http.Request) { - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - // submission := r.Context().Value(common.CtxKeySubmission).(*model.Submission) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // students can only access their own files - if submission.UserID != accessClaims.LoginID { - if givenRole == authorize.STUDENT { - render.Render(w, r, ErrUnauthorized) - return - } - } - - hnd := helper.NewSubmissionFileHandle(submission.ID) - - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } - - if err := hnd.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + // submission := r.Context().Value(common.CtxKeySubmission).(*model.Submission) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // students can only access their own files + if submission.UserID != accessClaims.LoginID { + if givenRole == authorize.STUDENT { + render.Render(w, r, ErrUnauthorized) + return + } + } + + hnd := helper.NewSubmissionFileHandle(submission.ID) + + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } + + if err := hnd.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } @@ -107,51 +107,51 @@ func (rs *SubmissionResource) GetFileHandler(w http.ResponseWriter, r *http.Requ // RESPONSE: 403,Unauthorized // SUMMARY: get the path to the zip file containing all submissions for a given task and a given group if exists func (rs *SubmissionResource) GetCollectionHandler(w http.ResponseWriter, r *http.Request) { - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - if givenRole == authorize.STUDENT { - render.Render(w, r, ErrUnauthorized) - return - } - - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - - var groupID int64 - var err error - - // try to get id from URL - if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific group in database - group, err := rs.Stores.Group.Get(groupID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - hnd := helper.NewSubmissionsCollectionFileHandle(course.ID, sheet.ID, task.ID, group.ID) - - text := "" - if hnd.Exists() { - text = fmt.Sprintf("%s/courses/%d/tasks/%d/groups/%d/file", - viper.GetString("url"), - course.ID, task.ID, group.ID, - ) - } - if err := render.Render(w, r, newRawResponse(text)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + if givenRole == authorize.STUDENT { + render.Render(w, r, ErrUnauthorized) + return + } + + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + + var groupID int64 + var err error + + // try to get id from URL + if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific group in database + group, err := rs.Stores.Group.Get(groupID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + hnd := helper.NewSubmissionsCollectionFileHandle(course.ID, sheet.ID, task.ID, group.ID) + + text := "" + if hnd.Exists() { + text = fmt.Sprintf("%s/courses/%d/tasks/%d/groups/%d/file", + viper.GetString("url"), + course.ID, task.ID, group.ID, + ) + } + if err := render.Render(w, r, newRawResponse(text)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // GetCollectionFileHandler is public endpoint for @@ -168,49 +168,49 @@ func (rs *SubmissionResource) GetCollectionHandler(w http.ResponseWriter, r *htt // RESPONSE: 403,Unauthorized // SUMMARY: get the zip file containing all submissions for a given task and a given group func (rs *SubmissionResource) GetCollectionFileHandler(w http.ResponseWriter, r *http.Request) { - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - - if givenRole == authorize.STUDENT { - render.Render(w, r, ErrUnauthorized) - return - } - - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - - var groupID int64 - var err error - - // try to get id from URL - if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific group in database - group, err := rs.Stores.Group.Get(groupID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - hnd := helper.NewSubmissionsCollectionFileHandle(course.ID, sheet.ID, task.ID, group.ID) - - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } - - if err := hnd.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + + if givenRole == authorize.STUDENT { + render.Render(w, r, ErrUnauthorized) + return + } + + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + + var groupID int64 + var err error + + // try to get id from URL + if groupID, err = strconv.ParseInt(chi.URLParam(r, "group_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific group in database + group, err := rs.Stores.Group.Get(groupID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + hnd := helper.NewSubmissionsCollectionFileHandle(course.ID, sheet.ID, task.ID, group.ID) + + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } + + if err := hnd.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } // GetFileByIDHandler is public endpoint for @@ -226,35 +226,35 @@ func (rs *SubmissionResource) GetCollectionFileHandler(w http.ResponseWriter, r // SUMMARY: get the zip file of a specific submission func (rs *SubmissionResource) GetFileByIDHandler(w http.ResponseWriter, r *http.Request) { - submission := r.Context().Value(common.CtxKeySubmission).(*model.Submission) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + submission := r.Context().Value(common.CtxKeySubmission).(*model.Submission) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - submission, err := rs.Stores.Submission.Get(submission.ID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } + submission, err := rs.Stores.Submission.Get(submission.ID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } - // students can only access their own files - if submission.UserID != accessClaims.LoginID { - if givenRole == authorize.STUDENT { - render.Render(w, r, ErrUnauthorized) - return - } - } + // students can only access their own files + if submission.UserID != accessClaims.LoginID { + if givenRole == authorize.STUDENT { + render.Render(w, r, ErrUnauthorized) + return + } + } - hnd := helper.NewSubmissionFileHandle(submission.ID) + hnd := helper.NewSubmissionFileHandle(submission.ID) - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := hnd.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + if err := hnd.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } } @@ -271,172 +271,172 @@ func (rs *SubmissionResource) GetFileByIDHandler(w http.ResponseWriter, r *http. // RESPONSE: 403,Unauthorized // SUMMARY: changes the zip file of a submission belonging to the request identity func (rs *SubmissionResource) UploadFileHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - // todo create submission if not exists - - if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { - render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) - return - } - - if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && OverTime(sheet.DueAt) { - render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("too late deadline was %v but now it is %v", sheet.DueAt, NowUTC()))) - return - } - - var grade *model.Grade - - defaultPublicTestLog := "submission received and will be tested" - if !task.PublicDockerImage.Valid || !helper.NewPublicTestFileHandle(task.ID).Exists() { - // .Valid == true iff not null - defaultPublicTestLog = "no unit tests for this task are available" - } - - defaultPrivateTestLog := "submission received and will be tested" - if !task.PrivateDockerImage.Valid || !helper.NewPrivateTestFileHandle(task.ID).Exists() { - // .Valid == true iff not null - defaultPrivateTestLog = "no unit tests for this task are available" - } - - // create ssubmisison if not exists - submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) - if err != nil { - // no such submission - submission, err = rs.Stores.Submission.Create(&model.Submission{UserID: accessClaims.LoginID, TaskID: task.ID}) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // create also empty grade, which will be filled in later - grade = &model.Grade{ - PublicExecutionState: 0, - PrivateExecutionState: 0, - PublicTestLog: defaultPublicTestLog, - PrivateTestLog: defaultPrivateTestLog, - PublicTestStatus: 0, - PrivateTestStatus: 0, - AcquiredPoints: 0, - Feedback: "", - TutorID: 1, - SubmissionID: submission.ID, - } - - // fetch id from grade as we need it - grade, err = rs.Stores.Grade.Create(grade) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - } else { - // submission exists, we only need to get the grade - grade, err = rs.Stores.Grade.GetForSubmission(submission.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // and update the grade - grade.PublicExecutionState = 0 - grade.PrivateExecutionState = 0 - grade.PublicTestLog = defaultPublicTestLog - grade.PrivateTestLog = defaultPrivateTestLog - - err = rs.Stores.Grade.Update(grade) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - } - - // the file will be located - if _, err := helper.NewSubmissionFileHandle(submission.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - sha256, err := helper.NewSubmissionFileHandle(submission.ID).Sha256() - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // enqueue file into testing queue - // By definition user with id 1 is the system itself with root access - tokenManager, err := authenticate.NewTokenAuth() - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - accessToken, err := tokenManager.CreateAccessJWT( - authenticate.NewAccessClaims(1, true)) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if task.PublicDockerImage.Valid && helper.NewPublicTestFileHandle(task.ID).Exists() { - // enqueue public test - - request := shared.NewSubmissionAMQPWorkerRequest( - course.ID, task.ID, submission.ID, grade.ID, - accessToken, viper.GetString("url"), task.PublicDockerImage.String, sha256, "public") - - body, err := json.Marshal(request) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - err = DefaultSubmissionProducer.Publish(body) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } else { - grade.PublicTestLog = "No public dockerimage was specified --> will not run any public test" - err = rs.Stores.Grade.Update(grade) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } - - if task.PrivateDockerImage.Valid && helper.NewPrivateTestFileHandle(task.ID).Exists() { - // enqueue private test - - request := shared.NewSubmissionAMQPWorkerRequest( - course.ID, task.ID, submission.ID, grade.ID, - accessToken, viper.GetString("url"), task.PrivateDockerImage.String, sha256, "private") - - body, err := json.Marshal(request) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - err = DefaultSubmissionProducer.Publish(body) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } else { - grade.PrivateTestLog = "No private dockerimage was specified --> will not run any private test" - err = rs.Stores.Grade.Update(grade) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } - - totalSubmissionCounterVec.WithLabelValues(fmt.Sprintf("%d", task.ID)).Inc() - - render.Status(r, http.StatusOK) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + // todo create submission if not exists + + if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { + render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) + return + } + + if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && OverTime(sheet.DueAt) { + render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("too late deadline was %v but now it is %v", sheet.DueAt, NowUTC()))) + return + } + + var grade *model.Grade + + defaultPublicTestLog := "submission received and will be tested" + if !task.PublicDockerImage.Valid || !helper.NewPublicTestFileHandle(task.ID).Exists() { + // .Valid == true iff not null + defaultPublicTestLog = "no unit tests for this task are available" + } + + defaultPrivateTestLog := "submission received and will be tested" + if !task.PrivateDockerImage.Valid || !helper.NewPrivateTestFileHandle(task.ID).Exists() { + // .Valid == true iff not null + defaultPrivateTestLog = "no unit tests for this task are available" + } + + // create ssubmisison if not exists + submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) + if err != nil { + // no such submission + submission, err = rs.Stores.Submission.Create(&model.Submission{UserID: accessClaims.LoginID, TaskID: task.ID}) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // create also empty grade, which will be filled in later + grade = &model.Grade{ + PublicExecutionState: 0, + PrivateExecutionState: 0, + PublicTestLog: defaultPublicTestLog, + PrivateTestLog: defaultPrivateTestLog, + PublicTestStatus: 0, + PrivateTestStatus: 0, + AcquiredPoints: 0, + Feedback: "", + TutorID: 1, + SubmissionID: submission.ID, + } + + // fetch id from grade as we need it + grade, err = rs.Stores.Grade.Create(grade) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + } else { + // submission exists, we only need to get the grade + grade, err = rs.Stores.Grade.GetForSubmission(submission.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // and update the grade + grade.PublicExecutionState = 0 + grade.PrivateExecutionState = 0 + grade.PublicTestLog = defaultPublicTestLog + grade.PrivateTestLog = defaultPrivateTestLog + + err = rs.Stores.Grade.Update(grade) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + } + + // the file will be located + if _, err := helper.NewSubmissionFileHandle(submission.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + sha256, err := helper.NewSubmissionFileHandle(submission.ID).Sha256() + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // enqueue file into testing queue + // By definition user with id 1 is the system itself with root access + tokenManager, err := authenticate.NewTokenAuth() + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + accessToken, err := tokenManager.CreateAccessJWT( + authenticate.NewAccessClaims(1, true)) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if task.PublicDockerImage.Valid && helper.NewPublicTestFileHandle(task.ID).Exists() { + // enqueue public test + + request := shared.NewSubmissionAMQPWorkerRequest( + course.ID, task.ID, submission.ID, grade.ID, + accessToken, viper.GetString("url"), task.PublicDockerImage.String, sha256, "public") + + body, err := json.Marshal(request) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + err = DefaultSubmissionProducer.Publish(body) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } else { + grade.PublicTestLog = "No public dockerimage was specified --> will not run any public test" + err = rs.Stores.Grade.Update(grade) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } + + if task.PrivateDockerImage.Valid && helper.NewPrivateTestFileHandle(task.ID).Exists() { + // enqueue private test + + request := shared.NewSubmissionAMQPWorkerRequest( + course.ID, task.ID, submission.ID, grade.ID, + accessToken, viper.GetString("url"), task.PrivateDockerImage.String, sha256, "private") + + body, err := json.Marshal(request) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + err = DefaultSubmissionProducer.Publish(body) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } else { + grade.PrivateTestLog = "No private dockerimage was specified --> will not run any private test" + err = rs.Stores.Grade.Update(grade) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } + + totalSubmissionCounterVec.WithLabelValues(fmt.Sprintf("%d", task.ID)).Inc() + + render.Status(r, http.StatusOK) } // IndexHandler is public endpoint for @@ -454,26 +454,26 @@ func (rs *SubmissionResource) UploadFileHandler(w http.ResponseWriter, r *http.R // RESPONSE: 403,Unauthorized // SUMMARY: Query submissions in a course func (rs *SubmissionResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - filterGroupID := helper.Int64FromURL(r, "group_id", 0) - filterUserID := helper.Int64FromURL(r, "user_id", 0) - filterSheetID := helper.Int64FromURL(r, "sheet_id", 0) - filterTaskID := helper.Int64FromURL(r, "task_id", 0) + filterGroupID := helper.Int64FromURL(r, "group_id", 0) + filterUserID := helper.Int64FromURL(r, "user_id", 0) + filterSheetID := helper.Int64FromURL(r, "sheet_id", 0) + filterTaskID := helper.Int64FromURL(r, "task_id", 0) - submissions, err := rs.Stores.Submission.GetFiltered(course.ID, filterGroupID, filterUserID, filterSheetID, filterTaskID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + submissions, err := rs.Stores.Submission.GetFiltered(course.ID, filterGroupID, filterUserID, filterSheetID, filterTaskID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - // render JSON reponse - if err = render.RenderList(w, r, newSubmissionListResponse(submissions, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, newSubmissionListResponse(submissions, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } @@ -484,67 +484,67 @@ func (rs *SubmissionResource) IndexHandler(w http.ResponseWriter, r *http.Reques // the Submission could not be found, we stop here and return a 404. // We do NOT check whether the identity is authorized to get this Submission. func (rs *SubmissionResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // course_from_url := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - var submissionID int64 - var err error - - // try to get id from URL - if submissionID, err = strconv.ParseInt(chi.URLParam(r, "submission_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific Submission in database - submission, err := rs.Stores.Submission.Get(submissionID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - ctx := context.WithValue(r.Context(), common.CtxKeySubmission, submission) - - // when there is a submissionID in the url, there is NOT a taskID in the url, - // BUT: when there is a Submission, there is a task - // BUT: when there is a task, there is a course (other middlewarwe) - - // find specific Task in database - var task *model.Task - task, err = rs.Stores.Task.Get(submission.TaskID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyTask, task) - - // find sheet - sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if sheetID, err := strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err == nil { - if sheetID != sheet.ID { - render.Render(w, r, ErrNotFound) - return - } - } else { - ctx = context.WithValue(ctx, common.CtxKeySheet, sheet) - } - - // find course - course, err := rs.Stores.Task.IdentifyCourseOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - - // serve next - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // course_from_url := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + var submissionID int64 + var err error + + // try to get id from URL + if submissionID, err = strconv.ParseInt(chi.URLParam(r, "submission_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific Submission in database + submission, err := rs.Stores.Submission.Get(submissionID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), common.CtxKeySubmission, submission) + + // when there is a submissionID in the url, there is NOT a taskID in the url, + // BUT: when there is a Submission, there is a task + // BUT: when there is a task, there is a course (other middlewarwe) + + // find specific Task in database + var task *model.Task + task, err = rs.Stores.Task.Get(submission.TaskID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyTask, task) + + // find sheet + sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if sheetID, err := strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err == nil { + if sheetID != sheet.ID { + render.Render(w, r, ErrNotFound) + return + } + } else { + ctx = context.WithValue(ctx, common.CtxKeySheet, sheet) + } + + // find course + course, err := rs.Stores.Task.IdentifyCourseOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + + // serve next + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/submission_producer.go b/api/app/submission_producer.go index 6c665c6..b759a34 100644 --- a/api/app/submission_producer.go +++ b/api/app/submission_producer.go @@ -19,13 +19,13 @@ package app import ( - "github.com/cgtuebingen/infomark-backend/service" - "github.com/spf13/viper" + "github.com/cgtuebingen/infomark-backend/service" + "github.com/spf13/viper" ) // Producer is interface to pipe the workload over AMPQ to the backend workers type Producer interface { - Publish(body []byte) error + Publish(body []byte) error } // DefaultSubmissionProducer is the producer which broadcasts all submissions @@ -40,25 +40,25 @@ type VoidProducer struct{} func (t *VoidProducer) Publish(body []byte) error { return nil } func init() { - var err error + var err error - cfg := &service.Config{ - Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), - Key: viper.GetString("rabbitmq_key"), - Tag: "SimpleSubmission", - } + cfg := &service.Config{ + Connection: viper.GetString("rabbitmq_connection"), + Exchange: viper.GetString("rabbitmq_exchange"), + ExchangeType: viper.GetString("rabbitmq_exchangeType"), + Queue: viper.GetString("rabbitmq_queue"), + Key: viper.GetString("rabbitmq_key"), + Tag: "SimpleSubmission", + } - if viper.GetBool("use_backend_worker") { - DefaultSubmissionProducer, err = service.NewProducer(cfg) - if err != nil { - panic(err) - } - } else { - DefaultSubmissionProducer = &VoidProducer{} + if viper.GetBool("use_backend_worker") { + DefaultSubmissionProducer, err = service.NewProducer(cfg) + if err != nil { + panic(err) + } + } else { + DefaultSubmissionProducer = &VoidProducer{} - } + } } diff --git a/api/app/submission_test.go b/api/app/submission_test.go index 5c0a291..e2d3b46 100644 --- a/api/app/submission_test.go +++ b/api/app/submission_test.go @@ -19,346 +19,346 @@ package app import ( - "encoding/json" - "fmt" - "net/http" - "testing" - "time" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + "github.com/spf13/viper" ) func TestSubmission(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail - // DefaultSubmissionProducer = &VoidProducer{} + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail + // DefaultSubmissionProducer = &VoidProducer{} - tape := &Tape{} + tape := &Tape{} - var stores *Stores + var stores *Stores - g.Describe("Submission", func() { + g.Describe("Submission", func() { - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) - g.It("Query should require access claims", func() { + g.It("Query should require access claims", func() { - w := tape.Get("/api/v1/courses/1/tasks/1/submission") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/tasks/1/submission") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusNotFound) - }) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusNotFound) + }) - g.It("Tutors can download a collection of submissions", func() { + g.It("Tutors can download a collection of submissions", func() { - courseID := int64(1) - taskID := int64(1) - groupID := int64(1) + courseID := int64(1) + taskID := int64(1) + groupID := int64(1) - sheet, err := stores.Task.IdentifySheetOfTask(taskID) - g.Assert(err).Equal(nil) + sheet, err := stores.Task.IdentifySheetOfTask(taskID) + g.Assert(err).Equal(nil) - hnd := helper.NewSubmissionsCollectionFileHandle(courseID, sheet.ID, taskID, groupID) + hnd := helper.NewSubmissionsCollectionFileHandle(courseID, sheet.ID, taskID, groupID) - defer hnd.Delete() + defer hnd.Delete() - // no files so far - g.Assert(hnd.Exists()).Equal(false) + // no files so far + g.Assert(hnd.Exists()).Equal(false) - src := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - copyFile(src, hnd.Path()) + src := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + copyFile(src, hnd.Path()) - g.Assert(hnd.Exists()).Equal(true) + g.Assert(hnd.Exists()).Equal(true) - w := tape.Get("/api/v1/courses/1/tasks/1/groups/1/file") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/tasks/1/groups/1/file") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 1, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/groups/1/file", 1, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Students can upload solution (create)", func() { + g.It("Students can upload solution (create)", func() { - deadlineAt := NowUTC().Add(time.Hour) - publishedAt := NowUTC().Add(-time.Hour) + deadlineAt := NowUTC().Add(time.Hour) + publishedAt := NowUTC().Add(-time.Hour) - // make sure the upload date is good - task, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) - sheet, err := stores.Task.IdentifySheetOfTask(task.ID) - g.Assert(err).Equal(nil) + // make sure the upload date is good + task, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) + sheet, err := stores.Task.IdentifySheetOfTask(task.ID) + g.Assert(err).Equal(nil) - sheet.PublishAt = publishedAt - sheet.DueAt = deadlineAt - err = stores.Sheet.Update(sheet) - g.Assert(err).Equal(nil) + sheet.PublishAt = publishedAt + sheet.DueAt = deadlineAt + err = stores.Sheet.Update(sheet) + g.Assert(err).Equal(nil) - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // remove all submission from student - _, err = tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") - g.Assert(err).Equal(nil) + // remove all submission from student + _, err = tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusNotFound) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusNotFound) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) - createdSubmission, err := stores.Submission.GetByUserAndTask(112, 1) - g.Assert(err).Equal(nil) + createdSubmission, err := stores.Submission.GetByUserAndTask(112, 1) + g.Assert(err).Equal(nil) - g.Assert(helper.NewSubmissionFileHandle(createdSubmission.ID).Exists()).Equal(true) - defer helper.NewSubmissionFileHandle(createdSubmission.ID).Delete() + g.Assert(helper.NewSubmissionFileHandle(createdSubmission.ID).Exists()).Equal(true) + defer helper.NewSubmissionFileHandle(createdSubmission.ID).Delete() - // files exists - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // files exists + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Students cannot upload solution (create) since too late", func() { + g.It("Students cannot upload solution (create) since too late", func() { - deadlineAt := NowUTC().Add(-2 * time.Hour) - publishedAt := NowUTC().Add(-10 * time.Hour) + deadlineAt := NowUTC().Add(-2 * time.Hour) + publishedAt := NowUTC().Add(-10 * time.Hour) - // make sure the upload date is good - task, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) - sheet, err := stores.Task.IdentifySheetOfTask(task.ID) - g.Assert(err).Equal(nil) + // make sure the upload date is good + task, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) + sheet, err := stores.Task.IdentifySheetOfTask(task.ID) + g.Assert(err).Equal(nil) - sheet.PublishAt = publishedAt - sheet.DueAt = deadlineAt - err = stores.Sheet.Update(sheet) - g.Assert(err).Equal(nil) + sheet.PublishAt = publishedAt + sheet.DueAt = deadlineAt + err = stores.Sheet.Update(sheet) + g.Assert(err).Equal(nil) - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // remove all submission from student - _, err = tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") - g.Assert(err).Equal(nil) + // remove all submission from student + _, err = tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusNotFound) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusNotFound) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - fmt.Println(err) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + fmt.Println(err) - g.Assert(err).Equal(nil) + g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusBadRequest) + g.Assert(w.Code).Equal(http.StatusBadRequest) - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - defer helper.NewSubmissionFileHandle(3001).Delete() + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + defer helper.NewSubmissionFileHandle(3001).Delete() - }) + }) - g.It("Students can upload solution (update)", func() { + g.It("Students can upload solution (update)", func() { - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - deadlineAt := NowUTC().Add(time.Hour) - publishedAt := NowUTC().Add(-time.Hour) + deadlineAt := NowUTC().Add(time.Hour) + publishedAt := NowUTC().Add(-time.Hour) - // make sure the upload date is good - task, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) - sheet, err := stores.Task.IdentifySheetOfTask(task.ID) - g.Assert(err).Equal(nil) + // make sure the upload date is good + task, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) + sheet, err := stores.Task.IdentifySheetOfTask(task.ID) + g.Assert(err).Equal(nil) - sheet.PublishAt = publishedAt - sheet.DueAt = deadlineAt - err = stores.Sheet.Update(sheet) - g.Assert(err).Equal(nil) + sheet.PublishAt = publishedAt + sheet.DueAt = deadlineAt + err = stores.Sheet.Update(sheet) + g.Assert(err).Equal(nil) - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(true) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(true) - // files exists - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // files exists + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - }) + }) - g.It("Students cannot upload solution (update) too late", func() { + g.It("Students cannot upload solution (update) too late", func() { - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - deadlineAt := NowUTC().Add(-time.Hour) - publishedAt := NowUTC().Add(-2 * time.Hour) + deadlineAt := NowUTC().Add(-time.Hour) + publishedAt := NowUTC().Add(-2 * time.Hour) - // make sure the upload date is good - task, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) - sheet, err := stores.Task.IdentifySheetOfTask(task.ID) - g.Assert(err).Equal(nil) + // make sure the upload date is good + task, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) + sheet, err := stores.Task.IdentifySheetOfTask(task.ID) + g.Assert(err).Equal(nil) - sheet.PublishAt = publishedAt - sheet.DueAt = deadlineAt - err = stores.Sheet.Update(sheet) - g.Assert(err).Equal(nil) + sheet.PublishAt = publishedAt + sheet.DueAt = deadlineAt + err = stores.Sheet.Update(sheet) + g.Assert(err).Equal(nil) - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusBadRequest) - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusBadRequest) + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - }) + }) - g.It("creating a submission will crate an empty grade entry as well", func() { + g.It("creating a submission will crate an empty grade entry as well", func() { - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // remove all submission from student - _, err := tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") - g.Assert(err).Equal(nil) + // remove all submission from student + _, err := tape.DB.Exec("DELETE FROM submissions WHERE user_id = 112;") + g.Assert(err).Equal(nil) - // remove all grades from student - _, err = tape.DB.Exec("TRUNCATE TABLE grades;") - g.Assert(err).Equal(nil) + // remove all grades from student + _, err = tape.DB.Exec("TRUNCATE TABLE grades;") + g.Assert(err).Equal(nil) - // no submission - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusNotFound) + // no submission + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusNotFound) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err = tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) - createdSubmission, err := stores.Submission.GetByUserAndTask(112, 1) - g.Assert(err).Equal(nil) + createdSubmission, err := stores.Submission.GetByUserAndTask(112, 1) + g.Assert(err).Equal(nil) - g.Assert(helper.NewSubmissionFileHandle(createdSubmission.ID).Exists()).Equal(true) - defer helper.NewSubmissionFileHandle(createdSubmission.ID).Delete() + g.Assert(helper.NewSubmissionFileHandle(createdSubmission.ID).Exists()).Equal(true) + defer helper.NewSubmissionFileHandle(createdSubmission.ID).Delete() - // files exists - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // files exists + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/submission", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // verify there is also a grade - state := int64(88) - err = tape.DB.Get(&state, "SELECT public_execution_state from grades WHERE submission_id = $1;", createdSubmission.ID) - g.Assert(err).Equal(nil) - g.Assert(state).Equal(int64(0)) - }) + // verify there is also a grade + state := int64(88) + err = tape.DB.Get(&state, "SELECT public_execution_state from grades WHERE submission_id = $1;", createdSubmission.ID) + g.Assert(err).Equal(nil) + g.Assert(state).Equal(int64(0)) + }) - g.It("Students can only access their own submissions", func() { + g.It("Students can only access their own submissions", func() { - defer helper.NewSubmissionFileHandle(3001).Delete() + defer helper.NewSubmissionFileHandle(3001).Delete() - // no files so far - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) + // no files so far + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(false) - // upload - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(true) + // upload + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/submission", filename, "application/zip", 112, false) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + g.Assert(helper.NewSubmissionFileHandle(3001).Exists()).Equal(true) - // access own submission - w = tape.GetWithClaims("/api/v1/courses/1/submissions/3001/file", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // access own submission + w = tape.GetWithClaims("/api/v1/courses/1/submissions/3001/file", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // access others submission - w = tape.GetWithClaims("/api/v1/courses/1/submissions/3001/file", 113, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + // access others submission + w = tape.GetWithClaims("/api/v1/courses/1/submissions/3001/file", 113, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - }) + }) - g.It("tutors/admins can filter submissions", func() { + g.It("tutors/admins can filter submissions", func() { - w := tape.Get("/api/v1/courses/1/submissions") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/submissions") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/submissions", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.GetWithClaims("/api/v1/courses/1/submissions", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.GetWithClaims("/api/v1/courses/1/submissions", 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/submissions", 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - submissionsAllActual := []SubmissionResponse{} - err := json.NewDecoder(w.Body).Decode(&submissionsAllActual) - g.Assert(err).Equal(nil) + submissionsAllActual := []SubmissionResponse{} + err := json.NewDecoder(w.Body).Decode(&submissionsAllActual) + g.Assert(err).Equal(nil) - w = tape.GetWithClaims("/api/v1/courses/1/submissions?group_id=4", 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/submissions?group_id=4", 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - submissionsG4Actual := []SubmissionResponse{} - err = json.NewDecoder(w.Body).Decode(&submissionsG4Actual) - g.Assert(err).Equal(nil) + submissionsG4Actual := []SubmissionResponse{} + err = json.NewDecoder(w.Body).Decode(&submissionsG4Actual) + g.Assert(err).Equal(nil) - w = tape.GetWithClaims("/api/v1/courses/1/submissions?task_id=2", 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/submissions?task_id=2", 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - submissionsT4Actual := []SubmissionResponse{} - err = json.NewDecoder(w.Body).Decode(&submissionsT4Actual) - g.Assert(err).Equal(nil) + submissionsT4Actual := []SubmissionResponse{} + err = json.NewDecoder(w.Body).Decode(&submissionsT4Actual) + g.Assert(err).Equal(nil) - for _, el := range submissionsT4Actual { - g.Assert(el.TaskID).Equal(int64(2)) - } + for _, el := range submissionsT4Actual { + g.Assert(el.TaskID).Equal(int64(2)) + } - w = tape.GetWithClaims("/api/v1/courses/1/submissions?user_id=112", 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/submissions?user_id=112", 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - submissionsU112Actual := []SubmissionResponse{} - err = json.NewDecoder(w.Body).Decode(&submissionsU112Actual) - g.Assert(err).Equal(nil) + submissionsU112Actual := []SubmissionResponse{} + err = json.NewDecoder(w.Body).Decode(&submissionsU112Actual) + g.Assert(err).Equal(nil) - for _, el := range submissionsU112Actual { - g.Assert(el.UserID).Equal(int64(112)) - } + for _, el := range submissionsU112Actual { + g.Assert(el.UserID).Equal(int64(112)) + } - }) + }) - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/app/tape_test.go b/api/app/tape_test.go index 7e2fddc..dbed5c6 100644 --- a/api/app/tape_test.go +++ b/api/app/tape_test.go @@ -19,169 +19,169 @@ package app import ( - "encoding/json" - olog "log" - "net/http" - "net/http/httptest" - "os" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - otape "github.com/cgtuebingen/infomark-backend/tape" - "github.com/jmoiron/sqlx" - "github.com/spf13/viper" + "encoding/json" + olog "log" + "net/http" + "net/http/httptest" + "os" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + otape "github.com/cgtuebingen/infomark-backend/tape" + "github.com/jmoiron/sqlx" + "github.com/spf13/viper" ) // just wrap tape func addJWTClaims(r *http.Request, loginID int64, root bool) { - accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(loginID, root)) - if err != nil { - panic(err) - } - // we use JWT here - r.Header.Add("Authorization", "Bearer "+accessToken) + accessToken, err := tokenManager.CreateAccessJWT(authenticate.NewAccessClaims(loginID, root)) + if err != nil { + panic(err) + } + // we use JWT here + r.Header.Add("Authorization", "Bearer "+accessToken) } var tokenManager *authenticate.TokenAuth func SetConfigFile() { - var err error - home := os.Getenv("INFOMARK_CONFIG_DIR") + var err error + home := os.Getenv("INFOMARK_CONFIG_DIR") - if home == "" { - // Find home directory. - home, err = os.Getwd() - if err != nil { - olog.Fatal(err) - } - } + if home == "" { + // Find home directory. + home, err = os.Getwd() + if err != nil { + olog.Fatal(err) + } + } - viper.AddConfigPath(home) - viper.SetConfigName(".infomark") + viper.AddConfigPath(home) + viper.SetConfigName(".infomark") } func InitConfig() { - SetConfigFile() - viper.AutomaticEnv() + SetConfigFile() + viper.AutomaticEnv() - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err != nil { - panic(err) - } + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err != nil { + panic(err) + } } func init() { - // prepate token management - tokenManager, _ = authenticate.NewTokenAuth() - InitConfig() + // prepate token management + tokenManager, _ = authenticate.NewTokenAuth() + InitConfig() } type Tape struct { - otape.Tape + otape.Tape - DB *sqlx.DB + DB *sqlx.DB } func NewTape() *Tape { - t := &Tape{} - return t + t := &Tape{} + return t } func (t *Tape) AfterEach() { - t.DB.Close() + t.DB.Close() } func (t *Tape) BeforeEach() { - var err error + var err error - t.DB, err = helper.TransactionDB() - if err != nil { - panic(err) - } + t.DB, err = helper.TransactionDB() + if err != nil { + panic(err) + } - t.Router, _ = New(t.DB, false) + t.Router, _ = New(t.DB, false) } // PlayWithClaims will send a request without any request body (like GET) but with JWT bearer. func (t *Tape) PlayWithClaims(method, url string, loginID int64, root bool) *httptest.ResponseRecorder { - h := make(map[string]interface{}) - r := otape.BuildDataRequest(method, url, h) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + h := make(map[string]interface{}) + r := otape.BuildDataRequest(method, url, h) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } // PlayDataWithClaims will send a request with given data in body and add JWT bearer. func (t *Tape) PlayDataWithClaims(method, url string, data map[string]interface{}, loginID int64, root bool) *httptest.ResponseRecorder { - r := otape.BuildDataRequest(method, url, data) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + r := otape.BuildDataRequest(method, url, data) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) PlayRequestWithClaims(r *http.Request, loginID int64, root bool) *httptest.ResponseRecorder { - w := httptest.NewRecorder() - addJWTClaims(r, loginID, root) - t.Router.ServeHTTP(w, r) - return w + w := httptest.NewRecorder() + addJWTClaims(r, loginID, root) + t.Router.ServeHTTP(w, r) + return w } func (t *Tape) GetWithClaims(url string, loginID int64, root bool) *httptest.ResponseRecorder { - h := make(map[string]interface{}) - r := otape.BuildDataRequest("GET", url, h) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + h := make(map[string]interface{}) + r := otape.BuildDataRequest("GET", url, h) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) PostWithClaims(url string, data map[string]interface{}, loginID int64, root bool) *httptest.ResponseRecorder { - r := otape.BuildDataRequest("POST", url, data) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + r := otape.BuildDataRequest("POST", url, data) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) PutWithClaims(url string, data map[string]interface{}, loginID int64, root bool) *httptest.ResponseRecorder { - r := otape.BuildDataRequest("PUT", url, data) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + r := otape.BuildDataRequest("PUT", url, data) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) PatchWithClaims(url string, data map[string]interface{}, loginID int64, root bool) *httptest.ResponseRecorder { - r := otape.BuildDataRequest("PATCH", url, data) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + r := otape.BuildDataRequest("PATCH", url, data) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) DeleteWithClaims(url string, loginID int64, root bool) *httptest.ResponseRecorder { - h := make(map[string]interface{}) - r := otape.BuildDataRequest("DELETE", url, h) - addJWTClaims(r, loginID, root) - return t.PlayRequest(r) + h := make(map[string]interface{}) + r := otape.BuildDataRequest("DELETE", url, h) + addJWTClaims(r, loginID, root) + return t.PlayRequest(r) } func (t *Tape) UploadWithClaims(url string, filename string, contentType string, loginID int64, root bool) (*httptest.ResponseRecorder, error) { - body, ct, err := otape.CreateFileRequestBody(filename, contentType) - if err != nil { - return nil, err - } + body, ct, err := otape.CreateFileRequestBody(filename, contentType) + if err != nil { + return nil, err + } - r, err := http.NewRequest("POST", url, body) - if err != nil { - return nil, err - } - r.Header.Set("Content-Type", ct) - r.Header.Add("X-Forwarded-For", "1.2.3.4") - r.Header.Set("User-Agent", "Test-Agent") + r, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + r.Header.Set("Content-Type", ct) + r.Header.Add("X-Forwarded-For", "1.2.3.4") + r.Header.Set("User-Agent", "Test-Agent") - addJWTClaims(r, loginID, root) - return t.PlayRequest(r), nil + addJWTClaims(r, loginID, root) + return t.PlayRequest(r), nil } func (t *Tape) ToH(z interface{}) map[string]interface{} { - data, _ := json.Marshal(z) - var msgMapTemplate interface{} - _ = json.Unmarshal(data, &msgMapTemplate) - return msgMapTemplate.(map[string]interface{}) + data, _ := json.Marshal(z) + var msgMapTemplate interface{} + _ = json.Unmarshal(data, &msgMapTemplate) + return msgMapTemplate.(map[string]interface{}) } diff --git a/api/app/task.go b/api/app/task.go index be29a39..510bd2e 100644 --- a/api/app/task.go +++ b/api/app/task.go @@ -19,31 +19,31 @@ package app import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" - null "gopkg.in/guregu/null.v3" + "context" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" + null "gopkg.in/guregu/null.v3" ) // TaskResource specifies Task management handler. type TaskResource struct { - Stores *Stores + Stores *Stores } // NewTaskResource create and returns a TaskResource. func NewTaskResource(stores *Stores) *TaskResource { - return &TaskResource{ - Stores: stores, - } + return &TaskResource{ + Stores: stores, + } } // IndexHandler is public endpoint for @@ -58,17 +58,17 @@ func NewTaskResource(stores *Stores) *TaskResource { // SUMMARY: Get all tasks of a given sheet func (rs *TaskResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - var tasks []model.Task - var err error - // we use middle to detect whether there is a sheet given - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - tasks, err = rs.Stores.Task.TasksOfSheet(sheet.ID) - - // render JSON reponse - if err = render.RenderList(w, r, newTaskListResponse(tasks)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + var tasks []model.Task + var err error + // we use middle to detect whether there is a sheet given + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + tasks, err = rs.Stores.Task.TasksOfSheet(sheet.ID) + + // render JSON reponse + if err = render.RenderList(w, r, newTaskListResponse(tasks)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // MissingIndexHandler is public endpoint for @@ -82,23 +82,23 @@ func (rs *TaskResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: Get all tasks which are not solved by the request identity func (rs *TaskResource) MissingIndexHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - tasks, err := rs.Stores.Task.GetAllMissingTasksForUser(accessClaims.LoginID) + tasks, err := rs.Stores.Task.GetAllMissingTasksForUser(accessClaims.LoginID) - // TODO empty list - if err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // TODO empty list + if err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - // render JSON reponse - if err = render.RenderList(w, r, newMissingTaskListResponse(tasks, givenRole)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, newMissingTaskListResponse(tasks, givenRole)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // CreateHandler is public endpoint for @@ -115,38 +115,38 @@ func (rs *TaskResource) MissingIndexHandler(w http.ResponseWriter, r *http.Reque // SUMMARY: create a new task func (rs *TaskResource) CreateHandler(w http.ResponseWriter, r *http.Request) { - sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) + sheet := r.Context().Value(common.CtxKeySheet).(*model.Sheet) - // start from empty Request - data := &TaskRequest{} + // start from empty Request + data := &TaskRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - task := &model.Task{ - Name: data.Name, - MaxPoints: data.MaxPoints, - PublicDockerImage: null.StringFrom(data.PublicDockerImage), - PrivateDockerImage: null.StringFrom(data.PrivateDockerImage), - } + task := &model.Task{ + Name: data.Name, + MaxPoints: data.MaxPoints, + PublicDockerImage: null.StringFrom(data.PublicDockerImage), + PrivateDockerImage: null.StringFrom(data.PrivateDockerImage), + } - // create Task entry in database - newTask, err := rs.Stores.Task.Create(task, sheet.ID) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // create Task entry in database + newTask, err := rs.Stores.Task.Create(task, sheet.ID) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusCreated) + render.Status(r, http.StatusCreated) - // return Task information of created entry - if err := render.Render(w, r, newTaskResponse(newTask)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // return Task information of created entry + if err := render.Render(w, r, newTaskResponse(newTask)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } @@ -162,16 +162,16 @@ func (rs *TaskResource) CreateHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: get a specific task func (rs *TaskResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `Task` is retrieved via middle-ware - task := r.Context().Value(common.CtxKeyTask).(*model.Task) + // `Task` is retrieved via middle-ware + task := r.Context().Value(common.CtxKeyTask).(*model.Task) - // render JSON reponse - if err := render.Render(w, r, newTaskResponse(task)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err := render.Render(w, r, newTaskResponse(task)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } - render.Status(r, http.StatusOK) + render.Status(r, http.StatusOK) } // EditHandler is public endpoint for @@ -187,28 +187,28 @@ func (rs *TaskResource) GetHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: edit a specific task func (rs *TaskResource) EditHandler(w http.ResponseWriter, r *http.Request) { - // start from empty Request - data := &TaskRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - task.Name = data.Name - task.MaxPoints = data.MaxPoints - task.PublicDockerImage = null.StringFrom(data.PublicDockerImage) - task.PrivateDockerImage = null.StringFrom(data.PrivateDockerImage) - - // update database entry - if err := rs.Stores.Task.Update(task); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + // start from empty Request + data := &TaskRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + task.Name = data.Name + task.MaxPoints = data.MaxPoints + task.PublicDockerImage = null.StringFrom(data.PublicDockerImage) + task.PrivateDockerImage = null.StringFrom(data.PrivateDockerImage) + + // update database entry + if err := rs.Stores.Task.Update(task); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // DeleteHandler is public endpoint for @@ -223,15 +223,15 @@ func (rs *TaskResource) EditHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 403,Unauthorized // SUMMARY: delete a specific task func (rs *TaskResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - Task := r.Context().Value(common.CtxKeyTask).(*model.Task) + Task := r.Context().Value(common.CtxKeyTask).(*model.Task) - // update database entry - if err := rs.Stores.Task.Delete(Task.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.Task.Delete(Task.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // GetPublicTestFileHandler is public endpoint for @@ -247,17 +247,17 @@ func (rs *TaskResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: get the zip with the testing framework for the public tests func (rs *TaskResource) GetPublicTestFileHandler(w http.ResponseWriter, r *http.Request) { - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - hnd := helper.NewPublicTestFileHandle(task.ID) + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + hnd := helper.NewPublicTestFileHandle(task.ID) - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := hnd.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err := hnd.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } @@ -274,17 +274,17 @@ func (rs *TaskResource) GetPublicTestFileHandler(w http.ResponseWriter, r *http. // SUMMARY: get the zip with the testing framework for the private tests func (rs *TaskResource) GetPrivateTestFileHandler(w http.ResponseWriter, r *http.Request) { - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - hnd := helper.NewPrivateTestFileHandle(task.ID) + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + hnd := helper.NewPrivateTestFileHandle(task.ID) - if !hnd.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !hnd.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := hnd.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err := hnd.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } @@ -301,15 +301,15 @@ func (rs *TaskResource) GetPrivateTestFileHandler(w http.ResponseWriter, r *http // RESPONSE: 403,Unauthorized // SUMMARY: change the zip with the testing framework for the public tests func (rs *TaskResource) ChangePublicTestFileHandler(w http.ResponseWriter, r *http.Request) { - // will always be a POST - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - - // the file will be located - if _, err := helper.NewPublicTestFileHandle(task.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - render.Status(r, http.StatusOK) + // will always be a POST + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + + // the file will be located + if _, err := helper.NewPublicTestFileHandle(task.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + render.Status(r, http.StatusOK) } // ChangePrivateTestFileHandler is public endpoint for @@ -325,15 +325,15 @@ func (rs *TaskResource) ChangePublicTestFileHandler(w http.ResponseWriter, r *ht // RESPONSE: 403,Unauthorized // SUMMARY: change the zip with the testing framework for the private tests func (rs *TaskResource) ChangePrivateTestFileHandler(w http.ResponseWriter, r *http.Request) { - // will always be a POST - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - - // the file will be located - if _, err := helper.NewPrivateTestFileHandle(task.ID).WriteToDisk(r, "file_data"); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - render.Status(r, http.StatusOK) + // will always be a POST + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + + // the file will be located + if _, err := helper.NewPrivateTestFileHandle(task.ID).WriteToDisk(r, "file_data"); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + render.Status(r, http.StatusOK) } // GetSubmissionResultHandler is public endpoint for @@ -348,40 +348,40 @@ func (rs *TaskResource) ChangePrivateTestFileHandler(w http.ResponseWriter, r *h // RESPONSE: 403,Unauthorized // SUMMARY: the the public results (grades) for a test and the request identity func (rs *TaskResource) GetSubmissionResultHandler(w http.ResponseWriter, r *http.Request) { - givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) - course := r.Context().Value(common.CtxKeyCourse).(*model.Course) - if givenRole != authorize.STUDENT { - render.Render(w, r, ErrBadRequest) - return - } - - // `Task` is retrieved via middle-ware - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - grade, err := rs.Stores.Grade.GetForSubmission(submission.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // TODO (patwie): does not make sense for TUTOR, ADMIN anyway - grade.PrivateTestStatus = -1 - grade.PrivateTestLog = "" - - // render JSON reponse - if err := render.Render(w, r, newGradeResponse(grade, course.ID)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + givenRole := r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) + course := r.Context().Value(common.CtxKeyCourse).(*model.Course) + if givenRole != authorize.STUDENT { + render.Render(w, r, ErrBadRequest) + return + } + + // `Task` is retrieved via middle-ware + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + submission, err := rs.Stores.Submission.GetByUserAndTask(accessClaims.LoginID, task.ID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + grade, err := rs.Stores.Grade.GetForSubmission(submission.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // TODO (patwie): does not make sense for TUTOR, ADMIN anyway + grade.PrivateTestStatus = -1 + grade.PrivateTestLog = "" + + // render JSON reponse + if err := render.Render(w, r, newGradeResponse(grade, course.ID)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // ............................................................................. @@ -391,66 +391,66 @@ func (rs *TaskResource) GetSubmissionResultHandler(w http.ResponseWriter, r *htt // the Task could not be found, we stop here and return a 404. // We do NOT check whether the identity is authorized to get this Task. func (rs *TaskResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) - - var taskID int64 - var err error - - // try to get id from URL - if taskID, err = strconv.ParseInt(chi.URLParam(r, "task_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific Task in database - task, err := rs.Stores.Task.Get(taskID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - ctx := context.WithValue(r.Context(), common.CtxKeyTask, task) - - // find sheet - sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if sheetID, err := strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err == nil { - if sheetID != sheet.ID { - render.Render(w, r, ErrNotFound) - return - } - } else { - ctx = context.WithValue(ctx, common.CtxKeySheet, sheet) - } - - // public yet? - if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { - render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) - return - } - - // when there is a taskID in the url, there is NOT a courseID in the url, - // BUT: when there is a task, there is a course - - course, err := rs.Stores.Task.IdentifyCourseOfTask(task.ID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - if courseFromURL.ID != course.ID { - render.Render(w, r, ErrNotFound) - return - } - - ctx = context.WithValue(ctx, common.CtxKeyCourse, course) - - // serve next - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + courseFromURL := r.Context().Value(common.CtxKeyCourse).(*model.Course) + + var taskID int64 + var err error + + // try to get id from URL + if taskID, err = strconv.ParseInt(chi.URLParam(r, "task_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific Task in database + task, err := rs.Stores.Task.Get(taskID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), common.CtxKeyTask, task) + + // find sheet + sheet, err := rs.Stores.Task.IdentifySheetOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if sheetID, err := strconv.ParseInt(chi.URLParam(r, "sheet_id"), 10, 64); err == nil { + if sheetID != sheet.ID { + render.Render(w, r, ErrNotFound) + return + } + } else { + ctx = context.WithValue(ctx, common.CtxKeySheet, sheet) + } + + // public yet? + if r.Context().Value(common.CtxKeyCourseRole).(authorize.CourseRole) == authorize.STUDENT && !PublicYet(sheet.PublishAt) { + render.Render(w, r, ErrBadRequestWithDetails(fmt.Errorf("sheet not published yet"))) + return + } + + // when there is a taskID in the url, there is NOT a courseID in the url, + // BUT: when there is a task, there is a course + + course, err := rs.Stores.Task.IdentifyCourseOfTask(task.ID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + if courseFromURL.ID != course.ID { + render.Render(w, r, ErrNotFound) + return + } + + ctx = context.WithValue(ctx, common.CtxKeyCourse, course) + + // serve next + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/task_rating.go b/api/app/task_rating.go index ae742fa..62eacba 100644 --- a/api/app/task_rating.go +++ b/api/app/task_rating.go @@ -19,24 +19,24 @@ package app import ( - "net/http" + "net/http" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/render" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/render" ) // TaskRatingResource specifies TaskRating management handler. type TaskRatingResource struct { - Stores *Stores + Stores *Stores } // NewTaskRatingResource create and returns a TaskRatingResource. func NewTaskRatingResource(stores *Stores) *TaskRatingResource { - return &TaskRatingResource{ - Stores: stores, - } + return &TaskRatingResource{ + Stores: stores, + } } // GetHandler is public endpoint for @@ -51,35 +51,35 @@ func NewTaskRatingResource(stores *Stores) *TaskRatingResource { // RESPONSE: 403,Unauthorized // SUMMARY: get all stats (average rating, own rating, ..) for a task func (rs *TaskRatingResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `TaskRating` is retrieved via middle-ware - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - - // get rating - averageRating, err := rs.Stores.Task.GetAverageRating(task.ID) - if err != nil { - // no entries so far - averageRating = 0 - } - - // get own rating - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - taskRating, err := rs.Stores.Task.GetRatingOfTaskByUser(task.ID, accessClaims.LoginID) - if err != nil { - // record not found - taskRating = &model.TaskRating{ - UserID: accessClaims.LoginID, - TaskID: task.ID, - Rating: 0, - } - } - - // render JSON reponse - if err := render.Render(w, r, rs.newTaskRatingResponse(taskRating, averageRating)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - render.Status(r, http.StatusOK) + // `TaskRating` is retrieved via middle-ware + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + + // get rating + averageRating, err := rs.Stores.Task.GetAverageRating(task.ID) + if err != nil { + // no entries so far + averageRating = 0 + } + + // get own rating + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + taskRating, err := rs.Stores.Task.GetRatingOfTaskByUser(task.ID, accessClaims.LoginID) + if err != nil { + // record not found + taskRating = &model.TaskRating{ + UserID: accessClaims.LoginID, + TaskID: task.ID, + Rating: 0, + } + } + + // render JSON reponse + if err := render.Render(w, r, rs.newTaskRatingResponse(taskRating, averageRating)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + render.Status(r, http.StatusOK) } // ChangeHandler is public endpoint for @@ -95,57 +95,57 @@ func (rs *TaskRatingResource) GetHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 403,Unauthorized // SUMMARY: updates and gets all stats (average rating, own rating, ..) for a task func (rs *TaskRatingResource) ChangeHandler(w http.ResponseWriter, r *http.Request) { - // `TaskRating` is retrieved via middle-ware - task := r.Context().Value(common.CtxKeyTask).(*model.Task) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - data := &TaskRatingRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - givenRating, err := rs.Stores.Task.GetRatingOfTaskByUser(task.ID, accessClaims.LoginID) - - if err == nil { - // there is a rating - givenRating.Rating = data.Rating - if err := rs.Stores.Task.UpdateRating(givenRating); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - render.Status(r, http.StatusNoContent) - - } else { - // there is no rating so far from the user - - rating := &model.TaskRating{ - UserID: accessClaims.LoginID, - TaskID: task.ID, - Rating: data.Rating, - } - - newTaskRating, err := rs.Stores.Task.CreateRating(rating) - if err != nil { - render.Render(w, r, ErrRender(err)) - return - } - - // get rating - averageRating, err := rs.Stores.Task.GetAverageRating(task.ID) - if err != nil { - // no entries so far - averageRating = 0 - } - - render.Status(r, http.StatusCreated) - - // return Task information of created entry - if err := render.Render(w, r, rs.newTaskRatingResponse(newTaskRating, averageRating)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } - } + // `TaskRating` is retrieved via middle-ware + task := r.Context().Value(common.CtxKeyTask).(*model.Task) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + data := &TaskRatingRequest{} + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + givenRating, err := rs.Stores.Task.GetRatingOfTaskByUser(task.ID, accessClaims.LoginID) + + if err == nil { + // there is a rating + givenRating.Rating = data.Rating + if err := rs.Stores.Task.UpdateRating(givenRating); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + render.Status(r, http.StatusNoContent) + + } else { + // there is no rating so far from the user + + rating := &model.TaskRating{ + UserID: accessClaims.LoginID, + TaskID: task.ID, + Rating: data.Rating, + } + + newTaskRating, err := rs.Stores.Task.CreateRating(rating) + if err != nil { + render.Render(w, r, ErrRender(err)) + return + } + + // get rating + averageRating, err := rs.Stores.Task.GetAverageRating(task.ID) + if err != nil { + // no entries so far + averageRating = 0 + } + + render.Status(r, http.StatusCreated) + + // return Task information of created entry + if err := render.Render(w, r, rs.newTaskRatingResponse(newTaskRating, averageRating)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } + } } diff --git a/api/app/task_rating_test.go b/api/app/task_rating_test.go index e6d5344..a18124a 100644 --- a/api/app/task_rating_test.go +++ b/api/app/task_rating_test.go @@ -19,102 +19,102 @@ package app import ( - "encoding/json" - "net/http" - "testing" + "encoding/json" + "net/http" + "testing" - "github.com/cgtuebingen/infomark-backend/database" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" + "github.com/cgtuebingen/infomark-backend/database" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" ) func TestTaskRating(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail - tape := &Tape{} + tape := &Tape{} - var stores *Stores + var stores *Stores - g.Describe("TaskRating", func() { + g.Describe("TaskRating", func() { - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - }) + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + }) - g.It("Should get own rating", func() { - userID := int64(112) - taskID := int64(1) + g.It("Should get own rating", func() { + userID := int64(112) + taskID := int64(1) - givenRating, err := stores.Task.GetRatingOfTaskByUser(taskID, userID) - g.Assert(err).Equal(nil) + givenRating, err := stores.Task.GetRatingOfTaskByUser(taskID, userID) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - taskRatingActual := &TaskRatingResponse{} - err = json.NewDecoder(w.Body).Decode(taskRatingActual) - g.Assert(err).Equal(nil) + taskRatingActual := &TaskRatingResponse{} + err = json.NewDecoder(w.Body).Decode(taskRatingActual) + g.Assert(err).Equal(nil) - g.Assert(taskRatingActual.OwnRating).Equal(givenRating.Rating) - g.Assert(taskRatingActual.TaskID).Equal(taskID) + g.Assert(taskRatingActual.OwnRating).Equal(givenRating.Rating) + g.Assert(taskRatingActual.TaskID).Equal(taskID) - // update rating (mock had rating 2) - w = tape.PostWithClaims("/api/v1/courses/1/tasks/1/ratings", H{"rating": 4}, userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + // update rating (mock had rating 2) + w = tape.PostWithClaims("/api/v1/courses/1/tasks/1/ratings", H{"rating": 4}, userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - // new query - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + // new query + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - taskRatingActual2 := &TaskRatingResponse{} - err = json.NewDecoder(w.Body).Decode(taskRatingActual2) - g.Assert(err).Equal(nil) + taskRatingActual2 := &TaskRatingResponse{} + err = json.NewDecoder(w.Body).Decode(taskRatingActual2) + g.Assert(err).Equal(nil) - g.Assert(taskRatingActual2.OwnRating).Equal(4) - g.Assert(taskRatingActual2.TaskID).Equal(taskID) - }) + g.Assert(taskRatingActual2.OwnRating).Equal(4) + g.Assert(taskRatingActual2.TaskID).Equal(taskID) + }) - g.It("Should create own rating", func() { - userID := int64(112) - taskID := int64(1) + g.It("Should create own rating", func() { + userID := int64(112) + taskID := int64(1) - // delete and create (see mock.py) - prevRatingModel, err := stores.Task.GetRatingOfTaskByUser(taskID, userID) - g.Assert(err).Equal(nil) - database.Delete(tape.DB, "task_ratings", prevRatingModel.ID) + // delete and create (see mock.py) + prevRatingModel, err := stores.Task.GetRatingOfTaskByUser(taskID, userID) + g.Assert(err).Equal(nil) + database.Delete(tape.DB, "task_ratings", prevRatingModel.ID) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - taskRatingActual3 := &TaskRatingResponse{} - err = json.NewDecoder(w.Body).Decode(taskRatingActual3) - g.Assert(err).Equal(nil) + taskRatingActual3 := &TaskRatingResponse{} + err = json.NewDecoder(w.Body).Decode(taskRatingActual3) + g.Assert(err).Equal(nil) - g.Assert(taskRatingActual3.OwnRating).Equal(0) - g.Assert(taskRatingActual3.TaskID).Equal(taskID) + g.Assert(taskRatingActual3.OwnRating).Equal(0) + g.Assert(taskRatingActual3.TaskID).Equal(taskID) - // update rating (mock had rating 2) - w = tape.PostWithClaims("/api/v1/courses/1/tasks/1/ratings", H{"rating": 4}, userID, false) - g.Assert(w.Code).Equal(http.StatusCreated) + // update rating (mock had rating 2) + w = tape.PostWithClaims("/api/v1/courses/1/tasks/1/ratings", H{"rating": 4}, userID, false) + g.Assert(w.Code).Equal(http.StatusCreated) - // new query - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) - g.Assert(w.Code).Equal(http.StatusOK) + // new query + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/ratings", userID, false) + g.Assert(w.Code).Equal(http.StatusOK) - taskRatingActual2 := &TaskRatingResponse{} - err = json.NewDecoder(w.Body).Decode(taskRatingActual2) - g.Assert(err).Equal(nil) + taskRatingActual2 := &TaskRatingResponse{} + err = json.NewDecoder(w.Body).Decode(taskRatingActual2) + g.Assert(err).Equal(nil) - g.Assert(taskRatingActual2.OwnRating).Equal(4) - g.Assert(taskRatingActual2.TaskID).Equal(taskID) - }) + g.Assert(taskRatingActual2.OwnRating).Equal(4) + g.Assert(taskRatingActual2.TaskID).Equal(taskID) + }) - g.AfterEach(func() { - tape.AfterEach() - }) + g.AfterEach(func() { + tape.AfterEach() + }) - }) + }) } diff --git a/api/app/task_test.go b/api/app/task_test.go index fdde747..e4726e1 100644 --- a/api/app/task_test.go +++ b/api/app/task_test.go @@ -19,327 +19,327 @@ package app import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/franela/goblin" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/franela/goblin" + "github.com/spf13/viper" ) func TestTask(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail - tape := &Tape{} + tape := &Tape{} - var stores *Stores + var stores *Stores - g.Describe("Task", func() { + g.Describe("Task", func() { - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - }) + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + }) - g.It("Query should require access claims", func() { + g.It("Query should require access claims", func() { - w := tape.Get("/api/v1/courses/1/sheets/1/tasks") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Get("/api/v1/courses/1/sheets/1/tasks") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/tasks", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) + w = tape.GetWithClaims("/api/v1/courses/1/sheets/1/tasks", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) - g.It("Should list all tasks from a sheet", func() { - w := tape.GetWithClaims("/api/v1/courses/1/sheets/1/tasks", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + g.It("Should list all tasks from a sheet", func() { + w := tape.GetWithClaims("/api/v1/courses/1/sheets/1/tasks", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - tasksExpected, err := stores.Task.TasksOfSheet(1) - g.Assert(err).Equal(nil) + tasksExpected, err := stores.Task.TasksOfSheet(1) + g.Assert(err).Equal(nil) - tasksActual := []TaskResponse{} - err = json.NewDecoder(w.Body).Decode(&tasksActual) - g.Assert(err).Equal(nil) - g.Assert(len(tasksActual)).Equal(3) + tasksActual := []TaskResponse{} + err = json.NewDecoder(w.Body).Decode(&tasksActual) + g.Assert(err).Equal(nil) + g.Assert(len(tasksActual)).Equal(3) - for k := range tasksActual { - g.Assert(tasksExpected[k].ID).Equal(tasksActual[k].ID) - g.Assert(tasksExpected[k].MaxPoints).Equal(tasksActual[k].MaxPoints) - g.Assert(tasksExpected[k].Name).Equal(tasksActual[k].Name) - g.Assert(tasksExpected[k].PublicDockerImage.String).Equal(tasksActual[k].PublicDockerImage.String) - g.Assert(tasksExpected[k].PrivateDockerImage.String).Equal(tasksActual[k].PrivateDockerImage.String) + for k := range tasksActual { + g.Assert(tasksExpected[k].ID).Equal(tasksActual[k].ID) + g.Assert(tasksExpected[k].MaxPoints).Equal(tasksActual[k].MaxPoints) + g.Assert(tasksExpected[k].Name).Equal(tasksActual[k].Name) + g.Assert(tasksExpected[k].PublicDockerImage.String).Equal(tasksActual[k].PublicDockerImage.String) + g.Assert(tasksExpected[k].PrivateDockerImage.String).Equal(tasksActual[k].PrivateDockerImage.String) - g.Assert(tasksExpected[k].PublicDockerImage.Valid).Equal(true) - g.Assert(tasksExpected[k].PrivateDockerImage.Valid).Equal(true) - } - }) + g.Assert(tasksExpected[k].PublicDockerImage.Valid).Equal(true) + g.Assert(tasksExpected[k].PrivateDockerImage.Valid).Equal(true) + } + }) - g.It("Should get a specific task", func() { + g.It("Should get a specific task", func() { - taskExpected, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) + taskExpected, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - taskActual := &TaskResponse{} - err = json.NewDecoder(w.Body).Decode(taskActual) - g.Assert(err).Equal(nil) + taskActual := &TaskResponse{} + err = json.NewDecoder(w.Body).Decode(taskActual) + g.Assert(err).Equal(nil) - g.Assert(taskActual.ID).Equal(taskExpected.ID) - g.Assert(taskActual.MaxPoints).Equal(taskExpected.MaxPoints) - g.Assert(taskActual.Name).Equal(taskExpected.Name) - g.Assert(taskActual.PublicDockerImage.String).Equal(taskExpected.PublicDockerImage.String) - g.Assert(taskActual.PrivateDockerImage.String).Equal(taskExpected.PrivateDockerImage.String) + g.Assert(taskActual.ID).Equal(taskExpected.ID) + g.Assert(taskActual.MaxPoints).Equal(taskExpected.MaxPoints) + g.Assert(taskActual.Name).Equal(taskExpected.Name) + g.Assert(taskActual.PublicDockerImage.String).Equal(taskExpected.PublicDockerImage.String) + g.Assert(taskActual.PrivateDockerImage.String).Equal(taskExpected.PrivateDockerImage.String) - g.Assert(taskActual.PublicDockerImage.Valid).Equal(true) - g.Assert(taskActual.PrivateDockerImage.Valid).Equal(true) + g.Assert(taskActual.PublicDockerImage.Valid).Equal(true) + g.Assert(taskActual.PrivateDockerImage.Valid).Equal(true) - }) + }) - g.It("Creating should require claims", func() { - w := tape.Post("/api/v1/courses/1/sheets/1/tasks", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) + g.It("Creating should require claims", func() { + w := tape.Post("/api/v1/courses/1/sheets/1/tasks", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) - g.Xit("Creating should require body", func() { - // TODO empty request with claims - }) + g.Xit("Creating should require body", func() { + // TODO empty request with claims + }) - g.It("Should create valid task", func() { - tasksBefore, err := stores.Task.TasksOfSheet(1) - g.Assert(err).Equal(nil) + g.It("Should create valid task", func() { + tasksBefore, err := stores.Task.TasksOfSheet(1) + g.Assert(err).Equal(nil) - taskSent := TaskRequest{ - Name: "new Task", - MaxPoints: 88, - PublicDockerImage: "testimage_public", - PrivateDockerImage: "testimage_private", - } + taskSent := TaskRequest{ + Name: "new Task", + MaxPoints: 88, + PublicDockerImage: "testimage_public", + PrivateDockerImage: "testimage_private", + } - err = taskSent.Validate() - g.Assert(err).Equal(nil) + err = taskSent.Validate() + g.Assert(err).Equal(nil) - w := tape.PostWithClaims("/api/v1/courses/1/sheets/1/tasks", helper.ToH(taskSent), 1, true) - g.Assert(w.Code).Equal(http.StatusCreated) + w := tape.PostWithClaims("/api/v1/courses/1/sheets/1/tasks", helper.ToH(taskSent), 1, true) + g.Assert(w.Code).Equal(http.StatusCreated) - taskReturn := &TaskResponse{} - err = json.NewDecoder(w.Body).Decode(&taskReturn) - g.Assert(taskReturn.Name).Equal("new Task") - g.Assert(taskReturn.MaxPoints).Equal(88) - g.Assert(taskReturn.PrivateDockerImage.Valid).Equal(true) - g.Assert(taskReturn.PrivateDockerImage.String).Equal(taskSent.PrivateDockerImage) - g.Assert(taskReturn.PublicDockerImage.Valid).Equal(true) - g.Assert(taskReturn.PublicDockerImage.String).Equal(taskSent.PublicDockerImage) - - tasksAfter, err := stores.Task.TasksOfSheet(1) - g.Assert(err).Equal(nil) - g.Assert(len(tasksAfter)).Equal(len(tasksBefore) + 1) - }) - - g.It("Should skip non-existent test files", func() { - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - }) - - g.It("Should upload public test file", func() { - defer helper.NewPublicTestFileHandle(1).Delete() - defer helper.NewPrivateTestFileHandle(1).Delete() - - // no files so far - g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) - g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) - - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - - // public test - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/public_file", filename, "application/zip", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) - - // only public file - g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(true) - g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) - - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + taskReturn := &TaskResponse{} + err = json.NewDecoder(w.Body).Decode(&taskReturn) + g.Assert(taskReturn.Name).Equal("new Task") + g.Assert(taskReturn.MaxPoints).Equal(88) + g.Assert(taskReturn.PrivateDockerImage.Valid).Equal(true) + g.Assert(taskReturn.PrivateDockerImage.String).Equal(taskSent.PrivateDockerImage) + g.Assert(taskReturn.PublicDockerImage.Valid).Equal(true) + g.Assert(taskReturn.PublicDockerImage.String).Equal(taskSent.PublicDockerImage) + + tasksAfter, err := stores.Task.TasksOfSheet(1) + g.Assert(err).Equal(nil) + g.Assert(len(tasksAfter)).Equal(len(tasksBefore) + 1) + }) + + g.It("Should skip non-existent test files", func() { + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + }) + + g.It("Should upload public test file", func() { + defer helper.NewPublicTestFileHandle(1).Delete() + defer helper.NewPrivateTestFileHandle(1).Delete() + + // no files so far + g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) + g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) + + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + + // public test + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/public_file", filename, "application/zip", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) + + // only public file + g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(true) + g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) + + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 122, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/public_file", 122, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) - g.It("Should upload private test file", func() { - defer helper.NewPublicTestFileHandle(1).Delete() - defer helper.NewPrivateTestFileHandle(1).Delete() - - // no files so far - g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) - g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) - - w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) - g.Assert(w.Code).Equal(http.StatusNotFound) - - // public test - filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) - w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/private_file", filename, "application/zip", 1, true) - g.Assert(err).Equal(nil) - g.Assert(w.Code).Equal(http.StatusOK) + g.It("Should upload private test file", func() { + defer helper.NewPublicTestFileHandle(1).Delete() + defer helper.NewPrivateTestFileHandle(1).Delete() + + // no files so far + g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) + g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(false) + + w := tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) + g.Assert(w.Code).Equal(http.StatusNotFound) + + // public test + filename := fmt.Sprintf("%s/empty.zip", viper.GetString("fixtures_dir")) + w, err := tape.UploadWithClaims("/api/v1/courses/1/tasks/1/private_file", filename, "application/zip", 1, true) + g.Assert(err).Equal(nil) + g.Assert(w.Code).Equal(http.StatusOK) - // only public file - g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) - g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(true) + // only public file + g.Assert(helper.NewPublicTestFileHandle(1).Exists()).Equal(false) + g.Assert(helper.NewPrivateTestFileHandle(1).Exists()).Equal(true) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 122, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) - - g.It("Changes should require claims", func() { - w := tape.Put("/api/v1/courses/1/sheets/1/tasks", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) - - g.It("Should perform updates", func() { - data := H{ - "max_points": 555, - "name": "new blub", - "public_docker_image": "new_public", - "private_docker_image": "new_private", - } + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/private_file", 122, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) + + g.It("Changes should require claims", func() { + w := tape.Put("/api/v1/courses/1/sheets/1/tasks", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) + + g.It("Should perform updates", func() { + data := H{ + "max_points": 555, + "name": "new blub", + "public_docker_image": "new_public", + "private_docker_image": "new_private", + } - w := tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - taskAfter, err := stores.Task.Get(1) - g.Assert(err).Equal(nil) - g.Assert(taskAfter.MaxPoints).Equal(555) - g.Assert(taskAfter.Name).Equal("new blub") - g.Assert(taskAfter.PublicDockerImage.Valid).Equal(true) - g.Assert(taskAfter.PublicDockerImage.String).Equal("new_public") - g.Assert(taskAfter.PrivateDockerImage.Valid).Equal(true) - g.Assert(taskAfter.PrivateDockerImage.String).Equal("new_private") + taskAfter, err := stores.Task.Get(1) + g.Assert(err).Equal(nil) + g.Assert(taskAfter.MaxPoints).Equal(555) + g.Assert(taskAfter.Name).Equal("new blub") + g.Assert(taskAfter.PublicDockerImage.Valid).Equal(true) + g.Assert(taskAfter.PublicDockerImage.String).Equal("new_public") + g.Assert(taskAfter.PrivateDockerImage.Valid).Equal(true) + g.Assert(taskAfter.PrivateDockerImage.String).Equal("new_private") - w = tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) + w = tape.PutWithClaims("/api/v1/courses/1/tasks/1", data, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) - g.It("Should delete when valid access claims", func() { + g.It("Should delete when valid access claims", func() { - entriesBefore, err := stores.Task.GetAll() - g.Assert(err).Equal(nil) + entriesBefore, err := stores.Task.GetAll() + g.Assert(err).Equal(nil) - w := tape.Delete("/api/v1/courses/1/tasks/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Delete("/api/v1/courses/1/tasks/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 2, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 2, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) + w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) - // verify nothing has changes - entriesAfter, err := stores.Task.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) + // verify nothing has changes + entriesAfter, err := stores.Task.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore)) - w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.DeleteWithClaims("/api/v1/courses/1/tasks/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - // verify a sheet less exists - entriesAfter, err = stores.Task.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) + // verify a sheet less exists + entriesAfter, err = stores.Task.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(entriesAfter)).Equal(len(entriesBefore) - 1) - }) + }) - g.It("students should see public results", func() { - w := tape.Get("/api/v1/courses/1/tasks/1/result") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + g.It("students should see public results", func() { + w := tape.Get("/api/v1/courses/1/tasks/1/result") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 1, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 1, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 2, false) - g.Assert(w.Code).Equal(http.StatusBadRequest) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 2, false) + g.Assert(w.Code).Equal(http.StatusBadRequest) - w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.GetWithClaims("/api/v1/courses/1/tasks/1/result", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - actual := &GradeResponse{} - err := json.NewDecoder(w.Body).Decode(actual) - g.Assert(err).Equal(nil) - g.Assert(actual.PrivateTestLog).Equal("") - g.Assert(actual.PrivateTestStatus).Equal(-1) + actual := &GradeResponse{} + err := json.NewDecoder(w.Body).Decode(actual) + g.Assert(err).Equal(nil) + g.Assert(actual.PrivateTestLog).Equal("") + g.Assert(actual.PrivateTestStatus).Equal(-1) - }) + }) - g.It("Permission test", func() { - // sheet (id=1) belongs to group(id=1) - url := "/api/v1/courses/1/sheets/1/tasks" + g.It("Permission test", func() { + // sheet (id=1) belongs to group(id=1) + url := "/api/v1/courses/1/sheets/1/tasks" - // global root can do whatever they want - w := tape.GetWithClaims(url, 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + // global root can do whatever they want + w := tape.GetWithClaims(url, 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled tutors can access - w = tape.GetWithClaims(url, 2, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled tutors can access + w = tape.GetWithClaims(url, 2, false) + g.Assert(w.Code).Equal(http.StatusOK) - // enrolled students can access - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // enrolled students can access + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // disenroll student - w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + // disenroll student + w = tape.DeleteWithClaims("/api/v1/courses/1/enrollments", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - // cannot access anymore - w = tape.GetWithClaims(url, 112, false) - g.Assert(w.Code).Equal(http.StatusForbidden) - }) + // cannot access anymore + w = tape.GetWithClaims(url, 112, false) + g.Assert(w.Code).Equal(http.StatusForbidden) + }) - g.It("Should get all missing tasks", func() { - // in the mock script each student has a submission to all tasks - // therefore we need to delete it temporarily - _, err := tape.DB.Exec("DELETE FROM submissions WHERE task_id = 2") - g.Assert(err).Equal(nil) + g.It("Should get all missing tasks", func() { + // in the mock script each student has a submission to all tasks + // therefore we need to delete it temporarily + _, err := tape.DB.Exec("DELETE FROM submissions WHERE task_id = 2") + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/courses/1/tasks/missing", 112, false) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/courses/1/tasks/missing", 112, false) + g.Assert(w.Code).Equal(http.StatusOK) - result := []MissingTaskResponse{} - err = json.NewDecoder(w.Body).Decode(&result) - g.Assert(err).Equal(nil) - for _, el := range result { - g.Assert(el.Task.ID).Equal(int64(2)) - } + result := []MissingTaskResponse{} + err = json.NewDecoder(w.Body).Decode(&result) + g.Assert(err).Equal(nil) + for _, el := range result { + g.Assert(el.Task.ID).Equal(int64(2)) + } - }) + }) - g.AfterEach(func() { - tape.AfterEach() - }) + g.AfterEach(func() { + tape.AfterEach() + }) - }) + }) } diff --git a/api/app/user.go b/api/app/user.go index 92cec4d..196e745 100644 --- a/api/app/user.go +++ b/api/app/user.go @@ -19,30 +19,30 @@ package app import ( - "context" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-chi/chi" - "github.com/go-chi/render" + "context" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-chi/chi" + "github.com/go-chi/render" ) // UserResource specifies user management handler. type UserResource struct { - Stores *Stores + Stores *Stores } // NewUserResource create and returns a UserResource. func NewUserResource(stores *Stores) *UserResource { - return &UserResource{ - Stores: stores, - } + return &UserResource{ + Stores: stores, + } } // ............................................................................. @@ -57,21 +57,21 @@ func NewUserResource(stores *Stores) *UserResource { // SUMMARY: Get own user details (requires root) func (rs *UserResource) IndexHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - if !accessClaims.Root { - render.Render(w, r, ErrUnauthorized) - return - } + if !accessClaims.Root { + render.Render(w, r, ErrUnauthorized) + return + } - // fetch collection of users from database - users, err := rs.Stores.User.GetAll() + // fetch collection of users from database + users, err := rs.Stores.User.GetAll() - // render JSON reponse - if err = render.RenderList(w, r, newUserListResponse(users)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // render JSON reponse + if err = render.RenderList(w, r, newUserListResponse(users)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // GetMeHandler is public endpoint for @@ -83,20 +83,20 @@ func (rs *UserResource) IndexHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 401,Unauthenticated // SUMMARY: Get own user details func (rs *UserResource) GetMeHandler(w http.ResponseWriter, r *http.Request) { - // `user` is retrieved via middle-ware - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - user, err := rs.Stores.User.Get(accessClaims.LoginID) - - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - // render JSON reponse - if err := render.Render(w, r, newUserResponse(user)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // `user` is retrieved via middle-ware + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + user, err := rs.Stores.User.Get(accessClaims.LoginID) + + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + // render JSON reponse + if err := render.Render(w, r, newUserResponse(user)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // GetHandler is public endpoint for @@ -109,23 +109,23 @@ func (rs *UserResource) GetMeHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 401,Unauthenticated // SUMMARY: Get user details func (rs *UserResource) GetHandler(w http.ResponseWriter, r *http.Request) { - // `user` is retrieved via middle-ware - user := r.Context().Value(common.CtxKeyUser).(*model.User) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - // is request identity allowed to get informaition about this user - if user.ID != accessClaims.LoginID { - if !accessClaims.Root { - render.Render(w, r, ErrUnauthorized) - return - } - } - - // render JSON reponse - if err := render.Render(w, r, newUserResponse(user)); err != nil { - render.Render(w, r, ErrRender(err)) - return - } + // `user` is retrieved via middle-ware + user := r.Context().Value(common.CtxKeyUser).(*model.User) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + // is request identity allowed to get informaition about this user + if user.ID != accessClaims.LoginID { + if !accessClaims.Root { + render.Render(w, r, ErrUnauthorized) + return + } + } + + // render JSON reponse + if err := render.Render(w, r, newUserResponse(user)); err != nil { + render.Render(w, r, ErrRender(err)) + return + } } // GetAvatarHandler is public endpoint for @@ -138,19 +138,19 @@ func (rs *UserResource) GetHandler(w http.ResponseWriter, r *http.Request) { // RESPONSE: 401,Unauthenticated // SUMMARY: Get user details func (rs *UserResource) GetAvatarHandler(w http.ResponseWriter, r *http.Request) { - // `user` is retrieved via middle-ware - user := r.Context().Value(common.CtxKeyUser).(*model.User) + // `user` is retrieved via middle-ware + user := r.Context().Value(common.CtxKeyUser).(*model.User) - file := helper.NewAvatarFileHandle(user.ID) + file := helper.NewAvatarFileHandle(user.ID) - if !file.Exists() { - render.Render(w, r, ErrNotFound) - return - } + if !file.Exists() { + render.Render(w, r, ErrNotFound) + return + } - if err := file.WriteToBody(w); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - } + if err := file.WriteToBody(w); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + } } // EditMeHandler is public endpoint for @@ -164,38 +164,38 @@ func (rs *UserResource) GetAvatarHandler(w http.ResponseWriter, r *http.Request) // SUMMARY: updating a the user record of the request identity func (rs *UserResource) EditMeHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - data := &userMeRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - // user is not allowed to change all entries, we use the database entry as a starting point - user, err := rs.Stores.User.Get(accessClaims.LoginID) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - user.FirstName = data.FirstName - user.LastName = data.LastName - // no email update here - user.StudentNumber = data.StudentNumber - user.Semester = data.Semester - user.Subject = data.Subject - user.Language = data.Language - - // update database entry - if err := rs.Stores.User.Update(user); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + data := &userMeRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + // user is not allowed to change all entries, we use the database entry as a starting point + user, err := rs.Stores.User.Get(accessClaims.LoginID) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + user.FirstName = data.FirstName + user.LastName = data.LastName + // no email update here + user.StudentNumber = data.StudentNumber + user.Semester = data.Semester + user.Subject = data.Subject + user.Language = data.Language + + // update database entry + if err := rs.Stores.User.Update(user); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // EditHandler is public endpoint for @@ -210,48 +210,48 @@ func (rs *UserResource) EditMeHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: updating a specific user with given id. func (rs *UserResource) EditHandler(w http.ResponseWriter, r *http.Request) { - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - - if !accessClaims.Root { - render.Render(w, r, ErrUnauthorized) - return - } - - data := &userRequest{} - - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } - - user := r.Context().Value(common.CtxKeyUser).(*model.User) - - user.FirstName = data.FirstName - user.LastName = data.LastName - user.Email = data.Email - user.StudentNumber = data.StudentNumber - user.Semester = data.Semester - user.Subject = data.Subject - user.Language = data.Language - - // all identities allowed to this endpoint are allowed to change the password - if data.PlainPassword != "" { - var err error - user.EncryptedPassword, err = auth.HashPassword(data.PlainPassword) - if err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - } - - // update database entry - if err := rs.Stores.User.Update(user); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } - - render.Status(r, http.StatusNoContent) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + + if !accessClaims.Root { + render.Render(w, r, ErrUnauthorized) + return + } + + data := &userRequest{} + + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } + + user := r.Context().Value(common.CtxKeyUser).(*model.User) + + user.FirstName = data.FirstName + user.LastName = data.LastName + user.Email = data.Email + user.StudentNumber = data.StudentNumber + user.Semester = data.Semester + user.Subject = data.Subject + user.Language = data.Language + + // all identities allowed to this endpoint are allowed to change the password + if data.PlainPassword != "" { + var err error + user.EncryptedPassword, err = auth.HashPassword(data.PlainPassword) + if err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + } + + // update database entry + if err := rs.Stores.User.Update(user); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } + + render.Status(r, http.StatusNoContent) } // SendEmailHandler is public endpoint for @@ -267,27 +267,27 @@ func (rs *UserResource) EditHandler(w http.ResponseWriter, r *http.Request) { // SUMMARY: send email to a specific user func (rs *UserResource) SendEmailHandler(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(common.CtxKeyUser).(*model.User) - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) + user := r.Context().Value(common.CtxKeyUser).(*model.User) + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + accessUser, _ := rs.Stores.User.Get(accessClaims.LoginID) - data := &EmailRequest{} + data := &EmailRequest{} - // parse JSON request into struct - if err := render.Bind(r, data); err != nil { - render.Render(w, r, ErrBadRequestWithDetails(err)) - return - } + // parse JSON request into struct + if err := render.Bind(r, data); err != nil { + render.Render(w, r, ErrBadRequestWithDetails(err)) + return + } - // add sender identity - msg := email.NewEmailFromUser( - user.Email, - data.Subject, - data.Body, - accessUser, - ) + // add sender identity + msg := email.NewEmailFromUser( + user.Email, + data.Subject, + data.Body, + accessUser, + ) - email.OutgoingEmailsChannel <- msg + email.OutgoingEmailsChannel <- msg } @@ -302,15 +302,15 @@ func (rs *UserResource) SendEmailHandler(w http.ResponseWriter, r *http.Request) // RESPONSE: 401,Unauthenticated // SUMMARY: updating a specific user with given id. func (rs *UserResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value(common.CtxKeyUser).(*model.User) + user := r.Context().Value(common.CtxKeyUser).(*model.User) - // update database entry - if err := rs.Stores.User.Delete(user.ID); err != nil { - render.Render(w, r, ErrInternalServerErrorWithDetails(err)) - return - } + // update database entry + if err := rs.Stores.User.Delete(user.ID); err != nil { + render.Render(w, r, ErrInternalServerErrorWithDetails(err)) + return + } - render.Status(r, http.StatusNoContent) + render.Status(r, http.StatusNoContent) } // ............................................................................. @@ -320,27 +320,27 @@ func (rs *UserResource) DeleteHandler(w http.ResponseWriter, r *http.Request) { // the User could not be found, we stop here and return a 404. // We do NOT check whether the user is authorized to get this user. func (rs *UserResource) Context(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO: check permission if inquirer of request is allowed to access this user - // Should be done via another middleware - var userID int64 - var err error - - // try to get id from URL - if userID, err = strconv.ParseInt(chi.URLParam(r, "user_id"), 10, 64); err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // find specific user in database - user, err := rs.Stores.User.Get(userID) - if err != nil { - render.Render(w, r, ErrNotFound) - return - } - - // serve next - ctx := context.WithValue(r.Context(), common.CtxKeyUser, user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO: check permission if inquirer of request is allowed to access this user + // Should be done via another middleware + var userID int64 + var err error + + // try to get id from URL + if userID, err = strconv.ParseInt(chi.URLParam(r, "user_id"), 10, 64); err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // find specific user in database + user, err := rs.Stores.User.Get(userID) + if err != nil { + render.Render(w, r, ErrNotFound) + return + } + + // serve next + ctx := context.WithValue(r.Context(), common.CtxKeyUser, user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/api/app/user_test.go b/api/app/user_test.go index 1520270..a9aea72 100644 --- a/api/app/user_test.go +++ b/api/app/user_test.go @@ -19,217 +19,217 @@ package app import ( - "encoding/json" - "net/http" - "testing" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/franela/goblin" + "encoding/json" + "net/http" + "testing" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/franela/goblin" ) func TestUser(t *testing.T) { - g := goblin.Goblin(t) - email.DefaultMail = email.VoidMail + g := goblin.Goblin(t) + email.DefaultMail = email.VoidMail - tape := &Tape{} + tape := &Tape{} - var stores *Stores + var stores *Stores - g.Describe("User", func() { + g.Describe("User", func() { - g.BeforeEach(func() { - tape.BeforeEach() - stores = NewStores(tape.DB) - _ = stores - }) + g.BeforeEach(func() { + tape.BeforeEach() + stores = NewStores(tape.DB) + _ = stores + }) - g.It("Query should require access claims", func() { - w := tape.Get("/api/v1/users") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + g.It("Query should require access claims", func() { + w := tape.Get("/api/v1/users") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/users", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) + w = tape.GetWithClaims("/api/v1/users", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) - g.It("Query should list all users", func() { - usersExpected, err := stores.User.GetAll() - g.Assert(err).Equal(nil) + g.It("Query should list all users", func() { + usersExpected, err := stores.User.GetAll() + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/users", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/users", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - usersActual := []model.User{} - err = json.NewDecoder(w.Body).Decode(&usersActual) - g.Assert(err).Equal(nil) - g.Assert(len(usersActual)).Equal(len(usersExpected)) - }) + usersActual := []model.User{} + err = json.NewDecoder(w.Body).Decode(&usersActual) + g.Assert(err).Equal(nil) + g.Assert(len(usersActual)).Equal(len(usersExpected)) + }) - g.It("Should get a specific user", func() { + g.It("Should get a specific user", func() { - userExpected, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + userExpected, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/users/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/users/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - userActual := &userResponse{} - err = json.NewDecoder(w.Body).Decode(userActual) - g.Assert(err).Equal(nil) + userActual := &userResponse{} + err = json.NewDecoder(w.Body).Decode(userActual) + g.Assert(err).Equal(nil) - g.Assert(userActual.ID).Equal(userExpected.ID) + g.Assert(userActual.ID).Equal(userExpected.ID) - g.Assert(userActual.FirstName).Equal(userExpected.FirstName) - g.Assert(userActual.LastName).Equal(userExpected.LastName) - g.Assert(userActual.Email).Equal(userExpected.Email) - g.Assert(userActual.StudentNumber).Equal(userExpected.StudentNumber) - g.Assert(userActual.Semester).Equal(userExpected.Semester) - g.Assert(userActual.Subject).Equal(userExpected.Subject) - g.Assert(userActual.Language).Equal(userExpected.Language) + g.Assert(userActual.FirstName).Equal(userExpected.FirstName) + g.Assert(userActual.LastName).Equal(userExpected.LastName) + g.Assert(userActual.Email).Equal(userExpected.Email) + g.Assert(userActual.StudentNumber).Equal(userExpected.StudentNumber) + g.Assert(userActual.Semester).Equal(userExpected.Semester) + g.Assert(userActual.Subject).Equal(userExpected.Subject) + g.Assert(userActual.Language).Equal(userExpected.Language) - }) + }) - g.Xit("Should send email", func() {}) + g.Xit("Should send email", func() {}) - g.It("Changes should require access claims", func() { - w := tape.Put("/api/v1/users/1", H{}) - g.Assert(w.Code).Equal(http.StatusUnauthorized) - }) + g.It("Changes should require access claims", func() { + w := tape.Put("/api/v1/users/1", H{}) + g.Assert(w.Code).Equal(http.StatusUnauthorized) + }) - g.It("Should perform updates (incl email)", func() { - // this is NOT the /me enpoint, we can update the user here + g.It("Should perform updates (incl email)", func() { + // this is NOT the /me enpoint, we can update the user here - userDb, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + userDb, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - userSent := &userRequest{ - FirstName: "Info2_update", - LastName: "Lorem Ipsum_update", - Email: "new@mail.com", - Semester: 1, + userSent := &userRequest{ + FirstName: "Info2_update", + LastName: "Lorem Ipsum_update", + Email: "new@mail.com", + Semester: 1, - StudentNumber: userDb.StudentNumber, - Subject: userDb.Subject, - Language: userDb.Language, - } + StudentNumber: userDb.StudentNumber, + Subject: userDb.Subject, + Language: userDb.Language, + } - err = userSent.Validate() - g.Assert(err).Equal(nil) + err = userSent.Validate() + g.Assert(err).Equal(nil) - w := tape.PutWithClaims("/api/v1/users/1", helper.ToH(userSent), 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.PutWithClaims("/api/v1/users/1", helper.ToH(userSent), 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - g.Assert(userAfter.FirstName).Equal(userSent.FirstName) - g.Assert(userAfter.LastName).Equal(userSent.LastName) - g.Assert(userAfter.Email).Equal(userSent.Email) + g.Assert(userAfter.FirstName).Equal(userSent.FirstName) + g.Assert(userAfter.LastName).Equal(userSent.LastName) + g.Assert(userAfter.Email).Equal(userSent.Email) - }) + }) - g.It("Should delete whith claims", func() { - usersBefore, err := stores.User.GetAll() - g.Assert(err).Equal(nil) + g.It("Should delete whith claims", func() { + usersBefore, err := stores.User.GetAll() + g.Assert(err).Equal(nil) - w := tape.Delete("/api/v1/users/1") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + w := tape.Delete("/api/v1/users/1") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.DeleteWithClaims("/api/v1/users/1", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w = tape.DeleteWithClaims("/api/v1/users/1", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - usersAfter, err := stores.User.GetAll() - g.Assert(err).Equal(nil) - g.Assert(len(usersAfter)).Equal(len(usersBefore) - 1) - }) + usersAfter, err := stores.User.GetAll() + g.Assert(err).Equal(nil) + g.Assert(len(usersAfter)).Equal(len(usersBefore) - 1) + }) - g.It("Self-query require claims", func() { - w := tape.Get("/api/v1/me") - g.Assert(w.Code).Equal(http.StatusUnauthorized) + g.It("Self-query require claims", func() { + w := tape.Get("/api/v1/me") + g.Assert(w.Code).Equal(http.StatusUnauthorized) - w = tape.GetWithClaims("/api/v1/me", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) - }) + w = tape.GetWithClaims("/api/v1/me", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) + }) - g.It("Should list myself", func() { - userExpected, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + g.It("Should list myself", func() { + userExpected, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - w := tape.GetWithClaims("/api/v1/me", 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + w := tape.GetWithClaims("/api/v1/me", 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - userActual := &userResponse{} - err = json.NewDecoder(w.Body).Decode(&userActual) - g.Assert(err).Equal(nil) + userActual := &userResponse{} + err = json.NewDecoder(w.Body).Decode(&userActual) + g.Assert(err).Equal(nil) - g.Assert(userActual.FirstName).Equal(userExpected.FirstName) - g.Assert(userActual.LastName).Equal(userExpected.LastName) - g.Assert(userActual.Email).Equal(userExpected.Email) - g.Assert(userActual.StudentNumber).Equal(userExpected.StudentNumber) - g.Assert(userActual.Semester).Equal(userExpected.Semester) - g.Assert(userActual.Subject).Equal(userExpected.Subject) - g.Assert(userActual.Language).Equal(userExpected.Language) - }) - - g.Xit("Should not perform self-updates when some data is missing", func() { - // This endpoint acts like a PATCH, since we need to start anyway from the - // database entry to avoid overriding "email". - // Theoretically (by definition of PUT) this endpoint must fail. - // But in practise, it is ever to act like PATCH here and pass also this request. - dataSent := H{ - "first_name": "blub", - } - - w := tape.PutWithClaims("/api/v1/me", dataSent, 1, true) - g.Assert(w.Code).Equal(http.StatusBadRequest) - }) + g.Assert(userActual.FirstName).Equal(userExpected.FirstName) + g.Assert(userActual.LastName).Equal(userExpected.LastName) + g.Assert(userActual.Email).Equal(userExpected.Email) + g.Assert(userActual.StudentNumber).Equal(userExpected.StudentNumber) + g.Assert(userActual.Semester).Equal(userExpected.Semester) + g.Assert(userActual.Subject).Equal(userExpected.Subject) + g.Assert(userActual.Language).Equal(userExpected.Language) + }) + + g.Xit("Should not perform self-updates when some data is missing", func() { + // This endpoint acts like a PATCH, since we need to start anyway from the + // database entry to avoid overriding "email". + // Theoretically (by definition of PUT) this endpoint must fail. + // But in practise, it is ever to act like PATCH here and pass also this request. + dataSent := H{ + "first_name": "blub", + } + + w := tape.PutWithClaims("/api/v1/me", dataSent, 1, true) + g.Assert(w.Code).Equal(http.StatusBadRequest) + }) - g.It("Should perform self-updates (excl email)", func() { - userBefore, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + g.It("Should perform self-updates (excl email)", func() { + userBefore, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - userDb, err := stores.User.Get(1) - g.Assert(err).Equal(nil) - - // this is NOT the /me enpoint, we can update the user here - - userSent := &userRequest{ - FirstName: "Info2_update", - LastName: "Lorem Ipsum_update", - Email: "new@mail.com", - Semester: 1, - - StudentNumber: userDb.StudentNumber, - Subject: userDb.Subject, - Language: userDb.Language, - } - - err = userSent.Validate() - g.Assert(err).Equal(nil) - - err = userSent.Validate() - g.Assert(err).Equal(nil) - - w := tape.PutWithClaims("/api/v1/me", helper.ToH(userSent), 1, true) - g.Assert(w.Code).Equal(http.StatusOK) + userDb, err := stores.User.Get(1) + g.Assert(err).Equal(nil) + + // this is NOT the /me enpoint, we can update the user here + + userSent := &userRequest{ + FirstName: "Info2_update", + LastName: "Lorem Ipsum_update", + Email: "new@mail.com", + Semester: 1, + + StudentNumber: userDb.StudentNumber, + Subject: userDb.Subject, + Language: userDb.Language, + } + + err = userSent.Validate() + g.Assert(err).Equal(nil) + + err = userSent.Validate() + g.Assert(err).Equal(nil) + + w := tape.PutWithClaims("/api/v1/me", helper.ToH(userSent), 1, true) + g.Assert(w.Code).Equal(http.StatusOK) - userAfter, err := stores.User.Get(1) - g.Assert(err).Equal(nil) + userAfter, err := stores.User.Get(1) + g.Assert(err).Equal(nil) - g.Assert(userAfter.FirstName).Equal(userSent.FirstName) - g.Assert(userAfter.LastName).Equal(userSent.LastName) - g.Assert(userAfter.Email).Equal(userBefore.Email) // should be really before - g.Assert(userAfter.StudentNumber).Equal(userSent.StudentNumber) - g.Assert(userAfter.Semester).Equal(userSent.Semester) - g.Assert(userAfter.Subject).Equal(userSent.Subject) - g.Assert(userAfter.Language).Equal(userSent.Language) - - }) - - g.AfterEach(func() { - tape.AfterEach() - }) - }) + g.Assert(userAfter.FirstName).Equal(userSent.FirstName) + g.Assert(userAfter.LastName).Equal(userSent.LastName) + g.Assert(userAfter.Email).Equal(userBefore.Email) // should be really before + g.Assert(userAfter.StudentNumber).Equal(userSent.StudentNumber) + g.Assert(userAfter.Semester).Equal(userSent.Semester) + g.Assert(userAfter.Subject).Equal(userSent.Subject) + g.Assert(userAfter.Language).Equal(userSent.Language) + + }) + + g.AfterEach(func() { + tape.AfterEach() + }) + }) } diff --git a/api/cronjob/submission_zipper.go b/api/cronjob/submission_zipper.go index 60fce2b..8efdca2 100644 --- a/api/cronjob/submission_zipper.go +++ b/api/cronjob/submission_zipper.go @@ -19,22 +19,22 @@ package cronjob import ( - "archive/zip" - "fmt" - "io" - "os" - - "github.com/cgtuebingen/infomark-backend/api/app" - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/email" - "github.com/jmoiron/sqlx" + "archive/zip" + "fmt" + "io" + "os" + + "github.com/cgtuebingen/infomark-backend/api/app" + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/email" + "github.com/jmoiron/sqlx" ) // SubmissionFileZipper links all ressource to zip submissions type SubmissionFileZipper struct { - Stores *app.Stores - DB *sqlx.DB - Directory string + Stores *app.Stores + DB *sqlx.DB + Directory string } // SELECT s.*, u.* FROM submissions s @@ -46,141 +46,141 @@ type SubmissionFileZipper struct { // StudentSubmission is a view from the database used to identify which // submissions should be included in the final zip file type StudentSubmission struct { - ID int64 `db:"id"` - StudentFirstName string `db:"first_name"` - StudentLastName string `db:"last_name"` + ID int64 `db:"id"` + StudentFirstName string `db:"first_name"` + StudentLastName string `db:"last_name"` } // FetchStudentSubmissions queries the database to gather all submissions for a given group and task func FetchStudentSubmissions(db *sqlx.DB, groupID int64, taskID int64) ([]StudentSubmission, error) { - p := []StudentSubmission{} - err := db.Select(&p, ` + p := []StudentSubmission{} + err := db.Select(&p, ` SELECT s.id, u.first_name, u.last_name FROM submissions s INNER JOIN user_group ug ON ug.user_id = s.user_id INNER JOIN users u ON u.id = s.user_id WHERE ug.group_id = $1 AND s.task_id = $2`, groupID, taskID) - return p, err + return p, err } // Run executes a job to zip all submissions for each group and task func (job *SubmissionFileZipper) Run() { - // for each sheet - // if ended - // touch generated/infomark-sheet{sheetID}.lock - // for each group - // touch generated/infomark-sheet{sheetID}-task{taskID}-group{groupID}-submissions.lock - // create generated/infomark-sheet{sheetID}-task{taskID}-group{groupID}-submissions.zip - // zip nested task - sheets, _ := job.Stores.Sheet.GetAll() + // for each sheet + // if ended + // touch generated/infomark-sheet{sheetID}.lock + // for each group + // touch generated/infomark-sheet{sheetID}-task{taskID}-group{groupID}-submissions.lock + // create generated/infomark-sheet{sheetID}-task{taskID}-group{groupID}-submissions.zip + // zip nested task + sheets, _ := job.Stores.Sheet.GetAll() - for _, sheet := range sheets { - if app.OverTime(sheet.DueAt) { - // fmt.Println("work on ", sheet.ID) - sheetLockPath := fmt.Sprintf("%s/infomark-sheet%d.lock", job.Directory, sheet.ID) + for _, sheet := range sheets { + if app.OverTime(sheet.DueAt) { + // fmt.Println("work on ", sheet.ID) + sheetLockPath := fmt.Sprintf("%s/infomark-sheet%d.lock", job.Directory, sheet.ID) - fmt.Printf("test lock file '%s'\n", sheetLockPath) + fmt.Printf("test lock file '%s'\n", sheetLockPath) - if !helper.FileExists(sheetLockPath) { - fmt.Println(" --> create", sheet.ID) - helper.FileTouch(sheetLockPath) + if !helper.FileExists(sheetLockPath) { + fmt.Println(" --> create", sheet.ID) + helper.FileTouch(sheetLockPath) - courseID := int64(0) - job.DB.Get(&courseID, "SELECT course_id FROM sheet_course WHERE sheet_id = $1;", sheet.ID) + courseID := int64(0) + job.DB.Get(&courseID, "SELECT course_id FROM sheet_course WHERE sheet_id = $1;", sheet.ID) - groups, _ := job.Stores.Group.GroupsOfCourse(courseID) - tasks, _ := job.Stores.Task.TasksOfSheet(sheet.ID) + groups, _ := job.Stores.Group.GroupsOfCourse(courseID) + tasks, _ := job.Stores.Task.TasksOfSheet(sheet.ID) - for _, task := range tasks { - fmt.Println(" work on task ", task.ID) + for _, task := range tasks { + fmt.Println(" work on task ", task.ID) - for _, group := range groups { - archivLockPath := fmt.Sprintf("%s/collection-course%d-sheet%d-task%d-group%d.lock", job.Directory, courseID, sheet.ID, task.ID, group.ID) - // archiv_zip_path := fmt.Sprintf("%s/infomark-course%d-sheet%d-task%d-group%d.zip", job.Directory, courseID, sheet.ID, task.ID, group.ID) + for _, group := range groups { + archivLockPath := fmt.Sprintf("%s/collection-course%d-sheet%d-task%d-group%d.lock", job.Directory, courseID, sheet.ID, task.ID, group.ID) + // archiv_zip_path := fmt.Sprintf("%s/infomark-course%d-sheet%d-task%d-group%d.zip", job.Directory, courseID, sheet.ID, task.ID, group.ID) - archivZip := helper.NewSubmissionsCollectionFileHandle(courseID, sheet.ID, task.ID, group.ID) + archivZip := helper.NewSubmissionsCollectionFileHandle(courseID, sheet.ID, task.ID, group.ID) - if !helper.FileExists(archivLockPath) && !archivZip.Exists() { + if !helper.FileExists(archivLockPath) && !archivZip.Exists() { - // we gonna zip all submissions from students in group x for task y - helper.FileTouch(archivLockPath) + // we gonna zip all submissions from students in group x for task y + helper.FileTouch(archivLockPath) - submissions, _ := FetchStudentSubmissions(job.DB, group.ID, task.ID) + submissions, _ := FetchStudentSubmissions(job.DB, group.ID, task.ID) - newZipFile, err := os.Create(archivZip.Path()) - if err != nil { - return - } - defer newZipFile.Close() + newZipFile, err := os.Create(archivZip.Path()) + if err != nil { + return + } + defer newZipFile.Close() - zipWriter := zip.NewWriter(newZipFile) - defer zipWriter.Close() + zipWriter := zip.NewWriter(newZipFile) + defer zipWriter.Close() - for _, submission := range submissions { - // fmt.Println(submission) + for _, submission := range submissions { + // fmt.Println(submission) - submissionHnd := helper.NewSubmissionFileHandle(submission.ID) + submissionHnd := helper.NewSubmissionFileHandle(submission.ID) - // student did upload a zip file - if submissionHnd.Exists() { - // see https://stackoverflow.com/a/53802396/7443104 - // fmt.Println("add sbmission ", submission.ID, " to ", archivZip) + // student did upload a zip file + if submissionHnd.Exists() { + // see https://stackoverflow.com/a/53802396/7443104 + // fmt.Println("add sbmission ", submission.ID, " to ", archivZip) - // refer to the zip file - zipfile, err := os.Open(submissionHnd.Path()) - if err != nil { - return - } - defer zipfile.Close() + // refer to the zip file + zipfile, err := os.Open(submissionHnd.Path()) + if err != nil { + return + } + defer zipfile.Close() - // Get the file information - info, err := zipfile.Stat() - if err != nil { - return - } + // Get the file information + info, err := zipfile.Stat() + if err != nil { + return + } - header, err := zip.FileInfoHeader(info) - if err != nil { - return - } + header, err := zip.FileInfoHeader(info) + if err != nil { + return + } - // Using FileInfoHeader() above only uses the basename of the file. If we want - // to preserve the folder structure we can overwrite this with the full path. - header.Name = fmt.Sprintf("%s-%s.zip", submission.StudentLastName, submission.StudentFirstName) + // Using FileInfoHeader() above only uses the basename of the file. If we want + // to preserve the folder structure we can overwrite this with the full path. + header.Name = fmt.Sprintf("%s-%s.zip", submission.StudentLastName, submission.StudentFirstName) - // Change to deflate to gain better compression - // see http://golang.org/pkg/archive/zip/#pkg-constants - header.Method = zip.Deflate + // Change to deflate to gain better compression + // see http://golang.org/pkg/archive/zip/#pkg-constants + header.Method = zip.Deflate - writer, err := zipWriter.CreateHeader(header) - if err != nil { - return - } - if _, err = io.Copy(writer, zipfile); err != nil { - return - } - } + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return + } + if _, err = io.Copy(writer, zipfile); err != nil { + return + } + } - } + } - // notify the tutor - tutor, _ := job.Stores.User.Get(group.TutorID) + // notify the tutor + tutor, _ := job.Stores.User.Get(group.TutorID) - email.DefaultMail.Send(email.NewEmail(tutor.Email, "Submission-Zip", fmt.Sprintf(`Hi %s, + email.DefaultMail.Send(email.NewEmail(tutor.Email, "Submission-Zip", fmt.Sprintf(`Hi %s, the deadline for the exercise sheet '%s' is over. We have collected all submissions in a single zip file. Please log in to grade these solutions. `, tutor.FullName(), sheet.Name))) - } - } + } + } - } - } else { - fmt.Println(" --> already done", sheet.ID) - } + } + } else { + fmt.Println(" --> already done", sheet.ID) + } - } else { - // fmt.Println("ok", sheet.ID) - } + } else { + // fmt.Println("ok", sheet.ID) + } - } + } } diff --git a/api/helper/file_carrier.go b/api/helper/file_carrier.go index 210e392..8ffcd51 100644 --- a/api/helper/file_carrier.go +++ b/api/helper/file_carrier.go @@ -19,18 +19,18 @@ package helper import ( - "crypto/sha256" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "os" - pathpkg "path" - "strconv" - "strings" - - "github.com/spf13/viper" + "crypto/sha256" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + pathpkg "path" + "strconv" + "strings" + + "github.com/spf13/viper" ) // FaileCarrier is a unified way to handle uploads and downloads of different @@ -41,196 +41,196 @@ type FileCategory int32 // all categories const ( - AvatarCategory FileCategory = 0 - SheetCategory FileCategory = 1 - PublicTestCategory FileCategory = 2 - PrivateTestCategory FileCategory = 3 - MaterialCategory FileCategory = 4 - SubmissionCategory FileCategory = 5 - SubmissionsCollectionCategory FileCategory = 6 + AvatarCategory FileCategory = 0 + SheetCategory FileCategory = 1 + PublicTestCategory FileCategory = 2 + PrivateTestCategory FileCategory = 3 + MaterialCategory FileCategory = 4 + SubmissionCategory FileCategory = 5 + SubmissionsCollectionCategory FileCategory = 6 ) // FileManager contains all operations we need to handle files // within HTTP type FileManager interface { - WriteToBody(w http.ResponseWriter) error - WriteToDisk(req multipart.File) error - GetContentType() (string, error) - Path(fallback bool) bool - Delete() error - Exists() bool + WriteToBody(w http.ResponseWriter) error + WriteToDisk(req multipart.File) error + GetContentType() (string, error) + Path(fallback bool) bool + Delete() error + Exists() bool } // FileHandle represents all information for file being uploaded or downloaded. type FileHandle struct { - Category FileCategory - ID int64 // an unique identifier (e.g. from database) - Extensions []string // - MaxBytes int64 // 0 means no limit - Infos []int64 + Category FileCategory + ID int64 // an unique identifier (e.g. from database) + Extensions []string // + MaxBytes int64 // 0 means no limit + Infos []int64 } // NewAvatarFileHandle will handle user avatars. We support jpg only. func NewAvatarFileHandle(userID int64) *FileHandle { - return &FileHandle{ - Category: AvatarCategory, - ID: userID, - Extensions: []string{"jpg", "jpeg", "png"}, - MaxBytes: viper.GetInt64("max_request_avatar_bytes"), - } + return &FileHandle{ + Category: AvatarCategory, + ID: userID, + Extensions: []string{"jpg", "jpeg", "png"}, + MaxBytes: viper.GetInt64("max_request_avatar_bytes"), + } } // NewSheetFileHandle will handle exercise sheets (zip files). func NewSheetFileHandle(ID int64) *FileHandle { - return &FileHandle{ - Category: SheetCategory, - ID: ID, - Extensions: []string{"zip"}, - MaxBytes: 0, - } + return &FileHandle{ + Category: SheetCategory, + ID: ID, + Extensions: []string{"zip"}, + MaxBytes: 0, + } } // NewPublicTestFileHandle will handle the testing framework for // public unit tests (zip files). func NewPublicTestFileHandle(ID int64) *FileHandle { - return &FileHandle{ - Category: PublicTestCategory, - ID: ID, - Extensions: []string{"zip"}, - MaxBytes: 0, - } + return &FileHandle{ + Category: PublicTestCategory, + ID: ID, + Extensions: []string{"zip"}, + MaxBytes: 0, + } } // NewPrivateTestFileHandle will handle the testing framework for // private unit tests (zip files). func NewPrivateTestFileHandle(ID int64) *FileHandle { - return &FileHandle{ - Category: PrivateTestCategory, - ID: ID, - Extensions: []string{"zip"}, - MaxBytes: 0, - } + return &FileHandle{ + Category: PrivateTestCategory, + ID: ID, + Extensions: []string{"zip"}, + MaxBytes: 0, + } } // NewMaterialFileHandle will handle course slides or extra material (zip files). func NewMaterialFileHandle(ID int64) *FileHandle { - return &FileHandle{ - Category: MaterialCategory, - ID: ID, - Extensions: []string{"zip", "pdf"}, - MaxBytes: 0, - } + return &FileHandle{ + Category: MaterialCategory, + ID: ID, + Extensions: []string{"zip", "pdf"}, + MaxBytes: 0, + } } // NewSubmissionFileHandle will handle homework/exercise submissions (zip files). func NewSubmissionFileHandle(ID int64) *FileHandle { - return &FileHandle{ - Category: SubmissionCategory, - ID: ID, - Extensions: []string{"zip"}, - MaxBytes: viper.GetInt64("max_request_submission_bytes"), - } + return &FileHandle{ + Category: SubmissionCategory, + ID: ID, + Extensions: []string{"zip"}, + MaxBytes: viper.GetInt64("max_request_submission_bytes"), + } } // NewSubmissionsCollectionFileHandle will handle a collection of submissions. func NewSubmissionsCollectionFileHandle(courseID int64, sheetID int64, taskID int64, groupID int64) *FileHandle { - return &FileHandle{ - Category: SubmissionsCollectionCategory, - ID: 0, - Extensions: []string{"zip"}, - MaxBytes: 0, - Infos: []int64{courseID, sheetID, taskID, groupID}, - } + return &FileHandle{ + Category: SubmissionsCollectionCategory, + ID: 0, + Extensions: []string{"zip"}, + MaxBytes: 0, + Infos: []int64{courseID, sheetID, taskID, groupID}, + } } // Sha256 computes the checksum and return it as a string func (f *FileHandle) Sha256() (string, error) { - hnd, err := os.Open(f.Path()) - if err != nil { - return "", err - } - defer hnd.Close() + hnd, err := os.Open(f.Path()) + if err != nil { + return "", err + } + defer hnd.Close() - h := sha256.New() - if _, err := io.Copy(h, hnd); err != nil { - return "", err - } + h := sha256.New() + if _, err := io.Copy(h, hnd); err != nil { + return "", err + } - return fmt.Sprintf("%x", h.Sum(nil)), nil + return fmt.Sprintf("%x", h.Sum(nil)), nil } // Path return the path to a file using the config func (f *FileHandle) Path() string { - switch f.Category { - case AvatarCategory: - - for _, ext := range f.Extensions { - path := fmt.Sprintf("%s/avatars/%d.%s", viper.GetString("uploads_dir"), f.ID, ext) - if FileExists(path) { - return path - } - } - return "" - - case SheetCategory: - return fmt.Sprintf("%s/sheets/%d.zip", viper.GetString("uploads_dir"), f.ID) - - case PublicTestCategory: - return fmt.Sprintf("%s/tasks/%d-public.zip", viper.GetString("uploads_dir"), f.ID) - - case PrivateTestCategory: - return fmt.Sprintf("%s/tasks/%d-private.zip", viper.GetString("uploads_dir"), f.ID) - - case MaterialCategory: - - for _, ext := range f.Extensions { - path := fmt.Sprintf("%s/materials/%d.%s", viper.GetString("uploads_dir"), f.ID, ext) - if FileExists(path) { - return path - } - } - return "" - - case SubmissionCategory: - return fmt.Sprintf("%s/submissions/%d.zip", viper.GetString("uploads_dir"), f.ID) - case SubmissionsCollectionCategory: - return fmt.Sprintf("%s/collection-course%d-sheet%d-task%d-group%d.zip", - viper.GetString("generated_files_dir"), f.Infos[0], f.Infos[1], f.Infos[2], f.Infos[3]) - } - return "" + switch f.Category { + case AvatarCategory: + + for _, ext := range f.Extensions { + path := fmt.Sprintf("%s/avatars/%d.%s", viper.GetString("uploads_dir"), f.ID, ext) + if FileExists(path) { + return path + } + } + return "" + + case SheetCategory: + return fmt.Sprintf("%s/sheets/%d.zip", viper.GetString("uploads_dir"), f.ID) + + case PublicTestCategory: + return fmt.Sprintf("%s/tasks/%d-public.zip", viper.GetString("uploads_dir"), f.ID) + + case PrivateTestCategory: + return fmt.Sprintf("%s/tasks/%d-private.zip", viper.GetString("uploads_dir"), f.ID) + + case MaterialCategory: + + for _, ext := range f.Extensions { + path := fmt.Sprintf("%s/materials/%d.%s", viper.GetString("uploads_dir"), f.ID, ext) + if FileExists(path) { + return path + } + } + return "" + + case SubmissionCategory: + return fmt.Sprintf("%s/submissions/%d.zip", viper.GetString("uploads_dir"), f.ID) + case SubmissionsCollectionCategory: + return fmt.Sprintf("%s/collection-course%d-sheet%d-task%d-group%d.zip", + viper.GetString("generated_files_dir"), f.Infos[0], f.Infos[1], f.Infos[2], f.Infos[3]) + } + return "" } // FileExists checks if a file really exists. func FileExists(path string) bool { - if _, err := os.Stat(path); os.IsNotExist(err) { - return false - } + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } - return true + return true } // FileTouch creates an empty file func FileTouch(path string) error { - emptyFile, err := os.Create(path) - defer emptyFile.Close() - return err + emptyFile, err := os.Create(path) + defer emptyFile.Close() + return err } // FileDelete deletes an file func FileDelete(path string) error { - return os.Remove(path) + return os.Remove(path) } // Exists checks if a file really exists. func (f *FileHandle) Exists() bool { - return FileExists(f.Path()) + return FileExists(f.Path()) } // Delete deletes a file from disk. func (f *FileHandle) Delete() error { - return os.Remove(f.Path()) + return os.Remove(f.Path()) } // GetContentType tries to predict the content type without reading the entire @@ -238,25 +238,25 @@ func (f *FileHandle) Delete() error { // between zip and octstream. func (f *FileHandle) GetContentType() (string, error) { - // Only the first 512 bytes are used to sniff the content type. - buffer := make([]byte, 512) + // Only the first 512 bytes are used to sniff the content type. + buffer := make([]byte, 512) - file, err := os.Open(f.Path()) - if err != nil { - return "", err - } - defer file.Close() + file, err := os.Open(f.Path()) + if err != nil { + return "", err + } + defer file.Close() - _, err = file.Read(buffer) - if err != nil { - return "", err - } + _, err = file.Read(buffer) + if err != nil { + return "", err + } - // Use the net/http package's handy DectectContentType function. Always returns a valid - // content-type by returning "application/octet-stream" if no others seemed to match. - contentType := http.DetectContentType(buffer) + // Use the net/http package's handy DectectContentType function. Always returns a valid + // content-type by returning "application/octet-stream" if no others seemed to match. + contentType := http.DetectContentType(buffer) - return contentType, nil + return contentType, nil } // DummyWriter is a writer which does nothing (use when writing to disk) @@ -264,12 +264,12 @@ type DummyWriter struct{} // Header returns empty header func (h DummyWriter) Header() http.Header { - return make(map[string][]string) + return make(map[string][]string) } // Write does nothing func (h DummyWriter) Write([]byte) (int, error) { - return 0, nil + return 0, nil } // WriteHeader does nothing @@ -278,174 +278,174 @@ func (h DummyWriter) WriteHeader(statusCode int) {} // WriteToBody will write a file from disk to the http reponse (download process) func (f *FileHandle) WriteToBody(w http.ResponseWriter) error { - // check if file exists - file, err := os.Open(f.Path()) - if err != nil { - return err - } - defer file.Close() + // check if file exists + file, err := os.Open(f.Path()) + if err != nil { + return err + } + defer file.Close() - pathSplit := strings.Split(f.Path(), "/") - publicFilename := fmt.Sprintf("%s-%s", pathSplit[len(pathSplit)-2], pathSplit[len(pathSplit)-1]) + pathSplit := strings.Split(f.Path(), "/") + publicFilename := fmt.Sprintf("%s-%s", pathSplit[len(pathSplit)-2], pathSplit[len(pathSplit)-1]) - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"infomark-%s\"", publicFilename)) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"infomark-%s\"", publicFilename)) - // prepare header - fileType, err := f.GetContentType() - if err != nil { - return err - } - w.Header().Set("Content-Type", fileType) + // prepare header + fileType, err := f.GetContentType() + if err != nil { + return err + } + w.Header().Set("Content-Type", fileType) - // return file - _, err = io.Copy(w, file) - if err != nil { - return err - } + // return file + _, err = io.Copy(w, file) + if err != nil { + return err + } - return nil + return nil } // WriteToBodyWithName reads a file from disk a writes it in the HTTP response (download) func (f *FileHandle) WriteToBodyWithName(publicFilename string, w http.ResponseWriter) error { - // check if file exists - file, err := os.Open(f.Path()) - if err != nil { - return err - } - defer file.Close() - - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", publicFilename)) - - // prepare header - fileType, err := f.GetContentType() - if err != nil { - return err - } - w.Header().Set("Content-Type", fileType) - - // return file - _, err = io.Copy(w, file) - if err != nil { - return err - } - - return nil + // check if file exists + file, err := os.Open(f.Path()) + if err != nil { + return err + } + defer file.Close() + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", publicFilename)) + + // prepare header + fileType, err := f.GetContentType() + if err != nil { + return err + } + w.Header().Set("Content-Type", fileType) + + // return file + _, err = io.Copy(w, file) + if err != nil { + return err + } + + return nil } // IsZipFile checks if file is zip file based on magic number func IsZipFile(buf []byte) bool { - return len(buf) > 3 && - buf[0] == 0x50 && buf[1] == 0x4B && - (buf[2] == 0x3 || buf[2] == 0x5 || buf[2] == 0x7) && - (buf[3] == 0x4 || buf[3] == 0x6 || buf[3] == 0x8) + return len(buf) > 3 && + buf[0] == 0x50 && buf[1] == 0x4B && + (buf[2] == 0x3 || buf[2] == 0x5 || buf[2] == 0x7) && + (buf[3] == 0x4 || buf[3] == 0x6 || buf[3] == 0x8) } // IsPdfFile checks if file is pdf file based on magic number func IsPdfFile(buf []byte) bool { - return len(buf) > 3 && - buf[0] == 0x25 && buf[1] == 0x50 && - buf[2] == 0x44 && buf[3] == 0x46 + return len(buf) > 3 && + buf[0] == 0x25 && buf[1] == 0x50 && + buf[2] == 0x44 && buf[3] == 0x46 } // IsJpegFile checks if file is jpg file based on magic number func IsJpegFile(buf []byte) bool { - return len(buf) > 2 && - buf[0] == 0xFF && - buf[1] == 0xD8 && - buf[2] == 0xFF + return len(buf) > 2 && + buf[0] == 0xFF && + buf[1] == 0xD8 && + buf[2] == 0xFF } // IsPngFile checks if file is png file based on magic number func IsPngFile(buf []byte) bool { - return len(buf) > 3 && - buf[0] == 0x89 && buf[1] == 0x50 && - buf[2] == 0x4E && buf[3] == 0x47 + return len(buf) > 3 && + buf[0] == 0x89 && buf[1] == 0x50 && + buf[2] == 0x4E && buf[3] == 0x47 } // WriteToDisk will save uploads from a http request to the directory specified // in the config. func (f *FileHandle) WriteToDisk(r *http.Request, fieldName string) (string, error) { - w := DummyWriter{} - - if f.MaxBytes != 0 { - r.Body = http.MaxBytesReader(w, r.Body, f.MaxBytes) - } - - // receive data from post request - if err := r.ParseMultipartForm(32 << 20); err != nil { - return "", err - } - - // we are interested in the field "file_data" - file, handler, err := r.FormFile(fieldName) - if err != nil { - return "", err - } - defer file.Close() - - path := f.Path() - - // Extract magic number from file - fileMagic := make([]byte, 4) - if n, err := file.Read(fileMagic); err != nil || n != 4 { - return "", errors.New("Unable to extract 4 Bytes for magic number determination") - } - if n, err := file.Seek(0, io.SeekStart); n != 0 || err != nil { - return "", errors.New("Fail to seek to beginning of file") - } - - switch f.Category { - case AvatarCategory: - pathToDelete := fmt.Sprintf("%s/avatars/%s.png", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - FileDelete(pathToDelete) - pathToDelete = fmt.Sprintf("%s/avatars/%s.jpg", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - FileDelete(pathToDelete) - if IsJpegFile(fileMagic) { - path = fmt.Sprintf("%s/avatars/%s.jpg", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - } else if IsPngFile(fileMagic) { - path = fmt.Sprintf("%s/avatars/%s.png", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - } else { - return "", errors.New("We support JPG/JPEG/PNG files only") - } - - case SheetCategory, - PublicTestCategory, - PrivateTestCategory, - SubmissionCategory: - if !IsZipFile(fileMagic) { - return "", errors.New("We support ZIP files only. But the given file is no Zip file") - } - case MaterialCategory: - // delete both possible files - // ids are unique. Hence we only delete the file associated with the id - pathToDelete := fmt.Sprintf("%s/materials/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - FileDelete(pathToDelete) - pathToDelete = fmt.Sprintf("%s/materials/%s.pdf", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - FileDelete(pathToDelete) - - if IsPdfFile(fileMagic) { - path = fmt.Sprintf("%s/materials/%s.pdf", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - } else if IsZipFile(fileMagic) { - path = fmt.Sprintf("%s/materials/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) - } else { - return "", errors.New("Only PDF and ZIP files are allowed") - } - } - - // delete path - FileDelete(path) - // try to open new file - hnd, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return "", err - } - defer hnd.Close() - - // copy file from request - _, err = io.Copy(hnd, file) - return pathpkg.Base(handler.Filename), err + w := DummyWriter{} + + if f.MaxBytes != 0 { + r.Body = http.MaxBytesReader(w, r.Body, f.MaxBytes) + } + + // receive data from post request + if err := r.ParseMultipartForm(32 << 20); err != nil { + return "", err + } + + // we are interested in the field "file_data" + file, handler, err := r.FormFile(fieldName) + if err != nil { + return "", err + } + defer file.Close() + + path := f.Path() + + // Extract magic number from file + fileMagic := make([]byte, 4) + if n, err := file.Read(fileMagic); err != nil || n != 4 { + return "", errors.New("Unable to extract 4 Bytes for magic number determination") + } + if n, err := file.Seek(0, io.SeekStart); n != 0 || err != nil { + return "", errors.New("Fail to seek to beginning of file") + } + + switch f.Category { + case AvatarCategory: + pathToDelete := fmt.Sprintf("%s/avatars/%s.png", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + FileDelete(pathToDelete) + pathToDelete = fmt.Sprintf("%s/avatars/%s.jpg", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + FileDelete(pathToDelete) + if IsJpegFile(fileMagic) { + path = fmt.Sprintf("%s/avatars/%s.jpg", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + } else if IsPngFile(fileMagic) { + path = fmt.Sprintf("%s/avatars/%s.png", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + } else { + return "", errors.New("We support JPG/JPEG/PNG files only") + } + + case SheetCategory, + PublicTestCategory, + PrivateTestCategory, + SubmissionCategory: + if !IsZipFile(fileMagic) { + return "", errors.New("We support ZIP files only. But the given file is no Zip file") + } + case MaterialCategory: + // delete both possible files + // ids are unique. Hence we only delete the file associated with the id + pathToDelete := fmt.Sprintf("%s/materials/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + FileDelete(pathToDelete) + pathToDelete = fmt.Sprintf("%s/materials/%s.pdf", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + FileDelete(pathToDelete) + + if IsPdfFile(fileMagic) { + path = fmt.Sprintf("%s/materials/%s.pdf", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + } else if IsZipFile(fileMagic) { + path = fmt.Sprintf("%s/materials/%s.zip", viper.GetString("uploads_dir"), strconv.FormatInt(f.ID, 10)) + } else { + return "", errors.New("Only PDF and ZIP files are allowed") + } + } + + // delete path + FileDelete(path) + // try to open new file + hnd, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return "", err + } + defer hnd.Close() + + // copy file from request + _, err = io.Copy(hnd, file) + return pathpkg.Base(handler.Filename), err } diff --git a/api/shared/shared_structs.go b/api/shared/shared_structs.go index f69908a..2e00ce4 100644 --- a/api/shared/shared_structs.go +++ b/api/shared/shared_structs.go @@ -19,49 +19,49 @@ package shared import ( - "fmt" + "fmt" ) // SubmissionAMQPWorkerRequest is the message which is handed over to the background workers type SubmissionAMQPWorkerRequest struct { - SubmissionID int64 `json:"submission_id"` - AccessToken string `json:"access_token"` - FrameworkFileURL string `json:"framework_file_url"` - SubmissionFileURL string `json:"submission_file_url"` - ResultEndpointURL string `json:"result_endpoint_url"` - DockerImage string `json:"docker_image"` - Sha256 string `json:"sha_256"` + SubmissionID int64 `json:"submission_id"` + AccessToken string `json:"access_token"` + FrameworkFileURL string `json:"framework_file_url"` + SubmissionFileURL string `json:"submission_file_url"` + ResultEndpointURL string `json:"result_endpoint_url"` + DockerImage string `json:"docker_image"` + Sha256 string `json:"sha_256"` } // SubmissionWorkerResponse is the message handed from the workers to the server type SubmissionWorkerResponse struct { - Log string `json:"log"` - Status int `json:"status"` + Log string `json:"log"` + Status int `json:"status"` } // NewSubmissionAMQPWorkerRequest creates a new message for the workers func NewSubmissionAMQPWorkerRequest( - courseID int64, taskID int64, submissionID int64, gradeID int64, - accessToken string, url string, dockerimage string, sha256 string, visibility string) *SubmissionAMQPWorkerRequest { + courseID int64, taskID int64, submissionID int64, gradeID int64, + accessToken string, url string, dockerimage string, sha256 string, visibility string) *SubmissionAMQPWorkerRequest { - return &SubmissionAMQPWorkerRequest{ - SubmissionID: submissionID, - AccessToken: accessToken, - FrameworkFileURL: fmt.Sprintf("%s/api/v1/courses/%d/tasks/%d/%s_file", - url, - courseID, - taskID, - visibility), - SubmissionFileURL: fmt.Sprintf("%s/api/v1/courses/%d/submissions/%d/file", - url, - courseID, - submissionID), - ResultEndpointURL: fmt.Sprintf("%s/api/v1/courses/%d/grades/%d/%s_result", - url, - courseID, - gradeID, - visibility), - DockerImage: dockerimage, - Sha256: sha256, - } + return &SubmissionAMQPWorkerRequest{ + SubmissionID: submissionID, + AccessToken: accessToken, + FrameworkFileURL: fmt.Sprintf("%s/api/v1/courses/%d/tasks/%d/%s_file", + url, + courseID, + taskID, + visibility), + SubmissionFileURL: fmt.Sprintf("%s/api/v1/courses/%d/submissions/%d/file", + url, + courseID, + submissionID), + ResultEndpointURL: fmt.Sprintf("%s/api/v1/courses/%d/grades/%d/%s_result", + url, + courseID, + gradeID, + visibility), + DockerImage: dockerimage, + Sha256: sha256, + } } diff --git a/api/worker/submission_handler.go b/api/worker/submission_handler.go index 5b776b2..3b08659 100644 --- a/api/worker/submission_handler.go +++ b/api/worker/submission_handler.go @@ -19,27 +19,27 @@ package background import ( - "crypto/sha256" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/api/shared" - "github.com/cgtuebingen/infomark-backend/service" - "github.com/cgtuebingen/infomark-backend/tape" - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/api/shared" + "github.com/cgtuebingen/infomark-backend/service" + "github.com/cgtuebingen/infomark-backend/tape" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) // SubmissionHandler is any handler capable to work on submissions type SubmissionHandler interface { - Handle(body []byte) error + Handle(body []byte) error } // DummySubmissionHandler is doing nothing (for testing) @@ -55,258 +55,258 @@ var DefaultSubmissionHandler SubmissionHandler var DefaultLogger *logrus.Logger func init() { - // fmt.Println(viper.GetString("rabbitmq_connection")) - // fmt.Println("worker_void", viper.GetBool("worker_void")) - if viper.GetBool("worker_void") { - DefaultSubmissionHandler = &DummySubmissionHandler{} - } else { - DefaultSubmissionHandler = &RealSubmissionHandler{} - } - - DefaultLogger = logrus.StandardLogger() - file, err := os.OpenFile("submission_handler.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err == nil { - DefaultLogger.Out = file - } else { - fmt.Println(err) - DefaultLogger.Info("Failed to log to file, using default stderr") - } - // defer file.Close() + // fmt.Println(viper.GetString("rabbitmq_connection")) + // fmt.Println("worker_void", viper.GetBool("worker_void")) + if viper.GetBool("worker_void") { + DefaultSubmissionHandler = &DummySubmissionHandler{} + } else { + DefaultSubmissionHandler = &RealSubmissionHandler{} + } + + DefaultLogger = logrus.StandardLogger() + file, err := os.OpenFile("submission_handler.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err == nil { + DefaultLogger.Out = file + } else { + fmt.Println(err) + DefaultLogger.Info("Failed to log to file, using default stderr") + } + // defer file.Close() } // Handle reads message and does nothing func (h *DummySubmissionHandler) Handle(workerBody []byte) error { - // decode incoming message from AMQP - msg := &shared.SubmissionAMQPWorkerRequest{} - err := json.Unmarshal(workerBody, msg) - - if err != nil { - DefaultLogger.WithFields(logrus.Fields{ - "SubmissionID": msg.SubmissionID, - "AccessToken": msg.AccessToken, - "SubmissionFileURL": msg.SubmissionFileURL, - "FrameworkFileURL": msg.FrameworkFileURL, - "ResultEndpointURL": msg.ResultEndpointURL, - "Sha256": msg.Sha256, - }).Warn(err) - return err - } - - fmt.Println("--> void") - - return nil + // decode incoming message from AMQP + msg := &shared.SubmissionAMQPWorkerRequest{} + err := json.Unmarshal(workerBody, msg) + + if err != nil { + DefaultLogger.WithFields(logrus.Fields{ + "SubmissionID": msg.SubmissionID, + "AccessToken": msg.AccessToken, + "SubmissionFileURL": msg.SubmissionFileURL, + "FrameworkFileURL": msg.FrameworkFileURL, + "ResultEndpointURL": msg.ResultEndpointURL, + "Sha256": msg.Sha256, + }).Warn(err) + return err + } + + fmt.Println("--> void") + + return nil } func verifySha256(filePath string, expectedChecksum string) error { - f, err := os.Open(filePath) - if err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - defer f.Close() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - - actualChecksum := fmt.Sprintf("%x", h.Sum(nil)) - - if actualChecksum != expectedChecksum { - return fmt.Errorf("Sha256 missmatch, actual %s vs. expected %s for file %s", - actualChecksum, - expectedChecksum, - filePath, - ) - } - - return nil + f, err := os.Open(filePath) + if err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + + actualChecksum := fmt.Sprintf("%x", h.Sum(nil)) + + if actualChecksum != expectedChecksum { + return fmt.Errorf("Sha256 missmatch, actual %s vs. expected %s for file %s", + actualChecksum, + expectedChecksum, + filePath, + ) + } + + return nil } func newHTTPClientSingleRequest() *http.Client { - return &http.Client{ - Transport: &http.Transport{ - TLSHandshakeTimeout: 30 * time.Second, - DisableKeepAlives: true, - }, - Timeout: 1 * time.Minute, - } + return &http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: 30 * time.Second, + DisableKeepAlives: true, + }, + Timeout: 1 * time.Minute, + } } func downloadFile(r *http.Request, dst string) error { - client := newHTTPClientSingleRequest() - - w, err := client.Do(r) - if err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - defer w.Body.Close() - - out, err := os.Create(dst) - if err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - defer out.Close() - - _, err = io.Copy(out, w.Body) - return err + client := newHTTPClientSingleRequest() + + w, err := client.Do(r) + if err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + defer w.Body.Close() + + out, err := os.Create(dst) + if err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + defer out.Close() + + _, err = io.Copy(out, w.Body) + return err } func cleanDockerOutput(stdout string) string { - logStart := "--- BEGIN --- INFOMARK -- WORKER" - logEnd := "--- END --- INFOMARK -- WORKER" + logStart := "--- BEGIN --- INFOMARK -- WORKER" + logEnd := "--- END --- INFOMARK -- WORKER" - rsl := strings.Split(stdout, logStart) + rsl := strings.Split(stdout, logStart) - if len(rsl) > 1 { - rsl = strings.Split(rsl[1], logEnd) - return rsl[0] + if len(rsl) > 1 { + rsl = strings.Split(rsl[1], logEnd) + return rsl[0] - } - return stdout + } + return stdout } // Handle reads message and test submission using docker func (h *RealSubmissionHandler) Handle(body []byte) error { - // HandleSubmission is responsible to - // 1. parse request - msg := &shared.SubmissionAMQPWorkerRequest{} - err := json.Unmarshal(body, msg) - if err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - - // DefaultLogger.WithFields(logrus.Fields{ - // "SubmissionID": msg.SubmissionID, - // "AccessToken": msg.AccessToken, - // "SubmissionFileURL": msg.SubmissionFileURL, - // "FrameworkFileURL": msg.FrameworkFileURL, - // "ResultEndpointURL": msg.ResultEndpointURL, - // "Sha256": msg.Sha256, - // }).Info("handle") - - uuid, err := uuid.NewRandom() - if err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - submissionPath := fmt.Sprintf("%s/%s-submission.zip", viper.GetString("worker_workdir"), uuid) - frameworkPath := fmt.Sprintf("%s/%s-framework.zip", viper.GetString("worker_workdir"), uuid) - - // 2. fetch submission file from server - r, err := http.NewRequest("GET", msg.SubmissionFileURL, nil) - r.Header.Add("Authorization", "Bearer "+msg.AccessToken) - if err := downloadFile(r, submissionPath); err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - - defer helper.FileDelete(submissionPath) - - // 3. fetch framework file from server - r, err = http.NewRequest("GET", msg.FrameworkFileURL, nil) - r.Header.Add("Authorization", "Bearer "+msg.AccessToken) - if err := downloadFile(r, frameworkPath); err != nil { - DefaultLogger.Printf("error: %v\n", err) - return err - } - defer helper.FileDelete(frameworkPath) - - // Under circumstances there is no guarantee that the following request will be issues - // BEFORE the actual test result. - // we use a HTTP Request to send the answer - // r = tape.BuildDataRequest("POST", msg.ResultEndpointURL, tape.ToH(&shared.SubmissionWorkerResponse{ - // Log: "submission is currently being tested ...", - // Status: 1, - // })) - // r.Header.Add("Authorization", "Bearer "+msg.AccessToken) - // resp, err := client.Do(r) - // if err != nil { - // DefaultLogger.WithFields(logrus.Fields{ - // "action": "send result to backend", - // "SubmissionID": msg.SubmissionID, - // "resp": resp, - // }).Warn(err) - - // return err - // } - - // 4. verify checksums to avoid race conditions - if err := verifySha256(submissionPath, msg.Sha256); err != nil { - DefaultLogger.WithFields(logrus.Fields{ - "SubmissionID": msg.SubmissionID, - "SubmissionFileURL": msg.SubmissionFileURL, - "FrameworkFileURL": msg.FrameworkFileURL, - "Sha256": msg.Sha256, - }).Warn(err) - return err - } - - // 5. run docker test - ds := service.NewDockerService() - defer ds.Client.Close() - - var exit int64 - var stdout string - - var workerResp *shared.SubmissionWorkerResponse - - stdout, exit, err = ds.Run( - msg.DockerImage, - submissionPath, - frameworkPath, - viper.GetInt64("worker_docker_memory_bytes"), - ) - if err != nil { - DefaultLogger.WithFields(logrus.Fields{ - "SubmissionID": msg.SubmissionID, - "stdout": stdout, - "exitcode": exit, - }).Warn(err) - return err - } - - if exit == 0 { - stdout = cleanDockerOutput(stdout) - // 3. push result back to server - workerResp = &shared.SubmissionWorkerResponse{ - Log: stdout, - Status: int(exit), - } - } else { - workerResp = &shared.SubmissionWorkerResponse{ - Log: fmt.Sprintf(` + // HandleSubmission is responsible to + // 1. parse request + msg := &shared.SubmissionAMQPWorkerRequest{} + err := json.Unmarshal(body, msg) + if err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + + // DefaultLogger.WithFields(logrus.Fields{ + // "SubmissionID": msg.SubmissionID, + // "AccessToken": msg.AccessToken, + // "SubmissionFileURL": msg.SubmissionFileURL, + // "FrameworkFileURL": msg.FrameworkFileURL, + // "ResultEndpointURL": msg.ResultEndpointURL, + // "Sha256": msg.Sha256, + // }).Info("handle") + + uuid, err := uuid.NewRandom() + if err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + submissionPath := fmt.Sprintf("%s/%s-submission.zip", viper.GetString("worker_workdir"), uuid) + frameworkPath := fmt.Sprintf("%s/%s-framework.zip", viper.GetString("worker_workdir"), uuid) + + // 2. fetch submission file from server + r, err := http.NewRequest("GET", msg.SubmissionFileURL, nil) + r.Header.Add("Authorization", "Bearer "+msg.AccessToken) + if err := downloadFile(r, submissionPath); err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + + defer helper.FileDelete(submissionPath) + + // 3. fetch framework file from server + r, err = http.NewRequest("GET", msg.FrameworkFileURL, nil) + r.Header.Add("Authorization", "Bearer "+msg.AccessToken) + if err := downloadFile(r, frameworkPath); err != nil { + DefaultLogger.Printf("error: %v\n", err) + return err + } + defer helper.FileDelete(frameworkPath) + + // Under circumstances there is no guarantee that the following request will be issues + // BEFORE the actual test result. + // we use a HTTP Request to send the answer + // r = tape.BuildDataRequest("POST", msg.ResultEndpointURL, tape.ToH(&shared.SubmissionWorkerResponse{ + // Log: "submission is currently being tested ...", + // Status: 1, + // })) + // r.Header.Add("Authorization", "Bearer "+msg.AccessToken) + // resp, err := client.Do(r) + // if err != nil { + // DefaultLogger.WithFields(logrus.Fields{ + // "action": "send result to backend", + // "SubmissionID": msg.SubmissionID, + // "resp": resp, + // }).Warn(err) + + // return err + // } + + // 4. verify checksums to avoid race conditions + if err := verifySha256(submissionPath, msg.Sha256); err != nil { + DefaultLogger.WithFields(logrus.Fields{ + "SubmissionID": msg.SubmissionID, + "SubmissionFileURL": msg.SubmissionFileURL, + "FrameworkFileURL": msg.FrameworkFileURL, + "Sha256": msg.Sha256, + }).Warn(err) + return err + } + + // 5. run docker test + ds := service.NewDockerService() + defer ds.Client.Close() + + var exit int64 + var stdout string + + var workerResp *shared.SubmissionWorkerResponse + + stdout, exit, err = ds.Run( + msg.DockerImage, + submissionPath, + frameworkPath, + viper.GetInt64("worker_docker_memory_bytes"), + ) + if err != nil { + DefaultLogger.WithFields(logrus.Fields{ + "SubmissionID": msg.SubmissionID, + "stdout": stdout, + "exitcode": exit, + }).Warn(err) + return err + } + + if exit == 0 { + stdout = cleanDockerOutput(stdout) + // 3. push result back to server + workerResp = &shared.SubmissionWorkerResponse{ + Log: stdout, + Status: int(exit), + } + } else { + workerResp = &shared.SubmissionWorkerResponse{ + Log: fmt.Sprintf(` There has been an issue during testing your upload (The ID is %v). The testing-framework has failed (not the server).\n`, - msg.SubmissionID), - Status: int(exit), - } - } - - // we use a HTTP Request to send the answer - r = tape.BuildDataRequest("POST", msg.ResultEndpointURL, tape.ToH(workerResp)) - r.Header.Add("Authorization", "Bearer "+msg.AccessToken) - - // run request - client := newHTTPClientSingleRequest() - resp, err := client.Do(r) - if err != nil { - DefaultLogger.WithFields(logrus.Fields{ - "action": "send result to backend", - "SubmissionID": msg.SubmissionID, - "stdout": stdout, - "exitcode": exit, - "resp": resp, - }).Warn(err) - - return err - } - defer resp.Body.Close() - - return nil + msg.SubmissionID), + Status: int(exit), + } + } + + // we use a HTTP Request to send the answer + r = tape.BuildDataRequest("POST", msg.ResultEndpointURL, tape.ToH(workerResp)) + r.Header.Add("Authorization", "Bearer "+msg.AccessToken) + + // run request + client := newHTTPClientSingleRequest() + resp, err := client.Do(r) + if err != nil { + DefaultLogger.WithFields(logrus.Fields{ + "action": "send result to backend", + "SubmissionID": msg.SubmissionID, + "stdout": stdout, + "exitcode": exit, + "resp": resp, + }).Warn(err) + + return err + } + defer resp.Body.Close() + + return nil } diff --git a/auth/authenticate/claims.go b/auth/authenticate/claims.go index 0f09b60..107e00a 100644 --- a/auth/authenticate/claims.go +++ b/auth/authenticate/claims.go @@ -19,154 +19,154 @@ package authenticate import ( - "errors" - "net/http" + "errors" + "net/http" - jwt "github.com/dgrijalva/jwt-go" - "github.com/spf13/viper" + jwt "github.com/dgrijalva/jwt-go" + "github.com/spf13/viper" ) // AccessClaims represent the claims parsed from JWT access token. type AccessClaims struct { - jwt.StandardClaims - AccessNotRefresh bool `json:"anr"` // to distinguish between access and refresh code - LoginID int64 `json:"login_id"` // the id to get user information - Root bool `json:"root"` // a global flag to bypass all permission checks + jwt.StandardClaims + AccessNotRefresh bool `json:"anr"` // to distinguish between access and refresh code + LoginID int64 `json:"login_id"` // the id to get user information + Root bool `json:"root"` // a global flag to bypass all permission checks } func NewAccessClaims(loginId int64, root bool) AccessClaims { - return AccessClaims{ - LoginID: loginId, - AccessNotRefresh: true, - Root: root, - } + return AccessClaims{ + LoginID: loginId, + AccessNotRefresh: true, + Root: root, + } } // RefreshClaims represent the claims parsed from JWT refresh token. type RefreshClaims struct { - jwt.StandardClaims - AccessNotRefresh bool `json:"anr"` - LoginID int64 `json:"login_id"` + jwt.StandardClaims + AccessNotRefresh bool `json:"anr"` + LoginID int64 `json:"login_id"` } func NewRefreshClaims(loginId int64) RefreshClaims { - return RefreshClaims{ - LoginID: loginId, - AccessNotRefresh: false, - } + return RefreshClaims{ + LoginID: loginId, + AccessNotRefresh: false, + } } // Parse refresh claims from a token string func (ret *RefreshClaims) ParseRefreshClaimsFromToken(tokenStr string) error { - secret := viper.GetString("auth_jwt_secret") + secret := viper.GetString("auth_jwt_secret") - // verify the token - token, err := jwt.ParseWithClaims(tokenStr, &RefreshClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(secret), nil - }) + // verify the token + token, err := jwt.ParseWithClaims(tokenStr, &RefreshClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) - if err != nil { - return err - } + if err != nil { + return err + } - if claims, ok := token.Claims.(*RefreshClaims); ok && token.Valid { + if claims, ok := token.Claims.(*RefreshClaims); ok && token.Valid { - if !claims.AccessNotRefresh { - ret.LoginID = claims.LoginID - ret.AccessNotRefresh = claims.AccessNotRefresh - return nil - } else { - return errors.New("token is an access token, but refresh token was required") - } + if !claims.AccessNotRefresh { + ret.LoginID = claims.LoginID + ret.AccessNotRefresh = claims.AccessNotRefresh + return nil + } else { + return errors.New("token is an access token, but refresh token was required") + } - } else { - return errors.New("token is invalid") - } + } else { + return errors.New("token is invalid") + } } // Parse access claims from a JWT token string func (ret *AccessClaims) ParseAccessClaimsFromToken(tokenStr string) error { - secret := viper.GetString("auth_jwt_secret") + secret := viper.GetString("auth_jwt_secret") - // verify the token - token, err := jwt.ParseWithClaims(tokenStr, &AccessClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(secret), nil - }) + // verify the token + token, err := jwt.ParseWithClaims(tokenStr, &AccessClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) - if err != nil { - return err - } + if err != nil { + return err + } - if claims, ok := token.Claims.(*AccessClaims); ok && token.Valid { + if claims, ok := token.Claims.(*AccessClaims); ok && token.Valid { - if claims.AccessNotRefresh { - ret.LoginID = claims.LoginID - ret.AccessNotRefresh = claims.AccessNotRefresh - ret.Root = claims.Root - return nil - } else { - return errors.New("token is an refresh token, but access token was required") - } + if claims.AccessNotRefresh { + ret.LoginID = claims.LoginID + ret.AccessNotRefresh = claims.AccessNotRefresh + ret.Root = claims.Root + return nil + } else { + return errors.New("token is an refresh token, but access token was required") + } - } else { - return errors.New("token is invalid") - } + } else { + return errors.New("token is invalid") + } } // Parse access claims from a cookie func (ret *AccessClaims) ParseRefreshClaimsFromSession(r *http.Request) error { - session := SessionManager.Load(r) - - loginId, err := session.GetInt64("login_id") - if err != nil { - return err - } - root, err := session.GetBool("root") - if err != nil { - return err - } - - ret.LoginID = loginId - // cookie based authentification is access-token only - ret.AccessNotRefresh = true - ret.Root = root - return nil + session := SessionManager.Load(r) + + loginId, err := session.GetInt64("login_id") + if err != nil { + return err + } + root, err := session.GetBool("root") + if err != nil { + return err + } + + ret.LoginID = loginId + // cookie based authentification is access-token only + ret.AccessNotRefresh = true + ret.Root = root + return nil } func (ret *AccessClaims) WriteToSession(w http.ResponseWriter, r *http.Request) http.ResponseWriter { - session := SessionManager.Load(r) - - err := session.PutInt64(w, "login_id", ret.LoginID) - if err != nil { - panic("hh") - } - // fmt.Println("Wrote ret.LoginID", ret.LoginID) - err = session.PutBool(w, "root", ret.Root) - if err != nil { - panic("hh") - } - // fmt.Println("Wrote ret.Root", ret.Root) - - return w + session := SessionManager.Load(r) + + err := session.PutInt64(w, "login_id", ret.LoginID) + if err != nil { + panic("hh") + } + // fmt.Println("Wrote ret.LoginID", ret.LoginID) + err = session.PutBool(w, "root", ret.Root) + if err != nil { + panic("hh") + } + // fmt.Println("Wrote ret.Root", ret.Root) + + return w } func (ret *AccessClaims) UpdateSession(w http.ResponseWriter, r *http.Request) http.ResponseWriter { - session := SessionManager.Load(r) + session := SessionManager.Load(r) - err := session.Touch(w) - if err != nil { - panic("hh") - } + err := session.Touch(w) + if err != nil { + panic("hh") + } - return w + return w } func (ret *AccessClaims) DestroyInSession(w http.ResponseWriter, r *http.Request) error { - session := SessionManager.Load(r) - return session.Destroy(w) + session := SessionManager.Load(r) + return session.Destroy(w) } diff --git a/auth/authenticate/middleware.go b/auth/authenticate/middleware.go index b29c3df..f95805c 100644 --- a/auth/authenticate/middleware.go +++ b/auth/authenticate/middleware.go @@ -19,181 +19,181 @@ package authenticate import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/go-chi/jwtauth" - "github.com/go-chi/render" - "github.com/spf13/viper" - "github.com/ulule/limiter/v3" - - // "github.com/ulule/limiter/v3/drivers/store/memory" - redis "github.com/go-redis/redis" - sredis "github.com/ulule/limiter/v3/drivers/store/redis" + "context" + "fmt" + "net/http" + "strconv" + + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/go-chi/jwtauth" + "github.com/go-chi/render" + "github.com/spf13/viper" + "github.com/ulule/limiter/v3" + + // "github.com/ulule/limiter/v3/drivers/store/memory" + redis "github.com/go-redis/redis" + sredis "github.com/ulule/limiter/v3/drivers/store/redis" ) // RequiredValidAccessClaimsMiddleware tries to get information about the identity which // issues a request by looking into the authorization header and then into // the cookie. func RequiredValidAccessClaims(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - accessClaims := &AccessClaims{} - - if viper.GetInt64("debug_user_id") == 0 { - // first we test the JWT autorization - if HasHeaderToken(r) { - - // parse token from from header - tokenStr := jwtauth.TokenFromHeader(r) - - // ok, there is a access token in the header - err := accessClaims.ParseAccessClaimsFromToken(tokenStr) - if err != nil { - // fmt.Println(err) - render.Render(w, r, auth.ErrUnauthorized) - return - } - - } else { - // fmt.Println("no token, try session") - if HasSessionToken(r) { - // fmt.Println("found session") - - // session data is stored in cookie - err := accessClaims.ParseRefreshClaimsFromSession(r) - if err != nil { - // fmt.Println(err) - render.Render(w, r, auth.ErrUnauthorized) - return - } - - // session is valid --> we will extend the session - w = accessClaims.UpdateSession(w, r) - } else { - // fmt.Println("NO session found") - - render.Render(w, r, auth.ErrUnauthenticated) - return - - } - - } - } else { - accessClaims.LoginID = viper.GetInt64("debug_user_id") - accessClaims.Root = true - } - - // nothing given - // serve next - ctx := context.WithValue(r.Context(), common.CtxKeyAccessClaims, accessClaims) - next.ServeHTTP(w, r.WithContext(ctx)) - return - - }) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + accessClaims := &AccessClaims{} + + if viper.GetInt64("debug_user_id") == 0 { + // first we test the JWT autorization + if HasHeaderToken(r) { + + // parse token from from header + tokenStr := jwtauth.TokenFromHeader(r) + + // ok, there is a access token in the header + err := accessClaims.ParseAccessClaimsFromToken(tokenStr) + if err != nil { + // fmt.Println(err) + render.Render(w, r, auth.ErrUnauthorized) + return + } + + } else { + // fmt.Println("no token, try session") + if HasSessionToken(r) { + // fmt.Println("found session") + + // session data is stored in cookie + err := accessClaims.ParseRefreshClaimsFromSession(r) + if err != nil { + // fmt.Println(err) + render.Render(w, r, auth.ErrUnauthorized) + return + } + + // session is valid --> we will extend the session + w = accessClaims.UpdateSession(w, r) + } else { + // fmt.Println("NO session found") + + render.Render(w, r, auth.ErrUnauthenticated) + return + + } + + } + } else { + accessClaims.LoginID = viper.GetInt64("debug_user_id") + accessClaims.Root = true + } + + // nothing given + // serve next + ctx := context.WithValue(r.Context(), common.CtxKeyAccessClaims, accessClaims) + next.ServeHTTP(w, r.WithContext(ctx)) + return + + }) } type LoginLimiterKey interface { - Key() string + Key() string } type LoginLimiter struct { - Store *limiter.Store - Rate *limiter.Rate - Prefix string + Store *limiter.Store + Rate *limiter.Rate + Prefix string } type LoginLimiterKeyFromIP struct { - R *http.Request + R *http.Request } func NewLoginLimiterKeyFromIP(r *http.Request) *LoginLimiterKeyFromIP { - return &LoginLimiterKeyFromIP{R: r} + return &LoginLimiterKeyFromIP{R: r} } func (obj *LoginLimiterKeyFromIP) Key() string { - options := limiter.Options{ - IPv4Mask: limiter.DefaultIPv4Mask, - IPv6Mask: limiter.DefaultIPv6Mask, - TrustForwardHeader: true, - } + options := limiter.Options{ + IPv4Mask: limiter.DefaultIPv4Mask, + IPv6Mask: limiter.DefaultIPv6Mask, + TrustForwardHeader: true, + } - return limiter.GetIP(obj.R, options).String() + return limiter.GetIP(obj.R, options).String() } func NewLoginLimiter(prefix string, limit string, redisURL string) (*LoginLimiter, error) { - // Define a limit rate to 4 requests per hour. - rate, err := limiter.NewRateFromFormatted(limit) - if err != nil { - return nil, err - } - - // Create a redis client. - option, err := redis.ParseURL(redisURL) - if err != nil { - return nil, err - } - client := redis.NewClient(option) - - // Create a store with the redis client. - store, err := sredis.NewStoreWithOptions(client, limiter.StoreOptions{ - Prefix: prefix, - MaxRetry: 3, - }) - - // store := memory.NewStore() - - return &LoginLimiter{Store: &store, Rate: &rate, Prefix: prefix}, nil + // Define a limit rate to 4 requests per hour. + rate, err := limiter.NewRateFromFormatted(limit) + if err != nil { + return nil, err + } + + // Create a redis client. + option, err := redis.ParseURL(redisURL) + if err != nil { + return nil, err + } + client := redis.NewClient(option) + + // Create a store with the redis client. + store, err := sredis.NewStoreWithOptions(client, limiter.StoreOptions{ + Prefix: prefix, + MaxRetry: 3, + }) + + // store := memory.NewStore() + + return &LoginLimiter{Store: &store, Rate: &rate, Prefix: prefix}, nil } func (ll *LoginLimiter) Get(r *http.Request, KeyFunc LoginLimiterKey) (limiter.Context, error) { - return limiter.Store.Get( - *ll.Store, - r.Context(), - fmt.Sprintf("%s-%s", KeyFunc.Key(), ll.Prefix), - *ll.Rate, - ) + return limiter.Store.Get( + *ll.Store, + r.Context(), + fmt.Sprintf("%s-%s", KeyFunc.Key(), ll.Prefix), + *ll.Rate, + ) } func (ll *LoginLimiter) WriteHeaders(w http.ResponseWriter, context limiter.Context) { - w.Header().Add("X-RateLimit-Limit", strconv.FormatInt(context.Limit, 10)) - w.Header().Add("X-RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10)) - w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(context.Reset, 10)) + w.Header().Add("X-RateLimit-Limit", strconv.FormatInt(context.Limit, 10)) + w.Header().Add("X-RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10)) + w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(context.Reset, 10)) } func RateLimitMiddleware(prefix string, limit string, redisURL string) func(h http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - ll, err := NewLoginLimiter(prefix, limit, redisURL) + return func(h http.Handler) http.Handler { + ll, err := NewLoginLimiter(prefix, limit, redisURL) - if err != nil { - panic(err) - } + if err != nil { + panic(err) + } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - keyFunc := NewLoginLimiterKeyFromIP(r) + keyFunc := NewLoginLimiterKeyFromIP(r) - context, err := ll.Get(r, keyFunc) - if err != nil { - panic(err) - return - } + context, err := ll.Get(r, keyFunc) + if err != nil { + panic(err) + return + } - ll.WriteHeaders(w, context) + ll.WriteHeaders(w, context) - if context.Reached { - http.Error(w, "Limit exceeded", http.StatusTooManyRequests) - return - } + if context.Reached { + http.Error(w, "Limit exceeded", http.StatusTooManyRequests) + return + } - h.ServeHTTP(w, r) - }) - } + h.ServeHTTP(w, r) + }) + } } diff --git a/auth/authenticate/tokenauth.go b/auth/authenticate/tokenauth.go index 83dceef..9037359 100644 --- a/auth/authenticate/tokenauth.go +++ b/auth/authenticate/tokenauth.go @@ -19,53 +19,53 @@ package authenticate import ( - "net/http" - "time" + "net/http" + "time" - "github.com/go-chi/jwtauth" - "github.com/spf13/viper" + "github.com/go-chi/jwtauth" + "github.com/spf13/viper" ) // TokenAuth implements JWT authentication flow. type TokenAuth struct { - JwtAuth *jwtauth.JWTAuth - JwtAccessExpiry time.Duration - JwtRefreshExpiry time.Duration + JwtAuth *jwtauth.JWTAuth + JwtAccessExpiry time.Duration + JwtRefreshExpiry time.Duration } // NewTokenAuth configures and returns a JWT authentication instance. func NewTokenAuth() (*TokenAuth, error) { - secret := viper.GetString("auth_jwt_secret") + secret := viper.GetString("auth_jwt_secret") - a := &TokenAuth{ - JwtAuth: jwtauth.New("HS256", []byte(secret), nil), - JwtAccessExpiry: viper.GetDuration("auth_jwt_access_expiry"), - JwtRefreshExpiry: viper.GetDuration("auth_jwt_refresh_expiry"), - } + a := &TokenAuth{ + JwtAuth: jwtauth.New("HS256", []byte(secret), nil), + JwtAccessExpiry: viper.GetDuration("auth_jwt_access_expiry"), + JwtRefreshExpiry: viper.GetDuration("auth_jwt_refresh_expiry"), + } - return a, nil + return a, nil } // Verifier http middleware will verify a jwt string from a http request. func (a *TokenAuth) Verifier() func(http.Handler) http.Handler { - return jwtauth.Verifier(a.JwtAuth) + return jwtauth.Verifier(a.JwtAuth) } // CreateAccessJWT returns an access token for provided account claims. func (a *TokenAuth) CreateAccessJWT(claims AccessClaims) (string, error) { - claims.StandardClaims.IssuedAt = time.Now().UTC().Unix() - claims.StandardClaims.ExpiresAt = time.Now().UTC().Unix() + int64(a.JwtAccessExpiry) + claims.StandardClaims.IssuedAt = time.Now().UTC().Unix() + claims.StandardClaims.ExpiresAt = time.Now().UTC().Unix() + int64(a.JwtAccessExpiry) - _, tokenString, err := a.JwtAuth.Encode(claims) - return tokenString, err + _, tokenString, err := a.JwtAuth.Encode(claims) + return tokenString, err } // CreateRefreshJWT returns a refresh token for provided token Claims. func (a *TokenAuth) CreateRefreshJWT(claims RefreshClaims) (string, error) { - claims.StandardClaims.IssuedAt = time.Now().UTC().Unix() - claims.StandardClaims.ExpiresAt = time.Now().UTC().Unix() + int64(a.JwtRefreshExpiry) + claims.StandardClaims.IssuedAt = time.Now().UTC().Unix() + claims.StandardClaims.ExpiresAt = time.Now().UTC().Unix() + int64(a.JwtRefreshExpiry) - _, tokenString, err := a.JwtAuth.Encode(claims) - return tokenString, err + _, tokenString, err := a.JwtAuth.Encode(claims) + return tokenString, err } diff --git a/auth/authorize/roles.go b/auth/authorize/roles.go index 7269502..02966de 100644 --- a/auth/authorize/roles.go +++ b/auth/authorize/roles.go @@ -19,70 +19,70 @@ package authorize import ( - "net/http" + "net/http" - "github.com/cgtuebingen/infomark-backend/auth" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/common" - "github.com/go-chi/render" + "github.com/cgtuebingen/infomark-backend/auth" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/common" + "github.com/go-chi/render" ) type CourseRole int32 const ( - NOCOURSEROLE CourseRole = -1 - STUDENT CourseRole = 0 - TUTOR CourseRole = 1 - ADMIN CourseRole = 2 + NOCOURSEROLE CourseRole = -1 + STUDENT CourseRole = 0 + TUTOR CourseRole = 1 + ADMIN CourseRole = 2 ) func (r CourseRole) ToInt() int { - switch r { - default: - return -1 - case NOCOURSEROLE: - return -1 - case STUDENT: - return 0 - case TUTOR: - return 1 - case ADMIN: - return 2 - } + switch r { + default: + return -1 + case NOCOURSEROLE: + return -1 + case STUDENT: + return 0 + case TUTOR: + return 1 + case ADMIN: + return 2 + } } // RequiresRole middleware restricts access to accounts having role parameter in their jwt claims. func RequiresAtLeastCourseRole(requiredRole CourseRole) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - hfn := func(w http.ResponseWriter, r *http.Request) { - if HasAtLeastRole(requiredRole, r) { - next.ServeHTTP(w, r) - } else { - render.Render(w, r, auth.ErrUnauthorized) - } - } - return http.HandlerFunc(hfn) - } + return func(next http.Handler) http.Handler { + hfn := func(w http.ResponseWriter, r *http.Request) { + if HasAtLeastRole(requiredRole, r) { + next.ServeHTTP(w, r) + } else { + render.Render(w, r, auth.ErrUnauthorized) + } + } + return http.HandlerFunc(hfn) + } } func HasAtLeastRole(requiredRole CourseRole, r *http.Request) bool { - // global root can lever out this check - accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) - if accessClaims.Root { - // oh dear, sorry to ask. Please pass this check - return true - } + // global root can lever out this check + accessClaims := r.Context().Value(common.CtxKeyAccessClaims).(*authenticate.AccessClaims) + if accessClaims.Root { + // oh dear, sorry to ask. Please pass this check + return true + } - givenRole, ok := r.Context().Value(common.CtxKeyCourseRole).(CourseRole) - if !ok { - return false - } + givenRole, ok := r.Context().Value(common.CtxKeyCourseRole).(CourseRole) + if !ok { + return false + } - if givenRole < requiredRole { - return false - } + if givenRole < requiredRole { + return false + } - return true + return true } // func EndpointRequiresRole(endpoint http.HandlerFunc, requiredRole CourseRole) http.HandlerFunc { diff --git a/auth/errors.go b/auth/errors.go index b3df7fd..9b318de 100644 --- a/auth/errors.go +++ b/auth/errors.go @@ -19,66 +19,66 @@ package auth import ( - "errors" - "net/http" + "errors" + "net/http" - "github.com/go-chi/render" + "github.com/go-chi/render" ) // The list of jwt token errors presented to the end user. var ( - ErrTokenUnauthorized = errors.New("token unauthorized") - ErrTokenExpired = errors.New("token expired") - ErrInvalidAccessToken = errors.New("invalid access token") - ErrInvalidRefreshToken = errors.New("invalid refresh token") + ErrTokenUnauthorized = errors.New("token unauthorized") + ErrTokenExpired = errors.New("token expired") + ErrInvalidAccessToken = errors.New("invalid access token") + ErrInvalidRefreshToken = errors.New("invalid refresh token") ) // ErrResponse renderer type for handling all sorts of errors. type ErrResponse struct { - Err error `json:"-"` // low-level runtime error - HTTPStatusCode int `json:"-"` // http response status code + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code - StatusText string `json:"status"` // user-level status message - AppCode int64 `json:"code,omitempty"` // application-specific error code - ErrorText string `json:"error,omitempty"` // application-level error message, for debugging + StatusText string `json:"status"` // user-level status message + AppCode int64 `json:"code,omitempty"` // application-specific error code + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging } // Render sets the application-specific error code in AppCode. func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { - render.Status(r, e.HTTPStatusCode) - return nil + render.Status(r, e.HTTPStatusCode) + return nil } // ErrUnauthenticatedWithDetails renders status 401 Unauthorized with custom error message. // The request has no credentials at all. func ErrUnauthenticatedWithDetails(err error) render.Renderer { - // StatusUnauthorized = 401 // RFC 7235, 3.1 - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusUnauthorized, - StatusText: http.StatusText(http.StatusUnauthorized), - ErrorText: err.Error(), - } + // StatusUnauthorized = 401 // RFC 7235, 3.1 + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusUnauthorized, + StatusText: http.StatusText(http.StatusUnauthorized), + ErrorText: err.Error(), + } } // ErrUnauthorizedWithDetails renders status 403 Unauthorized with custom error message. // The request is issued with credential, but these are invalid or not sufficient // to gain access to a ressource. func ErrUnauthorizedWithDetails(err error) render.Renderer { - // StatusForbidden = 403 // RFC 7231, 6.5.3 - return &ErrResponse{ - Err: err, - HTTPStatusCode: http.StatusUnauthorized, - StatusText: http.StatusText(http.StatusUnauthorized), - ErrorText: err.Error(), - } + // StatusForbidden = 403 // RFC 7231, 6.5.3 + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusUnauthorized, + StatusText: http.StatusText(http.StatusUnauthorized), + ErrorText: err.Error(), + } } var ( - // ErrUnauthenticated means no credentials are given - ErrUnauthenticated = &ErrResponse{HTTPStatusCode: http.StatusUnauthorized, StatusText: http.StatusText(http.StatusUnauthorized)} + // ErrUnauthenticated means no credentials are given + ErrUnauthenticated = &ErrResponse{HTTPStatusCode: http.StatusUnauthorized, StatusText: http.StatusText(http.StatusUnauthorized)} - // ErrUnauthorized returns status 403 Forbidden for unauthorized request. - // e.g. "User doesn't have enough privilege" - ErrUnauthorized = &ErrResponse{HTTPStatusCode: http.StatusForbidden, StatusText: http.StatusText(http.StatusForbidden)} + // ErrUnauthorized returns status 403 Forbidden for unauthorized request. + // e.g. "User doesn't have enough privilege" + ErrUnauthorized = &ErrResponse{HTTPStatusCode: http.StatusForbidden, StatusText: http.StatusText(http.StatusForbidden)} ) diff --git a/cmd/console.go b/cmd/console.go index 01071be..58ac574 100644 --- a/cmd/console.go +++ b/cmd/console.go @@ -19,83 +19,83 @@ package cmd import ( - "fmt" - "log" - "os" - "path" - "path/filepath" - "strings" - "time" - - "github.com/cgtuebingen/infomark-backend/cmd/console" - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" + "fmt" + "log" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/cgtuebingen/infomark-backend/cmd/console" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" ) // ConsoleCmd starts the infomark console var ConsoleCmd = &cobra.Command{ - Use: "console", - Short: "infomark console commands", + Use: "console", + Short: "infomark console commands", } func init() { - ConsoleCmd.AddCommand(console.AdminCmd) - ConsoleCmd.AddCommand(console.UserCmd) - ConsoleCmd.AddCommand(console.CourseCmd) - ConsoleCmd.AddCommand(console.SubmissionCmd) - ConsoleCmd.AddCommand(console.GroupCmd) - ConsoleCmd.AddCommand(console.DatabaseCmd) + ConsoleCmd.AddCommand(console.AdminCmd) + ConsoleCmd.AddCommand(console.UserCmd) + ConsoleCmd.AddCommand(console.CourseCmd) + ConsoleCmd.AddCommand(console.SubmissionCmd) + ConsoleCmd.AddCommand(console.GroupCmd) + ConsoleCmd.AddCommand(console.DatabaseCmd) - UtilsCmd.AddCommand(UtilsCompletionCmd) - UtilsCmd.AddCommand(UtilsDocCmd) - ConsoleCmd.AddCommand(UtilsCmd) + UtilsCmd.AddCommand(UtilsCompletionCmd) + UtilsCmd.AddCommand(UtilsDocCmd) + ConsoleCmd.AddCommand(UtilsCmd) - RootCmd.AddCommand(ConsoleCmd) + RootCmd.AddCommand(ConsoleCmd) - // doc.GenMarkdownTree(RootCmd, "/tmp") + // doc.GenMarkdownTree(RootCmd, "/tmp") } var UtilsCmd = &cobra.Command{ - Use: "utils", - Short: "Some helper functions.", + Use: "utils", + Short: "Some helper functions.", } var UtilsCompletionCmd = &cobra.Command{ - Use: "completion [shell]", - Short: "Output (bash/zsh) shell completion code", - Long: `Pipe the stdout to your completion collection, e.g., + Use: "completion [shell]", + Short: "Output (bash/zsh) shell completion code", + Long: `Pipe the stdout to your completion collection, e.g., ./infomark console utils completion zsh > ~/.my-shell/completions/_infomark `, - // Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - log.Fatalln("Expected one argument with the desired shell") - } - - switch args[0] { - case "bash": - RootCmd.GenBashCompletion(os.Stdout) - case "zsh": - RootCmd.GenZshCompletion(os.Stdout) - default: - log.Fatalln("Unknown shell %s, only bash and zsh are available\n", args[0]) - } - }, + // Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + log.Fatalln("Expected one argument with the desired shell") + } + + switch args[0] { + case "bash": + RootCmd.GenBashCompletion(os.Stdout) + case "zsh": + RootCmd.GenZshCompletion(os.Stdout) + default: + log.Fatalln("Unknown shell %s, only bash and zsh are available\n", args[0]) + } + }, } var UtilsDocCmd = &cobra.Command{ - Use: "doc", - Short: "Generates docs for console", - // Hidden: true, - Run: func(cmd *cobra.Command, args []string) { + Use: "doc", + Short: "Generates docs for console", + // Hidden: true, + Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - log.Fatalln("Expected one argument with the destination dir") - } + if len(args) != 1 { + log.Fatalln("Expected one argument with the destination dir") + } - const fmTemplate = `--- + const fmTemplate = `--- date: %s title: "%s" slug: %s @@ -106,22 +106,22 @@ layout: subpagewithout ` - filePrepender := func(filename string) string { - now := time.Now().Format(time.RFC3339) - name := filepath.Base(filename) - base := strings.TrimSuffix(name, path.Ext(name)) - url := "/guides/console/commands/" + strings.ToLower(base) + "/" - return fmt.Sprintf(fmTemplate, now, "Console", base, url, now) - } - - linkHandler := func(name string) string { - base := strings.TrimSuffix(name, path.Ext(name)) - return "/guides/console/commands/" + strings.ToLower(base) + "/" - } - - err := doc.GenMarkdownTreeCustom(RootCmd, args[0], filePrepender, linkHandler) - if err != nil { - log.Fatal(err) - } - }, + filePrepender := func(filename string) string { + now := time.Now().Format(time.RFC3339) + name := filepath.Base(filename) + base := strings.TrimSuffix(name, path.Ext(name)) + url := "/guides/console/commands/" + strings.ToLower(base) + "/" + return fmt.Sprintf(fmTemplate, now, "Console", base, url, now) + } + + linkHandler := func(name string) string { + base := strings.TrimSuffix(name, path.Ext(name)) + return "/guides/console/commands/" + strings.ToLower(base) + "/" + } + + err := doc.GenMarkdownTreeCustom(RootCmd, args[0], filePrepender, linkHandler) + if err != nil { + log.Fatal(err) + } + }, } diff --git a/cmd/console/admin_cmd.go b/cmd/console/admin_cmd.go index d01aa10..9d7ae4f 100644 --- a/cmd/console/admin_cmd.go +++ b/cmd/console/admin_cmd.go @@ -19,68 +19,68 @@ package console import ( - "fmt" - "log" + "fmt" + "log" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - AdminCmd.AddCommand(AdminRemoveCmd) - AdminCmd.AddCommand(AdminAddCmd) + AdminCmd.AddCommand(AdminRemoveCmd) + AdminCmd.AddCommand(AdminAddCmd) } var AdminCmd = &cobra.Command{ - Use: "admin", - Short: "Management of global admins.", + Use: "admin", + Short: "Management of global admins.", } var AdminAddCmd = &cobra.Command{ - Use: "add [userID]", - Short: "set gives an user global admin permission", - Long: `Will set the gobal root flag to "true" for a given user + Use: "add [userID]", + Short: "set gives an user global admin permission", + Long: `Will set the gobal root flag to "true" for a given user bypassing all permission tests`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - userID := MustInt64Parameter(args[0], "userID") + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + userID := MustInt64Parameter(args[0], "userID") - _, stores := MustConnectAndStores() + _, stores := MustConnectAndStores() - user, err := stores.User.Get(userID) - if err != nil { - log.Fatalf("user with id %v not found\n", userID) - } + user, err := stores.User.Get(userID) + if err != nil { + log.Fatalf("user with id %v not found\n", userID) + } - user.Root = true - if err := stores.User.Update(user); err != nil { - panic(err) - } + user.Root = true + if err := stores.User.Update(user); err != nil { + panic(err) + } - fmt.Printf("The user %s %s (id:%v) has now global admin privileges\n", - user.FirstName, user.LastName, user.ID) - }, + fmt.Printf("The user %s %s (id:%v) has now global admin privileges\n", + user.FirstName, user.LastName, user.ID) + }, } var AdminRemoveCmd = &cobra.Command{ - Use: "remove [userID]", - Short: "removes global admin permission from a user", - Long: `Will set the gobal root flag to false for a user `, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - userID := MustInt64Parameter(args[0], "userID") - - _, stores := MustConnectAndStores() - - user, err := stores.User.Get(userID) - if err != nil { - log.Fatalf("user with id %v not found\n", userID) - } - user.Root = false - if err := stores.User.Update(user); err != nil { - panic(err) - } - - fmt.Printf("user %s %s (%v) is not an admin anymore\n", user.FirstName, user.LastName, user.ID) - - }, + Use: "remove [userID]", + Short: "removes global admin permission from a user", + Long: `Will set the gobal root flag to false for a user `, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + userID := MustInt64Parameter(args[0], "userID") + + _, stores := MustConnectAndStores() + + user, err := stores.User.Get(userID) + if err != nil { + log.Fatalf("user with id %v not found\n", userID) + } + user.Root = false + if err := stores.User.Update(user); err != nil { + panic(err) + } + + fmt.Printf("user %s %s (%v) is not an admin anymore\n", user.FirstName, user.LastName, user.ID) + + }, } diff --git a/cmd/console/course_cmd.go b/cmd/console/course_cmd.go index ee28a07..f6e433e 100644 --- a/cmd/console/course_cmd.go +++ b/cmd/console/course_cmd.go @@ -19,60 +19,60 @@ package console import ( - "fmt" - "log" + "fmt" + "log" - "github.com/spf13/cobra" + "github.com/spf13/cobra" ) func init() { - CourseCmd.AddCommand(UserEnrollInCourse) + CourseCmd.AddCommand(UserEnrollInCourse) } var CourseCmd = &cobra.Command{ - Use: "course", - Short: "Management of cours assignment", + Use: "course", + Short: "Management of cours assignment", } var UserEnrollInCourse = &cobra.Command{ - Use: "enroll [courseID] [userID] [role]", - Short: "will enroll a user into course", - Args: cobra.ExactArgs(3), - Run: func(cmd *cobra.Command, args []string) { - var err error + Use: "enroll [courseID] [userID] [role]", + Short: "will enroll a user into course", + Args: cobra.ExactArgs(3), + Run: func(cmd *cobra.Command, args []string) { + var err error - courseID := MustInt64Parameter(args[0], "courseID") - userID := MustInt64Parameter(args[1], "userID") + courseID := MustInt64Parameter(args[0], "courseID") + userID := MustInt64Parameter(args[1], "userID") - role := int64(0) - switch args[2] { - case "admin": - role = int64(2) - case "tutor": - role = int64(1) - case "student": - role = int64(0) - default: - log.Fatalf("role '%s' must be one of 'student', 'tutor', 'admin'\n", args[2]) - } + role := int64(0) + switch args[2] { + case "admin": + role = int64(2) + case "tutor": + role = int64(1) + case "student": + role = int64(0) + default: + log.Fatalf("role '%s' must be one of 'student', 'tutor', 'admin'\n", args[2]) + } - _, stores := MustConnectAndStores() + _, stores := MustConnectAndStores() - user, err := stores.User.Get(userID) - if err != nil { - log.Fatal("user with id %v not found\n", userID) - } + user, err := stores.User.Get(userID) + if err != nil { + log.Fatal("user with id %v not found\n", userID) + } - course, err := stores.Course.Get(courseID) - if err != nil { - log.Fatal("user with id %v not found\n", userID) - } + course, err := stores.Course.Get(courseID) + if err != nil { + log.Fatal("user with id %v not found\n", userID) + } - if err := stores.Course.Enroll(course.ID, user.ID, role); err != nil { - panic(err) - } + if err := stores.Course.Enroll(course.ID, user.ID, role); err != nil { + panic(err) + } - fmt.Printf("user %s %s is now enrolled in course %v with role %v\n", - user.FirstName, user.LastName, course.ID, role) - }, + fmt.Printf("user %s %s is now enrolled in course %v with role %v\n", + user.FirstName, user.LastName, course.ID, role) + }, } diff --git a/cmd/console/database_cmd.go b/cmd/console/database_cmd.go index df6a531..4795e79 100644 --- a/cmd/console/database_cmd.go +++ b/cmd/console/database_cmd.go @@ -19,208 +19,208 @@ package console import ( - "bytes" - "fmt" - "io" - "log" - "os" - "os/exec" - "strings" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/lib/pq" - "github.com/spf13/cobra" - "github.com/spf13/viper" + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/lib/pq" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) func parseConnectionString(conn string) map[string]string { - if len(conn) == 0 { - log.Fatal("database_connection-string from config is empty") - } - connParts, err := pq.ParseURL(conn) - failWhenSmallestWhiff(err) - parts := strings.Split(connParts, " ") - - infoMap := make(map[string]string) - for _, part := range parts { - v := strings.Split(part, "=") - if len(v) >= 2 { - switch v[0] { - case "dbname", "host", "password", "port", "user": - infoMap[v[0]] = v[1] - } - } - - } - return infoMap + if len(conn) == 0 { + log.Fatal("database_connection-string from config is empty") + } + connParts, err := pq.ParseURL(conn) + failWhenSmallestWhiff(err) + parts := strings.Split(connParts, " ") + + infoMap := make(map[string]string) + for _, part := range parts { + v := strings.Split(part, "=") + if len(v) >= 2 { + switch v[0] { + case "dbname", "host", "password", "port", "user": + infoMap[v[0]] = v[1] + } + } + + } + return infoMap } func init() { - DatabaseCmd.AddCommand(DatabaseRunCmd) - DatabaseCmd.AddCommand(DatabaseRestoreCmd) - DatabaseCmd.AddCommand(DatabaseBackupCmd) + DatabaseCmd.AddCommand(DatabaseRunCmd) + DatabaseCmd.AddCommand(DatabaseRestoreCmd) + DatabaseCmd.AddCommand(DatabaseBackupCmd) } var DatabaseCmd = &cobra.Command{ - Use: "database", - Short: "Management of database.", + Use: "database", + Short: "Management of database.", } var DatabaseRunCmd = &cobra.Command{ - Use: "run [sql]", - Short: "run a sql command", - Long: `run a SQl statement. This statement will persistently changes entries!`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - sql := args[0] - - infoMap := parseConnectionString(viper.GetString("database_connection")) - - // PGPASSWORD=pass psql -hlocalhost -Uuser -p5433 -d db -c "..." - shell := exec.Command("psql", - "-h", infoMap["host"], - "-U", infoMap["user"], - "-p", infoMap["port"], - "-d", infoMap["dbname"], - "-c", sql) - shell.Env = os.Environ() - shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) - out, err := shell.CombinedOutput() - fmt.Printf("%s", out) - if err != nil { - log.Fatal("executing SQL-statement was not successfull") - } - - }, + Use: "run [sql]", + Short: "run a sql command", + Long: `run a SQl statement. This statement will persistently changes entries!`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + sql := args[0] + + infoMap := parseConnectionString(viper.GetString("database_connection")) + + // PGPASSWORD=pass psql -hlocalhost -Uuser -p5433 -d db -c "..." + shell := exec.Command("psql", + "-h", infoMap["host"], + "-U", infoMap["user"], + "-p", infoMap["port"], + "-d", infoMap["dbname"], + "-c", sql) + shell.Env = os.Environ() + shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) + out, err := shell.CombinedOutput() + fmt.Printf("%s", out) + if err != nil { + log.Fatal("executing SQL-statement was not successfull") + } + + }, } var DatabaseRestoreCmd = &cobra.Command{ - Use: "restore [file.sql.gz]", - Short: "restore database from a file", - Long: `Will clean entire database and load a snapshot`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - file := args[0] - - if !helper.FileExists(file) { - log.Fatalf("The file %s does not exists!\n", file) - } - - infoMap := parseConnectionString(viper.GetString("database_connection")) - - // // dbname := args[1] - - // dropdb -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} db - // PGPASSWORD=pass dropdb -hlocalhost -Uuser -p5433 db - shell := exec.Command("dropdb", - "-h", infoMap["host"], - "-U", infoMap["user"], - "-p", infoMap["port"], - infoMap["dbname"]) - shell.Env = os.Environ() - shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) - out, err := shell.CombinedOutput() - fmt.Printf("%s", out) - if err != nil { - log.Fatal("dropping db was not successfull") - } - - // createdb -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} --owner="${POSTGRES_USER}" ${POSTGRES_DB} - // PGPASSWORD=pass createdb -hlocalhost -Uuser -p5433 --owner=user db - shell = exec.Command("createdb", - "-h", infoMap["host"], - "-U", infoMap["user"], - "-p", infoMap["port"], - "--owner", infoMap["user"], - infoMap["dbname"]) - shell.Env = os.Environ() - shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) - out, err = shell.CombinedOutput() - fmt.Printf("%s", out) - if err != nil { - log.Fatal("creating db was not successfull") - } - - // gunzip -c "${backup_filename}" | psql -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} "${POSTGRES_DB}" - shell1 := exec.Command("gunzip", - "-c", file) - shell2 := exec.Command("psql", - "-h", infoMap["host"], - "-U", infoMap["user"], - "-p", infoMap["port"], - infoMap["dbname"]) - shell2.Env = os.Environ() - shell2.Env = append(shell2.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) - - r, w := io.Pipe() - shell1.Stdout = w - shell2.Stdin = r - - var b2 bytes.Buffer - shell2.Stdout = &b2 - - shell1.Start() - shell2.Start() - shell1.Wait() - w.Close() - err = shell2.Wait() - // io.Copy(os.Stdout, &b2) - if err != nil { - log.Fatal("load db from was not successfull\n %s", err) - } - }, + Use: "restore [file.sql.gz]", + Short: "restore database from a file", + Long: `Will clean entire database and load a snapshot`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + file := args[0] + + if !helper.FileExists(file) { + log.Fatalf("The file %s does not exists!\n", file) + } + + infoMap := parseConnectionString(viper.GetString("database_connection")) + + // // dbname := args[1] + + // dropdb -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} db + // PGPASSWORD=pass dropdb -hlocalhost -Uuser -p5433 db + shell := exec.Command("dropdb", + "-h", infoMap["host"], + "-U", infoMap["user"], + "-p", infoMap["port"], + infoMap["dbname"]) + shell.Env = os.Environ() + shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) + out, err := shell.CombinedOutput() + fmt.Printf("%s", out) + if err != nil { + log.Fatal("dropping db was not successfull") + } + + // createdb -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} --owner="${POSTGRES_USER}" ${POSTGRES_DB} + // PGPASSWORD=pass createdb -hlocalhost -Uuser -p5433 --owner=user db + shell = exec.Command("createdb", + "-h", infoMap["host"], + "-U", infoMap["user"], + "-p", infoMap["port"], + "--owner", infoMap["user"], + infoMap["dbname"]) + shell.Env = os.Environ() + shell.Env = append(shell.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) + out, err = shell.CombinedOutput() + fmt.Printf("%s", out) + if err != nil { + log.Fatal("creating db was not successfull") + } + + // gunzip -c "${backup_filename}" | psql -h ${POSTGRES_HOST} -p ${POSTGRES_PORT} -U ${POSTGRES_USER} "${POSTGRES_DB}" + shell1 := exec.Command("gunzip", + "-c", file) + shell2 := exec.Command("psql", + "-h", infoMap["host"], + "-U", infoMap["user"], + "-p", infoMap["port"], + infoMap["dbname"]) + shell2.Env = os.Environ() + shell2.Env = append(shell2.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) + + r, w := io.Pipe() + shell1.Stdout = w + shell2.Stdin = r + + var b2 bytes.Buffer + shell2.Stdout = &b2 + + shell1.Start() + shell2.Start() + shell1.Wait() + w.Close() + err = shell2.Wait() + // io.Copy(os.Stdout, &b2) + if err != nil { + log.Fatal("load db from was not successfull\n %s", err) + } + }, } var DatabaseBackupCmd = &cobra.Command{ - Use: "backup [file.sql.gz]", - Short: "backup database to a file", - Long: `Will dump the entire database to a snapshot file`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - file := args[0] - - if helper.FileExists(file) { - log.Fatalf("The file %s does exists! Will not override!\n", file) - } - - infoMap := parseConnectionString(viper.GetString("database_connection")) - - // export backup_filename="infomark_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" - // pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} -p ${POSTGRES_PORT} | gzip > ${backup_filename} - // PGPASSWORD=pass pg_dump -h localhost -U user -d db -p 5433 | gzip > ${backup_filename} - // shell1 := exec.Command("echo", "hi") - shell1 := exec.Command("pg_dump", - "-h", infoMap["host"], - "-U", infoMap["user"], - "-p", infoMap["port"], - "-d", infoMap["dbname"]) - shell1.Env = os.Environ() - shell1.Env = append(shell1.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) - - shell2 := exec.Command("gzip") - - r, w := io.Pipe() - shell1.Stdout = w - shell2.Stdin = r - - var b2 bytes.Buffer - shell2.Stdout = &b2 - - shell1.Start() - shell2.Start() - shell1.Wait() - w.Close() - err := shell2.Wait() - - destination, err := os.Create(file) - if err != nil { - panic(err) - } - defer destination.Close() - _, err = io.Copy(destination, &b2) - if err != nil { - log.Fatal("storing snapshot was not successfull\n %s", err) - } - - }, + Use: "backup [file.sql.gz]", + Short: "backup database to a file", + Long: `Will dump the entire database to a snapshot file`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + file := args[0] + + if helper.FileExists(file) { + log.Fatalf("The file %s does exists! Will not override!\n", file) + } + + infoMap := parseConnectionString(viper.GetString("database_connection")) + + // export backup_filename="infomark_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" + // pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} -p ${POSTGRES_PORT} | gzip > ${backup_filename} + // PGPASSWORD=pass pg_dump -h localhost -U user -d db -p 5433 | gzip > ${backup_filename} + // shell1 := exec.Command("echo", "hi") + shell1 := exec.Command("pg_dump", + "-h", infoMap["host"], + "-U", infoMap["user"], + "-p", infoMap["port"], + "-d", infoMap["dbname"]) + shell1.Env = os.Environ() + shell1.Env = append(shell1.Env, fmt.Sprintf("PGPASSWORD=%s", infoMap["password"])) + + shell2 := exec.Command("gzip") + + r, w := io.Pipe() + shell1.Stdout = w + shell2.Stdin = r + + var b2 bytes.Buffer + shell2.Stdout = &b2 + + shell1.Start() + shell2.Start() + shell1.Wait() + w.Close() + err := shell2.Wait() + + destination, err := os.Create(file) + if err != nil { + panic(err) + } + defer destination.Close() + _, err = io.Copy(destination, &b2) + if err != nil { + log.Fatal("storing snapshot was not successfull\n %s", err) + } + + }, } diff --git a/cmd/console/group_cmd.go b/cmd/console/group_cmd.go index 3374ac8..a7ba48f 100644 --- a/cmd/console/group_cmd.go +++ b/cmd/console/group_cmd.go @@ -19,93 +19,93 @@ package console import ( - "bufio" - "fmt" - "io" - "log" - "os" - "strconv" - "strings" - - "github.com/cgtuebingen/infomark-backend/model" - "github.com/lib/pq" - "github.com/spf13/cobra" + "bufio" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "github.com/cgtuebingen/infomark-backend/model" + "github.com/lib/pq" + "github.com/spf13/cobra" ) func init() { - GroupCmd.AddCommand(GroupReadBids) - GroupCmd.AddCommand(GroupParseBidsSolution) - GroupCmd.AddCommand(GroupEnroll) - GroupCmd.AddCommand(GroupList) - GroupCmd.AddCommand(GroupLocate) - GroupCmd.AddCommand(GroupUserBids) + GroupCmd.AddCommand(GroupReadBids) + GroupCmd.AddCommand(GroupParseBidsSolution) + GroupCmd.AddCommand(GroupEnroll) + GroupCmd.AddCommand(GroupList) + GroupCmd.AddCommand(GroupLocate) + GroupCmd.AddCommand(GroupUserBids) } var GroupCmd = &cobra.Command{ - Use: "group", - Short: "Management of groups", + Use: "group", + Short: "Management of groups", } type groupSummary struct { - Count int `db:"count"` - GroupID int `db:"group_id"` - Description string `db:"description"` + Count int `db:"count"` + GroupID int `db:"group_id"` + Description string `db:"description"` } var GroupLocate = &cobra.Command{ - Use: "locate [courseID] [userID]", - Short: "locate a student in a group", - Long: `show the exercise group for a given student`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - courseID := MustInt64Parameter(args[0], "courseID") - userID := MustInt64Parameter(args[1], "userID") - - _, stores := MustConnectAndStores() - - user, err := stores.User.Get(userID) - if err != nil { - log.Fatalf("user with id %v not found\n", userID) - } - - course, err := stores.Course.Get(courseID) - if err != nil { - log.Fatalf("course with id %v not found\n", courseID) - } - - groups, err := stores.Group.GetInCourseWithUser(user.ID, course.ID) - failWhenSmallestWhiff(err) - - if len(groups) == 0 { - log.Fatalf("user %s %s (%d) is not enrolled as a student in course %s (%d)", - user.FirstName, user.LastName, user.ID, course.Name, course.ID) - } - - group := groups[0] - - fmt.Printf("found\n") - fmt.Printf(" - Group (%d): %s\n", group.ID, group.Description) - fmt.Printf(" - Tutor (%d): %s %s\n", group.TutorID, group.TutorFirstName, group.TutorLastName) - fmt.Printf(" - Student (%d): %s %s \n", user.ID, user.FirstName, user.LastName) - - }, + Use: "locate [courseID] [userID]", + Short: "locate a student in a group", + Long: `show the exercise group for a given student`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + courseID := MustInt64Parameter(args[0], "courseID") + userID := MustInt64Parameter(args[1], "userID") + + _, stores := MustConnectAndStores() + + user, err := stores.User.Get(userID) + if err != nil { + log.Fatalf("user with id %v not found\n", userID) + } + + course, err := stores.Course.Get(courseID) + if err != nil { + log.Fatalf("course with id %v not found\n", courseID) + } + + groups, err := stores.Group.GetInCourseWithUser(user.ID, course.ID) + failWhenSmallestWhiff(err) + + if len(groups) == 0 { + log.Fatalf("user %s %s (%d) is not enrolled as a student in course %s (%d)", + user.FirstName, user.LastName, user.ID, course.Name, course.ID) + } + + group := groups[0] + + fmt.Printf("found\n") + fmt.Printf(" - Group (%d): %s\n", group.ID, group.Description) + fmt.Printf(" - Tutor (%d): %s %s\n", group.TutorID, group.TutorFirstName, group.TutorLastName) + fmt.Printf(" - Student (%d): %s %s \n", user.ID, user.FirstName, user.LastName) + + }, } var GroupList = &cobra.Command{ - Use: "list [courseID]", - Short: "list all groups from a specific course", - Long: `shows information about exercise groups with their description and + Use: "list [courseID]", + Short: "list all groups from a specific course", + Long: `shows information about exercise groups with their description and number of assigned students`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - courseID := MustInt64Parameter(args[0], "courseID") + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + courseID := MustInt64Parameter(args[0], "courseID") - db, _ := MustConnectAndStores() + db, _ := MustConnectAndStores() - groupSummaries := []groupSummary{} + groupSummaries := []groupSummary{} - err := db.Select(&groupSummaries, ` + err := db.Select(&groupSummaries, ` SELECT count(*), ug.group_id, g.description FROM @@ -117,112 +117,112 @@ GROUP BY ug.group_id, g.description ORDER BY g.description `, courseID) - failWhenSmallestWhiff(err) - - fmt.Printf("count groupID description\n") - for k, v := range groupSummaries { - fmt.Printf("%5d %7d %s\n", v.Count, v.GroupID, v.Description) - if k%5 == 0 { - fmt.Println("") - } - } - }, + failWhenSmallestWhiff(err) + + fmt.Printf("count groupID description\n") + for k, v := range groupSummaries { + fmt.Printf("%5d %7d %s\n", v.Count, v.GroupID, v.Description) + if k%5 == 0 { + fmt.Println("") + } + } + }, } var GroupEnroll = &cobra.Command{ - Use: "enroll [groupID] [userID]", - Short: "enroll a student to a group", - Long: `enroll a student to a group or update enrollment if student is + Use: "enroll [groupID] [userID]", + Short: "enroll a student to a group", + Long: `enroll a student to a group or update enrollment if student is already enrolled in another group`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - groupID := MustInt64Parameter(args[0], "groupID") - userID := MustInt64Parameter(args[1], "userID") + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + groupID := MustInt64Parameter(args[0], "groupID") + userID := MustInt64Parameter(args[1], "userID") - // same as POST "/courses/{course_id}/groups/{group_id}/enrollments" - // TODO(patwie): good candidate for a remote cli + // same as POST "/courses/{course_id}/groups/{group_id}/enrollments" + // TODO(patwie): good candidate for a remote cli - _, stores := MustConnectAndStores() + _, stores := MustConnectAndStores() - user, err := stores.User.Get(userID) - failWhenSmallestWhiff(err) + user, err := stores.User.Get(userID) + failWhenSmallestWhiff(err) - course, err := stores.Group.IdentifyCourseOfGroup(groupID) - failWhenSmallestWhiff(err) + course, err := stores.Group.IdentifyCourseOfGroup(groupID) + failWhenSmallestWhiff(err) - enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(userID, course.ID) + enrollment, err := stores.Group.GetGroupEnrollmentOfUserInCourse(userID, course.ID) - if err != nil { - // does not exists yet - enrollment := &model.GroupEnrollment{ - UserID: userID, - GroupID: groupID, - } + if err != nil { + // does not exists yet + enrollment := &model.GroupEnrollment{ + UserID: userID, + GroupID: groupID, + } - _, err := stores.Group.CreateGroupEnrollmentOfUserInCourse(enrollment) - failWhenSmallestWhiff(err) + _, err := stores.Group.CreateGroupEnrollmentOfUserInCourse(enrollment) + failWhenSmallestWhiff(err) - } else { - group, err := stores.Group.Get(enrollment.GroupID) - failWhenSmallestWhiff(err) + } else { + group, err := stores.Group.Get(enrollment.GroupID) + failWhenSmallestWhiff(err) - fmt.Printf("user %s %s (id: %v) was enrolled in group (%v) %s\n", - user.FirstName, - user.LastName, - user.ID, - group.ID, group.Description) + fmt.Printf("user %s %s (id: %v) was enrolled in group (%v) %s\n", + user.FirstName, + user.LastName, + user.ID, + group.ID, group.Description) - // does exists --> simply change it - enrollment.GroupID = groupID - err = stores.Group.ChangeGroupEnrollmentOfUserInCourse(enrollment) - failWhenSmallestWhiff(err) + // does exists --> simply change it + enrollment.GroupID = groupID + err = stores.Group.ChangeGroupEnrollmentOfUserInCourse(enrollment) + failWhenSmallestWhiff(err) - } + } - group, err := stores.Group.Get(groupID) - failWhenSmallestWhiff(err) + group, err := stores.Group.Get(groupID) + failWhenSmallestWhiff(err) - fmt.Printf("user %s %s (id: %v) is now enrolled in group (%v) %s\n", - user.FirstName, - user.LastName, - user.ID, - group.ID, group.Description) + fmt.Printf("user %s %s (id: %v) is now enrolled in group (%v) %s\n", + user.FirstName, + user.LastName, + user.ID, + group.ID, group.Description) - }, + }, } type userBidSummary struct { - Bid int `db:"bid"` - GroupID int `db:"group_id"` - Description string `db:"description"` + Bid int `db:"bid"` + GroupID int `db:"group_id"` + Description string `db:"description"` } var GroupUserBids = &cobra.Command{ - Use: "bids [courseID] [userID]", - Short: "list all bids of a user", - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - courseID := MustInt64Parameter(args[0], "courseID") - userID := MustInt64Parameter(args[1], "userID") + Use: "bids [courseID] [userID]", + Short: "list all bids of a user", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + courseID := MustInt64Parameter(args[0], "courseID") + userID := MustInt64Parameter(args[1], "userID") - // same as POST "/courses/{course_id}/groups/{group_id}/enrollments" - // TODO(patwie): good candidate for a remote cli + // same as POST "/courses/{course_id}/groups/{group_id}/enrollments" + // TODO(patwie): good candidate for a remote cli - db, stores := MustConnectAndStores() + db, stores := MustConnectAndStores() - user, err := stores.User.Get(userID) - if err != nil { - log.Fatalf("user with id %v not found\n", userID) - } + user, err := stores.User.Get(userID) + if err != nil { + log.Fatalf("user with id %v not found\n", userID) + } - course, err := stores.Course.Get(courseID) - if err != nil { - log.Fatalf("course with id %v not found\n", courseID) - } + course, err := stores.Course.Get(courseID) + if err != nil { + log.Fatalf("course with id %v not found\n", courseID) + } - userBidSummaries := []userBidSummary{} + userBidSummaries := []userBidSummary{} - err = db.Select(&userBidSummaries, ` + err = db.Select(&userBidSummaries, ` SELECT bid, group_id, description FROM @@ -233,94 +233,94 @@ WHERE AND g.course_id = $2 `, user.ID, course.ID) - failWhenSmallestWhiff(err) + failWhenSmallestWhiff(err) - fmt.Printf(" bid groupID description\n") - for k, v := range userBidSummaries { - fmt.Printf("%5d %7d %s\n", v.Bid, v.GroupID, v.Description) - if k%5 == 0 && k > 0 { - fmt.Println("") - } - } + fmt.Printf(" bid groupID description\n") + for k, v := range userBidSummaries { + fmt.Printf("%5d %7d %s\n", v.Bid, v.GroupID, v.Description) + if k%5 == 0 && k > 0 { + fmt.Println("") + } + } - }, + }, } var GroupReadBids = &cobra.Command{ - Use: "dump-bids [courseID] [file] [min_per_group] [max_per_group]", - Short: "export group-bids of all users in a course", - Long: `for assignment`, - Args: cobra.ExactArgs(4), - Run: func(cmd *cobra.Command, args []string) { - - courseID := MustInt64Parameter(args[0], "courseID") - minPerGroup := MustIntParameter(args[2], "minPerGroup") - maxPerGroup := MustIntParameter(args[3], "maxPerGroup") - - if minPerGroup > maxPerGroup { - log.Fatalf("minPerGroup %d > maxPerGroup %d is infeasible", - minPerGroup, maxPerGroup) - } - - fmt.Printf("bound Group Capacitiy >= %v\n", minPerGroup) - fmt.Printf("bound Group Capacitiy <= %v\n", maxPerGroup) - - f, err := os.Create(fmt.Sprintf("%s.dat", args[1])) - failWhenSmallestWhiff(err) - defer f.Close() - - db, stores := MustConnectAndStores() - - course, err := stores.Course.Get(courseID) - if err != nil { - log.Fatalf("course with id %v not found\n", course.ID) - } - - groups, err := stores.Group.GroupsOfCourse(course.ID) - failWhenSmallestWhiff(err) - - fmt.Printf("found %v groups\n", len(groups)) - - groupIDArray := []int64{} - for _, group := range groups { - groupIDArray = append(groupIDArray, group.ID) - } - // collect all user-ids to anonymize output - gids := strings.Trim(strings.Replace(fmt.Sprint(groupIDArray), " ", " g", -1), "[]") - - students, err := stores.Course.EnrolledUsers(course.ID, - []string{"0"}, - "%%", - "%%", - "%%", - "%%", - "%%", - ) - failWhenSmallestWhiff(err) - - fmt.Printf("found %v students\n", len(students)) - fmt.Printf("\n") - fmt.Printf("\n") - - // collect all user-ids to anonymize output - uids := "" - for _, student := range students { - g := strconv.FormatInt(student.ID, 10) - uids = uids + " u" + g - } - - f.WriteString(fmt.Sprintf("set group := g%s;\n", gids)) - f.WriteString(fmt.Sprintf("set student := %s;\n", uids)) - f.WriteString(fmt.Sprintf("\n")) - f.WriteString(fmt.Sprintf("param pref:\n")) - f.WriteString(fmt.Sprintf(" g%s:=\n", gids)) - - // this includes students without any selected preferences - for _, student := range students { - uid := strconv.FormatInt(student.ID, 10) - bids := []model.GroupBid{} - - err := db.Select(&bids, ` + Use: "dump-bids [courseID] [file] [min_per_group] [max_per_group]", + Short: "export group-bids of all users in a course", + Long: `for assignment`, + Args: cobra.ExactArgs(4), + Run: func(cmd *cobra.Command, args []string) { + + courseID := MustInt64Parameter(args[0], "courseID") + minPerGroup := MustIntParameter(args[2], "minPerGroup") + maxPerGroup := MustIntParameter(args[3], "maxPerGroup") + + if minPerGroup > maxPerGroup { + log.Fatalf("minPerGroup %d > maxPerGroup %d is infeasible", + minPerGroup, maxPerGroup) + } + + fmt.Printf("bound Group Capacitiy >= %v\n", minPerGroup) + fmt.Printf("bound Group Capacitiy <= %v\n", maxPerGroup) + + f, err := os.Create(fmt.Sprintf("%s.dat", args[1])) + failWhenSmallestWhiff(err) + defer f.Close() + + db, stores := MustConnectAndStores() + + course, err := stores.Course.Get(courseID) + if err != nil { + log.Fatalf("course with id %v not found\n", course.ID) + } + + groups, err := stores.Group.GroupsOfCourse(course.ID) + failWhenSmallestWhiff(err) + + fmt.Printf("found %v groups\n", len(groups)) + + groupIDArray := []int64{} + for _, group := range groups { + groupIDArray = append(groupIDArray, group.ID) + } + // collect all user-ids to anonymize output + gids := strings.Trim(strings.Replace(fmt.Sprint(groupIDArray), " ", " g", -1), "[]") + + students, err := stores.Course.EnrolledUsers(course.ID, + []string{"0"}, + "%%", + "%%", + "%%", + "%%", + "%%", + ) + failWhenSmallestWhiff(err) + + fmt.Printf("found %v students\n", len(students)) + fmt.Printf("\n") + fmt.Printf("\n") + + // collect all user-ids to anonymize output + uids := "" + for _, student := range students { + g := strconv.FormatInt(student.ID, 10) + uids = uids + " u" + g + } + + f.WriteString(fmt.Sprintf("set group := g%s;\n", gids)) + f.WriteString(fmt.Sprintf("set student := %s;\n", uids)) + f.WriteString(fmt.Sprintf("\n")) + f.WriteString(fmt.Sprintf("param pref:\n")) + f.WriteString(fmt.Sprintf(" g%s:=\n", gids)) + + // this includes students without any selected preferences + for _, student := range students { + uid := strconv.FormatInt(student.ID, 10) + bids := []model.GroupBid{} + + err := db.Select(&bids, ` SELECT * FROM @@ -331,137 +331,137 @@ AND group_id = ANY($2) ORDER BY group_id ASC`, student.ID, pq.Array(groupIDArray)) - if err != nil { - panic(err) - } - - f.WriteString(fmt.Sprintf("u%s", uid)) - - // make sure all students have a bid - // students without any preference for a course are given a 5 - // 0 means no interests, 10 means absolute favourite - // default is maximum to prefer students - for _, group := range groups { - bidValue := 10 - for _, bid := range bids { - if bid.GroupID == group.ID { - bidValue = bid.Bid - break - } - } - f.WriteString(fmt.Sprintf(" %v", bidValue)) - } - f.WriteString(fmt.Sprintf("\n")) - } - - f.WriteString(fmt.Sprintf(";\n")) - f.WriteString(fmt.Sprintf("\n")) - f.WriteString(fmt.Sprintf("end;\n")) - - // write mod - // - fmod, err := os.Create(fmt.Sprintf("%s.mod", args[1])) - failWhenSmallestWhiff(err) - defer fmod.Close() - - fmod.WriteString(fmt.Sprintf("set student;\n")) - fmod.WriteString(fmt.Sprintf("set group;\n")) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("var assign{i in student, j in group} binary;\n")) - fmod.WriteString(fmt.Sprintf("param pref{i in student, j in group};\n")) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("maximize totalPref:\n")) - fmod.WriteString(fmt.Sprintf(" sum{i in student, j in group} pref[i,j]*assign[i,j];\n")) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("subject to exactly_one_group {i in student}:\n")) - fmod.WriteString(fmt.Sprintf(" sum {j in group} assign[i,j] =1;\n")) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("subject to min3{j in group}:\n")) - fmod.WriteString(fmt.Sprintf(" sum{i in student} assign[i,j]>=%v;\n", minPerGroup)) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("subject to max4{j in group}:\n")) - fmod.WriteString(fmt.Sprintf(" sum{i in student} assign[i,j]<=%v;\n", maxPerGroup)) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("end;\n")) - fmod.WriteString(fmt.Sprintf("\n")) - fmod.WriteString(fmt.Sprintf("\n")) - - fmt.Println("run the command") - fmt.Println("") - fmt.Println("") - fmt.Printf("sudo docker run -v \"$PWD\":/data -it patwie/symphony /var/symphony/bin/symphony -F %s.mod -D /data/%s.dat -f /data/%s.par\n", args[1], args[1], args[1]) - fmt.Println("") - - fpar, err := os.Create(fmt.Sprintf("%s.par", args[1])) - failWhenSmallestWhiff(err) - fpar.WriteString(fmt.Sprintf("time_limit 50\n")) - fpar.WriteString(fmt.Sprintf("\n")) - defer fpar.Close() - - }, + if err != nil { + panic(err) + } + + f.WriteString(fmt.Sprintf("u%s", uid)) + + // make sure all students have a bid + // students without any preference for a course are given a 5 + // 0 means no interests, 10 means absolute favourite + // default is maximum to prefer students + for _, group := range groups { + bidValue := 10 + for _, bid := range bids { + if bid.GroupID == group.ID { + bidValue = bid.Bid + break + } + } + f.WriteString(fmt.Sprintf(" %v", bidValue)) + } + f.WriteString(fmt.Sprintf("\n")) + } + + f.WriteString(fmt.Sprintf(";\n")) + f.WriteString(fmt.Sprintf("\n")) + f.WriteString(fmt.Sprintf("end;\n")) + + // write mod + // + fmod, err := os.Create(fmt.Sprintf("%s.mod", args[1])) + failWhenSmallestWhiff(err) + defer fmod.Close() + + fmod.WriteString(fmt.Sprintf("set student;\n")) + fmod.WriteString(fmt.Sprintf("set group;\n")) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("var assign{i in student, j in group} binary;\n")) + fmod.WriteString(fmt.Sprintf("param pref{i in student, j in group};\n")) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("maximize totalPref:\n")) + fmod.WriteString(fmt.Sprintf(" sum{i in student, j in group} pref[i,j]*assign[i,j];\n")) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("subject to exactly_one_group {i in student}:\n")) + fmod.WriteString(fmt.Sprintf(" sum {j in group} assign[i,j] =1;\n")) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("subject to min3{j in group}:\n")) + fmod.WriteString(fmt.Sprintf(" sum{i in student} assign[i,j]>=%v;\n", minPerGroup)) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("subject to max4{j in group}:\n")) + fmod.WriteString(fmt.Sprintf(" sum{i in student} assign[i,j]<=%v;\n", maxPerGroup)) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("end;\n")) + fmod.WriteString(fmt.Sprintf("\n")) + fmod.WriteString(fmt.Sprintf("\n")) + + fmt.Println("run the command") + fmt.Println("") + fmt.Println("") + fmt.Printf("sudo docker run -v \"$PWD\":/data -it patwie/symphony /var/symphony/bin/symphony -F %s.mod -D /data/%s.dat -f /data/%s.par\n", args[1], args[1], args[1]) + fmt.Println("") + + fpar, err := os.Create(fmt.Sprintf("%s.par", args[1])) + failWhenSmallestWhiff(err) + fpar.WriteString(fmt.Sprintf("time_limit 50\n")) + fpar.WriteString(fmt.Sprintf("\n")) + defer fpar.Close() + + }, } var GroupParseBidsSolution = &cobra.Command{ - // ./infomark console submission enqueue 24 10 24 "test_java_submission:v1" - // cp files/fixtures/unittest.zip files/uploads/tasks/24-public.zip - // cp files/fixtures/submission.zip files/uploads/submissions/10.zip - - Use: "import-assignments [courseID] [file]", - Short: "parse solution and assign students to groups", - Long: `for assignment`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - - type Assignment struct { - GroupID int64 - UserID int64 - } - - courseID := MustInt64Parameter(args[0], "courseID") - - db, stores := MustConnectAndStores() - - course, err := stores.Course.Get(courseID) - if err != nil { - log.Fatalf("course with id %v not found\n", course.ID) - } - fmt.Println("work on course", course.ID) - - file, err := os.Open(args[1]) - failWhenSmallestWhiff(err) - defer file.Close() - - reader := bufio.NewReader(file) - uids := []int64{} - assignments := []Assignment{} - for { - line, _, err := reader.ReadLine() - if err == io.EOF { - break - } - // parse line - string := fmt.Sprintf("%s", line) - if strings.HasPrefix(string, "assign[") { - parts := strings.Split(string, "[") - parts = strings.Split(parts[1], "]") - parts = strings.Split(parts[0], ",") - - v, err := strconv.Atoi(parts[0][1:]) - failWhenSmallestWhiff(err) - w, err := strconv.Atoi(parts[1][1:]) - failWhenSmallestWhiff(err) - - assignments = append(assignments, Assignment{GroupID: int64(w), UserID: int64(v)}) - uids = append(uids, int64(v)) - } - - } - - // we perform the update as a transaction - tx, err := db.Begin() - // delete assignments so far - _, err = tx.Exec(` + // ./infomark console submission enqueue 24 10 24 "test_java_submission:v1" + // cp files/fixtures/unittest.zip files/uploads/tasks/24-public.zip + // cp files/fixtures/submission.zip files/uploads/submissions/10.zip + + Use: "import-assignments [courseID] [file]", + Short: "parse solution and assign students to groups", + Long: `for assignment`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + + type Assignment struct { + GroupID int64 + UserID int64 + } + + courseID := MustInt64Parameter(args[0], "courseID") + + db, stores := MustConnectAndStores() + + course, err := stores.Course.Get(courseID) + if err != nil { + log.Fatalf("course with id %v not found\n", course.ID) + } + fmt.Println("work on course", course.ID) + + file, err := os.Open(args[1]) + failWhenSmallestWhiff(err) + defer file.Close() + + reader := bufio.NewReader(file) + uids := []int64{} + assignments := []Assignment{} + for { + line, _, err := reader.ReadLine() + if err == io.EOF { + break + } + // parse line + string := fmt.Sprintf("%s", line) + if strings.HasPrefix(string, "assign[") { + parts := strings.Split(string, "[") + parts = strings.Split(parts[1], "]") + parts = strings.Split(parts[0], ",") + + v, err := strconv.Atoi(parts[0][1:]) + failWhenSmallestWhiff(err) + w, err := strconv.Atoi(parts[1][1:]) + failWhenSmallestWhiff(err) + + assignments = append(assignments, Assignment{GroupID: int64(w), UserID: int64(v)}) + uids = append(uids, int64(v)) + } + + } + + // we perform the update as a transaction + tx, err := db.Begin() + // delete assignments so far + _, err = tx.Exec(` DELETE FROM user_group ug USING @@ -472,30 +472,30 @@ AND g.course_id = $1 AND ug.user_id = ANY($2)`, course.ID, pq.Array(uids)) - if err != nil { - fmt.Println(err) - tx.Rollback() - failWhenSmallestWhiff(err) - } - - for _, assignment := range assignments { - fmt.Printf("%v %v\n", assignment.UserID, assignment.GroupID) - - _, err = tx.Exec("INSERT INTO user_group (id,user_id,group_id) VALUES (DEFAULT,$1,$2);", assignment.UserID, assignment.GroupID) - if err != nil { - tx.Rollback() - failWhenSmallestWhiff(err) - } - } - - // run transaction - err = tx.Commit() - if err != nil { - tx.Rollback() - failWhenSmallestWhiff(err) - } - - fmt.Println("Done") - - }, + if err != nil { + fmt.Println(err) + tx.Rollback() + failWhenSmallestWhiff(err) + } + + for _, assignment := range assignments { + fmt.Printf("%v %v\n", assignment.UserID, assignment.GroupID) + + _, err = tx.Exec("INSERT INTO user_group (id,user_id,group_id) VALUES (DEFAULT,$1,$2);", assignment.UserID, assignment.GroupID) + if err != nil { + tx.Rollback() + failWhenSmallestWhiff(err) + } + } + + // run transaction + err = tx.Commit() + if err != nil { + tx.Rollback() + failWhenSmallestWhiff(err) + } + + fmt.Println("Done") + + }, } diff --git a/cmd/console/helper.go b/cmd/console/helper.go index ff95855..07fd26a 100644 --- a/cmd/console/helper.go +++ b/cmd/console/helper.go @@ -19,56 +19,56 @@ package console import ( - "log" - "strconv" + "log" + "strconv" - "github.com/cgtuebingen/infomark-backend/api/app" - "github.com/jmoiron/sqlx" - "github.com/spf13/viper" + "github.com/cgtuebingen/infomark-backend/api/app" + "github.com/jmoiron/sqlx" + "github.com/spf13/viper" ) func failWhenSmallestWhiff(err error) { - if err != nil { - panic(err) - } + if err != nil { + panic(err) + } } func ConnectAndStores() (*sqlx.DB, *app.Stores, error) { - db, err := sqlx.Connect("postgres", viper.GetString("database_connection")) - if err != nil { - return nil, nil, err - } + db, err := sqlx.Connect("postgres", viper.GetString("database_connection")) + if err != nil { + return nil, nil, err + } - if err := db.Ping(); err != nil { - return nil, nil, err - } + if err := db.Ping(); err != nil { + return nil, nil, err + } - stores := app.NewStores(db) - return db, stores, nil + stores := app.NewStores(db) + return db, stores, nil } func MustConnectAndStores() (*sqlx.DB, *app.Stores) { - db, stores, err := ConnectAndStores() - failWhenSmallestWhiff(err) - return db, stores + db, stores, err := ConnectAndStores() + failWhenSmallestWhiff(err) + return db, stores } func MustInt64Parameter(argStr string, name string) int64 { - argInt, err := strconv.Atoi(argStr) - if err != nil { - log.Fatalf("cannot convert %s '%s' to int64\n", name, argStr) - return int64(0) - } - return int64(argInt) + argInt, err := strconv.Atoi(argStr) + if err != nil { + log.Fatalf("cannot convert %s '%s' to int64\n", name, argStr) + return int64(0) + } + return int64(argInt) } func MustIntParameter(argStr string, name string) int { - argInt, err := strconv.Atoi(argStr) - if err != nil { - log.Fatalf("cannot convert %s '%s' to int\n", name, argStr) - return int(0) - } - return int(argInt) + argInt, err := strconv.Atoi(argStr) + if err != nil { + log.Fatalf("cannot convert %s '%s' to int\n", name, argStr) + return int(0) + } + return int(argInt) } diff --git a/cmd/console/submission_cmd.go b/cmd/console/submission_cmd.go index 75b460e..76b58f1 100644 --- a/cmd/console/submission_cmd.go +++ b/cmd/console/submission_cmd.go @@ -19,184 +19,184 @@ package console import ( - "encoding/json" - "fmt" - "log" - - "github.com/cgtuebingen/infomark-backend/api/helper" - "github.com/cgtuebingen/infomark-backend/api/shared" - "github.com/cgtuebingen/infomark-backend/auth/authenticate" - "github.com/cgtuebingen/infomark-backend/service" - "github.com/spf13/cobra" - "github.com/spf13/viper" + "encoding/json" + "fmt" + "log" + + "github.com/cgtuebingen/infomark-backend/api/helper" + "github.com/cgtuebingen/infomark-backend/api/shared" + "github.com/cgtuebingen/infomark-backend/auth/authenticate" + "github.com/cgtuebingen/infomark-backend/service" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) func init() { - SubmissionCmd.AddCommand(SubmissionEnqueueCmd) - SubmissionCmd.AddCommand(SubmissionRunCmd) + SubmissionCmd.AddCommand(SubmissionEnqueueCmd) + SubmissionCmd.AddCommand(SubmissionRunCmd) } var SubmissionCmd = &cobra.Command{ - Use: "submission", - Short: "Management of submission", + Use: "submission", + Short: "Management of submission", } var SubmissionEnqueueCmd = &cobra.Command{ - Use: "enqeue [submissionID]", - Short: "put submission into testing queue", - Long: `will enqueue a submission again into the testing queue`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - submissionID := MustInt64Parameter(args[0], "submissionID") - - _, stores := MustConnectAndStores() - - submission, err := stores.Submission.Get(submissionID) - failWhenSmallestWhiff(err) - - task, err := stores.Task.Get(submission.TaskID) - failWhenSmallestWhiff(err) - - sheet, err := stores.Task.IdentifySheetOfTask(submission.TaskID) - failWhenSmallestWhiff(err) - - course, err := stores.Sheet.IdentifyCourseOfSheet(sheet.ID) - failWhenSmallestWhiff(err) - - grade, err := stores.Grade.GetForSubmission(submission.ID) - failWhenSmallestWhiff(err) - - log.Println("starting producer...") - - cfg := &service.Config{ - Connection: viper.GetString("rabbitmq_connection"), - Exchange: viper.GetString("rabbitmq_exchange"), - ExchangeType: viper.GetString("rabbitmq_exchangeType"), - Queue: viper.GetString("rabbitmq_queue"), - Key: viper.GetString("rabbitmq_key"), - Tag: "SimpleSubmission", - } - - sha256, err := helper.NewSubmissionFileHandle(submission.ID).Sha256() - failWhenSmallestWhiff(err) - - tokenManager, err := authenticate.NewTokenAuth() - failWhenSmallestWhiff(err) - accessToken, err := tokenManager.CreateAccessJWT( - authenticate.NewAccessClaims(1, true)) - failWhenSmallestWhiff(err) - - bodyPublic, err := json.Marshal(shared.NewSubmissionAMQPWorkerRequest( - course.ID, task.ID, submission.ID, grade.ID, - accessToken, viper.GetString("url"), task.PublicDockerImage.String, sha256, "public")) - if err != nil { - log.Fatalf("json.Marshal: %s", err) - } - - bodyPrivate, err := json.Marshal(shared.NewSubmissionAMQPWorkerRequest( - course.ID, task.ID, submission.ID, grade.ID, - accessToken, viper.GetString("url"), task.PrivateDockerImage.String, sha256, "private")) - if err != nil { - log.Fatalf("json.Marshal: %s", err) - } - - producer, _ := service.NewProducer(cfg) - producer.Publish(bodyPublic) - producer.Publish(bodyPrivate) - - }, + Use: "enqeue [submissionID]", + Short: "put submission into testing queue", + Long: `will enqueue a submission again into the testing queue`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + submissionID := MustInt64Parameter(args[0], "submissionID") + + _, stores := MustConnectAndStores() + + submission, err := stores.Submission.Get(submissionID) + failWhenSmallestWhiff(err) + + task, err := stores.Task.Get(submission.TaskID) + failWhenSmallestWhiff(err) + + sheet, err := stores.Task.IdentifySheetOfTask(submission.TaskID) + failWhenSmallestWhiff(err) + + course, err := stores.Sheet.IdentifyCourseOfSheet(sheet.ID) + failWhenSmallestWhiff(err) + + grade, err := stores.Grade.GetForSubmission(submission.ID) + failWhenSmallestWhiff(err) + + log.Println("starting producer...") + + cfg := &service.Config{ + Connection: viper.GetString("rabbitmq_connection"), + Exchange: viper.GetString("rabbitmq_exchange"), + ExchangeType: viper.GetString("rabbitmq_exchangeType"), + Queue: viper.GetString("rabbitmq_queue"), + Key: viper.GetString("rabbitmq_key"), + Tag: "SimpleSubmission", + } + + sha256, err := helper.NewSubmissionFileHandle(submission.ID).Sha256() + failWhenSmallestWhiff(err) + + tokenManager, err := authenticate.NewTokenAuth() + failWhenSmallestWhiff(err) + accessToken, err := tokenManager.CreateAccessJWT( + authenticate.NewAccessClaims(1, true)) + failWhenSmallestWhiff(err) + + bodyPublic, err := json.Marshal(shared.NewSubmissionAMQPWorkerRequest( + course.ID, task.ID, submission.ID, grade.ID, + accessToken, viper.GetString("url"), task.PublicDockerImage.String, sha256, "public")) + if err != nil { + log.Fatalf("json.Marshal: %s", err) + } + + bodyPrivate, err := json.Marshal(shared.NewSubmissionAMQPWorkerRequest( + course.ID, task.ID, submission.ID, grade.ID, + accessToken, viper.GetString("url"), task.PrivateDockerImage.String, sha256, "private")) + if err != nil { + log.Fatalf("json.Marshal: %s", err) + } + + producer, _ := service.NewProducer(cfg) + producer.Publish(bodyPublic) + producer.Publish(bodyPrivate) + + }, } var SubmissionRunCmd = &cobra.Command{ - Use: "run [submissionID]", - Short: "run tests for a submission without writing to db", - Long: `will enqueue a submission again into the testing queue`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - - submissionID := MustInt64Parameter(args[0], "submissionID") - - _, stores := MustConnectAndStores() - - submission, err := stores.Submission.Get(submissionID) - failWhenSmallestWhiff(err) - - task, err := stores.Task.Get(submission.TaskID) - failWhenSmallestWhiff(err) - - log.Println("try starting docker...") - - ds := service.NewDockerService() - defer ds.Client.Close() - - var exit int64 - var stdout string - - submissionHnd := helper.NewSubmissionFileHandle(submission.ID) - if !submissionHnd.Exists() { - log.Fatalf("submission file %s for id %v is missing", submissionHnd.Path(), submission.ID) - } - - // run public test - if task.PublicDockerImage.Valid { - frameworkHnd := helper.NewPublicTestFileHandle(task.ID) - if frameworkHnd.Exists() { - - log.Printf("use docker image \"%v\"\n", task.PublicDockerImage.String) - log.Printf("use framework file \"%v\"\n", frameworkHnd.Path()) - stdout, exit, err = ds.Run( - task.PublicDockerImage.String, - submissionHnd.Path(), - frameworkHnd.Path(), - viper.GetInt64("worker_docker_memory_bytes"), - ) - if err != nil { - log.Fatal(err) - } - - fmt.Println(" --- STDOUT -- BEGIN ---") - fmt.Println(stdout) - fmt.Println(" --- STDOUT -- END ---") - fmt.Printf("exit-code: %v\n", exit) - } else { - fmt.Println("skip public test, there is no framework file") - - } - - } else { - fmt.Println("skip public test, there is no docker file") - } - - // run private test - if task.PrivateDockerImage.Valid { - frameworkHnd := helper.NewPrivateTestFileHandle(task.ID) - if frameworkHnd.Exists() { - - log.Printf("use docker image \"%v\"\n", task.PrivateDockerImage.String) - log.Printf("use framework file \"%v\"\n", frameworkHnd.Path()) - stdout, exit, err = ds.Run( - task.PrivateDockerImage.String, - submissionHnd.Path(), - frameworkHnd.Path(), - viper.GetInt64("worker_docker_memory_bytes"), - ) - if err != nil { - log.Fatal(err) - } - - fmt.Println(" --- STDOUT -- BEGIN ---") - fmt.Println(stdout) - fmt.Println(" --- STDOUT -- END ---") - fmt.Printf("exit-code: %v\n", exit) - } else { - fmt.Println("skip private test, there is no framework file") - - } - - } else { - fmt.Println("skip private test, there is no docker file") - } - - }, + Use: "run [submissionID]", + Short: "run tests for a submission without writing to db", + Long: `will enqueue a submission again into the testing queue`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + submissionID := MustInt64Parameter(args[0], "submissionID") + + _, stores := MustConnectAndStores() + + submission, err := stores.Submission.Get(submissionID) + failWhenSmallestWhiff(err) + + task, err := stores.Task.Get(submission.TaskID) + failWhenSmallestWhiff(err) + + log.Println("try starting docker...") + + ds := service.NewDockerService() + defer ds.Client.Close() + + var exit int64 + var stdout string + + submissionHnd := helper.NewSubmissionFileHandle(submission.ID) + if !submissionHnd.Exists() { + log.Fatalf("submission file %s for id %v is missing", submissionHnd.Path(), submission.ID) + } + + // run public test + if task.PublicDockerImage.Valid { + frameworkHnd := helper.NewPublicTestFileHandle(task.ID) + if frameworkHnd.Exists() { + + log.Printf("use docker image \"%v\"\n", task.PublicDockerImage.String) + log.Printf("use framework file \"%v\"\n", frameworkHnd.Path()) + stdout, exit, err = ds.Run( + task.PublicDockerImage.String, + submissionHnd.Path(), + frameworkHnd.Path(), + viper.GetInt64("worker_docker_memory_bytes"), + ) + if err != nil { + log.Fatal(err) + } + + fmt.Println(" --- STDOUT -- BEGIN ---") + fmt.Println(stdout) + fmt.Println(" --- STDOUT -- END ---") + fmt.Printf("exit-code: %v\n", exit) + } else { + fmt.Println("skip public test, there is no framework file") + + } + + } else { + fmt.Println("skip public test, there is no docker file") + } + + // run private test + if task.PrivateDockerImage.Valid { + frameworkHnd := helper.NewPrivateTestFileHandle(task.ID) + if frameworkHnd.Exists() { + + log.Printf("use docker image \"%v\"\n", task.PrivateDockerImage.String) + log.Printf("use framework file \"%v\"\n", frameworkHnd.Path()) + stdout, exit, err = ds.Run( + task.PrivateDockerImage.String, + submissionHnd.Path(), + frameworkHnd.Path(), + viper.GetInt64("worker_docker_memory_bytes"), + ) + if err != nil { + log.Fatal(err) + } + + fmt.Println(" --- STDOUT -- BEGIN ---") + fmt.Println(stdout) + fmt.Println(" --- STDOUT -- END ---") + fmt.Printf("exit-code: %v\n", exit) + } else { + fmt.Println("skip private test, there is no framework file") + + } + + } else { + fmt.Println("skip private test, there is no docker file") + } + + }, } diff --git a/cmd/console/user_cmd.go b/cmd/console/user_cmd.go index 557fd4f..a773510 100644 --- a/cmd/console/user_cmd.go +++ b/cmd/console/user_cmd.go @@ -19,38 +19,38 @@ package console import ( - "fmt" - "log" + "fmt" + "log" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/go-ozzo/ozzo-validation/is" - "github.com/spf13/cobra" - null "gopkg.in/guregu/null.v3" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/go-ozzo/ozzo-validation/is" + "github.com/spf13/cobra" + null "gopkg.in/guregu/null.v3" ) func init() { - UserCmd.AddCommand(UserFindCmd) - UserCmd.AddCommand(UserConfirmCmd) - UserCmd.AddCommand(UserSetEmailCmd) + UserCmd.AddCommand(UserFindCmd) + UserCmd.AddCommand(UserConfirmCmd) + UserCmd.AddCommand(UserSetEmailCmd) } var UserCmd = &cobra.Command{ - Use: "user", - Short: "Management of users", + Use: "user", + Short: "Management of users", } var UserFindCmd = &cobra.Command{ - Use: "find [query]", - Short: "find user by first_name, last_name or email", - Long: `List all users matching the query`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - db, _ := MustConnectAndStores() + Use: "find [query]", + Short: "find user by first_name, last_name or email", + Long: `List all users matching the query`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + db, _ := MustConnectAndStores() - query := fmt.Sprintf("%%%s%%", args[0]) + query := fmt.Sprintf("%%%s%%", args[0]) - users := []model.User{} - err := db.Select(&users, ` + users := []model.User{} + err := db.Select(&users, ` SELECT * FROM @@ -61,75 +61,75 @@ OR first_name LIKE $1 OR email LIKE $1`, query) - failWhenSmallestWhiff(err) - - fmt.Printf("found %v users matching %s\n", len(users), query) - for k, user := range users { - fmt.Printf("%4d %20s %20s %50s\n", - user.ID, user.FirstName, user.LastName, user.Email) - if k%10 == 0 && k != 0 { - fmt.Println("") - } - } - - fmt.Printf("found %v users matching %s\n", len(users), query) - }, + failWhenSmallestWhiff(err) + + fmt.Printf("found %v users matching %s\n", len(users), query) + for k, user := range users { + fmt.Printf("%4d %20s %20s %50s\n", + user.ID, user.FirstName, user.LastName, user.Email) + if k%10 == 0 && k != 0 { + fmt.Println("") + } + } + + fmt.Printf("found %v users matching %s\n", len(users), query) + }, } var UserConfirmCmd = &cobra.Command{ - Use: "confirm [email]", - Short: "confirms the email address manually", - Long: `Will run confirmation procedure for an user `, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - _, stores := MustConnectAndStores() - - email := args[0] - if err := is.Email.Validate(email); err != nil { - log.Fatalf("email '%s' is not a valid email\n", email) - } - - user, err := stores.User.FindByEmail(email) - if err != nil { - log.Fatalf("user with email %v not found\n", email) - } - - user.ConfirmEmailToken = null.String{} - if err := stores.User.Update(user); err != nil { - panic(err) - } - - fmt.Printf("email %s of user %s %s has been confirmed\n", - email, user.FirstName, user.LastName) - }, + Use: "confirm [email]", + Short: "confirms the email address manually", + Long: `Will run confirmation procedure for an user `, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, stores := MustConnectAndStores() + + email := args[0] + if err := is.Email.Validate(email); err != nil { + log.Fatalf("email '%s' is not a valid email\n", email) + } + + user, err := stores.User.FindByEmail(email) + if err != nil { + log.Fatalf("user with email %v not found\n", email) + } + + user.ConfirmEmailToken = null.String{} + if err := stores.User.Update(user); err != nil { + panic(err) + } + + fmt.Printf("email %s of user %s %s has been confirmed\n", + email, user.FirstName, user.LastName) + }, } var UserSetEmailCmd = &cobra.Command{ - Use: "set-email [userID] [email]", - Short: "will alter the email address", - Long: `Will change email address of an user without confirmation procedure`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - userID := MustInt64Parameter(args[0], "userID") - email := args[1] - if err := is.Email.Validate(email); err != nil { - log.Fatalf("email '%s' is not a valid email\n", email) - } - - _, stores := MustConnectAndStores() - - user, err := stores.User.Get(userID) - if err != nil { - fmt.Printf("user with id %v not found\n", userID) - return - } - - user.Email = email - if err := stores.User.Update(user); err != nil { - panic(err) - } - - fmt.Printf("email of user %s %s is now %s\n", - user.FirstName, user.LastName, user.Email) - }, + Use: "set-email [userID] [email]", + Short: "will alter the email address", + Long: `Will change email address of an user without confirmation procedure`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + userID := MustInt64Parameter(args[0], "userID") + email := args[1] + if err := is.Email.Validate(email); err != nil { + log.Fatalf("email '%s' is not a valid email\n", email) + } + + _, stores := MustConnectAndStores() + + user, err := stores.User.Get(userID) + if err != nil { + fmt.Printf("user with id %v not found\n", userID) + return + } + + user.Email = email + if err := stores.User.Update(user); err != nil { + panic(err) + } + + fmt.Printf("email of user %s %s is now %s\n", + user.FirstName, user.LastName, user.Email) + }, } diff --git a/common/common.go b/common/common.go index aa72818..cb1d195 100644 --- a/common/common.go +++ b/common/common.go @@ -26,15 +26,15 @@ type key int // r.Context().Value(common.CtxKeyCourse) // TODO(): create a shared context-key package const ( - CtxKeyAccessClaims key = iota // must be 0 to work with the auth-package - CtxKeyGroup key = iota - CtxKeyMaterial key = iota - CtxKeyCourse key = iota - CtxKeyCourseRole key = iota - CtxKeyUser key = iota - CtxKeyTask key = iota - CtxKeySubmission key = iota - CtxKeySheet key = iota - CtxKeyGrade key = iota - // ... + CtxKeyAccessClaims key = iota // must be 0 to work with the auth-package + CtxKeyGroup key = iota + CtxKeyMaterial key = iota + CtxKeyCourse key = iota + CtxKeyCourseRole key = iota + CtxKeyUser key = iota + CtxKeyTask key = iota + CtxKeySubmission key = iota + CtxKeySheet key = iota + CtxKeyGrade key = iota + // ... ) diff --git a/database/course_store.go b/database/course_store.go index 299ac95..bdf2452 100644 --- a/database/course_store.go +++ b/database/course_store.go @@ -19,48 +19,48 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/auth/authorize" - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" - "github.com/lib/pq" + "github.com/cgtuebingen/infomark-backend/auth/authorize" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" ) type CourseStore struct { - db *sqlx.DB + db *sqlx.DB } func NewCourseStore(db *sqlx.DB) *CourseStore { - return &CourseStore{ - db: db, - } + return &CourseStore{ + db: db, + } } func (s *CourseStore) Get(courseID int64) (*model.Course, error) { - p := model.Course{ID: courseID} - err := s.db.Get(&p, "SELECT * FROM courses WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.Course{ID: courseID} + err := s.db.Get(&p, "SELECT * FROM courses WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *CourseStore) GetAll() ([]model.Course, error) { - p := []model.Course{} - err := s.db.Select(&p, "SELECT * FROM courses;") - return p, err + p := []model.Course{} + err := s.db.Select(&p, "SELECT * FROM courses;") + return p, err } func (s *CourseStore) Create(p *model.Course) (*model.Course, error) { - newID, err := Insert(s.db, "courses", p) - if err != nil { - return nil, err - } - return s.Get(newID) + newID, err := Insert(s.db, "courses", p) + if err != nil { + return nil, err + } + return s.Get(newID) } func (s *CourseStore) Update(p *model.Course) error { - return Update(s.db, "courses", p.ID, p) + return Update(s.db, "courses", p.ID, p) } func (s *CourseStore) UpdateRole(courseID, userID int64, role int) error { - _, err := s.db.Exec(` + _, err := s.db.Exec(` UPDATE user_course SET @@ -69,68 +69,68 @@ WHERE user_ID = $1 AND course_id = $2`, userID, courseID, role) - return err + return err } func (s *CourseStore) Delete(courseID int64) error { - // we handle the deletion iwth cascade foreign keys. - // This is just here in case we need again more complex logic. - // tx, err := s.db.Begin() + // we handle the deletion iwth cascade foreign keys. + // This is just here in case we need again more complex logic. + // tx, err := s.db.Begin() - // // disenroll all users - // if _, err = tx.Exec("DELETE FROM user_course WHERE course_id = $1;", courseID); err != nil { - // return err - // } + // // disenroll all users + // if _, err = tx.Exec("DELETE FROM user_course WHERE course_id = $1;", courseID); err != nil { + // return err + // } - // // remove all linked sheets - // if _, err = tx.Exec("DELETE FROM sheet_course WHERE course_id = $1;", courseID); err != nil { - // return err - // } + // // remove all linked sheets + // if _, err = tx.Exec("DELETE FROM sheet_course WHERE course_id = $1;", courseID); err != nil { + // return err + // } - // // remove course - // if _, err = tx.Exec("DELETE FROM courses WHERE id = $1;", courseID); err != nil { - // return err - // } + // // remove course + // if _, err = tx.Exec("DELETE FROM courses WHERE id = $1;", courseID); err != nil { + // return err + // } - // if err = tx.Commit(); err != nil { - // return err - // } - // - // return nil + // if err = tx.Commit(); err != nil { + // return err + // } + // + // return nil - return Delete(s.db, "courses", courseID) + return Delete(s.db, "courses", courseID) } func (s *CourseStore) Enroll(courseID int64, userID int64, role int64) error { - err := s.Disenroll(courseID, userID) - if err != nil { - return err - } - _, err = s.db.Exec(` + err := s.Disenroll(courseID, userID) + if err != nil { + return err + } + _, err = s.db.Exec(` INSERT INTO user_course (id, user_id, course_id, role) VALUES (DEFAULT, $1, $2, $3); `, userID, courseID, role) - return err + return err } func (s *CourseStore) Disenroll(courseID int64, userID int64) error { - _, err := s.db.Exec(` + _, err := s.db.Exec(` DELETE FROM user_course WHERE user_id = $1 AND course_id = $2; `, userID, courseID) - return err + return err } func (s *CourseStore) GetUserEnrollment(courseID int64, userID int64) (*model.UserCourse, error) { - p := model.UserCourse{} + p := model.UserCourse{} - // , u.avatar_path - err := s.db.Get(&p, ` + // , u.avatar_path + err := s.db.Get(&p, ` SELECT uc.role, u.id, @@ -148,19 +148,19 @@ WHERE uc.course_id = $1 AND u.id = $2`, courseID, userID, - ) - return &p, err + ) + return &p, err } func (s *CourseStore) FindEnrolledUsers( - courseID int64, - roleFilter []string, - filterQuery string, + courseID int64, + roleFilter []string, + filterQuery string, ) ([]model.UserCourse, error) { - p := []model.UserCourse{} + p := []model.UserCourse{} - // , u.avatar_path - err := s.db.Select(&p, ` + // , u.avatar_path + err := s.db.Select(&p, ` SELECT uc.role, u.id, @@ -187,23 +187,23 @@ OR OR LOWER(u.email) LIKE $3 )`, courseID, pq.Array(roleFilter), - filterQuery, - ) - return p, err + filterQuery, + ) + return p, err } func (s *CourseStore) EnrolledUsers( - courseID int64, - roleFilter []string, - filterFirstName string, - filterLastName string, - filterEmail string, - filterSubject string, - filterLanguage string) ([]model.UserCourse, error) { - p := []model.UserCourse{} - - // , u.avatar_path - err := s.db.Select(&p, ` + courseID int64, + roleFilter []string, + filterFirstName string, + filterLastName string, + filterEmail string, + filterSubject string, + filterLanguage string) ([]model.UserCourse, error) { + p := []model.UserCourse{} + + // , u.avatar_path + err := s.db.Select(&p, ` SELECT uc.role, u.id, @@ -232,17 +232,17 @@ AND LOWER(u.subject) LIKE $6 AND LOWER(u.language) LIKE $7`, courseID, pq.Array(roleFilter), - filterFirstName, filterLastName, filterEmail, - filterSubject, filterLanguage, - ) - return p, err + filterFirstName, filterLastName, filterEmail, + filterSubject, filterLanguage, + ) + return p, err } // PointsForUser returns all gather points in a given course for a given user accumulated. func (s *CourseStore) PointsForUser(userID int64, courseID int64) ([]model.SheetPoints, error) { - p := []model.SheetPoints{} + p := []model.SheetPoints{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT SUM(g.acquired_points) acquired_points, SUM(t.max_points) max_points, @@ -262,15 +262,15 @@ GROUP BY ts.sheet_id ORDER BY ts.sheet_id`, userID, courseID, - ) - return p, err + ) + return p, err } func (s *CourseStore) RoleInCourse(userID int64, courseID int64) (authorize.CourseRole, error) { - var role_int int + var role_int int - err := s.db.Get(&role_int, ` + err := s.db.Get(&role_int, ` SELECT role FROM @@ -279,22 +279,22 @@ WHERE user_id = $1 AND course_id = $2`, - userID, courseID, - ) - if err != nil { - // meaning there is no entry - return authorize.NOCOURSEROLE, nil - } else { - switch role_int { - case 0: - return authorize.STUDENT, nil - case 1: - return authorize.TUTOR, nil - case 2: - return authorize.ADMIN, nil - default: - return authorize.NOCOURSEROLE, nil - } - } + userID, courseID, + ) + if err != nil { + // meaning there is no entry + return authorize.NOCOURSEROLE, nil + } else { + switch role_int { + case 0: + return authorize.STUDENT, nil + case 1: + return authorize.TUTOR, nil + case 2: + return authorize.ADMIN, nil + default: + return authorize.NOCOURSEROLE, nil + } + } } diff --git a/database/grade_store.go b/database/grade_store.go index 97eb153..fdca9e2 100644 --- a/database/grade_store.go +++ b/database/grade_store.go @@ -19,18 +19,18 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type GradeStore struct { - db *sqlx.DB + db *sqlx.DB } func NewGradeStore(db *sqlx.DB) *GradeStore { - return &GradeStore{ - db: db, - } + return &GradeStore{ + db: db, + } } // func (s *GradeStore) Get(id int64) (*model.Grade, error) { @@ -40,8 +40,8 @@ func NewGradeStore(db *sqlx.DB) *GradeStore { // } func (s *GradeStore) Get(id int64) (*model.Grade, error) { - p := model.Grade{ID: id} - err := s.db.Get(&p, ` + p := model.Grade{ID: id} + err := s.db.Get(&p, ` SELECT g.*, s.user_id, @@ -55,19 +55,19 @@ INNER JOIN users u ON s.user_id = u.id WHERE g.id = $1 LIMIT 1 `, p.ID) - return &p, err + return &p, err } func (s *GradeStore) Create(p *model.Grade) (*model.Grade, error) { - newID, err := Insert(s.db, "grades", p) - if err != nil { - return nil, err - } - return s.Get(newID) + newID, err := Insert(s.db, "grades", p) + if err != nil { + return nil, err + } + return s.Get(newID) } func (s *GradeStore) UpdatePrivateTestInfo(gradeID int64, log string, status int) error { - _, err := s.db.Exec(` + _, err := s.db.Exec(` UPDATE grades SET private_execution_state=2, @@ -75,27 +75,27 @@ SET private_test_status=$3 WHERE id = $1 `, gradeID, log, status) - return err + return err } func (s *GradeStore) UpdatePublicTestInfo(gradeID int64, log string, status int) error { - _, err := s.db.Exec(` + _, err := s.db.Exec(` UPDATE grades SET public_execution_state=2, public_test_log=$2, public_test_status=$3 WHERE id = $1 `, gradeID, log, status) - return err + return err } func (s *GradeStore) GetForSubmission(id int64) (*model.Grade, error) { - p := model.Grade{} - err := s.db.Get(&p, "SELECT * FROM grades WHERE submission_id = $1 LIMIT 1;", id) - return &p, err + p := model.Grade{} + err := s.db.Get(&p, "SELECT * FROM grades WHERE submission_id = $1 LIMIT 1;", id) + return &p, err } func (s *GradeStore) GetOverviewGrades(courseID int64, groupID int64) ([]model.OverviewGrade, error) { - p := []model.OverviewGrade{} - err := s.db.Select(&p, ` + p := []model.OverviewGrade{} + err := s.db.Select(&p, ` SELECT sum(g.acquired_points) points, s.user_id, @@ -132,14 +132,14 @@ GROUP BY ORDER BY s.user_id `, courseID, groupID) - return p, err + return p, err } func (s *GradeStore) GetAllMissingGrades(courseID int64, tutorID int64, groupID int64) ([]model.MissingGrade, error) { - p := []model.MissingGrade{} + p := []model.MissingGrade{} - err := s.db.Select(&p, - ` + err := s.db.Select(&p, + ` SELECT g.*, ts.task_id, @@ -163,31 +163,31 @@ AND AND ($3 = 0 OR ug.group_id = $3) `, tutorID, courseID, groupID) - return p, err + return p, err } func (s *GradeStore) Update(p *model.Grade) error { - return Update(s.db, "grades", p.ID, p) + return Update(s.db, "grades", p.ID, p) } func (s *GradeStore) GetFiltered( - courseID int64, - sheetID int64, - taskID int64, - groupID int64, - userID int64, - tutorID int64, - feedback string, - acquiredPoints int, - publicTestStatus int, - privateTestStatus int, - publicExecutationState int, - privateExecutationState int, + courseID int64, + sheetID int64, + taskID int64, + groupID int64, + userID int64, + tutorID int64, + feedback string, + acquiredPoints int, + publicTestStatus int, + privateTestStatus int, + publicExecutationState int, + privateExecutationState int, ) ([]model.Grade, error) { - p := []model.Grade{} - err := s.db.Select(&p, - ` + p := []model.Grade{} + err := s.db.Select(&p, + ` SELECT g.*, s.user_id, u.last_name user_last_name, @@ -225,28 +225,28 @@ AND AND ($12 = -1 OR g.private_execution_state = $12) `, - // AND ($4 = 0 OR ug.group_id = $4) - courseID, // $1 - sheetID, // $2 - taskID, // $3 - groupID, // $4 - userID, // $5 - tutorID, // $6 - feedback, // $7 - acquiredPoints, // $8 - publicTestStatus, // $9 - privateTestStatus, // $10 - publicExecutationState, // $11 - privateExecutationState, // $12 - ) - return p, err + // AND ($4 = 0 OR ug.group_id = $4) + courseID, // $1 + sheetID, // $2 + taskID, // $3 + groupID, // $4 + userID, // $5 + tutorID, // $6 + feedback, // $7 + acquiredPoints, // $8 + publicTestStatus, // $9 + privateTestStatus, // $10 + publicExecutationState, // $11 + privateExecutationState, // $12 + ) + return p, err } func (s *GradeStore) IdentifyCourseOfGrade(gradeID int64) (*model.Course, error) { - course := &model.Course{} - err := s.db.Get(course, - ` + course := &model.Course{} + err := s.db.Get(course, + ` SELECT c.* FROM @@ -257,19 +257,19 @@ INNER JOIN sheet_course sc ON sc.sheet_id = ts.sheet_id INNER JOIN courses c ON sc.course_id = c.id WHERE g.id = $1`, - gradeID) - if err != nil { - return nil, err - } + gradeID) + if err != nil { + return nil, err + } - return course, err + return course, err } func (s *GradeStore) IdentifyTaskOfGrade(gradeID int64) (*model.Task, error) { - task := &model.Task{} - err := s.db.Get(task, - ` + task := &model.Task{} + err := s.db.Get(task, + ` SELECT t.* FROM @@ -279,10 +279,10 @@ INNER JOIN task_sheet ts ON ts.task_id = s.task_id INNER JOIN tasks t ON ts.task_id = t.id WHERE g.id = $1`, - gradeID) - if err != nil { - return nil, err - } + gradeID) + if err != nil { + return nil, err + } - return task, err + return task, err } diff --git a/database/group_store.go b/database/group_store.go index a217374..4814772 100644 --- a/database/group_store.go +++ b/database/group_store.go @@ -19,55 +19,55 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" - "github.com/lib/pq" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" ) type GroupStore struct { - db *sqlx.DB + db *sqlx.DB } func NewGroupStore(db *sqlx.DB) *GroupStore { - return &GroupStore{ - db: db, - } + return &GroupStore{ + db: db, + } } func (s *GroupStore) Get(groupID int64) (*model.Group, error) { - p := model.Group{ID: groupID} - err := s.db.Get(&p, "SELECT * FROM groups WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.Group{ID: groupID} + err := s.db.Get(&p, "SELECT * FROM groups WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *GroupStore) GetAll() ([]model.Group, error) { - p := []model.Group{} - err := s.db.Select(&p, "SELECT * FROM groups;") - return p, err + p := []model.Group{} + err := s.db.Select(&p, "SELECT * FROM groups;") + return p, err } func (s *GroupStore) Create(p *model.Group) (*model.Group, error) { - // create Group - newID, err := Insert(s.db, "groups", p) - if err != nil { - return nil, err - } + // create Group + newID, err := Insert(s.db, "groups", p) + if err != nil { + return nil, err + } - return s.Get(newID) + return s.Get(newID) } func (s *GroupStore) Update(p *model.Group) error { - return Update(s.db, "groups", p.ID, p) + return Update(s.db, "groups", p.ID, p) } func (s *GroupStore) Delete(taskID int64) error { - return Delete(s.db, "groups", taskID) + return Delete(s.db, "groups", taskID) } func (s *GroupStore) GroupsOfCourse(courseID int64) ([]model.GroupWithTutor, error) { - p := []model.GroupWithTutor{} + p := []model.GroupWithTutor{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT g.*, u.first_name as tutor_first_name, @@ -82,13 +82,13 @@ WHERE course_id = $1 ORDER BY g.id ASC`, courseID) - return p, err + return p, err } func (s *GroupStore) GetMembers(groupID int64) ([]model.User, error) { - p := []model.User{} + p := []model.User{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT u.* FROM @@ -97,16 +97,16 @@ INNER JOIN user_group ug ON ug.user_id = u.id WHERE ug.group_id = $1`, groupID) - return p, err + return p, err } func (s *GroupStore) GetInCourseWithUser(userID int64, courseID int64) ([]model.GroupWithTutor, error) { - // This is a list as it is used at the sample places where tutors will get a LIST - // of their groups (can be multiple ones). For the sake of simplicity, this - // is a list as well - p := []model.GroupWithTutor{} + // This is a list as it is used at the sample places where tutors will get a LIST + // of their groups (can be multiple ones). For the sake of simplicity, this + // is a list as well + p := []model.GroupWithTutor{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT g.*, u.first_name as tutor_first_name, @@ -124,22 +124,22 @@ AND ug.user_id = $1 ORDER BY g.id ASC`, userID, courseID) - return p, err + return p, err } func (s *GroupStore) EnrolledUsers( - courseID int64, - groupID int64, - roleFilter []string, - filterFirstName string, - filterLastName string, - filterEmail string, - filterSubject string, - filterLanguage string) ([]model.UserCourse, error) { - p := []model.UserCourse{} - - // , u.avatar_path - err := s.db.Select(&p, ` + courseID int64, + groupID int64, + roleFilter []string, + filterFirstName string, + filterLastName string, + filterEmail string, + filterSubject string, + filterLanguage string) ([]model.UserCourse, error) { + p := []model.UserCourse{} + + // , u.avatar_path + err := s.db.Select(&p, ` SELECT uc.role, u.id, @@ -172,15 +172,15 @@ AND AND LOWER(u.language) LIKE $8 `, courseID, groupID, pq.Array(roleFilter), - filterFirstName, filterLastName, filterEmail, - filterSubject, filterLanguage, - ) - return p, err + filterFirstName, filterLastName, filterEmail, + filterSubject, filterLanguage, + ) + return p, err } func (s *GroupStore) GetGroupEnrollmentOfUserInCourse(userID int64, courseID int64) (*model.GroupEnrollment, error) { - p := &model.GroupEnrollment{} - err := s.db.Get(p, ` + p := &model.GroupEnrollment{} + err := s.db.Get(p, ` SELECT ug.* FROM @@ -190,33 +190,33 @@ WHERE ug.user_id = $1 AND g.course_id = $2`, userID, courseID) - return p, err + return p, err } func (s *GroupStore) CreateGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) (*model.GroupEnrollment, error) { - newID, err := Insert(s.db, "user_group", p) - if err != nil { - return nil, err - } + newID, err := Insert(s.db, "user_group", p) + if err != nil { + return nil, err + } - res := &model.GroupEnrollment{} + res := &model.GroupEnrollment{} - err = s.db.Get(res, `SELECT * FROM user_group WHERE id= $1`, newID) - if err != nil { - return nil, err - } + err = s.db.Get(res, `SELECT * FROM user_group WHERE id= $1`, newID) + if err != nil { + return nil, err + } - return res, nil + return res, nil } func (s *GroupStore) ChangeGroupEnrollmentOfUserInCourse(p *model.GroupEnrollment) error { - return Update(s.db, "user_group", p.ID, p) + return Update(s.db, "user_group", p.ID, p) } func (s *GroupStore) GetOfTutor(tutorID int64, courseID int64) ([]model.GroupWithTutor, error) { - p := []model.GroupWithTutor{} + p := []model.GroupWithTutor{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT g.*, u.first_name as tutor_first_name, @@ -233,30 +233,30 @@ AND g.tutor_id = $1 ORDER BY g.id ASC`, tutorID, courseID) - return p, err + return p, err } func (s *GroupStore) IdentifyCourseOfGroup(groupID int64) (*model.Course, error) { - course := &model.Course{} - err := s.db.Get(course, - ` + course := &model.Course{} + err := s.db.Get(course, + ` SELECT c.* FROM groups g INNER JOIN courses c ON c.id = g.course_ID WHERE g.id = $1`, - groupID) - if err != nil { - return nil, err - } + groupID) + if err != nil { + return nil, err + } - return course, err + return course, err } func (s *GroupStore) GetBidOfUserForGroup(userID int64, groupID int64) (bid int, err error) { - err = s.db.Get(&bid, ` + err = s.db.Get(&bid, ` SELECT bid FROM @@ -264,24 +264,24 @@ FROM WHERE user_id = $1 and group_id = $2 LIMIT 1`, userID, groupID) - return bid, err + return bid, err } func (s *GroupStore) InsertBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) { - // insert - _, err := Insert(s.db, "group_bids", &model.GroupBid{UserID: userID, GroupID: groupID, Bid: bid}) - if err != nil { - return 0, err - } + // insert + _, err := Insert(s.db, "group_bids", &model.GroupBid{UserID: userID, GroupID: groupID, Bid: bid}) + if err != nil { + return 0, err + } - return s.GetBidOfUserForGroup(userID, groupID) + return s.GetBidOfUserForGroup(userID, groupID) } func (s *GroupStore) UpdateBidOfUserForGroup(userID int64, groupID int64, bid int) (int, error) { - // update - _, err := s.db.Exec(` + // update + _, err := s.db.Exec(` UPDATE group_bids SET bid = $3 @@ -289,18 +289,18 @@ WHERE user_id = $1 AND group_id = $2`, userID, groupID, bid) - if err != nil { - return 0, err - } + if err != nil { + return 0, err + } - return s.GetBidOfUserForGroup(userID, groupID) + return s.GetBidOfUserForGroup(userID, groupID) } func (s *GroupStore) GetBidsForCourseForUser(courseID int64, userID int64) ([]model.GroupBid, error) { - p := []model.GroupBid{} + p := []model.GroupBid{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT gb.* FROM @@ -310,15 +310,15 @@ WHERE gb.user_id = $2 AND g.course_id = $1`, courseID, userID) - return p, err + return p, err } func (s *GroupStore) GetBidsForCourse(courseID int64) ([]model.GroupBid, error) { - p := []model.GroupBid{} + p := []model.GroupBid{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT gb.* FROM @@ -326,6 +326,6 @@ FROM INNER JOIN groups g ON gb.group_id = g.id AND g.course_id = $1`, courseID) - return p, err + return p, err } diff --git a/database/material_store.go b/database/material_store.go index f22548f..36fbee0 100644 --- a/database/material_store.go +++ b/database/material_store.go @@ -19,65 +19,65 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type MaterialStore struct { - db *sqlx.DB + db *sqlx.DB } func NewMaterialStore(db *sqlx.DB) *MaterialStore { - return &MaterialStore{ - db: db, - } + return &MaterialStore{ + db: db, + } } func (s *MaterialStore) GetAll() ([]model.Material, error) { - p := []model.Material{} - err := s.db.Select(&p, "SELECT * FROM materials;") - return p, err + p := []model.Material{} + err := s.db.Select(&p, "SELECT * FROM materials;") + return p, err } func (s *MaterialStore) Get(sheetID int64) (*model.Material, error) { - p := model.Material{ID: sheetID} - err := s.db.Get(&p, "SELECT * FROM materials WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.Material{ID: sheetID} + err := s.db.Get(&p, "SELECT * FROM materials WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *MaterialStore) Create(p *model.Material, courseID int64) (*model.Material, error) { - newID, err := Insert(s.db, "materials", p) - if err != nil { - return nil, err - } + newID, err := Insert(s.db, "materials", p) + if err != nil { + return nil, err + } - // now associate sheet with course - _, err = s.db.Exec(` + // now associate sheet with course + _, err = s.db.Exec(` INSERT INTO material_course (id,material_id,course_id) VALUES (DEFAULT, $1, $2);`, - newID, courseID) - if err != nil { - return nil, err - } + newID, courseID) + if err != nil { + return nil, err + } - return s.Get(newID) + return s.Get(newID) } func (s *MaterialStore) Update(p *model.Material) error { - return Update(s.db, "materials", p.ID, p) + return Update(s.db, "materials", p.ID, p) } func (s *MaterialStore) Delete(sheetID int64) error { - return Delete(s.db, "materials", sheetID) + return Delete(s.db, "materials", sheetID) } func (s *MaterialStore) MaterialsOfCourse(courseID int64, requiredRole int) ([]model.Material, error) { - p := []model.Material{} + p := []model.Material{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT m.* FROM @@ -89,14 +89,14 @@ AND m.required_role <= $2 ORDER BY m.lecture_at ASC;`, courseID, requiredRole) - return p, err + return p, err } func (s *MaterialStore) IdentifyCourseOfMaterial(sheetID int64) (*model.Course, error) { - course := &model.Course{} - err := s.db.Get(course, - ` + course := &model.Course{} + err := s.db.Get(course, + ` SELECT c.* FROM @@ -105,10 +105,10 @@ INNER JOIN material_course mc ON mc.course_id = c.id WHERE mc.material_id = $1 LIMIT 1`, - sheetID) - if err != nil { - return nil, err - } + sheetID) + if err != nil { + return nil, err + } - return course, err + return course, err } diff --git a/database/oracle.go b/database/oracle.go index 7dfc738..0dabadb 100644 --- a/database/oracle.go +++ b/database/oracle.go @@ -22,16 +22,16 @@ package database import ( - "database/sql" - "fmt" - "time" + "database/sql" + "fmt" + "time" - "reflect" - "strconv" - "strings" - "sync" + "reflect" + "strconv" + "strings" + "sync" - null "gopkg.in/guregu/null.v3" + null "gopkg.in/guregu/null.v3" ) const tagName = "db" @@ -39,60 +39,60 @@ const tagName = "db" var ReflectCaching = true type DB interface { - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...interface{}) (sql.Result, error) + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row } // DatabaseSyntax contains driver specific settings. type DatabaseSyntax struct { - Quote string // the quote character for table and column names - Placeholder string // the placeholder style to use in generated queries - UseReturningToGetID bool // use PostgreSQL-style RETURNING "ID" instead of calling sql.Result.LastInsertID + Quote string // the quote character for table and column names + Placeholder string // the placeholder style to use in generated queries + UseReturningToGetID bool // use PostgreSQL-style RETURNING "ID" instead of calling sql.Result.LastInsertID } // MySQL contains database specific options for executing queries in a MySQL database var MySQLSyntax = &DatabaseSyntax{ - Quote: "`", - Placeholder: "?", - UseReturningToGetID: false, + Quote: "`", + Placeholder: "?", + UseReturningToGetID: false, } // PostgreSQL contains database specific options for executing queries in a PostgreSQL database var PostgreSQLSyntax = &DatabaseSyntax{ - Quote: `"`, - Placeholder: "$1", - UseReturningToGetID: true, + Quote: `"`, + Placeholder: "$1", + UseReturningToGetID: true, } // SQLite contains database specific options for executing queries in a SQLite database var SQLiteSyntax = &DatabaseSyntax{ - Quote: `"`, - Placeholder: "?", - UseReturningToGetID: false, + Quote: `"`, + Placeholder: "?", + UseReturningToGetID: false, } var DefaultSyntax = PostgreSQLSyntax func (d *DatabaseSyntax) quoted(s string) string { - return d.Quote + s + d.Quote + return d.Quote + s + d.Quote } func (d *DatabaseSyntax) placeholder(n int) string { - return strings.Replace(d.Placeholder, "1", strconv.FormatInt(int64(n), 10), 1) + return strings.Replace(d.Placeholder, "1", strconv.FormatInt(int64(n), 10), 1) } // represents an entry in a struct type structField struct { - column string - index int - readonly bool + column string + index int + readonly bool } // represents all entries from a struct type structInfo struct { - columns []string - fields map[string]*structField + columns []string + fields map[string]*structField } var fieldsCache = make(map[reflect.Type]*structInfo) @@ -100,117 +100,117 @@ var fieldsCacheMutex sync.Mutex // do some reflection on struct to parse "db" tags func parseStruct(objectType reflect.Type) (*structInfo, error) { - // use caching to speed things up - if ReflectCaching { - fieldsCacheMutex.Lock() - defer fieldsCacheMutex.Unlock() - - if result, present := fieldsCache[objectType]; present { - return result, nil - } - } - - // make sure dst is a non-nil pointer to a struct - if objectType.Kind() != reflect.Ptr { - return nil, fmt.Errorf("sqlorcale called with non-pointer destination %v", objectType) - } - - structType := objectType.Elem() - if structType.Kind() != reflect.Struct { - return nil, fmt.Errorf("sqlorcale called with pointer to non-struct %v", objectType) - } - - // gather the list of fields in the struct - data := new(structInfo) - data.fields = make(map[string]*structField) - - for i := 0; i < structType.NumField(); i++ { - f := structType.Field(i) - - // skip non-exported fields - if f.PkgPath != "" { - continue - } - - // examine the tag for metadata - tag := strings.Split(f.Tag.Get(tagName), ",") - - // was this field marked for skipping? - if len(tag) > 0 && tag[0] == "-" { - continue - } - - // default to the field name - name := f.Name - readonly := false - - // the tag can override the field name - if len(tag) > 0 && tag[0] != "" { - name = tag[0] - } - - if len(tag) > 1 && tag[1] == "readonly" { - readonly = true - } - - if name == "id" { - if f.Type.Kind() == reflect.Ptr { - return nil, fmt.Errorf("sqlorcale found field %s which is the primary key but is a pointer", f.Name) - } - - // make sure it is an int of some kind - switch f.Type.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - default: - return nil, fmt.Errorf("meddler found field %s which is marked as the primary key, but is not an integer type", f.Name) - } - } - - // prevent duplicates in tags - if _, present := data.fields[name]; present { - return nil, fmt.Errorf("sqlorcale found multiple fields for column %s", name) - } - data.fields[name] = &structField{ - column: name, - index: i, - readonly: readonly, - } - data.columns = append(data.columns, name) - - } - - fieldsCache[objectType] = data - return data, nil + // use caching to speed things up + if ReflectCaching { + fieldsCacheMutex.Lock() + defer fieldsCacheMutex.Unlock() + + if result, present := fieldsCache[objectType]; present { + return result, nil + } + } + + // make sure dst is a non-nil pointer to a struct + if objectType.Kind() != reflect.Ptr { + return nil, fmt.Errorf("sqlorcale called with non-pointer destination %v", objectType) + } + + structType := objectType.Elem() + if structType.Kind() != reflect.Struct { + return nil, fmt.Errorf("sqlorcale called with pointer to non-struct %v", objectType) + } + + // gather the list of fields in the struct + data := new(structInfo) + data.fields = make(map[string]*structField) + + for i := 0; i < structType.NumField(); i++ { + f := structType.Field(i) + + // skip non-exported fields + if f.PkgPath != "" { + continue + } + + // examine the tag for metadata + tag := strings.Split(f.Tag.Get(tagName), ",") + + // was this field marked for skipping? + if len(tag) > 0 && tag[0] == "-" { + continue + } + + // default to the field name + name := f.Name + readonly := false + + // the tag can override the field name + if len(tag) > 0 && tag[0] != "" { + name = tag[0] + } + + if len(tag) > 1 && tag[1] == "readonly" { + readonly = true + } + + if name == "id" { + if f.Type.Kind() == reflect.Ptr { + return nil, fmt.Errorf("sqlorcale found field %s which is the primary key but is a pointer", f.Name) + } + + // make sure it is an int of some kind + switch f.Type.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + default: + return nil, fmt.Errorf("meddler found field %s which is marked as the primary key, but is not an integer type", f.Name) + } + } + + // prevent duplicates in tags + if _, present := data.fields[name]; present { + return nil, fmt.Errorf("sqlorcale found multiple fields for column %s", name) + } + data.fields[name] = &structField{ + column: name, + index: i, + readonly: readonly, + } + data.columns = append(data.columns, name) + + } + + fieldsCache[objectType] = data + return data, nil } // Columns returns a list of column names for its input struct. func (d *DatabaseSyntax) Columns(src interface{}, includePk bool) ([]string, error) { - structInfo, err := parseStruct(reflect.TypeOf(src)) - if err != nil { - return nil, err - } - - var names []string - for _, elt := range structInfo.columns { - if !includePk && elt == "id" { - continue - } - names = append(names, elt) - } - - return names, nil + structInfo, err := parseStruct(reflect.TypeOf(src)) + if err != nil { + return nil, err + } + + var names []string + for _, elt := range structInfo.columns { + if !includePk && elt == "id" { + continue + } + names = append(names, elt) + } + + return names, nil } // Columns using the Default Database type. func Columns(src interface{}, includePk bool) ([]string, error) { - return DefaultSyntax.Columns(src, includePk) + return DefaultSyntax.Columns(src, includePk) } type StatementData struct { - Column string - Value interface{} + Column string + Value interface{} } // // isZero tests whether the incoming value is the default value. @@ -249,132 +249,132 @@ type StatementData struct { // } func isZero(value reflect.Value) bool { - switch value.Kind() { - case reflect.String: - return value.Len() == 0 - case reflect.Bool: - return !value.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return value.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return value.Uint() == 0 - case reflect.Float32, reflect.Float64: - return value.Float() == 0 - case reflect.Interface, reflect.Ptr: - return value.IsNil() - } - - return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) + switch value.Kind() { + case reflect.String: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) } // PackStatementData reads a struct and extract necessary data for a query. // This skips the primary key "id" automatically. No data modification is made. func (d *DatabaseSyntax) PackStatementData(src interface{}) ([]StatementData, error) { - var null_string null.String - - statementDatas := []StatementData{} - - // extract column names we are interested in - columns, err := d.Columns(src, false) - if err != nil { - return nil, err - } - - // extract struct info - // objectType := reflect.TypeOf(src) - structVal := reflect.ValueOf(src).Elem() - // structType := objectType.Elem() - data, err := parseStruct(reflect.TypeOf(src)) - - // structVal := reflect.ValueOf(src).Elem() - for _, name := range columns { - field, present := data.fields[name] - - if name == "id" { - continue - } - - if field.readonly { - continue - } - - // field is in tag and current struct - if present { - // fmt.Println("field ", name, " is present") // DEBUG - current_value := structVal.Field(field.index) - - if reflect.TypeOf(null_string) == current_value.Type() { - // sql.NUll - // Valid is true if String is not NULL - if current_value.Field(0).Field(1).Bool() == true { - statementDatas = append(statementDatas, StatementData{ - Column: name, - Value: current_value.Field(0).Field(0).String(), - }) - - } else { - statementDatas = append(statementDatas, StatementData{ - Column: name, - Value: nil, - }) - } - } else { - // cannot handle int(0) properly - // TODO(patwie): materials.kind = 0 is an issue - // if !isZero(current_value) { - if true { - // fmt.Println("field ", name, " is NOT zero", current_value.Interface()) // DEBUG - statementDatas = append(statementDatas, StatementData{ - Column: name, - Value: current_value.Interface(), //current_value.Interface(), - }) - } else { - // fmt.Println("field ", name, " is zero", current_value.Interface(), current_value.Kind()) // DEBUG - } - - } - - // if !isZero(current_value) { - - // // if current_value.Type() == null.String - // // This is an ugly case, but we want to nicely create JSON - // // and the null package does the job - // if reflect.TypeOf(null_string) == current_value.Type() { - // // sql.NUll - // // Valid is true if String is not NULL - // if current_value.Field(0).Field(1).Bool() == true { - // statementDatas = append(statementDatas, StatementData{ - // Column: name, - // Value: current_value.Field(0).Field(0).String(), - // }) - - // } else { - // statementDatas = append(statementDatas, StatementData{ - // Column: name, - // Value: nil, - // }) - // } - // } else { - // statementDatas = append(statementDatas, StatementData{ - // Column: name, - // Value: current_value.Interface(), //current_value.Interface(), - // }) - - // } - - // } - - } else { - // fmt.Println("field ", name, " is NOT present") // DEBUG - } - - } - return statementDatas, nil + var null_string null.String + + statementDatas := []StatementData{} + + // extract column names we are interested in + columns, err := d.Columns(src, false) + if err != nil { + return nil, err + } + + // extract struct info + // objectType := reflect.TypeOf(src) + structVal := reflect.ValueOf(src).Elem() + // structType := objectType.Elem() + data, err := parseStruct(reflect.TypeOf(src)) + + // structVal := reflect.ValueOf(src).Elem() + for _, name := range columns { + field, present := data.fields[name] + + if name == "id" { + continue + } + + if field.readonly { + continue + } + + // field is in tag and current struct + if present { + // fmt.Println("field ", name, " is present") // DEBUG + current_value := structVal.Field(field.index) + + if reflect.TypeOf(null_string) == current_value.Type() { + // sql.NUll + // Valid is true if String is not NULL + if current_value.Field(0).Field(1).Bool() == true { + statementDatas = append(statementDatas, StatementData{ + Column: name, + Value: current_value.Field(0).Field(0).String(), + }) + + } else { + statementDatas = append(statementDatas, StatementData{ + Column: name, + Value: nil, + }) + } + } else { + // cannot handle int(0) properly + // TODO(patwie): materials.kind = 0 is an issue + // if !isZero(current_value) { + if true { + // fmt.Println("field ", name, " is NOT zero", current_value.Interface()) // DEBUG + statementDatas = append(statementDatas, StatementData{ + Column: name, + Value: current_value.Interface(), //current_value.Interface(), + }) + } else { + // fmt.Println("field ", name, " is zero", current_value.Interface(), current_value.Kind()) // DEBUG + } + + } + + // if !isZero(current_value) { + + // // if current_value.Type() == null.String + // // This is an ugly case, but we want to nicely create JSON + // // and the null package does the job + // if reflect.TypeOf(null_string) == current_value.Type() { + // // sql.NUll + // // Valid is true if String is not NULL + // if current_value.Field(0).Field(1).Bool() == true { + // statementDatas = append(statementDatas, StatementData{ + // Column: name, + // Value: current_value.Field(0).Field(0).String(), + // }) + + // } else { + // statementDatas = append(statementDatas, StatementData{ + // Column: name, + // Value: nil, + // }) + // } + // } else { + // statementDatas = append(statementDatas, StatementData{ + // Column: name, + // Value: current_value.Interface(), //current_value.Interface(), + // }) + + // } + + // } + + } else { + // fmt.Println("field ", name, " is NOT present") // DEBUG + } + + } + return statementDatas, nil } func PackStatementData(src interface{}) ([]StatementData, error) { - return DefaultSyntax.PackStatementData(src) + return DefaultSyntax.PackStatementData(src) } @@ -384,100 +384,100 @@ func PackStatementData(src interface{}) ([]StatementData, error) { // var newPk int64 // err := db.QueryRow(stmt, values...).Scan(&newPk) func (d *DatabaseSyntax) InsertStatement(table string, src interface{}) (string, []interface{}, error) { - stmtData, err := PackStatementData(src) - if err != nil { - return "", nil, err - } - - // for _, el := range stmtData { - // fmt.Println("Column", el.Column) - // fmt.Println("Value", el.Value) - // } - - // structInfo, err := parseStruct(reflect.TypeOf(src)) - - var columns []string - var placeholders []string - var values []interface{} - for _, el := range stmtData { - if el.Column == "created_at" || el.Column == "updated_at" { - continue - } - columns = append(columns, d.quoted(el.Column)) - placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) - values = append(values, el.Value) - } - - // set "created_at" and "updated_at" - current_time := time.Now() - structInfo, err := parseStruct(reflect.TypeOf(src)) - _, present := structInfo.fields["created_at"] - if present { - // we will need to set created_at - columns = append(columns, d.quoted("created_at")) - placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) - values = append(values, current_time) - } - _, present = structInfo.fields["updated_at"] - if present { - // we will need to set updated_at - columns = append(columns, d.quoted("updated_at")) - placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) - values = append(values, current_time) - } - - column_string := strings.Join(columns, ", ") - placeholder_string := strings.Join(placeholders, ", ") - - stmt := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", table, column_string, placeholder_string) - - if d.UseReturningToGetID { - stmt += " RETURNING " + d.quoted("id") - } - stmt += ";" - - // fmt.Println(stmt)// DEBUG - // fmt.Println(values)// DEBUG - return stmt, values, nil + stmtData, err := PackStatementData(src) + if err != nil { + return "", nil, err + } + + // for _, el := range stmtData { + // fmt.Println("Column", el.Column) + // fmt.Println("Value", el.Value) + // } + + // structInfo, err := parseStruct(reflect.TypeOf(src)) + + var columns []string + var placeholders []string + var values []interface{} + for _, el := range stmtData { + if el.Column == "created_at" || el.Column == "updated_at" { + continue + } + columns = append(columns, d.quoted(el.Column)) + placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) + values = append(values, el.Value) + } + + // set "created_at" and "updated_at" + current_time := time.Now() + structInfo, err := parseStruct(reflect.TypeOf(src)) + _, present := structInfo.fields["created_at"] + if present { + // we will need to set created_at + columns = append(columns, d.quoted("created_at")) + placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) + values = append(values, current_time) + } + _, present = structInfo.fields["updated_at"] + if present { + // we will need to set updated_at + columns = append(columns, d.quoted("updated_at")) + placeholders = append(placeholders, d.placeholder(len(placeholders)+1)) + values = append(values, current_time) + } + + column_string := strings.Join(columns, ", ") + placeholder_string := strings.Join(placeholders, ", ") + + stmt := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", table, column_string, placeholder_string) + + if d.UseReturningToGetID { + stmt += " RETURNING " + d.quoted("id") + } + stmt += ";" + + // fmt.Println(stmt)// DEBUG + // fmt.Println(values)// DEBUG + return stmt, values, nil } func InsertStatement(table string, src interface{}) (string, []interface{}, error) { - return DefaultSyntax.InsertStatement(table, src) + return DefaultSyntax.InsertStatement(table, src) } func (d *DatabaseSyntax) Insert(db DB, table string, src interface{}) (int64, error) { - stmt, values, err := InsertStatement(table, src) - if err != nil { - return 0, err - } - - if d.UseReturningToGetID { - // database returns the last id - var newPk int64 - err := db.QueryRow(stmt, values...).Scan(&newPk) - return newPk, err - } else { - // we need to ask for the last - result, err := db.Exec(stmt, values...) - if err != nil { - return 0, err - } - - newPk, err := result.LastInsertId() - if err != nil { - return 0, err - } - - return newPk, nil - - } + stmt, values, err := InsertStatement(table, src) + if err != nil { + return 0, err + } + + if d.UseReturningToGetID { + // database returns the last id + var newPk int64 + err := db.QueryRow(stmt, values...).Scan(&newPk) + return newPk, err + } else { + // we need to ask for the last + result, err := db.Exec(stmt, values...) + if err != nil { + return 0, err + } + + newPk, err := result.LastInsertId() + if err != nil { + return 0, err + } + + return newPk, nil + + } } func Insert(db DB, table string, src interface{}) (int64, error) { - return DefaultSyntax.Insert(db, table, src) + return DefaultSyntax.Insert(db, table, src) } // Build an sql statement to update values @@ -486,77 +486,77 @@ func Insert(db DB, table string, src interface{}) (int64, error) { // var newPk int64 // err := db.QueryRow(stmt, values...).Scan(&newPk) func (d *DatabaseSyntax) UpdateStatement(table string, id int64, src interface{}) (string, []interface{}, error) { - data, err := PackStatementData(src) - if err != nil { - return "", nil, err - } - _ = data - - var values []interface{} - values = append(values, id) - var pairs []string - for _, el := range data { - if el.Column == "created_at" || el.Column == "updated_at" { - continue - } - pairs = append(pairs, fmt.Sprintf("%s = %s", d.quoted(el.Column), d.placeholder(len(pairs)+2))) - values = append(values, el.Value) - } - - structInfo, err := parseStruct(reflect.TypeOf(src)) - _, present := structInfo.fields["updated_at"] - if present { - // we will need to set updated_at - pairs = append(pairs, fmt.Sprintf("%s = %s", d.quoted("updated_at"), d.placeholder(len(pairs)+2))) - values = append(values, time.Now()) - } - - pairs_string := strings.Join(pairs, ", ") - stmt := fmt.Sprintf("UPDATE %s SET %s WHERE id = $1;", table, pairs_string) - // fmt.Println(stmt) - // fmt.Println(values) - return stmt, values, nil + data, err := PackStatementData(src) + if err != nil { + return "", nil, err + } + _ = data + + var values []interface{} + values = append(values, id) + var pairs []string + for _, el := range data { + if el.Column == "created_at" || el.Column == "updated_at" { + continue + } + pairs = append(pairs, fmt.Sprintf("%s = %s", d.quoted(el.Column), d.placeholder(len(pairs)+2))) + values = append(values, el.Value) + } + + structInfo, err := parseStruct(reflect.TypeOf(src)) + _, present := structInfo.fields["updated_at"] + if present { + // we will need to set updated_at + pairs = append(pairs, fmt.Sprintf("%s = %s", d.quoted("updated_at"), d.placeholder(len(pairs)+2))) + values = append(values, time.Now()) + } + + pairs_string := strings.Join(pairs, ", ") + stmt := fmt.Sprintf("UPDATE %s SET %s WHERE id = $1;", table, pairs_string) + // fmt.Println(stmt) + // fmt.Println(values) + return stmt, values, nil } func UpdateStatement(table string, id int64, src interface{}) (string, []interface{}, error) { - return DefaultSyntax.UpdateStatement(table, id, src) + return DefaultSyntax.UpdateStatement(table, id, src) } func (d *DatabaseSyntax) Update(db DB, table string, id int64, src interface{}) error { - stmt, values, err := UpdateStatement(table, id, src) - if err != nil { - return err - } + stmt, values, err := UpdateStatement(table, id, src) + if err != nil { + return err + } - _, err = db.Exec(stmt, values...) - return err + _, err = db.Exec(stmt, values...) + return err } func Update(db DB, table string, id int64, src interface{}) error { - return DefaultSyntax.Update(db, table, id, src) + return DefaultSyntax.Update(db, table, id, src) } func (d *DatabaseSyntax) DeleteStatement(table string, id int64) (string, []interface{}) { - stmt := fmt.Sprintf("DELETE FROM %s WHERE id = $1;", table) + stmt := fmt.Sprintf("DELETE FROM %s WHERE id = $1;", table) - var values []interface{} - values = append(values, id) - return stmt, values + var values []interface{} + values = append(values, id) + return stmt, values } func DeleteStatement(table string, id int64) (string, []interface{}) { - return DefaultSyntax.DeleteStatement(table, id) + return DefaultSyntax.DeleteStatement(table, id) } func (d *DatabaseSyntax) Delete(db DB, table string, id int64) error { - stmt, values := DeleteStatement(table, id) - _, err := db.Exec(stmt, values...) - return err + stmt, values := DeleteStatement(table, id) + _, err := db.Exec(stmt, values...) + return err } func Delete(db DB, table string, id int64) error { - return DefaultSyntax.Delete(db, table, id) + return DefaultSyntax.Delete(db, table, id) } diff --git a/database/sheet_store.go b/database/sheet_store.go index d71f084..ff87d93 100644 --- a/database/sheet_store.go +++ b/database/sheet_store.go @@ -19,66 +19,66 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type SheetStore struct { - db *sqlx.DB + db *sqlx.DB } func NewSheetStore(db *sqlx.DB) *SheetStore { - return &SheetStore{ - db: db, - } + return &SheetStore{ + db: db, + } } func (s *SheetStore) Get(sheetID int64) (*model.Sheet, error) { - p := model.Sheet{ID: sheetID} - err := s.db.Get(&p, "SELECT * FROM sheets WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.Sheet{ID: sheetID} + err := s.db.Get(&p, "SELECT * FROM sheets WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *SheetStore) GetAll() ([]model.Sheet, error) { - p := []model.Sheet{} - err := s.db.Select(&p, "SELECT * FROM sheets;") - return p, err + p := []model.Sheet{} + err := s.db.Select(&p, "SELECT * FROM sheets;") + return p, err } func (s *SheetStore) Create(p *model.Sheet, courseID int64) (*model.Sheet, error) { - newID, err := Insert(s.db, "sheets", p) - if err != nil { - return nil, err - } + newID, err := Insert(s.db, "sheets", p) + if err != nil { + return nil, err + } - // now associate sheet with course - _, err = s.db.Exec(` + // now associate sheet with course + _, err = s.db.Exec(` INSERT INTO sheet_course (id,sheet_id,course_id) VALUES (DEFAULT, $1, $2);`, - newID, courseID) - if err != nil { - return nil, err - } + newID, courseID) + if err != nil { + return nil, err + } - return s.Get(newID) + return s.Get(newID) } func (s *SheetStore) Update(p *model.Sheet) error { - return Update(s.db, "sheets", p.ID, p) + return Update(s.db, "sheets", p.ID, p) } func (s *SheetStore) Delete(sheetID int64) error { - return Delete(s.db, "sheets", sheetID) + return Delete(s.db, "sheets", sheetID) } func (s *SheetStore) SheetsOfCourse(courseID int64) ([]model.Sheet, error) { - p := []model.Sheet{} + p := []model.Sheet{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT s.id, s.created_at, s.updated_at, s.name, s.publish_at, s.due_at FROM @@ -91,14 +91,14 @@ WHERE sc.course_id = $1 ORDER BY s.publish_at ASC;`, courseID) - return p, err + return p, err } func (s *SheetStore) IdentifyCourseOfSheet(sheetID int64) (*model.Course, error) { - course := &model.Course{} - err := s.db.Get(course, - ` + course := &model.Course{} + err := s.db.Get(course, + ` SELECT c.* FROM @@ -106,19 +106,19 @@ FROM INNER JOIN courses c ON sc.course_id = c.id WHERE sc.sheet_id = $1`, - sheetID) - if err != nil { - return nil, err - } + sheetID) + if err != nil { + return nil, err + } - return course, err + return course, err } // PointsForUser returns all gather points in a given sheet for a given user accumulated. func (s *SheetStore) PointsForUser(userID int64, sheetID int64) ([]model.TaskPoints, error) { - p := []model.TaskPoints{} + p := []model.TaskPoints{} - err := s.db.Select(&p, ` + err := s.db.Select(&p, ` SELECT t.id task_id, g.acquired_points, @@ -134,7 +134,7 @@ AND ts.sheet_id = $2 ORDER BY ts.sheet_id`, userID, sheetID, - ) - return p, err + ) + return p, err } diff --git a/database/submission_store.go b/database/submission_store.go index d98fd61..bbb95ec 100644 --- a/database/submission_store.go +++ b/database/submission_store.go @@ -19,29 +19,29 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type SubmissionStore struct { - db *sqlx.DB + db *sqlx.DB } func NewSubmissionStore(db *sqlx.DB) *SubmissionStore { - return &SubmissionStore{ - db: db, - } + return &SubmissionStore{ + db: db, + } } func (s *SubmissionStore) Get(submissionID int64) (*model.Submission, error) { - p := model.Submission{ID: submissionID} - err := s.db.Get(&p, `SELECT * FROM submissions WHERE id = $1 LIMIT 1;`, p.ID) - return &p, err + p := model.Submission{ID: submissionID} + err := s.db.Get(&p, `SELECT * FROM submissions WHERE id = $1 LIMIT 1;`, p.ID) + return &p, err } func (s *SubmissionStore) GetByUserAndTask(userID int64, taskID int64) (*model.Submission, error) { - p := model.Submission{} - err := s.db.Get(&p, ` + p := model.Submission{} + err := s.db.Get(&p, ` SELECT * FROM @@ -51,23 +51,23 @@ WHERE AND task_id = $2 LIMIT 1;`, - userID, taskID) - return &p, err + userID, taskID) + return &p, err } func (s *SubmissionStore) Create(p *model.Submission) (*model.Submission, error) { - newID, err := Insert(s.db, "submissions", p) - if err != nil { - return nil, err - } - return s.Get(newID) + newID, err := Insert(s.db, "submissions", p) + if err != nil { + return nil, err + } + return s.Get(newID) } func (s *SubmissionStore) GetFiltered(filterCourseID, filterGroupID, filterUserID, filterSheetID, filterTaskID int64) ([]model.Submission, error) { - p := []model.Submission{} - err := s.db.Select(&p, - ` + p := []model.Submission{} + err := s.db.Select(&p, + ` SELECT s.* FROM @@ -86,6 +86,6 @@ AND AND ($5 = 0 or g.course_id = $5) `, - filterUserID, filterTaskID, filterGroupID, filterSheetID, filterCourseID) - return p, err + filterUserID, filterTaskID, filterGroupID, filterSheetID, filterCourseID) + return p, err } diff --git a/database/task_store.go b/database/task_store.go index e5d1082..22d0cbf 100644 --- a/database/task_store.go +++ b/database/task_store.go @@ -19,23 +19,23 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type TaskStore struct { - db *sqlx.DB + db *sqlx.DB } func NewTaskStore(db *sqlx.DB) *TaskStore { - return &TaskStore{ - db: db, - } + return &TaskStore{ + db: db, + } } func (s *TaskStore) GetAllMissingTasksForUser(userID int64) ([]model.MissingTask, error) { - p := []model.MissingTask{} - err := s.db.Select(&p, ` + p := []model.MissingTask{} + err := s.db.Select(&p, ` SELECT t.*, ts.sheet_id, @@ -49,52 +49,52 @@ WHERE SELECT task_id FROM submissions s WHERE s.user_id = $1 ); `, userID) - return p, err + return p, err } func (s *TaskStore) Get(taskID int64) (*model.Task, error) { - p := model.Task{ID: taskID} - err := s.db.Get(&p, "SELECT * FROM tasks WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.Task{ID: taskID} + err := s.db.Get(&p, "SELECT * FROM tasks WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *TaskStore) GetAll() ([]model.Task, error) { - p := []model.Task{} - err := s.db.Select(&p, "SELECT * FROM tasks;") - return p, err + p := []model.Task{} + err := s.db.Select(&p, "SELECT * FROM tasks;") + return p, err } func (s *TaskStore) Create(p *model.Task, sheetID int64) (*model.Task, error) { - // create Task - newID, err := Insert(s.db, "tasks", p) - if err != nil { - return nil, err - } - - // now associate sheet with course - _, err = s.db.Exec(`INSERT INTO task_sheet + // create Task + newID, err := Insert(s.db, "tasks", p) + if err != nil { + return nil, err + } + + // now associate sheet with course + _, err = s.db.Exec(`INSERT INTO task_sheet (id,task_id,sheet_id) VALUES (DEFAULT, $1, $2);`, newID, sheetID) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - return s.Get(newID) + return s.Get(newID) } func (s *TaskStore) Update(p *model.Task) error { - return Update(s.db, "tasks", p.ID, p) + return Update(s.db, "tasks", p.ID, p) } func (s *TaskStore) Delete(taskID int64) error { - return Delete(s.db, "tasks", taskID) + return Delete(s.db, "tasks", taskID) } func (s *TaskStore) TasksOfSheet(sheetID int64) ([]model.Task, error) { - p := []model.Task{} + p := []model.Task{} - // t.public_test_path, t.private_test_path, - err := s.db.Select(&p, ` + // t.public_test_path, t.private_test_path, + err := s.db.Select(&p, ` SELECT t.id, t.created_at, @@ -111,14 +111,14 @@ WHERE s.id = $1 ORDER BY t.name ASC;`, sheetID) - return p, err + return p, err } func (s *TaskStore) IdentifyCourseOfTask(taskID int64) (*model.Course, error) { - course := &model.Course{} - err := s.db.Get(course, - ` + course := &model.Course{} + err := s.db.Get(course, + ` SELECT c.* FROM @@ -127,49 +127,49 @@ INNER JOIN sheet_course sc ON sc.sheet_id = ts.sheet_id INNER JOIN courses c ON c.id = sc.course_ID WHERE ts.task_id = $1`, - taskID) - if err != nil { - return nil, err - } + taskID) + if err != nil { + return nil, err + } - return course, err + return course, err } func (s *TaskStore) IdentifySheetOfTask(taskID int64) (*model.Sheet, error) { - sheet := &model.Sheet{} - err := s.db.Get(sheet, - ` + sheet := &model.Sheet{} + err := s.db.Get(sheet, + ` SELECT s.* FROM task_sheet ts INNER JOIN sheets s ON s.id = ts.sheet_id WHERE ts.task_id = $1`, - taskID) - if err != nil { - return nil, err - } + taskID) + if err != nil { + return nil, err + } - return sheet, err + return sheet, err } func (s *TaskStore) GetAverageRating(taskID int64) (float32, error) { - var averageRating float32 - err := s.db.Get(&averageRating, ` + var averageRating float32 + err := s.db.Get(&averageRating, ` SELECT AVG(rating) average_rating FROM task_ratings tr WHERE tr.task_id = $1`, taskID) - return averageRating, err + return averageRating, err } func (s *TaskStore) GetRatingOfTaskByUser(taskID int64, userID int64) (*model.TaskRating, error) { - p := model.TaskRating{} - err := s.db.Get(&p, ` + p := model.TaskRating{} + err := s.db.Get(&p, ` SELECT * FROM @@ -179,24 +179,24 @@ WHERE AND task_id = $2 LIMIT 1`, userID, taskID) - return &p, err + return &p, err } func (s *TaskStore) GetRating(taskRatingID int64) (*model.TaskRating, error) { - p := model.TaskRating{ID: taskRatingID} - err := s.db.Get(&p, "SELECT * FROM task_ratings WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.TaskRating{ID: taskRatingID} + err := s.db.Get(&p, "SELECT * FROM task_ratings WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *TaskStore) CreateRating(p *model.TaskRating) (*model.TaskRating, error) { - // create Task - newID, err := Insert(s.db, "task_ratings", p) - if err != nil { - return nil, err - } - return s.GetRating(newID) + // create Task + newID, err := Insert(s.db, "task_ratings", p) + if err != nil { + return nil, err + } + return s.GetRating(newID) } func (s *TaskStore) UpdateRating(p *model.TaskRating) error { - return Update(s.db, "task_ratings", p.ID, p) + return Update(s.db, "task_ratings", p.ID, p) } diff --git a/database/user_store.go b/database/user_store.go index ea62016..bbf9a8b 100644 --- a/database/user_store.go +++ b/database/user_store.go @@ -19,57 +19,57 @@ package database import ( - "github.com/cgtuebingen/infomark-backend/model" - "github.com/jmoiron/sqlx" + "github.com/cgtuebingen/infomark-backend/model" + "github.com/jmoiron/sqlx" ) type UserStore struct { - db *sqlx.DB + db *sqlx.DB } func NewUserStore(db *sqlx.DB) *UserStore { - return &UserStore{ - db: db, - } + return &UserStore{ + db: db, + } } func (s *UserStore) Get(userID int64) (*model.User, error) { - p := model.User{ID: userID} - err := s.db.Get(&p, "SELECT * FROM users WHERE id = $1 LIMIT 1;", p.ID) - return &p, err + p := model.User{ID: userID} + err := s.db.Get(&p, "SELECT * FROM users WHERE id = $1 LIMIT 1;", p.ID) + return &p, err } func (s *UserStore) FindByEmail(email string) (*model.User, error) { - p := model.User{Email: email} - err := s.db.Get(&p, "SELECT * FROM users WHERE email = $1 LIMIT 1;", p.Email) - return &p, err + p := model.User{Email: email} + err := s.db.Get(&p, "SELECT * FROM users WHERE email = $1 LIMIT 1;", p.Email) + return &p, err } func (s *UserStore) GetAll() ([]model.User, error) { - p := []model.User{} - err := s.db.Select(&p, "SELECT * FROM users;") - return p, err + p := []model.User{} + err := s.db.Select(&p, "SELECT * FROM users;") + return p, err } func (s *UserStore) Create(p *model.User) (*model.User, error) { - newID, err := Insert(s.db, "users", p) - if err != nil { - return nil, err - } - return s.Get(newID) + newID, err := Insert(s.db, "users", p) + if err != nil { + return nil, err + } + return s.Get(newID) } func (s *UserStore) Update(p *model.User) error { - return Update(s.db, "users", p.ID, p) + return Update(s.db, "users", p.ID, p) } func (s *UserStore) Delete(userID int64) error { - return Delete(s.db, "users", userID) + return Delete(s.db, "users", userID) } func (s *UserStore) GetEnrollments(userID int64) ([]model.Enrollment, error) { - p := []model.Enrollment{} - err := s.db.Select(&p, ` + p := []model.Enrollment{} + err := s.db.Select(&p, ` SELECT course_id, role @@ -78,6 +78,6 @@ FROM WHERE user_id = $1 `, userID) - return p, err + return p, err } diff --git a/docs/generate.go b/docs/generate.go index a88fbd0..e02d3ec 100644 --- a/docs/generate.go +++ b/docs/generate.go @@ -1,372 +1,372 @@ package main import ( - "fmt" - "go/parser" - "go/token" - "net/http" - "os" - "strings" - - "github.com/cgtuebingen/infomark-backend/api/app" - "github.com/cgtuebingen/infomark-backend/docs/swagger" - "github.com/go-chi/chi" - "github.com/jmoiron/sqlx" - _ "github.com/mattn/go-sqlite3" + "fmt" + "go/parser" + "go/token" + "net/http" + "os" + "strings" + + "github.com/cgtuebingen/infomark-backend/api/app" + "github.com/cgtuebingen/infomark-backend/docs/swagger" + "github.com/go-chi/chi" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" ) type Routes struct { - Method string - Path string + Method string + Path string } func GetAllRoutes() []*Routes { - db, _ := sqlx.Open("sqlite3", ":memory:") - r, _ := app.New(db, false) + db, _ := sqlx.Open("sqlite3", ":memory:") + r, _ := app.New(db, false) - routes := []*Routes{} + routes := []*Routes{} - walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - route = strings.Replace(route, "/*/", "/", -1) - route = strings.Replace(route, "/*/", "/", -1) - route = strings.Replace(route, "/*/", "/", -1) - route = strings.Replace(route, "/*/", "/", -1) - route = strings.Replace(route, "/*", "/", -1) - // f.WriteString(fmt.Sprintf("%s %s\n", method, route)) + walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + route = strings.Replace(route, "/*/", "/", -1) + route = strings.Replace(route, "/*/", "/", -1) + route = strings.Replace(route, "/*/", "/", -1) + route = strings.Replace(route, "/*/", "/", -1) + route = strings.Replace(route, "/*", "/", -1) + // f.WriteString(fmt.Sprintf("%s %s\n", method, route)) - if len(route) > 1 && strings.HasPrefix(route, "/api/v1") { + if len(route) > 1 && strings.HasPrefix(route, "/api/v1") { - if strings.HasSuffix(route, "/") { - route = route[:len(route)-1] - } + if strings.HasSuffix(route, "/") { + route = route[:len(route)-1] + } - routes = append(routes, &Routes{Method: strings.ToLower(method), Path: route[7:]}) + routes = append(routes, &Routes{Method: strings.ToLower(method), Path: route[7:]}) - } - return nil - } + } + return nil + } - if err := chi.Walk(r, walkFunc); err != nil { - panic(fmt.Sprintf("Logging err: %s\n", err.Error())) - } + if err := chi.Walk(r, walkFunc); err != nil { + panic(fmt.Sprintf("Logging err: %s\n", err.Error())) + } - return routes + return routes } // Main docs func main() { - fset := token.NewFileSet() // positions are relative to fset - - pkgs, err := parser.ParseDir(fset, "./api/app/", nil, parser.ParseComments) - if err != nil { - panic(err) - } - endpoints := swagger.GetEndpoints(pkgs, fset) - - // verify we have all api-routes in the swagger description - routes := GetAllRoutes() - for _, route := range routes { - found := false - for url, _ := range endpoints { - if route.Path == url { - - for _, action := range endpoints[url] { - if action.Details.Method == route.Method { - found = true - break - } - } - - if !found { - candidates := []string{} - for _, action := range endpoints[url] { - candidates = append(candidates, fmt.Sprintf("%v", action.Position)) - } - - panic(fmt.Sprintf("found '%s' but not the correct method want '%s' \n candiates are %v", url, route.Method, candidates)) - } - } - if found { - break - } - } - - if !found { - panic(fmt.Sprintf("did not found %v", route.Path)) - } - } - - f, err := os.Create("./api.yaml") - if err != nil { - panic(err) - } - defer f.Close() - // w := bufio.NewWriter(f) - - f.WriteString(fmt.Sprintf("# this file has been automatically created. Please do not edit it here\n")) - f.WriteString(fmt.Sprintf("openapi: 3.0.0\n")) - f.WriteString(fmt.Sprintf("info:\n")) - f.WriteString(fmt.Sprintf(" title: \"InfoMark\"\n")) - f.WriteString(fmt.Sprintf(" version: \"0.0.1\"\n")) - f.WriteString(fmt.Sprintf(" description: >\n")) - f.WriteString(fmt.Sprintf(" A CI based course framework. All enums should be send as strings and returned as strings.\n")) - f.WriteString(fmt.Sprintf(" Everything\n")) - f.WriteString(fmt.Sprintf(" contact:\n")) - f.WriteString(fmt.Sprintf(" - name: Mark Boss\n")) - f.WriteString(fmt.Sprintf(" email: mark.boss@uni-tuebingen.de\n")) - f.WriteString(fmt.Sprintf(" url: https://uni-tuebingen.de\n")) - f.WriteString(fmt.Sprintf(" - name: Patrick Wieschollek\n")) - f.WriteString(fmt.Sprintf(" email: Patrick.Wieschollek@uni-tuebingen.de\n")) - f.WriteString(fmt.Sprintf(" url: https://uni-tuebingen.de\n")) - f.WriteString(fmt.Sprintf("servers:\n")) - f.WriteString(fmt.Sprintf(" - url: http://localhost:3000/api/v1\n")) - f.WriteString(fmt.Sprintf("security:\n")) - f.WriteString(fmt.Sprintf(" - bearerAuth: []\n")) - f.WriteString(fmt.Sprintf(" - cookieAuth: []\n")) - f.WriteString(fmt.Sprintf("tags:\n")) - f.WriteString(fmt.Sprintf(" - name: common\n")) - f.WriteString(fmt.Sprintf(" description: common request\n")) - f.WriteString(fmt.Sprintf(" - name: auth\n")) - f.WriteString(fmt.Sprintf(" description: authenticated related requests\n")) - f.WriteString(fmt.Sprintf(" - name: account\n")) - f.WriteString(fmt.Sprintf(" description: account related requests\n")) - f.WriteString(fmt.Sprintf(" - name: email\n")) - f.WriteString(fmt.Sprintf(" description: Email related requests\n")) - f.WriteString(fmt.Sprintf(" - name: users\n")) - f.WriteString(fmt.Sprintf(" description: User related requests\n")) - f.WriteString(fmt.Sprintf(" - name: courses\n")) - f.WriteString(fmt.Sprintf(" description: Course related requests\n")) - f.WriteString(fmt.Sprintf(" - name: sheets\n")) - f.WriteString(fmt.Sprintf(" description: Exercise sheets related requests\n")) - f.WriteString(fmt.Sprintf(" - name: tasks\n")) - f.WriteString(fmt.Sprintf(" description: Exercise tasks related requests\n")) - f.WriteString(fmt.Sprintf(" - name: submissions\n")) - f.WriteString(fmt.Sprintf(" description: Submissions related requests\n")) - f.WriteString(fmt.Sprintf(" - name: grades\n")) - f.WriteString(fmt.Sprintf(" description: Gradings related requests\n")) - f.WriteString(fmt.Sprintf(" - name: groups\n")) - f.WriteString(fmt.Sprintf(" description: Exercise groups related requests\n")) - f.WriteString(fmt.Sprintf(" - name: enrollments\n")) - f.WriteString(fmt.Sprintf(" description: Enrollments related requests\n")) - f.WriteString(fmt.Sprintf(" - name: materials\n")) - f.WriteString(fmt.Sprintf(" description: Exercise material related requests\n")) - f.WriteString(fmt.Sprintf(" - name: internal\n")) - f.WriteString(fmt.Sprintf(" description: Endpoints for internal usage only\n")) - - f.WriteString(fmt.Sprintf("components:\n")) - f.WriteString(fmt.Sprintf(" securitySchemes:\n")) - f.WriteString(fmt.Sprintf(" bearerAuth:\n")) - f.WriteString(fmt.Sprintf(" type: http\n")) - f.WriteString(fmt.Sprintf(" scheme: bearer\n")) - f.WriteString(fmt.Sprintf(" bearerFormat: JWT\n")) - f.WriteString(fmt.Sprintf(" cookieAuth:\n")) - f.WriteString(fmt.Sprintf(" type: apiKey\n")) - f.WriteString(fmt.Sprintf(" in: cookie\n")) - f.WriteString(fmt.Sprintf(" name: SESSIONID\n")) - f.WriteString(fmt.Sprintf(" schemas:\n")) - f.WriteString(fmt.Sprintf(" Error:\n")) - f.WriteString(fmt.Sprintf(" type: object\n")) - f.WriteString(fmt.Sprintf(" properties:\n")) - f.WriteString(fmt.Sprintf(" code:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" message:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" required:\n")) - f.WriteString(fmt.Sprintf(" - code\n")) - f.WriteString(fmt.Sprintf(" - message\n")) - - f.WriteString(swagger.SwaggerStructsWithSuffix(fset, pkgs, "Request", 4)) - f.WriteString(swagger.SwaggerStructsWithSuffix(fset, pkgs, "Response", 4)) - - f.WriteString(fmt.Sprintf(" responses:\n")) - f.WriteString(fmt.Sprintf(" pongResponse:\n")) - f.WriteString(fmt.Sprintf(" description: Server is up and running\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" text/plain:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" example: pong\n")) - f.WriteString(fmt.Sprintf(" ZipFile:\n")) - f.WriteString(fmt.Sprintf(" description: A file as a download.\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" application/zip:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" format: binary\n")) - f.WriteString(fmt.Sprintf(" ImageFile:\n")) - f.WriteString(fmt.Sprintf(" description: A file as a download.\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" image/jpeg:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" format: binary\n")) - f.WriteString(fmt.Sprintf(" OK:\n")) - f.WriteString(fmt.Sprintf(" description: Post successfully delivered.\n")) - f.WriteString(fmt.Sprintf(" NoContent:\n")) - f.WriteString(fmt.Sprintf(" description: Update was successful.\n")) - f.WriteString(fmt.Sprintf(" BadRequest:\n")) - f.WriteString(fmt.Sprintf(" description: The request is in a wrong format or contains missing fields.\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" application/json:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) - f.WriteString(fmt.Sprintf(" Unauthenticated:\n")) - f.WriteString(fmt.Sprintf(" description: User is not logged in.\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" application/json:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) - f.WriteString(fmt.Sprintf(" Unauthorized:\n")) - f.WriteString(fmt.Sprintf(" description: User is logged in but has not the permission to perform the request.\n")) - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" application/json:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) - - // create all responses - f.WriteString(swagger.SwaggerResponsesWithSuffix(fset, pkgs, "Response", 4)) - - duplicateResponseLists := make(map[string]int) - - // create all list responses - pre := strings.Repeat(" ", 4) - for url, _ := range endpoints { - for _, action := range endpoints[url] { - for _, r := range action.Details.Responses { - text := strings.TrimSpace(r.Text) - if strings.HasSuffix(text, "List") { - - _, exists := duplicateResponseLists[strings.TrimSpace(text)] - - if !exists { - f.WriteString(fmt.Sprintf("%s%s:\n", pre, text)) - f.WriteString(fmt.Sprintf("%s description: done\n", pre)) - f.WriteString(fmt.Sprintf("%s content:\n", pre)) - f.WriteString(fmt.Sprintf("%s application/json:\n", pre)) - f.WriteString(fmt.Sprintf("%s schema:\n", pre)) - f.WriteString(fmt.Sprintf("%s type: array\n", pre)) - f.WriteString(fmt.Sprintf("%s items:\n", pre)) - f.WriteString(fmt.Sprintf("%s $ref: \"#/components/schemas/%s\"\n", pre, text[:len(text)-4])) - - duplicateResponseLists[strings.TrimSpace(text)] = 0 - } - } - - } - } - } - - f.WriteString(fmt.Sprintf("paths:\n")) - for url, _ := range endpoints { - - f.WriteString(fmt.Sprintf(" %s:\n", url)) - for _, action := range endpoints[url] { - - if action.Details.Method == "post" || action.Details.Method == "put" || action.Details.Method == "patch" { - if action.Details.Request == "" { - panic(fmt.Sprintf("endpoint '%s' is '%s' but has no request body in %v", - url, action.Details.Method, action.Position)) - } - } - - if action.Details.Method == "get" { - // test wether we have a 200 response - found := false - for _, r := range action.Details.Responses { - if r.Code == 200 { - found = true - break - } - } - if !found { - panic(fmt.Sprintf("endpoint '%s' is '%s' but has no 200 response in %v", - url, action.Details.Method, action.Position)) - } - } - - f.WriteString(fmt.Sprintf(" # implementation in %v\n", action.Position)) - f.WriteString(fmt.Sprintf(" %s:\n", action.Details.Method)) - f.WriteString(fmt.Sprintf(" summary: %s\n", action.Details.Summary)) - if action.Details.Description != "" { - f.WriteString(fmt.Sprintf(" description: >\n %s\n", action.Details.Description)) - } - if len(action.Details.Tags) > 0 { - f.WriteString(fmt.Sprintf(" tags: \n\n")) - for _, tag := range action.Details.Tags { - - f.WriteString(fmt.Sprintf(" - %s\n", tag)) - } - } - if len(action.Details.URLParams)+len(action.Details.QueryParams) > 0 { - f.WriteString(fmt.Sprintf(" parameters:\n")) - for _, el := range action.Details.URLParams { - f.WriteString(fmt.Sprintf(" - in: path\n")) - f.WriteString(fmt.Sprintf(" name: %s\n", el.Name)) - f.WriteString(fmt.Sprintf(" schema: \n")) - f.WriteString(fmt.Sprintf(" type: %s\n", el.Type)) - f.WriteString(fmt.Sprintf(" required: true \n")) - } - - for _, el := range action.Details.QueryParams { - f.WriteString(fmt.Sprintf(" - in: query\n")) - f.WriteString(fmt.Sprintf(" name: %s\n", el.Name)) - f.WriteString(fmt.Sprintf(" schema: \n")) - f.WriteString(fmt.Sprintf(" type: %s\n", el.Type)) - f.WriteString(fmt.Sprintf(" required: false \n")) - } - } - - if action.Details.Request != "" { - f.WriteString(fmt.Sprintf(" requestBody:\n")) - f.WriteString(fmt.Sprintf(" required: true\n")) - - switch action.Details.Request { - case "zipfile": - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" multipart/form-data:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" type: object\n")) - f.WriteString(fmt.Sprintf(" properties:\n")) - f.WriteString(fmt.Sprintf(" file_data:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" format: binary\n")) - f.WriteString(fmt.Sprintf(" encoding:\n")) - f.WriteString(fmt.Sprintf(" file_data:\n")) - f.WriteString(fmt.Sprintf(" contentType: application/zip\n")) - case "imagefile": - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" multipart/form-data:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" type: object\n")) - f.WriteString(fmt.Sprintf(" properties:\n")) - f.WriteString(fmt.Sprintf(" file_data:\n")) - f.WriteString(fmt.Sprintf(" type: string\n")) - f.WriteString(fmt.Sprintf(" format: binary\n")) - f.WriteString(fmt.Sprintf(" encoding:\n")) - f.WriteString(fmt.Sprintf(" file_data:\n")) - f.WriteString(fmt.Sprintf(" contentType: image/jpeg\n")) - case "empty": - - default: - f.WriteString(fmt.Sprintf(" content:\n")) - f.WriteString(fmt.Sprintf(" application/json:\n")) - f.WriteString(fmt.Sprintf(" schema:\n")) - f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/%s\"\n", action.Details.Request)) - } - - } - f.WriteString(fmt.Sprintf(" responses:\n")) - for _, r := range action.Details.Responses { - - f.WriteString(fmt.Sprintf(" \"%v\":\n", r.Code)) - f.WriteString(fmt.Sprintf(" $ref: \"#/components/responses/%s\"\n", r.Text)) - - } - } - - } - - f.Sync() + fset := token.NewFileSet() // positions are relative to fset + + pkgs, err := parser.ParseDir(fset, "./api/app/", nil, parser.ParseComments) + if err != nil { + panic(err) + } + endpoints := swagger.GetEndpoints(pkgs, fset) + + // verify we have all api-routes in the swagger description + routes := GetAllRoutes() + for _, route := range routes { + found := false + for url, _ := range endpoints { + if route.Path == url { + + for _, action := range endpoints[url] { + if action.Details.Method == route.Method { + found = true + break + } + } + + if !found { + candidates := []string{} + for _, action := range endpoints[url] { + candidates = append(candidates, fmt.Sprintf("%v", action.Position)) + } + + panic(fmt.Sprintf("found '%s' but not the correct method want '%s' \n candiates are %v", url, route.Method, candidates)) + } + } + if found { + break + } + } + + if !found { + panic(fmt.Sprintf("did not found %v", route.Path)) + } + } + + f, err := os.Create("./api.yaml") + if err != nil { + panic(err) + } + defer f.Close() + // w := bufio.NewWriter(f) + + f.WriteString(fmt.Sprintf("# this file has been automatically created. Please do not edit it here\n")) + f.WriteString(fmt.Sprintf("openapi: 3.0.0\n")) + f.WriteString(fmt.Sprintf("info:\n")) + f.WriteString(fmt.Sprintf(" title: \"InfoMark\"\n")) + f.WriteString(fmt.Sprintf(" version: \"0.0.1\"\n")) + f.WriteString(fmt.Sprintf(" description: >\n")) + f.WriteString(fmt.Sprintf(" A CI based course framework. All enums should be send as strings and returned as strings.\n")) + f.WriteString(fmt.Sprintf(" Everything\n")) + f.WriteString(fmt.Sprintf(" contact:\n")) + f.WriteString(fmt.Sprintf(" - name: Mark Boss\n")) + f.WriteString(fmt.Sprintf(" email: mark.boss@uni-tuebingen.de\n")) + f.WriteString(fmt.Sprintf(" url: https://uni-tuebingen.de\n")) + f.WriteString(fmt.Sprintf(" - name: Patrick Wieschollek\n")) + f.WriteString(fmt.Sprintf(" email: Patrick.Wieschollek@uni-tuebingen.de\n")) + f.WriteString(fmt.Sprintf(" url: https://uni-tuebingen.de\n")) + f.WriteString(fmt.Sprintf("servers:\n")) + f.WriteString(fmt.Sprintf(" - url: http://localhost:3000/api/v1\n")) + f.WriteString(fmt.Sprintf("security:\n")) + f.WriteString(fmt.Sprintf(" - bearerAuth: []\n")) + f.WriteString(fmt.Sprintf(" - cookieAuth: []\n")) + f.WriteString(fmt.Sprintf("tags:\n")) + f.WriteString(fmt.Sprintf(" - name: common\n")) + f.WriteString(fmt.Sprintf(" description: common request\n")) + f.WriteString(fmt.Sprintf(" - name: auth\n")) + f.WriteString(fmt.Sprintf(" description: authenticated related requests\n")) + f.WriteString(fmt.Sprintf(" - name: account\n")) + f.WriteString(fmt.Sprintf(" description: account related requests\n")) + f.WriteString(fmt.Sprintf(" - name: email\n")) + f.WriteString(fmt.Sprintf(" description: Email related requests\n")) + f.WriteString(fmt.Sprintf(" - name: users\n")) + f.WriteString(fmt.Sprintf(" description: User related requests\n")) + f.WriteString(fmt.Sprintf(" - name: courses\n")) + f.WriteString(fmt.Sprintf(" description: Course related requests\n")) + f.WriteString(fmt.Sprintf(" - name: sheets\n")) + f.WriteString(fmt.Sprintf(" description: Exercise sheets related requests\n")) + f.WriteString(fmt.Sprintf(" - name: tasks\n")) + f.WriteString(fmt.Sprintf(" description: Exercise tasks related requests\n")) + f.WriteString(fmt.Sprintf(" - name: submissions\n")) + f.WriteString(fmt.Sprintf(" description: Submissions related requests\n")) + f.WriteString(fmt.Sprintf(" - name: grades\n")) + f.WriteString(fmt.Sprintf(" description: Gradings related requests\n")) + f.WriteString(fmt.Sprintf(" - name: groups\n")) + f.WriteString(fmt.Sprintf(" description: Exercise groups related requests\n")) + f.WriteString(fmt.Sprintf(" - name: enrollments\n")) + f.WriteString(fmt.Sprintf(" description: Enrollments related requests\n")) + f.WriteString(fmt.Sprintf(" - name: materials\n")) + f.WriteString(fmt.Sprintf(" description: Exercise material related requests\n")) + f.WriteString(fmt.Sprintf(" - name: internal\n")) + f.WriteString(fmt.Sprintf(" description: Endpoints for internal usage only\n")) + + f.WriteString(fmt.Sprintf("components:\n")) + f.WriteString(fmt.Sprintf(" securitySchemes:\n")) + f.WriteString(fmt.Sprintf(" bearerAuth:\n")) + f.WriteString(fmt.Sprintf(" type: http\n")) + f.WriteString(fmt.Sprintf(" scheme: bearer\n")) + f.WriteString(fmt.Sprintf(" bearerFormat: JWT\n")) + f.WriteString(fmt.Sprintf(" cookieAuth:\n")) + f.WriteString(fmt.Sprintf(" type: apiKey\n")) + f.WriteString(fmt.Sprintf(" in: cookie\n")) + f.WriteString(fmt.Sprintf(" name: SESSIONID\n")) + f.WriteString(fmt.Sprintf(" schemas:\n")) + f.WriteString(fmt.Sprintf(" Error:\n")) + f.WriteString(fmt.Sprintf(" type: object\n")) + f.WriteString(fmt.Sprintf(" properties:\n")) + f.WriteString(fmt.Sprintf(" code:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" message:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" required:\n")) + f.WriteString(fmt.Sprintf(" - code\n")) + f.WriteString(fmt.Sprintf(" - message\n")) + + f.WriteString(swagger.SwaggerStructsWithSuffix(fset, pkgs, "Request", 4)) + f.WriteString(swagger.SwaggerStructsWithSuffix(fset, pkgs, "Response", 4)) + + f.WriteString(fmt.Sprintf(" responses:\n")) + f.WriteString(fmt.Sprintf(" pongResponse:\n")) + f.WriteString(fmt.Sprintf(" description: Server is up and running\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" text/plain:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" example: pong\n")) + f.WriteString(fmt.Sprintf(" ZipFile:\n")) + f.WriteString(fmt.Sprintf(" description: A file as a download.\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" application/zip:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" format: binary\n")) + f.WriteString(fmt.Sprintf(" ImageFile:\n")) + f.WriteString(fmt.Sprintf(" description: A file as a download.\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" image/jpeg:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" format: binary\n")) + f.WriteString(fmt.Sprintf(" OK:\n")) + f.WriteString(fmt.Sprintf(" description: Post successfully delivered.\n")) + f.WriteString(fmt.Sprintf(" NoContent:\n")) + f.WriteString(fmt.Sprintf(" description: Update was successful.\n")) + f.WriteString(fmt.Sprintf(" BadRequest:\n")) + f.WriteString(fmt.Sprintf(" description: The request is in a wrong format or contains missing fields.\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" application/json:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) + f.WriteString(fmt.Sprintf(" Unauthenticated:\n")) + f.WriteString(fmt.Sprintf(" description: User is not logged in.\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" application/json:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) + f.WriteString(fmt.Sprintf(" Unauthorized:\n")) + f.WriteString(fmt.Sprintf(" description: User is logged in but has not the permission to perform the request.\n")) + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" application/json:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/Error\"\n")) + + // create all responses + f.WriteString(swagger.SwaggerResponsesWithSuffix(fset, pkgs, "Response", 4)) + + duplicateResponseLists := make(map[string]int) + + // create all list responses + pre := strings.Repeat(" ", 4) + for url, _ := range endpoints { + for _, action := range endpoints[url] { + for _, r := range action.Details.Responses { + text := strings.TrimSpace(r.Text) + if strings.HasSuffix(text, "List") { + + _, exists := duplicateResponseLists[strings.TrimSpace(text)] + + if !exists { + f.WriteString(fmt.Sprintf("%s%s:\n", pre, text)) + f.WriteString(fmt.Sprintf("%s description: done\n", pre)) + f.WriteString(fmt.Sprintf("%s content:\n", pre)) + f.WriteString(fmt.Sprintf("%s application/json:\n", pre)) + f.WriteString(fmt.Sprintf("%s schema:\n", pre)) + f.WriteString(fmt.Sprintf("%s type: array\n", pre)) + f.WriteString(fmt.Sprintf("%s items:\n", pre)) + f.WriteString(fmt.Sprintf("%s $ref: \"#/components/schemas/%s\"\n", pre, text[:len(text)-4])) + + duplicateResponseLists[strings.TrimSpace(text)] = 0 + } + } + + } + } + } + + f.WriteString(fmt.Sprintf("paths:\n")) + for url, _ := range endpoints { + + f.WriteString(fmt.Sprintf(" %s:\n", url)) + for _, action := range endpoints[url] { + + if action.Details.Method == "post" || action.Details.Method == "put" || action.Details.Method == "patch" { + if action.Details.Request == "" { + panic(fmt.Sprintf("endpoint '%s' is '%s' but has no request body in %v", + url, action.Details.Method, action.Position)) + } + } + + if action.Details.Method == "get" { + // test wether we have a 200 response + found := false + for _, r := range action.Details.Responses { + if r.Code == 200 { + found = true + break + } + } + if !found { + panic(fmt.Sprintf("endpoint '%s' is '%s' but has no 200 response in %v", + url, action.Details.Method, action.Position)) + } + } + + f.WriteString(fmt.Sprintf(" # implementation in %v\n", action.Position)) + f.WriteString(fmt.Sprintf(" %s:\n", action.Details.Method)) + f.WriteString(fmt.Sprintf(" summary: %s\n", action.Details.Summary)) + if action.Details.Description != "" { + f.WriteString(fmt.Sprintf(" description: >\n %s\n", action.Details.Description)) + } + if len(action.Details.Tags) > 0 { + f.WriteString(fmt.Sprintf(" tags: \n\n")) + for _, tag := range action.Details.Tags { + + f.WriteString(fmt.Sprintf(" - %s\n", tag)) + } + } + if len(action.Details.URLParams)+len(action.Details.QueryParams) > 0 { + f.WriteString(fmt.Sprintf(" parameters:\n")) + for _, el := range action.Details.URLParams { + f.WriteString(fmt.Sprintf(" - in: path\n")) + f.WriteString(fmt.Sprintf(" name: %s\n", el.Name)) + f.WriteString(fmt.Sprintf(" schema: \n")) + f.WriteString(fmt.Sprintf(" type: %s\n", el.Type)) + f.WriteString(fmt.Sprintf(" required: true \n")) + } + + for _, el := range action.Details.QueryParams { + f.WriteString(fmt.Sprintf(" - in: query\n")) + f.WriteString(fmt.Sprintf(" name: %s\n", el.Name)) + f.WriteString(fmt.Sprintf(" schema: \n")) + f.WriteString(fmt.Sprintf(" type: %s\n", el.Type)) + f.WriteString(fmt.Sprintf(" required: false \n")) + } + } + + if action.Details.Request != "" { + f.WriteString(fmt.Sprintf(" requestBody:\n")) + f.WriteString(fmt.Sprintf(" required: true\n")) + + switch action.Details.Request { + case "zipfile": + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" multipart/form-data:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" type: object\n")) + f.WriteString(fmt.Sprintf(" properties:\n")) + f.WriteString(fmt.Sprintf(" file_data:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" format: binary\n")) + f.WriteString(fmt.Sprintf(" encoding:\n")) + f.WriteString(fmt.Sprintf(" file_data:\n")) + f.WriteString(fmt.Sprintf(" contentType: application/zip\n")) + case "imagefile": + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" multipart/form-data:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" type: object\n")) + f.WriteString(fmt.Sprintf(" properties:\n")) + f.WriteString(fmt.Sprintf(" file_data:\n")) + f.WriteString(fmt.Sprintf(" type: string\n")) + f.WriteString(fmt.Sprintf(" format: binary\n")) + f.WriteString(fmt.Sprintf(" encoding:\n")) + f.WriteString(fmt.Sprintf(" file_data:\n")) + f.WriteString(fmt.Sprintf(" contentType: image/jpeg\n")) + case "empty": + + default: + f.WriteString(fmt.Sprintf(" content:\n")) + f.WriteString(fmt.Sprintf(" application/json:\n")) + f.WriteString(fmt.Sprintf(" schema:\n")) + f.WriteString(fmt.Sprintf(" $ref: \"#/components/schemas/%s\"\n", action.Details.Request)) + } + + } + f.WriteString(fmt.Sprintf(" responses:\n")) + for _, r := range action.Details.Responses { + + f.WriteString(fmt.Sprintf(" \"%v\":\n", r.Code)) + f.WriteString(fmt.Sprintf(" $ref: \"#/components/responses/%s\"\n", r.Text)) + + } + } + + } + + f.Sync() } diff --git a/docs/swagger/endpoints.go b/docs/swagger/endpoints.go index 031d11f..9161b25 100644 --- a/docs/swagger/endpoints.go +++ b/docs/swagger/endpoints.go @@ -19,195 +19,195 @@ package swagger import ( - "errors" - "fmt" - "go/ast" - "go/token" - "strconv" - "strings" + "errors" + "fmt" + "go/ast" + "go/token" + "strconv" + "strings" ) type Response struct { - Code int - Text string + Code int + Text string } func ParseResponse(source string) (*Response, error) { - tmp := strings.Split(source, ",") - stringCode := strings.TrimSpace(tmp[0]) - stringText := strings.TrimSpace(tmp[1]) - code, err := strconv.Atoi(stringCode) - if err != nil { - return nil, err - } + tmp := strings.Split(source, ",") + stringCode := strings.TrimSpace(tmp[0]) + stringText := strings.TrimSpace(tmp[1]) + code, err := strconv.Atoi(stringCode) + if err != nil { + return nil, err + } - return &Response{Code: code, Text: stringText}, nil + return &Response{Code: code, Text: stringText}, nil } func ParseParameter(source string) (*Parameter, error) { - tmp := strings.Split(source, ",") - if len(tmp) != 2 { - return nil, errors.New(fmt.Sprintf("error in \"%s\"", source)) - } - stringName := strings.TrimSpace(tmp[0]) - stringType := strings.TrimSpace(tmp[1]) - - return &Parameter{Name: stringName, Type: stringType}, nil + tmp := strings.Split(source, ",") + if len(tmp) != 2 { + return nil, errors.New(fmt.Sprintf("error in \"%s\"", source)) + } + stringName := strings.TrimSpace(tmp[0]) + stringType := strings.TrimSpace(tmp[1]) + + return &Parameter{Name: stringName, Type: stringType}, nil } type Endpoint struct { - Node ast.Node - Info *ast.FuncDecl - Comments *ast.CommentGroup - Details EndpointDetails - Position token.Position + Node ast.Node + Info *ast.FuncDecl + Comments *ast.CommentGroup + Details EndpointDetails + Position token.Position } type Parameter struct { - Name string - Type string + Name string + Type string } type EndpointDetails struct { - URL string - Method string - URLParams []*Parameter - QueryParams []*Parameter - Request string - Tags []string - Responses []*Response - Description string - Summary string + URL string + Method string + URLParams []*Parameter + QueryParams []*Parameter + Request string + Tags []string + Responses []*Response + Description string + Summary string } func NewEndpoint(node ast.Node) *Endpoint { - return &Endpoint{ - Node: node, - Info: node.(*ast.FuncDecl), - Comments: node.(*ast.FuncDecl).Doc, - Details: parseComments(node.(*ast.FuncDecl).Doc), - } + return &Endpoint{ + Node: node, + Info: node.(*ast.FuncDecl), + Comments: node.(*ast.FuncDecl).Doc, + Details: parseComments(node.(*ast.FuncDecl).Doc), + } } func parseComments(group *ast.CommentGroup) EndpointDetails { - descrp := EndpointDetails{} - descrStart := -1 - for k, el := range group.List { - if el != nil { - // fmt.Println(el.Text) - if strings.Contains(el.Text, "METHOD:") { - tmp := strings.Split(el.Text, ":") - descrp.Method = strings.TrimSpace(tmp[1]) - } - if strings.Contains(el.Text, "URL:") { - tmp := strings.Split(el.Text, ":") - descrp.URL = strings.TrimSpace(tmp[1]) - } - if strings.Contains(el.Text, "REQUEST:") { - tmp := strings.Split(el.Text, ":") - descrp.Request = strings.TrimSpace(tmp[1]) - } - if strings.Contains(el.Text, "TAG:") { - tmp := strings.Split(el.Text, ":") - - descrp.Tags = append(descrp.Tags, strings.TrimSpace(tmp[1])) - } - if strings.Contains(el.Text, "SUMMARY:") { - tmp := strings.Split(el.Text, ":") - descrp.Summary = strings.TrimSpace(tmp[1]) - } - if strings.Contains(el.Text, "RESPONSE:") { - tmp := strings.Split(el.Text, ":") - resp, err := ParseResponse(tmp[1]) - if err == nil { - // descrp.Responses = append(descrp.Responses, strings.TrimSpace(tmp[1])) - descrp.Responses = append(descrp.Responses, resp) - } - } - if strings.Contains(el.Text, "URLPARAM:") { - tmp := strings.Split(el.Text, ":") - resp, err := ParseParameter(tmp[1]) - if err == nil { - descrp.URLParams = append(descrp.URLParams, resp) - } - } - if strings.Contains(el.Text, "QUERYPARAM:") { - tmp := strings.Split(el.Text, ":") - resp, err := ParseParameter(tmp[1]) - if err == nil { - descrp.QueryParams = append(descrp.QueryParams, resp) - } - } - if strings.Contains(el.Text, "DESCRIPTION:") { - descrStart = k + 1 - break - } - } - } - - if descrStart > -1 { - text := "" - for k, el := range group.List { - if k >= descrStart { - text = text + el.Text[2:] - } - } - descrp.Description = text - } - return descrp + descrp := EndpointDetails{} + descrStart := -1 + for k, el := range group.List { + if el != nil { + // fmt.Println(el.Text) + if strings.Contains(el.Text, "METHOD:") { + tmp := strings.Split(el.Text, ":") + descrp.Method = strings.TrimSpace(tmp[1]) + } + if strings.Contains(el.Text, "URL:") { + tmp := strings.Split(el.Text, ":") + descrp.URL = strings.TrimSpace(tmp[1]) + } + if strings.Contains(el.Text, "REQUEST:") { + tmp := strings.Split(el.Text, ":") + descrp.Request = strings.TrimSpace(tmp[1]) + } + if strings.Contains(el.Text, "TAG:") { + tmp := strings.Split(el.Text, ":") + + descrp.Tags = append(descrp.Tags, strings.TrimSpace(tmp[1])) + } + if strings.Contains(el.Text, "SUMMARY:") { + tmp := strings.Split(el.Text, ":") + descrp.Summary = strings.TrimSpace(tmp[1]) + } + if strings.Contains(el.Text, "RESPONSE:") { + tmp := strings.Split(el.Text, ":") + resp, err := ParseResponse(tmp[1]) + if err == nil { + // descrp.Responses = append(descrp.Responses, strings.TrimSpace(tmp[1])) + descrp.Responses = append(descrp.Responses, resp) + } + } + if strings.Contains(el.Text, "URLPARAM:") { + tmp := strings.Split(el.Text, ":") + resp, err := ParseParameter(tmp[1]) + if err == nil { + descrp.URLParams = append(descrp.URLParams, resp) + } + } + if strings.Contains(el.Text, "QUERYPARAM:") { + tmp := strings.Split(el.Text, ":") + resp, err := ParseParameter(tmp[1]) + if err == nil { + descrp.QueryParams = append(descrp.QueryParams, resp) + } + } + if strings.Contains(el.Text, "DESCRIPTION:") { + descrStart = k + 1 + break + } + } + } + + if descrStart > -1 { + text := "" + for k, el := range group.List { + if k >= descrStart { + text = text + el.Text[2:] + } + } + descrp.Description = text + } + return descrp } func isPublicEndpoint(group *ast.CommentGroup) (string, bool) { - if group != nil { - if group.List != nil { - for _, el := range group.List { - if el != nil { - // fmt.Println(el.Text) - if strings.Contains(el.Text, "is public endpoint for") { - return el.Text, true - } - } - } - } - } - return "", false + if group != nil { + if group.List != nil { + for _, el := range group.List { + if el != nil { + // fmt.Println(el.Text) + if strings.Contains(el.Text, "is public endpoint for") { + return el.Text, true + } + } + } + } + } + return "", false } // Main docs func GetEndpoints(pkg map[string]*ast.Package, fset *token.FileSet) map[string][]*Endpoint { - result := make(map[string][]*Endpoint) - // fmt.Println(fset) - // var endpoints []*Endpoint - - // gather endpoints - for _, pkg := range pkg { - ast.Inspect(pkg, func(n ast.Node) bool { - switch x := n.(type) { - case *ast.FuncDecl: - if str, is := isPublicEndpoint(x.Doc); is { - // commentNames := strings.Split(str[2:], " ") - // commentName := strings.TrimSpace(commentNames[0]) - functionName := strings.TrimSpace(n.(*ast.FuncDecl).Name.Name) - // fmt.Println("---------------", str) - // fmt.Println("--", functionName, fset.Position(n.Pos())) - // fmt.Println("--") - - if !strings.Contains(str, functionName) { - msg := fmt.Sprintf("\"%s\" does not contains \"%s\" in %v", str, functionName, fset.Position(n.Pos())) - panic(msg) - } - - ep := NewEndpoint(x) - ep.Position = fset.Position(n.Pos()) - result[ep.Details.URL] = append(result[ep.Details.URL], ep) - } - } - - return true - }) - } - - return result + result := make(map[string][]*Endpoint) + // fmt.Println(fset) + // var endpoints []*Endpoint + + // gather endpoints + for _, pkg := range pkg { + ast.Inspect(pkg, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.FuncDecl: + if str, is := isPublicEndpoint(x.Doc); is { + // commentNames := strings.Split(str[2:], " ") + // commentName := strings.TrimSpace(commentNames[0]) + functionName := strings.TrimSpace(n.(*ast.FuncDecl).Name.Name) + // fmt.Println("---------------", str) + // fmt.Println("--", functionName, fset.Position(n.Pos())) + // fmt.Println("--") + + if !strings.Contains(str, functionName) { + msg := fmt.Sprintf("\"%s\" does not contains \"%s\" in %v", str, functionName, fset.Position(n.Pos())) + panic(msg) + } + + ep := NewEndpoint(x) + ep.Position = fset.Position(n.Pos()) + result[ep.Details.URL] = append(result[ep.Details.URL], ep) + } + } + + return true + }) + } + + return result } diff --git a/docs/swagger/swagger.go b/docs/swagger/swagger.go index 463273a..e916da5 100644 --- a/docs/swagger/swagger.go +++ b/docs/swagger/swagger.go @@ -19,174 +19,174 @@ package swagger import ( - "fmt" - "go/ast" - "go/token" - "strings" + "fmt" + "go/ast" + "go/token" + "strings" ) func SwaggerStructs(structDescr *Struct, depth int) string { - source := "" - pre := strings.Repeat(" ", depth) - - if len(structDescr.Fields) > 0 { - source = source + fmt.Sprintf("%stype: object\n", pre) - source = source + fmt.Sprintf("%sproperties:\n", pre) - } - - requiredFields := []string{} - examples := make(map[string]string) - - for _, fieldDescr := range structDescr.Fields { - source = source + fmt.Sprintf("%s %s:\n", pre, fieldDescr.Tag.Name) - - if len(fieldDescr.Childs) > 0 { - for _, ch := range fieldDescr.Childs { - source = source + SwaggerStructs(ch, depth+4) - } - - } else { - - switch x := fieldDescr.Type.(type) { - case *ast.Ident: - switch x.Name { - case "string": - source = source + fmt.Sprintf("%s type: %v\n", pre, fieldDescr.Type) - if strings.Contains(fieldDescr.Tag.Name, "email") { - source = source + fmt.Sprintf("%s format: email\n", pre) - } - if strings.Contains(fieldDescr.Tag.Name, "password") { - source = source + fmt.Sprintf("%s format: password\n", pre) - } - case "int": - source = source + fmt.Sprintf("%s type: integer\n", pre) - case "bool": - source = source + fmt.Sprintf("%s type: boolean\n", pre) - case "int64": - source = source + fmt.Sprintf("%s type: integer\n", pre) - source = source + fmt.Sprintf("%s format: %v\n", pre, fieldDescr.Type) - case "float32", "float64": - source = source + fmt.Sprintf("%s type: number\n", pre) - source = source + fmt.Sprintf("%s format: %v\n", pre, fieldDescr.Type) - default: - - } - - if fieldDescr.Tag.Length != "" { - source = source + fmt.Sprintf("%s length: %s\n", pre, fieldDescr.Tag.Length) - } - if fieldDescr.Tag.MinValue != "" { - source = source + fmt.Sprintf("%s minimum: %s\n", pre, fieldDescr.Tag.MinValue) - } - if fieldDescr.Tag.MaxValue != "" { - source = source + fmt.Sprintf("%s maximum: %s\n", pre, fieldDescr.Tag.MaxValue) - } - - if fieldDescr.Tag.Example == "" { - if !strings.HasPrefix(structDescr.Name, "Err") { - panic(fmt.Sprintf("field '%s' has no example in struct '%v'", fieldDescr.Tag.Name, structDescr.Name)) - } - // - } else { - examples[fieldDescr.Tag.Name] = fieldDescr.Tag.Example - } - - case *ast.SelectorExpr: - if x.X.(*ast.Ident).Name == "null" && x.Sel.Name == "String" { - source = source + fmt.Sprintf("%s type: string\n", pre) - fieldDescr.Tag.Required = false - } - - if x.X.(*ast.Ident).Name == "time" && x.Sel.Name == "Time" { - source = source + fmt.Sprintf("%s type: string\n", pre) - source = source + fmt.Sprintf("%s format: date-time\n", pre) - fieldDescr.Tag.Required = true - examples[fieldDescr.Tag.Name] = "'2019-07-30T23:59:59Z'" - } - default: - // spew.Dump(fieldDescr) - // panic(fieldDescr) - // panic(x) - } - - } - - if fieldDescr.Tag.Required { - requiredFields = append(requiredFields, fieldDescr.Tag.Name) - - } - } - - source = source + fmt.Sprintf("%srequired:\n", pre) - for _, f := range requiredFields { - source = source + fmt.Sprintf("%s - %s\n", pre, f) - } - - if len(examples) > 0 { - source = source + fmt.Sprintf("%sexample:\n", pre) - for k, f := range examples { - source = source + fmt.Sprintf("%s %s: %s\n", pre, k, f) - } - - } - - return source + source := "" + pre := strings.Repeat(" ", depth) + + if len(structDescr.Fields) > 0 { + source = source + fmt.Sprintf("%stype: object\n", pre) + source = source + fmt.Sprintf("%sproperties:\n", pre) + } + + requiredFields := []string{} + examples := make(map[string]string) + + for _, fieldDescr := range structDescr.Fields { + source = source + fmt.Sprintf("%s %s:\n", pre, fieldDescr.Tag.Name) + + if len(fieldDescr.Childs) > 0 { + for _, ch := range fieldDescr.Childs { + source = source + SwaggerStructs(ch, depth+4) + } + + } else { + + switch x := fieldDescr.Type.(type) { + case *ast.Ident: + switch x.Name { + case "string": + source = source + fmt.Sprintf("%s type: %v\n", pre, fieldDescr.Type) + if strings.Contains(fieldDescr.Tag.Name, "email") { + source = source + fmt.Sprintf("%s format: email\n", pre) + } + if strings.Contains(fieldDescr.Tag.Name, "password") { + source = source + fmt.Sprintf("%s format: password\n", pre) + } + case "int": + source = source + fmt.Sprintf("%s type: integer\n", pre) + case "bool": + source = source + fmt.Sprintf("%s type: boolean\n", pre) + case "int64": + source = source + fmt.Sprintf("%s type: integer\n", pre) + source = source + fmt.Sprintf("%s format: %v\n", pre, fieldDescr.Type) + case "float32", "float64": + source = source + fmt.Sprintf("%s type: number\n", pre) + source = source + fmt.Sprintf("%s format: %v\n", pre, fieldDescr.Type) + default: + + } + + if fieldDescr.Tag.Length != "" { + source = source + fmt.Sprintf("%s length: %s\n", pre, fieldDescr.Tag.Length) + } + if fieldDescr.Tag.MinValue != "" { + source = source + fmt.Sprintf("%s minimum: %s\n", pre, fieldDescr.Tag.MinValue) + } + if fieldDescr.Tag.MaxValue != "" { + source = source + fmt.Sprintf("%s maximum: %s\n", pre, fieldDescr.Tag.MaxValue) + } + + if fieldDescr.Tag.Example == "" { + if !strings.HasPrefix(structDescr.Name, "Err") { + panic(fmt.Sprintf("field '%s' has no example in struct '%v'", fieldDescr.Tag.Name, structDescr.Name)) + } + // + } else { + examples[fieldDescr.Tag.Name] = fieldDescr.Tag.Example + } + + case *ast.SelectorExpr: + if x.X.(*ast.Ident).Name == "null" && x.Sel.Name == "String" { + source = source + fmt.Sprintf("%s type: string\n", pre) + fieldDescr.Tag.Required = false + } + + if x.X.(*ast.Ident).Name == "time" && x.Sel.Name == "Time" { + source = source + fmt.Sprintf("%s type: string\n", pre) + source = source + fmt.Sprintf("%s format: date-time\n", pre) + fieldDescr.Tag.Required = true + examples[fieldDescr.Tag.Name] = "'2019-07-30T23:59:59Z'" + } + default: + // spew.Dump(fieldDescr) + // panic(fieldDescr) + // panic(x) + } + + } + + if fieldDescr.Tag.Required { + requiredFields = append(requiredFields, fieldDescr.Tag.Name) + + } + } + + source = source + fmt.Sprintf("%srequired:\n", pre) + for _, f := range requiredFields { + source = source + fmt.Sprintf("%s - %s\n", pre, f) + } + + if len(examples) > 0 { + source = source + fmt.Sprintf("%sexample:\n", pre) + for k, f := range examples { + source = source + fmt.Sprintf("%s %s: %s\n", pre, k, f) + } + + } + + return source } func SwaggerStructsWithSuffix(fset *token.FileSet, pkgs map[string]*ast.Package, suffix string, depth int) string { - source := "" - // gather structs - for _, pkg := range pkgs { - ast.Inspect(pkg, func(n ast.Node) bool { - switch x := n.(type) { - case *ast.TypeSpec: - result, ok := x.Type.(*ast.StructType) - if ok { - name := n.(*ast.TypeSpec).Name.Name - if strings.HasSuffix(name, suffix) { - structDescr, err := ParseStruct(fset, n, result, name, 0) - if err == nil { - pre := strings.Repeat(" ", depth) - source = source + fmt.Sprintf("%s# implementation in %v:\n", pre, structDescr.Position) - source = source + fmt.Sprintf("%s%s:\n", pre, name) - - source = source + SwaggerStructs(structDescr, depth+2) - } - } - } - } - return true - }) - } - return source + source := "" + // gather structs + for _, pkg := range pkgs { + ast.Inspect(pkg, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + result, ok := x.Type.(*ast.StructType) + if ok { + name := n.(*ast.TypeSpec).Name.Name + if strings.HasSuffix(name, suffix) { + structDescr, err := ParseStruct(fset, n, result, name, 0) + if err == nil { + pre := strings.Repeat(" ", depth) + source = source + fmt.Sprintf("%s# implementation in %v:\n", pre, structDescr.Position) + source = source + fmt.Sprintf("%s%s:\n", pre, name) + + source = source + SwaggerStructs(structDescr, depth+2) + } + } + } + } + return true + }) + } + return source } func SwaggerResponsesWithSuffix(fset *token.FileSet, pkgs map[string]*ast.Package, suffix string, depth int) string { - source := "" - pre := strings.Repeat(" ", depth) - // gather structs - for _, pkg := range pkgs { - ast.Inspect(pkg, func(n ast.Node) bool { - switch x := n.(type) { - case *ast.TypeSpec: - _, ok := x.Type.(*ast.StructType) - if ok { - name := n.(*ast.TypeSpec).Name.Name - if strings.HasSuffix(name, suffix) { - source = source + fmt.Sprintf("%s# implementation in %v:\n", pre, fset.Position(n.Pos())) - source = source + fmt.Sprintf("%s%s:\n", pre, name) - source = source + fmt.Sprintf("%s description: done\n", pre) - source = source + fmt.Sprintf("%s content:\n", pre) - source = source + fmt.Sprintf("%s application/json:\n", pre) - source = source + fmt.Sprintf("%s schema:\n", pre) - source = source + fmt.Sprintf("%s $ref: \"#/components/schemas/%s\"\n", pre, name) - } - } - } - return true - }) - } - - return source + source := "" + pre := strings.Repeat(" ", depth) + // gather structs + for _, pkg := range pkgs { + ast.Inspect(pkg, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + _, ok := x.Type.(*ast.StructType) + if ok { + name := n.(*ast.TypeSpec).Name.Name + if strings.HasSuffix(name, suffix) { + source = source + fmt.Sprintf("%s# implementation in %v:\n", pre, fset.Position(n.Pos())) + source = source + fmt.Sprintf("%s%s:\n", pre, name) + source = source + fmt.Sprintf("%s description: done\n", pre) + source = source + fmt.Sprintf("%s content:\n", pre) + source = source + fmt.Sprintf("%s application/json:\n", pre) + source = source + fmt.Sprintf("%s schema:\n", pre) + source = source + fmt.Sprintf("%s $ref: \"#/components/schemas/%s\"\n", pre, name) + } + } + } + return true + }) + } + + return source } diff --git a/docs/swagger/tag.go b/docs/swagger/tag.go index ac7de77..a36f90c 100644 --- a/docs/swagger/tag.go +++ b/docs/swagger/tag.go @@ -19,153 +19,153 @@ package swagger import ( - "strconv" - "strings" + "strconv" + "strings" ) type rawTag struct { - // Key is the tag key, such as json, xml, etc.. - // i.e: `json:"foo,omitempty". Here key is: "json" - Key string + // Key is the tag key, such as json, xml, etc.. + // i.e: `json:"foo,omitempty". Here key is: "json" + Key string - // Name is a part of the value - // i.e: `json:"foo,omitempty". Here name is: "foo" - Name string + // Name is a part of the value + // i.e: `json:"foo,omitempty". Here name is: "foo" + Name string - // Options is a part of the value. It contains a slice of tag options i.e: - // `json:"foo,omitempty". Here options is: ["omitempty"] - Options []string + // Options is a part of the value. It contains a slice of tag options i.e: + // `json:"foo,omitempty". Here options is: ["omitempty"] + Options []string } type Tag struct { - Name string - Example string - Required bool - Length string - MinLength string - MaxLength string - MinValue string - MaxValue string + Name string + Example string + Required bool + Length string + MinLength string + MaxLength string + MinValue string + MaxValue string } func parseTag(tag string) (*Tag, error) { - var tags []*rawTag - - tag = tag[1 : len(tag)-1] - - // NOTE(arslan) following code is from reflect and vet package with some - // modifications to collect all necessary information and extend it with - // usable methods - for tag != "" { - // fmt.Println("parse:", tag) - if len(tag) < 3 { - return nil, nil - } - // Skip leading space. - i := 0 - for i < len(tag) && tag[i] == ' ' { - i++ - } - tag = tag[i:] - if tag == "" { - return nil, nil - } - - // Scan to colon. A space, a quote or a control character is a syntax - // error. Strictly speaking, control chars include the range [0x7f, - // 0x9f], not just [0x00, 0x1f], but in practice, we ignore the - // multi-byte control characters as it is simpler to inspect the tag's - // bytes than the tag's runes. - i = 0 - for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { - i++ - } - - if i == 0 { - return nil, errTagKeySyntax - } - if i+1 >= len(tag) || tag[i] != ':' { - return nil, errTagSyntax - } - if tag[i+1] != '"' { - return nil, errTagValueSyntax - } - - key := string(tag[:i]) - tag = tag[i+1:] - - // Scan quoted string to find value. - i = 1 - for i < len(tag) && tag[i] != '"' { - if tag[i] == '\\' { - i++ - } - i++ - } - if i >= len(tag) { - return nil, errTagValueSyntax - } - - qvalue := string(tag[:i+1]) - tag = tag[i+1:] - - value, err := strconv.Unquote(qvalue) - if err != nil { - return nil, errTagValueSyntax - } - - res := strings.Split(value, ",") - name := res[0] - options := res[1:] - if len(options) == 0 { - options = nil - } - // fmt.Println("got ", key, name) - tags = append(tags, &rawTag{ - Key: key, - Name: name, - Options: options, - }) - } - - field := &Tag{} - field.Required = true - for _, tag := range tags { - - if tag.Key == "json" { - field.Name = tag.Name - } - - if tag.Key == "example" { - field.Example = tag.Name - } - - if tag.Key == "minlen" { - field.MinLength = tag.Name - } - - if tag.Key == "maxlen" { - field.MaxLength = tag.Name - } - - if tag.Key == "len" { - field.Length = tag.Name - } - - if tag.Key == "minval" { - field.MinValue = tag.Name - } - - if tag.Key == "maxval" { - field.MaxValue = tag.Name - } - - if tag.Key == "required" { - if tag.Name == "false" { - field.Required = false - } - } - } - - return field, nil + var tags []*rawTag + + tag = tag[1 : len(tag)-1] + + // NOTE(arslan) following code is from reflect and vet package with some + // modifications to collect all necessary information and extend it with + // usable methods + for tag != "" { + // fmt.Println("parse:", tag) + if len(tag) < 3 { + return nil, nil + } + // Skip leading space. + i := 0 + for i < len(tag) && tag[i] == ' ' { + i++ + } + tag = tag[i:] + if tag == "" { + return nil, nil + } + + // Scan to colon. A space, a quote or a control character is a syntax + // error. Strictly speaking, control chars include the range [0x7f, + // 0x9f], not just [0x00, 0x1f], but in practice, we ignore the + // multi-byte control characters as it is simpler to inspect the tag's + // bytes than the tag's runes. + i = 0 + for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { + i++ + } + + if i == 0 { + return nil, errTagKeySyntax + } + if i+1 >= len(tag) || tag[i] != ':' { + return nil, errTagSyntax + } + if tag[i+1] != '"' { + return nil, errTagValueSyntax + } + + key := string(tag[:i]) + tag = tag[i+1:] + + // Scan quoted string to find value. + i = 1 + for i < len(tag) && tag[i] != '"' { + if tag[i] == '\\' { + i++ + } + i++ + } + if i >= len(tag) { + return nil, errTagValueSyntax + } + + qvalue := string(tag[:i+1]) + tag = tag[i+1:] + + value, err := strconv.Unquote(qvalue) + if err != nil { + return nil, errTagValueSyntax + } + + res := strings.Split(value, ",") + name := res[0] + options := res[1:] + if len(options) == 0 { + options = nil + } + // fmt.Println("got ", key, name) + tags = append(tags, &rawTag{ + Key: key, + Name: name, + Options: options, + }) + } + + field := &Tag{} + field.Required = true + for _, tag := range tags { + + if tag.Key == "json" { + field.Name = tag.Name + } + + if tag.Key == "example" { + field.Example = tag.Name + } + + if tag.Key == "minlen" { + field.MinLength = tag.Name + } + + if tag.Key == "maxlen" { + field.MaxLength = tag.Name + } + + if tag.Key == "len" { + field.Length = tag.Name + } + + if tag.Key == "minval" { + field.MinValue = tag.Name + } + + if tag.Key == "maxval" { + field.MaxValue = tag.Name + } + + if tag.Key == "required" { + if tag.Name == "false" { + field.Required = false + } + } + } + + return field, nil } diff --git a/go.sum b/go.sum index 706485c..24f441a 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-txdb v0.1.2 h1:Rkgv1GEOrnZhzMDS+DS81QTatJrYGxDpYI3ELIdamZo= github.com/DATA-DOG/go-txdb v0.1.2/go.mod h1:aDC9AAfOY+kLbhVTKKXOwkqr2844my+djxj+Ou4wNb4= -github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc= github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/alexedwards/scs v1.4.0 h1:8klmbSQv2jOxvY8VUcEyxbMWSNNKKtVp2IZdug5b+8g= github.com/alexedwards/scs v1.4.0/go.mod h1:JRIFiXthhMSivuGbxpzUa0/hT5rz2hpyw61Bmd+S1bg= @@ -24,7 +22,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= +github.com/docker/docker v1.13.1 h1:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M= github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -50,7 +48,6 @@ github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6m github.com/go-redis/redis v6.14.0+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -58,19 +55,14 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -86,9 +78,7 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -97,7 +87,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= @@ -113,7 +102,6 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.0 h1:O9FblXGxoTc51M+cqr74Bm2Tmt4PvkA5iu/j8HrkNuY= github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -131,20 +119,15 @@ github.com/streadway/amqp v0.0.0-20190225234609-30f8ed68076e h1:IsT9JYWmthEsrdMp github.com/streadway/amqp v0.0.0-20190225234609-30f8ed68076e/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulule/limiter/v3 v3.1.0 h1:QHg6gL4/mQQIfi2jOMyz9CnCN4Sph6MQWRpIUaZipec= github.com/ulule/limiter/v3 v3.1.0/go.mod h1:hgLFsUPxhPqrgqqLhtdhiwfI1PXAhq//DIrbANjAX5o= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -154,31 +137,23 @@ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb h1:1w588/yEchbPNpa9sEvOcMZYbWHedwJjg4VOAdDHWHk= golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/guregu/null.v3 v3.4.0 h1:AOpMtZ85uElRhQjEDsFx21BkXqFPwA7uoJukd4KErIs= gopkg.in/guregu/null.v3 v3.4.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/model/group.go b/model/group.go index 3e59a88..d77bf09 100644 --- a/model/group.go +++ b/model/group.go @@ -19,38 +19,38 @@ package model import ( - "time" + "time" - null "gopkg.in/guregu/null.v3" + null "gopkg.in/guregu/null.v3" ) // Group is a database view for a group entity type Group struct { - ID int64 `db:"id"` - CreatedAt time.Time `db:"created_at,omitempty"` - UpdatedAt time.Time `db:"updated_at,omitempty"` + ID int64 `db:"id"` + CreatedAt time.Time `db:"created_at,omitempty"` + UpdatedAt time.Time `db:"updated_at,omitempty"` - TutorID int64 `db:"tutor_id"` - CourseID int64 `db:"course_id"` - Description string `db:"description"` + TutorID int64 `db:"tutor_id"` + CourseID int64 `db:"course_id"` + Description string `db:"description"` } // GroupEnrollment is a database view for an enrollment of a student into a group. // Note, Tutors (a person who manage a group) is not enrolled in the group. type GroupEnrollment struct { - ID int64 `db:"id"` + ID int64 `db:"id"` - UserID int64 `db:"user_id"` - GroupID int64 `db:"group_id"` + UserID int64 `db:"user_id"` + GroupID int64 `db:"group_id"` } // GroupWithTutor is a database view of a group including tutor information type GroupWithTutor struct { - Group + Group - TutorFirstName string `db:"tutor_first_name"` - TutorLastName string `db:"tutor_last_name"` - TutorAvatarURL null.String `db:"tutor_avatar_url"` - TutorEmail string `db:"tutor_email"` - TutorLanguage string `db:"tutor_language"` + TutorFirstName string `db:"tutor_first_name"` + TutorLastName string `db:"tutor_last_name"` + TutorAvatarURL null.String `db:"tutor_avatar_url"` + TutorEmail string `db:"tutor_email"` + TutorLanguage string `db:"tutor_language"` } diff --git a/service/consumer.go b/service/consumer.go index 626b368..301ed11 100644 --- a/service/consumer.go +++ b/service/consumer.go @@ -19,187 +19,187 @@ package service import ( - "fmt" + "fmt" - "github.com/sirupsen/logrus" - "github.com/streadway/amqp" + "github.com/sirupsen/logrus" + "github.com/streadway/amqp" ) // Consumer is an object which can act on AMPQ messages type Consumer struct { - Config *Config + Config *Config - conn *amqp.Connection - channel *amqp.Channel - done chan error + conn *amqp.Connection + channel *amqp.Channel + done chan error - handleFunc func(body []byte) error + handleFunc func(body []byte) error } // NewConsumer creates new consumer which can act on AMPQ messages func NewConsumer(cfg *Config, handleFunc func(body []byte) error) (*Consumer, error) { - consumer := &Consumer{ - conn: nil, - channel: nil, - done: make(chan error), - handleFunc: handleFunc, + consumer := &Consumer{ + conn: nil, + channel: nil, + done: make(chan error), + handleFunc: handleFunc, - Config: cfg, - } + Config: cfg, + } - return consumer, nil + return consumer, nil } // Setup connects a consumer to the AMPQ queue from the config func (c *Consumer) Setup() (<-chan amqp.Delivery, error) { - logger := log.WithFields(logrus.Fields{ - // "connection": c.Config.Connection, - "exchange": c.Config.Exchange, - "exchangetype": c.Config.ExchangeType, - "queue": c.Config.Queue, - "key": c.Config.Key, - "tag": c.Config.Tag, - }) - - logger.Info("setup AMPQ connection") - - // fmt.Println("-- Connection", c.Config.Connection) - // fmt.Println("-- Exchange", c.Config.Exchange) - // fmt.Println("-- ExchangeType", c.Config.ExchangeType) - // fmt.Println("-- Queue", c.Config.Queue) - // fmt.Println("-- Key", c.Config.Key) - // fmt.Println("-- Tag", c.Config.Tag) - - var err error - - logger.Info("dialing", c.Config.Connection) - c.conn, err = amqp.Dial(c.Config.Connection) - if err != nil { - return nil, fmt.Errorf("Dial: %s", err) - } - - logger.Info("got Connection, getting Channel") - c.channel, err = c.conn.Channel() - if err != nil { - return nil, fmt.Errorf("Channel: %s", err) - } - - logger.Info("got Channel, declaring Exchange") - if err = c.channel.ExchangeDeclare( - c.Config.Exchange, // name of the exchange - c.Config.ExchangeType, // type - false, // durable - false, // delete when complete - false, // internal - false, // noWait - nil, // arguments - ); err != nil { - return nil, fmt.Errorf("Exchange Declare: %s", err) - } - - logger.Info("declared Exchange, declaring Queue") - state, err := c.channel.QueueDeclare( - c.Config.Queue, // name of the queue - true, // durable - false, // delete when usused - false, // exclusive - false, // noWait - nil, // arguments - ) - if err != nil { - return nil, fmt.Errorf("Queue Declare: %s", err) - } - - logger.Info("declared Queue (%d messages, %d consumers), binding to Exchange", - state.Messages, state.Consumers) - - if err = c.channel.QueueBind( - c.Config.Queue, // name of the queue - c.Config.Key, // bindingKey - c.Config.Exchange, // sourceExchange - false, // noWait - nil, // arguments - ); err != nil { - return nil, fmt.Errorf("Queue Bind: %s", err) - } - - logger.Info("Queue bound to Exchange, starting Consume (Consumer tag '%s')", c.Config.Tag) - deliveries, err := c.channel.Consume( - c.Config.Queue, // name - c.Config.Tag, // consumerTag, - false, // noAck - false, // exclusive - false, // noLocal - false, // noWait - nil, // arguments - ) - if err != nil { - return nil, fmt.Errorf("Queue Consume: %s", err) - } - - return deliveries, nil + logger := log.WithFields(logrus.Fields{ + // "connection": c.Config.Connection, + "exchange": c.Config.Exchange, + "exchangetype": c.Config.ExchangeType, + "queue": c.Config.Queue, + "key": c.Config.Key, + "tag": c.Config.Tag, + }) + + logger.Info("setup AMPQ connection") + + // fmt.Println("-- Connection", c.Config.Connection) + // fmt.Println("-- Exchange", c.Config.Exchange) + // fmt.Println("-- ExchangeType", c.Config.ExchangeType) + // fmt.Println("-- Queue", c.Config.Queue) + // fmt.Println("-- Key", c.Config.Key) + // fmt.Println("-- Tag", c.Config.Tag) + + var err error + + logger.Info("dialing", c.Config.Connection) + c.conn, err = amqp.Dial(c.Config.Connection) + if err != nil { + return nil, fmt.Errorf("Dial: %s", err) + } + + logger.Info("got Connection, getting Channel") + c.channel, err = c.conn.Channel() + if err != nil { + return nil, fmt.Errorf("Channel: %s", err) + } + + logger.Info("got Channel, declaring Exchange") + if err = c.channel.ExchangeDeclare( + c.Config.Exchange, // name of the exchange + c.Config.ExchangeType, // type + false, // durable + false, // delete when complete + false, // internal + false, // noWait + nil, // arguments + ); err != nil { + return nil, fmt.Errorf("Exchange Declare: %s", err) + } + + logger.Info("declared Exchange, declaring Queue") + state, err := c.channel.QueueDeclare( + c.Config.Queue, // name of the queue + true, // durable + false, // delete when usused + false, // exclusive + false, // noWait + nil, // arguments + ) + if err != nil { + return nil, fmt.Errorf("Queue Declare: %s", err) + } + + logger.Info("declared Queue (%d messages, %d consumers), binding to Exchange", + state.Messages, state.Consumers) + + if err = c.channel.QueueBind( + c.Config.Queue, // name of the queue + c.Config.Key, // bindingKey + c.Config.Exchange, // sourceExchange + false, // noWait + nil, // arguments + ); err != nil { + return nil, fmt.Errorf("Queue Bind: %s", err) + } + + logger.Info("Queue bound to Exchange, starting Consume (Consumer tag '%s')", c.Config.Tag) + deliveries, err := c.channel.Consume( + c.Config.Queue, // name + c.Config.Tag, // consumerTag, + false, // noAck + false, // exclusive + false, // noLocal + false, // noWait + nil, // arguments + ) + if err != nil { + return nil, fmt.Errorf("Queue Consume: %s", err) + } + + return deliveries, nil } // Shutdown will gracefully stop a consumer func (c *Consumer) Shutdown() error { - logger := log.WithFields(logrus.Fields{ - // "connection": c.Config.Connection, - "exchange": c.Config.Exchange, - "exchangetype": c.Config.ExchangeType, - "queue": c.Config.Queue, - "key": c.Config.Key, - "tag": c.Config.Tag, - }) + logger := log.WithFields(logrus.Fields{ + // "connection": c.Config.Connection, + "exchange": c.Config.Exchange, + "exchangetype": c.Config.ExchangeType, + "queue": c.Config.Queue, + "key": c.Config.Key, + "tag": c.Config.Tag, + }) - // will close() the deliveries channel - if err := c.channel.Cancel(c.Config.Tag, true); err != nil { - return fmt.Errorf("Consumer cancel failed: %s", err) - } + // will close() the deliveries channel + if err := c.channel.Cancel(c.Config.Tag, true); err != nil { + return fmt.Errorf("Consumer cancel failed: %s", err) + } - if err := c.conn.Close(); err != nil { - return fmt.Errorf("AMQP connection close error: %s", err) - } + if err := c.conn.Close(); err != nil { + return fmt.Errorf("AMQP connection close error: %s", err) + } - defer logger.Info("AMQP shutdown OK") + defer logger.Info("AMQP shutdown OK") - // wait for handle() to exit - return <-c.done + // wait for handle() to exit + return <-c.done } // HandleLoop is the message loop of a consumer func (c *Consumer) HandleLoop(deliveries <-chan amqp.Delivery) { - logger := log.WithFields(logrus.Fields{ - // "connection": c.Config.Connection, - "exchange": c.Config.Exchange, - "exchangetype": c.Config.ExchangeType, - "queue": c.Config.Queue, - "key": c.Config.Key, - "tag": c.Config.Tag, - }) - - for d := range deliveries { - logger.WithFields(logrus.Fields{ - "bytes": len(d.Body), - }).Info("got delivery") - // logger.Debug( - // "got %dB delivery: [%v] %s", - // len(d.Body), - // d.DeliveryTag, - // d.Body, - // ) - - if err := c.handleFunc(d.Body); err != nil { - fmt.Println(err) - d.Ack(false) - } else { - d.Ack(true) - } - - } - logger.Info("handle: deliveries channel closed") - c.done <- nil + logger := log.WithFields(logrus.Fields{ + // "connection": c.Config.Connection, + "exchange": c.Config.Exchange, + "exchangetype": c.Config.ExchangeType, + "queue": c.Config.Queue, + "key": c.Config.Key, + "tag": c.Config.Tag, + }) + + for d := range deliveries { + logger.WithFields(logrus.Fields{ + "bytes": len(d.Body), + }).Info("got delivery") + // logger.Debug( + // "got %dB delivery: [%v] %s", + // len(d.Body), + // d.DeliveryTag, + // d.Body, + // ) + + if err := c.handleFunc(d.Body); err != nil { + fmt.Println(err) + d.Ack(false) + } else { + d.Ack(true) + } + + } + logger.Info("handle: deliveries channel closed") + c.done <- nil } diff --git a/service/docker.go b/service/docker.go index 29c1b00..abe48fb 100644 --- a/service/docker.go +++ b/service/docker.go @@ -19,165 +19,165 @@ package service import ( - "bytes" - "context" - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/client" + "bytes" + "context" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/client" ) // DockerService contains all settings to talk to the docker api type DockerService struct { - // the context docker is relate to - Context context.Context - // client is the interface to the docker runtime - Client *client.Client + // the context docker is relate to + Context context.Context + // client is the interface to the docker runtime + Client *client.Client } // NewDockerService creates a new docker client func NewDockerService() *DockerService { - ctx := context.Background() - cli, err := client.NewEnvClient() - if err != nil { - panic(err) - } - - return &DockerService{ - Context: ctx, - Client: cli, - } + ctx := context.Background() + cli, err := client.NewEnvClient() + if err != nil { + panic(err) + } + + return &DockerService{ + Context: ctx, + Client: cli, + } } // ListContainers lists all docker containers func (ds *DockerService) ListContainers() { - containers, err := ds.Client.ContainerList(ds.Context, types.ContainerListOptions{}) - if err != nil { - panic(err) - } + containers, err := ds.Client.ContainerList(ds.Context, types.ContainerListOptions{}) + if err != nil { + panic(err) + } - for _, container := range containers { - fmt.Println(container.ID) - fmt.Println(container.Names) - } + for _, container := range containers { + fmt.Println(container.ID) + fmt.Println(container.Names) + } } // ListImages lists all docker images func (ds *DockerService) ListImages() { - images, err := ds.Client.ImageList(ds.Context, types.ImageListOptions{}) - if err != nil { - panic(err) - } - - for _, image := range images { - fmt.Println(image.ID) - fmt.Println(image.RepoTags) - fmt.Println(image.Size) - fmt.Println(image.VirtualSize) - if len(image.RepoTags) > 0 { - fmt.Println(image.RepoTags[0]) - } - fmt.Println("") - } + images, err := ds.Client.ImageList(ds.Context, types.ImageListOptions{}) + if err != nil { + panic(err) + } + + for _, image := range images { + fmt.Println(image.ID) + fmt.Println(image.RepoTags) + fmt.Println(image.Size) + fmt.Println(image.VirtualSize) + if len(image.RepoTags) > 0 { + fmt.Println(image.RepoTags[0]) + } + fmt.Println("") + } } // Pull pulls a docker image func (ds *DockerService) Pull(image string) (string, error) { - // image example: "docker.io/library/alpine" - outputReader, err := ds.Client.ImagePull(ds.Context, image, types.ImagePullOptions{}) - if err != nil { - return "", err - } - // io.Copy(os.Stdout, reader) + // image example: "docker.io/library/alpine" + outputReader, err := ds.Client.ImagePull(ds.Context, image, types.ImagePullOptions{}) + if err != nil { + return "", err + } + // io.Copy(os.Stdout, reader) - buf := new(bytes.Buffer) - buf.ReadFrom(outputReader) + buf := new(bytes.Buffer) + buf.ReadFrom(outputReader) - return buf.String(), nil + return buf.String(), nil } // Run executes a docker container and waits for the output func (ds *DockerService) Run( - imageName string, - submissionZipFile string, - frameworkZipFile string, - DockerMemoryBytes int64, + imageName string, + submissionZipFile string, + frameworkZipFile string, + DockerMemoryBytes int64, ) (string, int64, error) { - // create some context for docker - - // fmt.Println("imageName", imageName) - // fmt.Println("submissionZipFile", submissionZipFile) - // fmt.Println("frameworkZipFile", frameworkZipFile) - - // submissionZipFile := "/home/patwie/git/github.com/cgtuebingen/infomark/infomark-backend/.local/simple_ci_runner/submission.zip" - // frameworkZipFile := "/home/patwie/git/github.com/cgtuebingen/infomark/infomark-backend/.local/simple_ci_runner/unittest.zip" - - cmds := []string{} - - cfg := &container.Config{ - Image: imageName, - Cmd: cmds, - Tty: true, - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - NetworkDisabled: true, // no network activity required - // StopTimeout: - } - - hostCfg := &container.HostConfig{ - Resources: container.Resources{ - Memory: DockerMemoryBytes, // 200mb - MemorySwap: 0, - }, - // AutoRemove: true, - Mounts: []mount.Mount{ - { - ReadOnly: true, - Type: mount.TypeBind, - Source: submissionZipFile, - Target: "/data/submission.zip", - }, - { - ReadOnly: true, - Type: mount.TypeBind, - Source: frameworkZipFile, - Target: "/data/unittest.zip", - }, - }, - } - - resp, err := ds.Client.ContainerCreate(ds.Context, cfg, hostCfg, nil, "") - if err != nil { - return "", 0, err - } - - defer ds.Client.ContainerRemove(ds.Context, resp.ID, types.ContainerRemoveOptions{}) - - if err := ds.Client.ContainerStart(ds.Context, resp.ID, types.ContainerStartOptions{}); err != nil { - return "", 0, err - } - - exitCode, err := ds.Client.ContainerWait(ds.Context, resp.ID) - if err != nil { - return "", exitCode, err - } - - outputReader, err := ds.Client.ContainerLogs(ds.Context, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) - if err != nil { - return "", 0, err - } - - buf := new(bytes.Buffer) - buf.ReadFrom(outputReader) - - // io.Copy(os.Stdout, outputReader) - return buf.String(), exitCode, nil + // create some context for docker + + // fmt.Println("imageName", imageName) + // fmt.Println("submissionZipFile", submissionZipFile) + // fmt.Println("frameworkZipFile", frameworkZipFile) + + // submissionZipFile := "/home/patwie/git/github.com/cgtuebingen/infomark/infomark-backend/.local/simple_ci_runner/submission.zip" + // frameworkZipFile := "/home/patwie/git/github.com/cgtuebingen/infomark/infomark-backend/.local/simple_ci_runner/unittest.zip" + + cmds := []string{} + + cfg := &container.Config{ + Image: imageName, + Cmd: cmds, + Tty: true, + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + NetworkDisabled: true, // no network activity required + // StopTimeout: + } + + hostCfg := &container.HostConfig{ + Resources: container.Resources{ + Memory: DockerMemoryBytes, // 200mb + MemorySwap: 0, + }, + // AutoRemove: true, + Mounts: []mount.Mount{ + { + ReadOnly: true, + Type: mount.TypeBind, + Source: submissionZipFile, + Target: "/data/submission.zip", + }, + { + ReadOnly: true, + Type: mount.TypeBind, + Source: frameworkZipFile, + Target: "/data/unittest.zip", + }, + }, + } + + resp, err := ds.Client.ContainerCreate(ds.Context, cfg, hostCfg, nil, "") + if err != nil { + return "", 0, err + } + + defer ds.Client.ContainerRemove(ds.Context, resp.ID, types.ContainerRemoveOptions{}) + + if err := ds.Client.ContainerStart(ds.Context, resp.ID, types.ContainerStartOptions{}); err != nil { + return "", 0, err + } + + exitCode, err := ds.Client.ContainerWait(ds.Context, resp.ID) + if err != nil { + return "", exitCode, err + } + + outputReader, err := ds.Client.ContainerLogs(ds.Context, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) + if err != nil { + return "", 0, err + } + + buf := new(bytes.Buffer) + buf.ReadFrom(outputReader) + + // io.Copy(os.Stdout, outputReader) + return buf.String(), exitCode, nil } // func main() { diff --git a/service/producer.go b/service/producer.go index 184285b..e30c56f 100644 --- a/service/producer.go +++ b/service/producer.go @@ -19,89 +19,89 @@ package service import ( - "fmt" + "fmt" - "github.com/streadway/amqp" + "github.com/streadway/amqp" ) // Producer is an object which can emit a AMPQ messages type Producer struct { - Config *Config + Config *Config - conn *amqp.Connection - channel *amqp.Channel - done chan error + conn *amqp.Connection + channel *amqp.Channel + done chan error } // NewProducer creates a new producer which can emit AMPQ messages func NewProducer(cfg *Config) (*Producer, error) { - producer := &Producer{ - conn: nil, - channel: nil, - done: make(chan error), + producer := &Producer{ + conn: nil, + channel: nil, + done: make(chan error), - Config: cfg, - } + Config: cfg, + } - return producer, nil + return producer, nil } // Publish emits an AMPQ message func (c *Producer) Publish(body []byte) error { - // This function dials, connects, declares, publishes, and tears down, - // all in one go. In a real service, you probably want to maintain a - // long-lived connection as state, and publish against that. - - log.Printf("dialing %s", c.Config.Connection) - connection, err := amqp.Dial(c.Config.Connection) - if err != nil { - return fmt.Errorf("Dial: %s", err) - } - defer connection.Close() - - log.Printf("got Connection, getting Channel") - channel, err := connection.Channel() - if err != nil { - return fmt.Errorf("Channel: %s", err) - } - - log.Printf("got Channel, declaring %q Exchange (%s)", c.Config.ExchangeType, c.Config.Exchange) - if err := channel.ExchangeDeclare( - c.Config.Exchange, // name - c.Config.ExchangeType, // type - false, // durable - false, // auto-deleted - false, // internal - false, // noWait - nil, // arguments - ); err != nil { - return fmt.Errorf("Exchange Declare: %s", err) - } - - // Prepare this message to be persistent. Your publishing requirements may - // be different. - msg := amqp.Publishing{ - Headers: amqp.Table{}, - ContentType: "application/json", - ContentEncoding: "", - Body: body, - DeliveryMode: 1, // 1=non-persistent, 2=persistent - Priority: 0, // 0-9 - } - - log.Printf("declared Exchange, publishing %dB body (%s)", len(body), body) - if err = channel.Publish( - c.Config.Exchange, // publish to an exchange - c.Config.Key, // routing to 0 or more queues - false, // mandatory - false, // immediate - msg, - ); err != nil { - return fmt.Errorf("Exchange Publish: %s", err) - } - - return nil + // This function dials, connects, declares, publishes, and tears down, + // all in one go. In a real service, you probably want to maintain a + // long-lived connection as state, and publish against that. + + log.Printf("dialing %s", c.Config.Connection) + connection, err := amqp.Dial(c.Config.Connection) + if err != nil { + return fmt.Errorf("Dial: %s", err) + } + defer connection.Close() + + log.Printf("got Connection, getting Channel") + channel, err := connection.Channel() + if err != nil { + return fmt.Errorf("Channel: %s", err) + } + + log.Printf("got Channel, declaring %q Exchange (%s)", c.Config.ExchangeType, c.Config.Exchange) + if err := channel.ExchangeDeclare( + c.Config.Exchange, // name + c.Config.ExchangeType, // type + false, // durable + false, // auto-deleted + false, // internal + false, // noWait + nil, // arguments + ); err != nil { + return fmt.Errorf("Exchange Declare: %s", err) + } + + // Prepare this message to be persistent. Your publishing requirements may + // be different. + msg := amqp.Publishing{ + Headers: amqp.Table{}, + ContentType: "application/json", + ContentEncoding: "", + Body: body, + DeliveryMode: 1, // 1=non-persistent, 2=persistent + Priority: 0, // 0-9 + } + + log.Printf("declared Exchange, publishing %dB body (%s)", len(body), body) + if err = channel.Publish( + c.Config.Exchange, // publish to an exchange + c.Config.Key, // routing to 0 or more queues + false, // mandatory + false, // immediate + msg, + ); err != nil { + return fmt.Errorf("Exchange Publish: %s", err) + } + + return nil } diff --git a/tape/tape.go b/tape/tape.go index fd9a382..817d184 100644 --- a/tape/tape.go +++ b/tape/tape.go @@ -21,104 +21,104 @@ package tape import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/textproto" - "os" - "path/filepath" - "strings" - - "github.com/go-chi/chi" + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/textproto" + "os" + "path/filepath" + "strings" + + "github.com/go-chi/chi" ) // Tape can send requests to router endpoints. This is used by unit tests to // check the endpoints type Tape struct { - Router *chi.Mux + Router *chi.Mux } // NewTape creates a new Tape func NewTape() *Tape { - return &Tape{} + return &Tape{} } var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) + return quoteEscaper.Replace(s) } // ugly!! but see https://github.com/golang/go/issues/16425 func createFormFile(w *multipart.Writer, fieldname, filename string, contentType string) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", - fmt.Sprintf(`form-data; name="%s"; filename="%s"`, - escapeQuotes(fieldname), escapeQuotes(filename))) - h.Set("Content-Type", contentType) - return w.CreatePart(h) + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(fieldname), escapeQuotes(filename))) + h.Set("Content-Type", contentType) + return w.CreatePart(h) } // CreateFileRequestBody create a multi-part form data. We assume all endpoints // handling files are receicing a single file with form name "file_data" func CreateFileRequestBody(path, contentType string) (*bytes.Buffer, string, error) { - // open file on disk - file, err := os.Open(path) - if err != nil { - return nil, "", err - } - defer file.Close() - - // create body - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := createFormFile(writer, "file_data", filepath.Base(path), contentType) - if err != nil { - return nil, "", err - } - _, err = io.Copy(part, file) - if err != nil { - return nil, "", err - } - - err = writer.Close() - if err != nil { - return nil, "", err - } - - return body, writer.FormDataContentType(), nil + // open file on disk + file, err := os.Open(path) + if err != nil { + return nil, "", err + } + defer file.Close() + + // create body + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := createFormFile(writer, "file_data", filepath.Base(path), contentType) + if err != nil { + return nil, "", err + } + _, err = io.Copy(part, file) + if err != nil { + return nil, "", err + } + + err = writer.Close() + if err != nil { + return nil, "", err + } + + return body, writer.FormDataContentType(), nil } // BuildDataRequest creates a request func BuildDataRequest(method, url string, data map[string]interface{}) *http.Request { - var payloadJson *bytes.Buffer + var payloadJson *bytes.Buffer - if data != nil { - dat, err := json.Marshal(data) - if err != nil { - panic(err) - } - payloadJson = bytes.NewBuffer(dat) - } else { - payloadJson = nil - } + if data != nil { + dat, err := json.Marshal(data) + if err != nil { + panic(err) + } + payloadJson = bytes.NewBuffer(dat) + } else { + payloadJson = nil + } - r, err := http.NewRequest(method, url, payloadJson) - if err != nil { - panic(err) - } + r, err := http.NewRequest(method, url, payloadJson) + if err != nil { + panic(err) + } - r.Header.Set("Content-Type", "application/json") - r.Header.Add("X-Forwarded-For", "1.2.3.4") - r.Header.Set("User-Agent", "Test-Agent") + r.Header.Set("Content-Type", "application/json") + r.Header.Add("X-Forwarded-For", "1.2.3.4") + r.Header.Set("User-Agent", "Test-Agent") - return r + return r } // Play will send a request without any request body (like GET) @@ -136,74 +136,74 @@ func BuildDataRequest(method, url string, data map[string]interface{}) *http.Req // PlayRequest will send the request to the router and fetch the response func (t *Tape) PlayRequest(r *http.Request) *httptest.ResponseRecorder { - w := httptest.NewRecorder() - t.Router.ServeHTTP(w, r) - return w + w := httptest.NewRecorder() + t.Router.ServeHTTP(w, r) + return w } // ToH is a convenience wrapper create a json for any object func ToH(z interface{}) map[string]interface{} { - data, _ := json.Marshal(z) - var msgMapTemplate interface{} - _ = json.Unmarshal(data, &msgMapTemplate) - return msgMapTemplate.(map[string]interface{}) + data, _ := json.Marshal(z) + var msgMapTemplate interface{} + _ = json.Unmarshal(data, &msgMapTemplate) + return msgMapTemplate.(map[string]interface{}) } // Get creates, sends a GET request and fetches the response func (t *Tape) Get(url string) *httptest.ResponseRecorder { - h := make(map[string]interface{}) - r := BuildDataRequest("GET", url, h) - return t.PlayRequest(r) + h := make(map[string]interface{}) + r := BuildDataRequest("GET", url, h) + return t.PlayRequest(r) } // Post creates, sends a POST request and fetches the response func (t *Tape) Post(url string, data map[string]interface{}) *httptest.ResponseRecorder { - r := BuildDataRequest("POST", url, data) - return t.PlayRequest(r) + r := BuildDataRequest("POST", url, data) + return t.PlayRequest(r) } // Put creates, sends a PUT request and fetches the response func (t *Tape) Put(url string, data map[string]interface{}) *httptest.ResponseRecorder { - r := BuildDataRequest("PUT", url, data) - return t.PlayRequest(r) + r := BuildDataRequest("PUT", url, data) + return t.PlayRequest(r) } // Patch creates, sends a PATCH request and fetches the response func (t *Tape) Patch(url string, data map[string]interface{}) *httptest.ResponseRecorder { - r := BuildDataRequest("PATCH", url, data) - return t.PlayRequest(r) + r := BuildDataRequest("PATCH", url, data) + return t.PlayRequest(r) } // Delete creates, sends a DELETE request and fetches the response func (t *Tape) Delete(url string) *httptest.ResponseRecorder { - h := make(map[string]interface{}) - r := BuildDataRequest("DELETE", url, h) - return t.PlayRequest(r) + h := make(map[string]interface{}) + r := BuildDataRequest("DELETE", url, h) + return t.PlayRequest(r) } // FormatRequest pretty-formats a request and returns it as a string func (t *Tape) FormatRequest(r *http.Request) string { - // Create return string - var request []string - // Add the request string - url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) - request = append(request, url) - // Add the host - request = append(request, fmt.Sprintf("Host: %v", r.Host)) - // Loop through headers - for name, headers := range r.Header { - name = strings.ToLower(name) - for _, h := range headers { - request = append(request, fmt.Sprintf("%v: %v", name, h)) - } - } - - // If this is a POST, add post data - if r.Method == "POST" { - r.ParseForm() - request = append(request, "\n") - request = append(request, r.Form.Encode()) - } - // Return the request as a string - return strings.Join(request, "\n") + // Create return string + var request []string + // Add the request string + url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) + request = append(request, url) + // Add the host + request = append(request, fmt.Sprintf("Host: %v", r.Host)) + // Loop through headers + for name, headers := range r.Header { + name = strings.ToLower(name) + for _, h := range headers { + request = append(request, fmt.Sprintf("%v: %v", name, h)) + } + } + + // If this is a POST, add post data + if r.Method == "POST" { + r.ParseForm() + request = append(request, "\n") + request = append(request, r.Form.Encode()) + } + // Return the request as a string + return strings.Join(request, "\n") }