From 94fd2aa143678199da5a44f89d244d231e4db780 Mon Sep 17 00:00:00 2001 From: Thayol Date: Sun, 13 Sep 2026 09:43:08 +0200 Subject: [PATCH] Remove unused second part of the expiry counter --- internal/config/config_test.go | 23 +++++++++++++++++++++++ internal/config/parse.go | 9 ++++++++- internal/server/pages.go | 12 +++++++++--- internal/server/server_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f7049d1..74860dd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -89,6 +89,29 @@ func TestFormatRoundTrips(t *testing.T) { } } +// The spellings themselves, not just their round trip: Duration.String() pads +// out the units below the largest one, and trimming that tail is easy to get +// wrong in a way that eats a real digit ("1m30s" must not become "1m3"). +func TestFormatDurationSpelling(t *testing.T) { + for _, c := range []struct { + in time.Duration + want string + }{ + {Unlimited, "never"}, + {30 * time.Second, "30s"}, + {time.Minute, "1m"}, + {90 * time.Second, "1m30s"}, + {90 * time.Minute, "1h30m"}, + {2 * time.Hour, "2h"}, + {25 * time.Hour, "25h"}, + {72 * time.Hour, "3d"}, + } { + if got := FormatDuration(c.in); got != c.want { + t.Errorf("FormatDuration(%s) = %q, want %q", c.in, got, c.want) + } + } +} + // The convention is: one hyphen for a letter, two for a word. It is enforced // here because the stdlib flag package treats both forms as the same. func TestHyphenConvention(t *testing.T) { diff --git a/internal/config/parse.go b/internal/config/parse.go index e24d384..52a5fef 100644 --- a/internal/config/parse.go +++ b/internal/config/parse.go @@ -123,6 +123,13 @@ func FormatDuration(d time.Duration) string { case d%time.Hour == 0: return fmt.Sprintf("%dh", d/time.Hour) default: - return d.String() + // Duration.String() spells out every unit below the largest one, so a + // whole number of minutes arrives as "1m0s". Drop that tail, but only + // when the seconds really are zero: "1m30s" has to keep them. + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = strings.TrimSuffix(s, "0s") + } + return s } } diff --git a/internal/server/pages.go b/internal/server/pages.go index 63d6d11..41a5c58 100644 --- a/internal/server/pages.go +++ b/internal/server/pages.go @@ -169,10 +169,16 @@ func describeExpiry(t *time.Time, now time.Time) string { if t == nil { return "never" } + at := t.UTC().Format("2006-01-02 15:04 MST") d := t.Sub(now).Round(time.Minute) - if d < 0 { + switch { + case d < 0: return "expired" + case d == 0: + // Rounded away to nothing, and a zero duration is how this program + // spells "unlimited": saying "in never" of a file about to go would + // be exactly backwards. + return at + " (in under a minute)" } - return fmt.Sprintf("%s (in %s)", t.UTC().Format("2006-01-02 15:04 MST"), - config.FormatDuration(d)) + return fmt.Sprintf("%s (in %s)", at, config.FormatDuration(d)) } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 27f5fd5..24af11b 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -1686,6 +1686,32 @@ func TestCustomNameFieldIsAlwaysUsable(t *testing.T) { } } +// The share page's expiry line is rounded to the minute, so it must not offer +// a seconds tail it is not counting - nor, when the remainder rounds away to +// nothing, claim the file is kept forever. +func TestDescribeExpiryReadsAsWritten(t *testing.T) { + at := func(d time.Duration) *time.Time { + when := clock.Add(d) + return &when + } + for _, c := range []struct { + name string + in *time.Time + want string + }{ + {"no deadline", nil, "never"}, + {"already gone", at(-time.Hour), "expired"}, + {"about to go", at(20 * time.Second), "(in under a minute)"}, + {"a minute off", at(time.Minute), "(in 1m)"}, + {"an hour and a half", at(90 * time.Minute), "(in 1h30m)"}, + {"three days", at(72 * time.Hour), "(in 3d)"}, + } { + if got := describeExpiry(c.in, clock); !strings.Contains(got, c.want) { + t.Errorf("%s: describeExpiry = %q, want it to contain %q", c.name, got, c.want) + } + } +} + // A chosen passphrase has to work everywhere a generated token does: at the // login form, on an upload, and as a session. func TestChosenPassphraseWorksEndToEnd(t *testing.T) {