"ntfy tier" CLI command

This commit is contained in:
binwiederhier 2023-02-07 12:02:25 -05:00
parent e3b39f670f
commit a32e8abc12
8 changed files with 140 additions and 40 deletions

View File

@ -8,20 +8,27 @@ import (
"os"
"os/exec"
"strconv"
"strings"
"testing"
"time"
)
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
testMessage := util.RandomString(10)
app, _, _, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
app2, _, stdout, _ := newTestApp()
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
require.Contains(t, stdout.String(), testMessage)
_, err := util.Retry(func() (*int, error) {
app2, _, stdout, _ := newTestApp()
if err := app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}); err != nil {
return nil, err
}
if !strings.Contains(stdout.String(), testMessage) {
return nil, fmt.Errorf("test message %s not found in topic", testMessage)
}
return util.Int(1), nil
}, time.Second, 2*time.Second, 5*time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
require.Nil(t, err)
}
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {

View File

@ -56,8 +56,27 @@ var cmdTier = &cli.Command{
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
},
Description: `
FIXME
Description: `Add a new tier to the ntfy user database.
Tiers can be used to grant users higher limits based, such as daily message limits, attachment size, or
make it possible for users to reserve topics.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy tier add pro # Add tier with code "pro", using the defaults
ntfy tier add \ # Add a tier with custom limits
--name="Pro" \
--message-limit=10000 \
--message-expiry-duration=24h \
--email-limit=50 \
--reservation-limit=10 \
--attachment-file-size-limit=100M \
--attachment-total-size-limit=1G \
--attachment-expiry-duration=12h \
--attachment-bandwidth-limit=5G \
pro
`,
},
{
@ -78,8 +97,20 @@ FIXME
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
},
Description: `
FIXME
Description: `Updates a tier to change the limits.
After updating a tier, you may have to restart the ntfy server to apply them
to all visitors.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier change \ # Update multiple limits and fields
--message-expiry-duration=24h \
--stripe-price-id=price_1234 \
pro
`,
},
{
@ -88,8 +119,16 @@ FIXME
Usage: "Removes a tier",
UsageText: "ntfy tier remove CODE",
Action: execTierDel,
Description: `
FIXME
Description: `Remove a tier from the ntfy user database.
You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
to remove or switch their tier first.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Example:
ntfy tier del pro
`,
},
{
@ -97,22 +136,26 @@ FIXME
Aliases: []string{"l"},
Usage: "Shows a list of tiers",
Action: execTierList,
Description: `
FIXME
Description: `Shows a list of all configured tiers.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
`,
},
},
Description: `Manage tier of the ntfy server.
Description: `Manage tiers of the ntfy server.
The command allows you to add/remove/change tier in the ntfy user database. Tiers are used
to grant users higher limits based on their tier.
The command allows you to add/remove/change tiers in the ntfy user database. Tiers are used
to grant users higher limits, such as daily message limits, attachment size, or make it
possible for users to reserve topics.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy access'.
FIXME
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy tier add pro # Add tier with code "pro", using the defaults
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier del pro # Delete an existing tier
`,
}

46
cmd/tier_test.go Normal file
View File

@ -0,0 +1,46 @@
package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, _, _, stderr := newTestApp()
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
err := runTierCommand(app, conf, "add", "pro")
require.NotNil(t, err)
require.Equal(t, "tier pro already exists", err.Error())
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "list"))
require.Contains(t, stderr.String(), "tier pro (id: ti_")
require.Contains(t, stderr.String(), "- Name: Pro")
require.Contains(t, stderr.String(), "- Message limit: 1234")
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "change", "--message-limit", "999", "pro"))
require.Contains(t, stderr.String(), "- Message limit: 999")
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
require.Contains(t, stderr.String(), "tier pro removed")
}
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"tier",
"--auth-file=" + conf.AuthFile,
"--auth-default-access=" + conf.AuthDefault.String(),
}
return app.Run(append(userArgs, args...))
}

View File

@ -139,22 +139,22 @@ Example:
Action: execUserList,
Description: `Shows a list of all configured users, including the everyone ('*') user.
This command is an alias to calling 'ntfy access' (display access control list).
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
This command is an alias to calling 'ntfy access' (display access control list).
`,
},
},
Description: `Manage users of the ntfy server.
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy access'.
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
Examples:
ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil

View File

@ -11,13 +11,14 @@ import (
)
const (
tagField = "tag"
errorField = "error"
tagField = "tag"
errorField = "error"
timestampFormat = "2006-01-02T15:04:05.999Z07:00"
)
// Event represents a single log event
type Event struct {
Timestamp int64 `json:"time"`
Timestamp string `json:"time"`
Level Level `json:"level"`
Message string `json:"message"`
fields Context
@ -25,8 +26,9 @@ type Event struct {
// newEvent creates a new log event
func newEvent() *Event {
now := time.Now()
return &Event{
Timestamp: time.Now().UnixMilli(),
Timestamp: now.Format(timestampFormat),
fields: make(Context),
}
}
@ -70,8 +72,8 @@ func (e *Event) Tag(tag string) *Event {
}
// Time sets the time field
func (e *Event) Time(time time.Time) *Event {
e.Timestamp = time.UnixMilli()
func (e *Event) Time(t time.Time) *Event {
e.Timestamp = t.Format(timestampFormat)
return e
}

View File

@ -35,7 +35,7 @@ func TestLog_TagContextFieldFields(t *testing.T) {
Tag("mytag").
Field("field2", 123).
Field("field1", "value1").
Time(time.Unix(123, 0)).
Time(time.Unix(123, 999000000).UTC()).
Info("hi there %s", "phil")
log.
Tag("not-stripe").
@ -48,11 +48,11 @@ func TestLog_TagContextFieldFields(t *testing.T) {
}).
Tag("stripe").
Err(err).
Time(time.Unix(456, 0)).
Time(time.Unix(456, 123000000).UTC()).
Debug("Subscription status %s", "active")
expected := `{"time":123000,"level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
{"time":456000,"level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
expected := `{"time":"1970-01-01T00:02:03.999Z","level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
{"time":"1970-01-01T00:07:36.123Z","level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
`
require.Equal(t, expected, out.String())
}

View File

@ -24,7 +24,9 @@ func logv(v *visitor) *log.Event {
// logr creates a new log event with HTTP request and visitor fields
func logvr(v *visitor, r *http.Request) *log.Event {
return logv(v).Fields(httpContext(r))
return logv(v).
Fields(httpContext(r)).
Fields(requestLimiterFields(v.RequestLimiter()))
}
// logvrm creates a new log event with HTTP request, visitor fields and message fields

View File

@ -37,7 +37,7 @@ import (
- HIGH Rate limiting: Sensitive endpoints (account/login/change-password/...)
- HIGH Account limit creation triggers when account is taken!
- HIGH Docs
- HIGH CLI "ntfy tier [add|list|delete]"
- HIGH make request limit independent of message limit again
- HIGH Self-review
- MEDIUM: Test for expiring messages after reservation removal
- MEDIUM: Test new token endpoints & never-expiring token
@ -235,8 +235,8 @@ func (s *Server) Run() error {
}
log.Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
if log.IsFile() {
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s, log file is %s\n", listenStr, s.config.Version, log.File())
fmt.Fprintln(os.Stderr, "No more output is expected.")
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
fmt.Fprintf(os.Stderr, "Logs are written to %s\n", log.File())
}
mux := http.NewServeMux()
mux.HandleFunc("/", s.handle)