Add 'Cache: no' header, closes #41
This commit is contained in:
		
							parent
							
								
									d5be5d3e8c
								
							
						
					
					
						commit
						d6fbccab55
					
				
					 11 changed files with 191 additions and 22 deletions
				
			
		|  | @ -74,7 +74,7 @@ func execRun(c *cli.Context) error { | ||||||
| 		return errors.New("keepalive interval cannot be lower than five seconds") | 		return errors.New("keepalive interval cannot be lower than five seconds") | ||||||
| 	} else if managerInterval < 5*time.Second { | 	} else if managerInterval < 5*time.Second { | ||||||
| 		return errors.New("manager interval cannot be lower than five seconds") | 		return errors.New("manager interval cannot be lower than five seconds") | ||||||
| 	} else if cacheDuration < managerInterval { | 	} else if cacheDuration > 0 && cacheDuration < managerInterval { | ||||||
| 		return errors.New("cache duration cannot be lower than manager interval") | 		return errors.New("cache duration cannot be lower than manager interval") | ||||||
| 	} else if keyFile != "" && !util.FileExists(keyFile) { | 	} else if keyFile != "" && !util.FileExists(keyFile) { | ||||||
| 		return errors.New("if set, key file must exist") | 		return errors.New("if set, key file must exist") | ||||||
|  |  | ||||||
|  | @ -28,6 +28,8 @@ | ||||||
| # If set, messages are cached in a local SQLite database instead of only in-memory. This | # If set, messages are cached in a local SQLite database instead of only in-memory. This | ||||||
| # allows for service restarts without losing messages in support of the since= parameter. | # allows for service restarts without losing messages in support of the since= parameter. | ||||||
| # | # | ||||||
|  | # To disable the cache entirely (on-disk/in-memory), set "cache-duration" to 0. | ||||||
|  | # | ||||||
| # Note: If you are running ntfy with systemd, make sure this cache file is owned by the | # Note: If you are running ntfy with systemd, make sure this cache file is owned by the | ||||||
| #       ntfy user and group by running: chown ntfy.ntfy <filename>. | #       ntfy user and group by running: chown ntfy.ntfy <filename>. | ||||||
| # | # | ||||||
|  | @ -36,6 +38,8 @@ | ||||||
| # Duration for which messages will be buffered before they are deleted. | # Duration for which messages will be buffered before they are deleted. | ||||||
| # This is required to support the "since=..." and "poll=1" parameter. | # This is required to support the "since=..." and "poll=1" parameter. | ||||||
| # | # | ||||||
|  | # You can disable the cache entirely by setting this to 0. | ||||||
|  | # | ||||||
| # cache-duration: 12h | # cache-duration: 12h | ||||||
| 
 | 
 | ||||||
| # Interval in which keepalive messages are sent to the client. This is to prevent | # Interval in which keepalive messages are sent to the client. This is to prevent | ||||||
|  |  | ||||||
|  | @ -26,7 +26,11 @@ restart**. You can override this behavior using the following config settings: | ||||||
| 
 | 
 | ||||||
| * `cache-file`: if set, ntfy will store messages in a SQLite based cache (default is empty, which means in-memory cache). | * `cache-file`: if set, ntfy will store messages in a SQLite based cache (default is empty, which means in-memory cache). | ||||||
|   **This is required if you'd like messages to be retained across restarts**. |   **This is required if you'd like messages to be retained across restarts**. | ||||||
| * `cache-duration`: defines the duration for which messages are stored in the cache (default is `12h`) | * `cache-duration`: defines the duration for which messages are stored in the cache (default is `12h`).  | ||||||
|  | 
 | ||||||
|  | You can also entirely disable the cache by setting `cache-duration` to `0`. When the cache is disabled, messages are only | ||||||
|  | passed on to the connected subscribers, but never stored on disk or even kept in memory longer than is needed to forward | ||||||
|  | the message to the subscribers. | ||||||
| 
 | 
 | ||||||
| Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#polling), as well as the | Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#polling), as well as the | ||||||
| [`since=` parameter](subscribe/api.md#fetching-cached-messages). | [`since=` parameter](subscribe/api.md#fetching-cached-messages). | ||||||
|  | @ -302,7 +306,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | ||||||
| | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | | | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | | ||||||
| | `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | | | `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | | ||||||
| | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | ||||||
| | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. | | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | | ||||||
| | `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | | | `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | | ||||||
| | `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | | | `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | | ||||||
| | `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. | | | `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. | | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ If you have the [Android app](subscribe/phone.md) installed on your phone, this | ||||||
| </figure> | </figure> | ||||||
| 
 | 
 | ||||||
| There are more features related to publishing messages: You can set a [notification priority](#message-priority),  | There are more features related to publishing messages: You can set a [notification priority](#message-priority),  | ||||||
| a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an example that uses all of them at once: | a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an example that uses some of them at together: | ||||||
| 
 | 
 | ||||||
| === "Command line (curl)" | === "Command line (curl)" | ||||||
|     ``` |     ``` | ||||||
|  | @ -332,3 +332,56 @@ them with a comma, e.g. `tag1,tag2,tag3`. | ||||||
|   <figcaption>Detail view of notifications with tags</figcaption> |   <figcaption>Detail view of notifications with tags</figcaption> | ||||||
| </figure> | </figure> | ||||||
| 
 | 
 | ||||||
|  | ## Message caching | ||||||
|  | By default, the ntfy server caches messages on disk for 12 hours (see [message caching](config.md#message-cache)), so | ||||||
|  | all messages you publish are stored server-side for a little while. The reason for this is to overcome temporary  | ||||||
|  | client-side network disruptions, but arguably this feature also may raise privacy concerns. | ||||||
|  | 
 | ||||||
|  | To avoid messages being cached server-side entirely, you can set `X-Cache` header (or its alias: `Cache`) to `no`.  | ||||||
|  | This will make sure that your message is not cached on the server, even if server-side caching is enabled. Messages | ||||||
|  | are still delivered to connected subscribers, but [`since=`](subscribe/api.md#fetching-cached-messages) and  | ||||||
|  | [`poll=1`](subscribe/api.md#polling) won't return the message anymore. | ||||||
|  | 
 | ||||||
|  | === "Command line (curl)" | ||||||
|  |     ``` | ||||||
|  |     curl -H "X-Cache: no" -d "This message won't be stored server-side" ntfy.sh/mytopic | ||||||
|  |     curl -H "Cache: no" -d "This message won't be stored server-side" ntfy.sh/mytopic | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "HTTP" | ||||||
|  |     ``` http | ||||||
|  |     POST /mytopic HTTP/1.1 | ||||||
|  |     Host: ntfy.sh | ||||||
|  |     Cache: no | ||||||
|  | 
 | ||||||
|  |     This message won't be stored server-side | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "JavaScript" | ||||||
|  |     ``` javascript | ||||||
|  |     fetch('https://ntfy.sh/mytopic', { | ||||||
|  |         method: 'POST', | ||||||
|  |         body: 'This message won't be stored server-side', | ||||||
|  |         headers: { 'Cache': 'no' } | ||||||
|  |     }) | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "Go" | ||||||
|  |     ``` go | ||||||
|  |     req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", strings.NewReader("This message won't be stored server-side")) | ||||||
|  |     req.Header.Set("Cache", "no") | ||||||
|  |     http.DefaultClient.Do(req) | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "PHP" | ||||||
|  |     ``` php-inline | ||||||
|  |     file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ | ||||||
|  |         'http' => [ | ||||||
|  |             'method' => 'POST', | ||||||
|  |             'header' => | ||||||
|  |                 "Content-Type: text/plain\r\n" . | ||||||
|  |                 "Cache: no", | ||||||
|  |             'content' => 'This message won't be stored server-side' | ||||||
|  |         ] | ||||||
|  |     ])); | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  | @ -7,20 +7,35 @@ import ( | ||||||
| 
 | 
 | ||||||
| type memCache struct { | type memCache struct { | ||||||
| 	messages map[string][]*message | 	messages map[string][]*message | ||||||
|  | 	nop      bool | ||||||
| 	mu       sync.Mutex | 	mu       sync.Mutex | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var _ cache = (*memCache)(nil) | var _ cache = (*memCache)(nil) | ||||||
| 
 | 
 | ||||||
|  | // newMemCache creates an in-memory cache | ||||||
| func newMemCache() *memCache { | func newMemCache() *memCache { | ||||||
| 	return &memCache{ | 	return &memCache{ | ||||||
| 		messages: make(map[string][]*message), | 		messages: make(map[string][]*message), | ||||||
|  | 		nop:      false, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // newNopCache creates an in-memory cache that discards all messages; | ||||||
|  | // it is always empty and can be used if caching is entirely disabled | ||||||
|  | func newNopCache() *memCache { | ||||||
|  | 	return &memCache{ | ||||||
|  | 		messages: make(map[string][]*message), | ||||||
|  | 		nop:      true, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *memCache) AddMessage(m *message) error { | func (s *memCache) AddMessage(m *message) error { | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	defer s.mu.Unlock() | 	defer s.mu.Unlock() | ||||||
|  | 	if s.nop { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
| 	if m.Event != messageEvent { | 	if m.Event != messageEvent { | ||||||
| 		return errUnexpectedMessageType | 		return errUnexpectedMessageType | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package server | package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -19,3 +20,16 @@ func TestMemCache_MessagesTagsPrioAndTitle(t *testing.T) { | ||||||
| func TestMemCache_Prune(t *testing.T) { | func TestMemCache_Prune(t *testing.T) { | ||||||
| 	testCachePrune(t, newMemCache()) | 	testCachePrune(t, newMemCache()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestMemCache_NopCache(t *testing.T) { | ||||||
|  | 	c := newNopCache() | ||||||
|  | 	assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message"))) | ||||||
|  | 
 | ||||||
|  | 	messages, err := c.Messages("mytopic", sinceAllMessages) | ||||||
|  | 	assert.Nil(t, err) | ||||||
|  | 	assert.Empty(t, messages) | ||||||
|  | 
 | ||||||
|  | 	topics, err := c.Topics() | ||||||
|  | 	assert.Nil(t, err) | ||||||
|  | 	assert.Empty(t, topics) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -80,7 +80,7 @@ | ||||||
|         There are <a href="docs/publish/">more features</a> related to publishing messages: You can set a |         There are <a href="docs/publish/">more features</a> related to publishing messages: You can set a | ||||||
|         <a href="docs/publish/#message-priority">notification priority</a>, a <a href="docs/publish/#message-title">title</a>, |         <a href="docs/publish/#message-priority">notification priority</a>, a <a href="docs/publish/#message-title">title</a>, | ||||||
|         and <a href="docs/publish/#tags-emojis">tag messages</a>. |         and <a href="docs/publish/#tags-emojis">tag messages</a>. | ||||||
|         Here's an example using all of them: |         Here's an example using some of them together: | ||||||
|     </p> |     </p> | ||||||
|     <code> |     <code> | ||||||
|         curl \<br/> |         curl \<br/> | ||||||
|  | @ -203,7 +203,7 @@ | ||||||
|             Click the link to do so. |             Click the link to do so. | ||||||
|         </p> |         </p> | ||||||
|         <p class="smallMarginBottom"> |         <p class="smallMarginBottom"> | ||||||
|             <b>Recent notifications</b> (cached for {{.CacheDuration}}): |             <b>Recent notifications</b> ({{if .CacheDuration}}cached for {{.CacheDuration | durationToHuman}}{{else}}caching is disabled{{end}}): | ||||||
|         </p> |         </p> | ||||||
|         <p id="detailNoNotifications"> |         <p id="detailNoNotifications"> | ||||||
|             <i>You haven't received any notifications for this topic yet.</i> |             <i>You haven't received any notifications for this topic yet.</i> | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ func (e errHTTP) Error() string { | ||||||
| 
 | 
 | ||||||
| type indexPage struct { | type indexPage struct { | ||||||
| 	Topic         string | 	Topic         string | ||||||
| 	CacheDuration string | 	CacheDuration time.Duration | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type sinceTime time.Time | type sinceTime time.Time | ||||||
|  | @ -85,9 +85,13 @@ var ( | ||||||
| 	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`) | 	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`) | ||||||
| 	disallowedTopics = []string{"docs", "static"} | 	disallowedTopics = []string{"docs", "static"} | ||||||
| 
 | 
 | ||||||
|  | 	templateFnMap = template.FuncMap{ | ||||||
|  | 		"durationToHuman": util.DurationToHuman, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	//go:embed "index.gohtml" | 	//go:embed "index.gohtml" | ||||||
| 	indexSource   string | 	indexSource   string | ||||||
| 	indexTemplate = template.Must(template.New("index").Parse(indexSource)) | 	indexTemplate = template.Must(template.New("index").Funcs(templateFnMap).Parse(indexSource)) | ||||||
| 
 | 
 | ||||||
| 	//go:embed "example.html" | 	//go:embed "example.html" | ||||||
| 	exampleSource string | 	exampleSource string | ||||||
|  | @ -139,7 +143,9 @@ func New(conf *config.Config) (*Server, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createCache(conf *config.Config) (cache, error) { | func createCache(conf *config.Config) (cache, error) { | ||||||
| 	if conf.CacheFile != "" { | 	if conf.CacheDuration == 0 { | ||||||
|  | 		return newNopCache(), nil | ||||||
|  | 	} else if conf.CacheFile != "" { | ||||||
| 		return newSqliteCache(conf.CacheFile) | 		return newSqliteCache(conf.CacheFile) | ||||||
| 	} | 	} | ||||||
| 	return newMemCache(), nil | 	return newMemCache(), nil | ||||||
|  | @ -241,7 +247,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { | ||||||
| func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error { | func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error { | ||||||
| 	return indexTemplate.Execute(w, &indexPage{ | 	return indexTemplate.Execute(w, &indexPage{ | ||||||
| 		Topic:         r.URL.Path[1:], | 		Topic:         r.URL.Path[1:], | ||||||
| 		CacheDuration: util.DurationToHuman(s.config.CacheDuration), | 		CacheDuration: s.config.CacheDuration, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -278,15 +284,17 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito | ||||||
| 	if m.Message == "" { | 	if m.Message == "" { | ||||||
| 		return errHTTPBadRequest | 		return errHTTPBadRequest | ||||||
| 	} | 	} | ||||||
| 	title, priority, tags := parseHeaders(r.Header) | 	title, priority, tags, cache := parseHeaders(r.Header) | ||||||
| 	m.Title = title | 	m.Title = title | ||||||
| 	m.Priority = priority | 	m.Priority = priority | ||||||
| 	m.Tags = tags | 	m.Tags = tags | ||||||
| 	if err := t.Publish(m); err != nil { | 	if err := t.Publish(m); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := s.cache.AddMessage(m); err != nil { | 	if cache { | ||||||
| 		return err | 		if err := s.cache.AddMessage(m); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests | 	w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests | ||||||
| 	if err := json.NewEncoder(w).Encode(m); err != nil { | 	if err := json.NewEncoder(w).Encode(m); err != nil { | ||||||
|  | @ -298,7 +306,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func parseHeaders(header http.Header) (title string, priority int, tags []string) { | func parseHeaders(header http.Header) (title string, priority int, tags []string, cache bool) { | ||||||
| 	title = readHeader(header, "x-title", "title", "ti", "t") | 	title = readHeader(header, "x-title", "title", "ti", "t") | ||||||
| 	priorityStr := readHeader(header, "x-priority", "priority", "prio", "p") | 	priorityStr := readHeader(header, "x-priority", "priority", "prio", "p") | ||||||
| 	if priorityStr != "" { | 	if priorityStr != "" { | ||||||
|  | @ -324,7 +332,8 @@ func parseHeaders(header http.Header) (title string, priority int, tags []string | ||||||
| 			tags = append(tags, strings.TrimSpace(s)) | 			tags = append(tags, strings.TrimSpace(s)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return title, priority, tags | 	cache = readHeader(header, "x-cache", "cache") != "no" | ||||||
|  | 	return title, priority, tags, cache | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func readHeader(header http.Header, names ...string) string { | func readHeader(header http.Header, names ...string) string { | ||||||
|  |  | ||||||
|  | @ -150,7 +150,74 @@ func TestServer_StaticSites(t *testing.T) { | ||||||
| 	assert.Equal(t, 200, rr.Code) | 	assert.Equal(t, 200, rr.Code) | ||||||
| 	assert.Contains(t, rr.Body.String(), `Made with ❤️ by Philipp C. Heckel`) | 	assert.Contains(t, rr.Body.String(), `Made with ❤️ by Philipp C. Heckel`) | ||||||
| 	assert.Contains(t, rr.Body.String(), `<script src=static/js/extra.js></script>`) | 	assert.Contains(t, rr.Body.String(), `<script src=static/js/extra.js></script>`) | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|  | func TestServer_PublishNoCache(t *testing.T) { | ||||||
|  | 	s := newTestServer(t, newTestConfig(t)) | ||||||
|  | 
 | ||||||
|  | 	response := request(t, s, "PUT", "/mytopic", "this message is not cached", map[string]string{ | ||||||
|  | 		"Cache": "no", | ||||||
|  | 	}) | ||||||
|  | 	msg := toMessage(t, response.Body.String()) | ||||||
|  | 	assert.NotEmpty(t, msg.ID) | ||||||
|  | 	assert.Equal(t, "this message is not cached", msg.Message) | ||||||
|  | 
 | ||||||
|  | 	response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) | ||||||
|  | 	messages := toMessages(t, response.Body.String()) | ||||||
|  | 	assert.Empty(t, messages) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestServer_PublishAndMultiPoll(t *testing.T) { | ||||||
|  | 	s := newTestServer(t, newTestConfig(t)) | ||||||
|  | 
 | ||||||
|  | 	response := request(t, s, "PUT", "/mytopic1", "message 1", nil) | ||||||
|  | 	msg := toMessage(t, response.Body.String()) | ||||||
|  | 	assert.NotEmpty(t, msg.ID) | ||||||
|  | 	assert.Equal(t, "mytopic1", msg.Topic) | ||||||
|  | 	assert.Equal(t, "message 1", msg.Message) | ||||||
|  | 
 | ||||||
|  | 	response = request(t, s, "PUT", "/mytopic2", "message 2", nil) | ||||||
|  | 	msg = toMessage(t, response.Body.String()) | ||||||
|  | 	assert.NotEmpty(t, msg.ID) | ||||||
|  | 	assert.Equal(t, "mytopic2", msg.Topic) | ||||||
|  | 	assert.Equal(t, "message 2", msg.Message) | ||||||
|  | 
 | ||||||
|  | 	response = request(t, s, "GET", "/mytopic1/json?poll=1", "", nil) | ||||||
|  | 	messages := toMessages(t, response.Body.String()) | ||||||
|  | 	assert.Equal(t, 1, len(messages)) | ||||||
|  | 	assert.Equal(t, "mytopic1", messages[0].Topic) | ||||||
|  | 	assert.Equal(t, "message 1", messages[0].Message) | ||||||
|  | 
 | ||||||
|  | 	response = request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1", "", nil) | ||||||
|  | 	messages = toMessages(t, response.Body.String()) | ||||||
|  | 	assert.Equal(t, 2, len(messages)) | ||||||
|  | 	assert.Equal(t, "mytopic1", messages[0].Topic) | ||||||
|  | 	assert.Equal(t, "message 1", messages[0].Message) | ||||||
|  | 	assert.Equal(t, "mytopic2", messages[1].Topic) | ||||||
|  | 	assert.Equal(t, "message 2", messages[1].Message) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestServer_PublishWithNopCache(t *testing.T) { | ||||||
|  | 	c := newTestConfig(t) | ||||||
|  | 	c.CacheDuration = 0 | ||||||
|  | 	s := newTestServer(t, c) | ||||||
|  | 
 | ||||||
|  | 	subscribeRR := httptest.NewRecorder() | ||||||
|  | 	subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR) | ||||||
|  | 
 | ||||||
|  | 	publishRR := request(t, s, "PUT", "/mytopic", "my first message", nil) | ||||||
|  | 	assert.Equal(t, 200, publishRR.Code) | ||||||
|  | 
 | ||||||
|  | 	subscribeCancel() | ||||||
|  | 	messages := toMessages(t, subscribeRR.Body.String()) | ||||||
|  | 	assert.Equal(t, 2, len(messages)) | ||||||
|  | 	assert.Equal(t, openEvent, messages[0].Event) | ||||||
|  | 	assert.Equal(t, messageEvent, messages[1].Event) | ||||||
|  | 	assert.Equal(t, "my first message", messages[1].Message) | ||||||
|  | 
 | ||||||
|  | 	response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) | ||||||
|  | 	messages = toMessages(t, response.Body.String()) | ||||||
|  | 	assert.Empty(t, messages) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newTestConfig(t *testing.T) *config.Config { | func newTestConfig(t *testing.T) *config.Config { | ||||||
|  |  | ||||||
|  | @ -470,11 +470,19 @@ li { | ||||||
|     margin-bottom: 20px; |     margin-bottom: 20px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #detail .detailDate { | ||||||
|  |     margin-bottom: 2px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #detail .detailDate, #detail .detailTags { | #detail .detailDate, #detail .detailTags { | ||||||
|     color: #888; |     color: #888; | ||||||
|     font-size: 0.9em; |     font-size: 0.9em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #detail .detailTags { | ||||||
|  |     margin-top: 2px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #detail .detailDate img { | #detail .detailDate img { | ||||||
|     width: 20px; |     width: 20px; | ||||||
|     height: 20px; |     height: 20px; | ||||||
|  | @ -483,11 +491,6 @@ li { | ||||||
| 
 | 
 | ||||||
| #detail .detailTitle { | #detail .detailTitle { | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
|     font-size: 1.1em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #detail .detailMessage { |  | ||||||
|     font-size: 1.1em; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #detail #detailMain { | #detail #detailMain { | ||||||
|  |  | ||||||
|  | @ -294,7 +294,7 @@ const formatTitle = (m) => { | ||||||
| 
 | 
 | ||||||
| const formatTitleA = (m) => { | const formatTitleA = (m) => { | ||||||
|     const emojiList = toEmojis(m.tags); |     const emojiList = toEmojis(m.tags); | ||||||
|     if (emojiList) { |     if (emojiList.length > 0) { | ||||||
|         return `${emojiList.join(" ")} ${m.title}`; |         return `${emojiList.join(" ")} ${m.title}`; | ||||||
|     } else { |     } else { | ||||||
|         return m.title; |         return m.title; | ||||||
|  | @ -306,7 +306,7 @@ const formatMessage = (m) => { | ||||||
|         return m.message; |         return m.message; | ||||||
|     } else { |     } else { | ||||||
|         const emojiList = toEmojis(m.tags); |         const emojiList = toEmojis(m.tags); | ||||||
|         if (emojiList) { |         if (emojiList.length > 0) { | ||||||
|             return `${emojiList.join(" ")} ${m.message}`; |             return `${emojiList.join(" ")} ${m.message}`; | ||||||
|         } else { |         } else { | ||||||
|             return m.message; |             return m.message; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue