Add 'Cache: no' header, closes #41
parent
d5be5d3e8c
commit
d6fbccab55
|
@ -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…
Reference in New Issue