Tests
This commit is contained in:
		
							parent
							
								
									66cb35b5fc
								
							
						
					
					
						commit
						57814cf855
					
				
					 8 changed files with 209 additions and 37 deletions
				
			
		|  | @ -48,19 +48,21 @@ var ( | |||
| 	errHTTPBadRequestAttachmentsDisallowed           = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments"} | ||||
| 	errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"} | ||||
| 	errHTTPBadRequestWebSocketsUpgradeHeaderMissing  = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets"} | ||||
| 	errHTTPBadRequestJSONInvalid                     = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"} | ||||
| 	errHTTPBadRequestMessageJSONInvalid              = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"} | ||||
| 	errHTTPBadRequestActionsInvalid                  = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"} | ||||
| 	errHTTPBadRequestMatrixMessageInvalid            = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"} | ||||
| 	errHTTPBadRequestMatrixPushkeyBaseURLMismatch    = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"} | ||||
| 	errHTTPBadRequestIconURLInvalid                  = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"} | ||||
| 	errHTTPBadRequestSignupNotEnabled                = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config"} | ||||
| 	errHTTPBadRequestNoTokenProvided                 = &errHTTP{40023, http.StatusBadRequest, "invalid request: no token provided", ""} | ||||
| 	errHTTPBadRequestJSONInvalid                     = &errHTTP{40024, http.StatusBadRequest, "invalid request: request body must be valid JSON", ""} | ||||
| 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", ""} | ||||
| 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} | ||||
| 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"} | ||||
| 	errHTTPConflictUserExists                        = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", ""} | ||||
| 	errHTTPEntityTooLargeAttachmentTooLarge          = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} | ||||
| 	errHTTPEntityTooLargeMatrixRequestTooLarge       = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""} | ||||
| 	errHTTPEntityTooLargeAttachment                  = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} | ||||
| 	errHTTPEntityTooLargeMatrixRequest               = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""} | ||||
| 	errHTTPEntityTooLargeJSONBody                    = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", ""} | ||||
| 	errHTTPTooManyRequestsLimitRequests              = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||
| 	errHTTPTooManyRequestsLimitEmails                = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||
| 	errHTTPTooManyRequestsLimitSubscriptions         = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||
|  | @ -68,6 +70,6 @@ var ( | |||
| 	errHTTPTooManyRequestsAttachmentBandwidthLimit   = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} | ||||
| 	errHTTPTooManyRequestsAccountCreateLimit         = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: daily account creation limit reached", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit | ||||
| 	errHTTPInternalError                             = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} | ||||
| 	errHTTPInternalErrorInvalidFilePath              = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""} | ||||
| 	errHTTPInternalErrorInvalidPath                  = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""} | ||||
| 	errHTTPInternalErrorMissingBaseURL               = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"} | ||||
| ) | ||||
|  |  | |||
|  | @ -479,7 +479,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) | |||
| 	} | ||||
| 	matches := fileRegex.FindStringSubmatch(r.URL.Path) | ||||
| 	if len(matches) != 2 { | ||||
| 		return errHTTPInternalErrorInvalidFilePath | ||||
| 		return errHTTPInternalErrorInvalidPath | ||||
| 	} | ||||
| 	messageID := matches[1] | ||||
| 	file := filepath.Join(s.config.AttachmentCacheDir, messageID) | ||||
|  | @ -815,7 +815,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, | |||
| 	if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below | ||||
| 		contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) | ||||
| 		if err == nil && (contentLength > stats.AttachmentTotalSizeRemaining || contentLength > stats.AttachmentFileSizeLimit) { | ||||
| 			return errHTTPEntityTooLargeAttachmentTooLarge | ||||
| 			return errHTTPEntityTooLargeAttachment | ||||
| 		} | ||||
| 	} | ||||
| 	if m.Attachment == nil { | ||||
|  | @ -839,7 +839,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, | |||
| 	} | ||||
| 	m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...) | ||||
| 	if err == util.ErrLimitReached { | ||||
| 		return errHTTPEntityTooLargeAttachmentTooLarge | ||||
| 		return errHTTPEntityTooLargeAttachment | ||||
| 	} else if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -1426,15 +1426,10 @@ func (s *Server) ensureUser(next handleFunc) handleFunc { | |||
| // before passing it on to the next handler. This is meant to be used in combination with handlePublish. | ||||
| func (s *Server) transformBodyJSON(next handleFunc) handleFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 		body, err := util.Peek(r.Body, s.config.MessageLimit) | ||||
| 		m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageLimit) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer r.Body.Close() | ||||
| 		var m publishMessage | ||||
| 		if err := json.NewDecoder(body).Decode(&m); err != nil { | ||||
| 			return errHTTPBadRequestJSONInvalid | ||||
| 		} | ||||
| 		if !topicRegex.MatchString(m.Topic) { | ||||
| 			return errHTTPBadRequestTopicInvalid | ||||
| 		} | ||||
|  | @ -1467,7 +1462,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { | |||
| 		if len(m.Actions) > 0 { | ||||
| 			actionsStr, err := json.Marshal(m.Actions) | ||||
| 			if err != nil { | ||||
| 				return errHTTPBadRequestJSONInvalid | ||||
| 				return errHTTPBadRequestMessageJSONInvalid | ||||
| 			} | ||||
| 			r.Header.Set("X-Actions", string(actionsStr)) | ||||
| 		} | ||||
|  | @ -1535,7 +1530,9 @@ func (s *Server) visitor(r *http.Request) (v *visitor, err error) { | |||
| 	} else { | ||||
| 		v = s.visitorFromIP(ip) | ||||
| 	} | ||||
| 	v.user = u    // Update user -- FIXME race? | ||||
| 	v.mu.Lock() | ||||
| 	v.user = u | ||||
| 	v.mu.Unlock() | ||||
| 	return v, err // Always return visitor, even when error occurs! | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ package server | |||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"heckel.io/ntfy/user" | ||||
| 	"heckel.io/ntfy/util" | ||||
| 	"net/http" | ||||
|  | @ -21,7 +20,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * | |||
| 			return errHTTPUnauthorized // Cannot create account from user context | ||||
| 		} | ||||
| 	} | ||||
| 	newAccount, err := util.ReadJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit) | ||||
| 	newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -118,7 +117,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v * | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	newPassword, err := util.ReadJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit) | ||||
| 	newPassword, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -174,7 +173,7 @@ func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, r *http.Request | |||
| func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	// TODO rate limit | ||||
| 	if v.user.Token == "" { | ||||
| 		return errHTTPUnauthorized | ||||
| 		return errHTTPBadRequestNoTokenProvided | ||||
| 	} | ||||
| 	if err := s.userManager.RemoveToken(v.user); err != nil { | ||||
| 		return err | ||||
|  | @ -184,7 +183,7 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	newPrefs, err := util.ReadJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit) | ||||
| 	newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -218,7 +217,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	newSubscription, err := util.ReadJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit) | ||||
| 	newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -250,13 +249,13 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req | |||
| func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path) | ||||
| 	if len(matches) != 2 { | ||||
| 		return errHTTPInternalErrorInvalidFilePath // FIXME | ||||
| 		return errHTTPInternalErrorInvalidPath | ||||
| 	} | ||||
| 	updatedSubscription, err := util.ReadJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit) | ||||
| 	subscriptionID := matches[1] | ||||
| 	updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	subscriptionID := matches[1] | ||||
| 	if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil { | ||||
| 		return errHTTPNotFound | ||||
| 	} | ||||
|  | @ -283,14 +282,9 @@ func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http. | |||
| } | ||||
| 
 | ||||
| func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||
| 	if v.user == nil { | ||||
| 		return errors.New("no user") | ||||
| 	} | ||||
| 	w.Header().Set("Content-Type", "application/json") | ||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this | ||||
| 	matches := accountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path) | ||||
| 	if len(matches) != 2 { | ||||
| 		return errHTTPInternalErrorInvalidFilePath // FIXME | ||||
| 		return errHTTPInternalErrorInvalidPath | ||||
| 	} | ||||
| 	subscriptionID := matches[1] | ||||
| 	if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil { | ||||
|  | @ -308,5 +302,7 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http. | |||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	w.Header().Set("Content-Type", "application/json") | ||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -62,6 +62,25 @@ func TestAccount_Signup_LimitReached(t *testing.T) { | |||
| 	require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_Signup_AsUser(t *testing.T) { | ||||
| 	conf := newTestConfigWithUsers(t) | ||||
| 	conf.EnableSignup = true | ||||
| 	s := newTestServer(t, conf) | ||||
| 
 | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) | ||||
| 	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) | ||||
| 
 | ||||
| 	rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "POST", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("ben", "ben"), | ||||
| 	}) | ||||
| 	require.Equal(t, 401, rr.Code) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_Signup_Disabled(t *testing.T) { | ||||
| 	conf := newTestConfigWithUsers(t) | ||||
| 	conf.EnableSignup = false | ||||
|  | @ -112,6 +131,144 @@ func TestAccount_Get_Anonymous(t *testing.T) { | |||
| 	require.Equal(t, int64(23), account.Stats.EmailsRemaining) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_ChangeSettings(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) | ||||
| 	user, _ := s.userManager.User("phil") | ||||
| 	token, _ := s.userManager.CreateToken(user) | ||||
| 
 | ||||
| 	rr := request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"sound": "juntos"},"ignored": true}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"delete_after": 86400}, "language": "de"}`, map[string]string{ | ||||
| 		"Authorization": util.BearerAuth(token.Value), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ | ||||
| 		"Authorization": util.BearerAuth(token.Value), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	account, _ := util.ReadJSON[apiAccountResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Equal(t, "de", account.Language) | ||||
| 	require.Equal(t, 86400, account.Notification.DeleteAfter) | ||||
| 	require.Equal(t, "juntos", account.Notification.Sound) | ||||
| 	require.Equal(t, 0, account.Notification.MinPriority) // Not set | ||||
| } | ||||
| 
 | ||||
| func TestAccount_Subscription_AddUpdateDelete(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) | ||||
| 
 | ||||
| 	rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	account, _ := util.ReadJSON[apiAccountResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Equal(t, 1, len(account.Subscriptions)) | ||||
| 	require.NotEmpty(t, account.Subscriptions[0].ID) | ||||
| 	require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) | ||||
| 	require.Equal(t, "def", account.Subscriptions[0].Topic) | ||||
| 	require.Equal(t, "", account.Subscriptions[0].DisplayName) | ||||
| 
 | ||||
| 	subscriptionID := account.Subscriptions[0].ID | ||||
| 	rr = request(t, s, "PATCH", "/v1/account/subscription/"+subscriptionID, `{"display_name": "ding dong"}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	account, _ = util.ReadJSON[apiAccountResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Equal(t, 1, len(account.Subscriptions)) | ||||
| 	require.Equal(t, subscriptionID, account.Subscriptions[0].ID) | ||||
| 	require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) | ||||
| 	require.Equal(t, "def", account.Subscriptions[0].Topic) | ||||
| 	require.Equal(t, "ding dong", account.Subscriptions[0].DisplayName) | ||||
| 
 | ||||
| 	rr = request(t, s, "DELETE", "/v1/account/subscription/"+subscriptionID, "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	account, _ = util.ReadJSON[apiAccountResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Equal(t, 0, len(account.Subscriptions)) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_ChangePassword(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) | ||||
| 
 | ||||
| 	rr := request(t, s, "POST", "/v1/account/password", `{"password": "new password"}`, map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 401, rr.Code) | ||||
| 
 | ||||
| 	rr = request(t, s, "GET", "/v1/account", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "new password"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_ChangePassword_NoAccount(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 
 | ||||
| 	rr := request(t, s, "POST", "/v1/account/password", `{"password": "new password"}`, nil) | ||||
| 	require.Equal(t, 401, rr.Code) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_ExtendToken(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) | ||||
| 
 | ||||
| 	rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	token, err := util.ReadJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Nil(t, err) | ||||
| 
 | ||||
| 	time.Sleep(time.Second) | ||||
| 
 | ||||
| 	rr = request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ | ||||
| 		"Authorization": util.BearerAuth(token.Token), | ||||
| 	}) | ||||
| 	require.Equal(t, 200, rr.Code) | ||||
| 	extendedToken, err := util.ReadJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) | ||||
| 	require.Nil(t, err) | ||||
| 	require.Equal(t, token.Token, extendedToken.Token) | ||||
| 	require.True(t, token.Expires < extendedToken.Expires) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) { | ||||
| 	s := newTestServer(t, newTestConfigWithUsers(t)) | ||||
| 	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) | ||||
| 
 | ||||
| 	rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ | ||||
| 		"Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! | ||||
| 	}) | ||||
| 	require.Equal(t, 400, rr.Code) | ||||
| 	require.Equal(t, 40023, toHTTPError(t, rr.Body.String()).Code) | ||||
| } | ||||
| 
 | ||||
| func TestAccount_Delete_Success(t *testing.T) { | ||||
| 	conf := newTestConfigWithUsers(t) | ||||
| 	conf.EnableSignup = true | ||||
|  |  | |||
|  | @ -113,7 +113,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int) | |||
| 	} | ||||
| 	defer r.Body.Close() | ||||
| 	if body.LimitReached { | ||||
| 		return nil, errHTTPEntityTooLargeMatrixRequestTooLarge | ||||
| 		return nil, errHTTPEntityTooLargeMatrixRequest | ||||
| 	} | ||||
| 	var m matrixRequest | ||||
| 	if err := json.Unmarshal(body.PeekedBytes, &m); err != nil { | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ func TestMatrix_NewRequestFromMatrixJSON_TooLarge(t *testing.T) { | |||
| 	body := `{"notification":{"content":{"body":"I'm floating in a most peculiar way.","msgtype":"m.text"},"counts":{"missed_calls":1,"unread":2},"devices":[{"app_id":"org.matrix.matrixConsole.ios","data":{},"pushkey":"https://ntfy.sh/upABCDEFGHI?up=1","pushkey_ts":12345678,"tweaks":{"sound":"bing"}}],"event_id":"$3957tyerfgewrf384","prio":"high","room_alias":"#exampleroom:matrix.org","room_id":"!slw48wfj34rtnrf:example.com","room_name":"Mission Control","sender":"@exampleuser:matrix.org","sender_display_name":"Major Tom","type":"m.room.message"}}` | ||||
| 	r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", strings.NewReader(body)) | ||||
| 	_, err := newRequestFromMatrixJSON(r, baseURL, maxLength) | ||||
| 	require.Equal(t, errHTTPEntityTooLargeMatrixRequestTooLarge, err) | ||||
| 	require.Equal(t, errHTTPEntityTooLargeMatrixRequest, err) | ||||
| } | ||||
| 
 | ||||
| func TestMatrix_NewRequestFromMatrixJSON_InvalidJSON(t *testing.T) { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import ( | |||
| 	"github.com/emersion/go-smtp" | ||||
| 	"heckel.io/ntfy/log" | ||||
| 	"heckel.io/ntfy/util" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/netip" | ||||
| 	"strings" | ||||
|  | @ -121,3 +122,15 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr { | |||
| 	} | ||||
| 	return ip | ||||
| } | ||||
| 
 | ||||
| func readJSONWithLimit[T any](r io.ReadCloser, limit int) (*T, error) { | ||||
| 	obj, err := util.ReadJSONWithLimit[T](r, limit) | ||||
| 	if err == util.ErrInvalidJSON { | ||||
| 		return nil, errHTTPBadRequestJSONInvalid | ||||
| 	} else if err == util.ErrTooLargeJSON { | ||||
| 		return nil, errHTTPEntityTooLargeJSONBody | ||||
| 	} else if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return obj, nil | ||||
| } | ||||
|  |  | |||
							
								
								
									
										17
									
								
								util/util.go
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								util/util.go
									
										
									
									
									
								
							|  | @ -31,6 +31,11 @@ var ( | |||
| 	noQuotesRegex      = regexp.MustCompile(`^[-_./:@a-zA-Z0-9]+$`) | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrInvalidJSON  = errors.New("invalid JSON") | ||||
| 	ErrTooLargeJSON = errors.New("too large JSON") | ||||
| ) | ||||
| 
 | ||||
| // FileExists checks if a file exists, and returns true if it does | ||||
| func FileExists(filename string) bool { | ||||
| 	stat, _ := os.Stat(filename) | ||||
|  | @ -293,21 +298,23 @@ func QuoteCommand(command []string) string { | |||
| func ReadJSON[T any](body io.ReadCloser) (*T, error) { | ||||
| 	var obj T | ||||
| 	if err := json.NewDecoder(body).Decode(&obj); err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, ErrInvalidJSON | ||||
| 	} | ||||
| 	return &obj, nil | ||||
| } | ||||
| 
 | ||||
| // ReadJSONWithLimit reads the given io.ReadCloser into a struct, but only until limit is reached | ||||
| func ReadJSONWithLimit[T any](r io.ReadCloser, limit int) (*T, error) { | ||||
| 	r, err := Peek(r, limit) | ||||
| 	defer r.Close() | ||||
| 	p, err := Peek(r, limit) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if p.LimitReached { | ||||
| 		return nil, ErrTooLargeJSON | ||||
| 	} | ||||
| 	defer r.Close() | ||||
| 	var obj T | ||||
| 	if err := json.NewDecoder(r).Decode(&obj); err != nil { | ||||
| 		return nil, err | ||||
| 	if err := json.NewDecoder(p).Decode(&obj); err != nil { | ||||
| 		return nil, ErrInvalidJSON | ||||
| 	} | ||||
| 	return &obj, nil | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue