Gmail now connects. Authorisation-code flow with PKCE, the verifier kept server-side rather than in a cookie — the callback is a top-level navigation from Google, and a SameSite=Lax cookie is the kind of thing that works until a browser tightens its defaults. State is checked, the attempt expires after ten minutes, and the spent verifier is deleted rather than left at rest. Scope is gmail.readonly and the sync is one-way on purpose. Unstarring a message does not delete the task; completing the task does not touch the mailbox. Two-way would mean deciding what happens when both sides changed, and the only honest answer is a sync model far larger than the feature. The query carries newer_than:30d so connecting an account with a decade of starred mail does not create a decade of tasks. The update check is off unless the operator turns it on, asks GitHub at most every six hours, and sends nothing about the instance — no identifier, no version, no counts. A development build never reports an update: somebody running their own tree is ahead of every release, and telling them otherwise trains them to ignore the notice. Only administrators see it, because only they can act on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
1.9 KiB
Go
69 lines
1.9 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsNewer(t *testing.T) {
|
|
cases := []struct {
|
|
current, latest string
|
|
want bool
|
|
}{
|
|
{"v1.0.0", "v1.0.1", true},
|
|
{"v1.0.0", "v1.1.0", true},
|
|
{"v1.0.0", "v2.0.0", true},
|
|
{"v1.2.3", "v1.2.3", false},
|
|
{"v1.2.3", "v1.2.2", false},
|
|
{"v2.0.0", "v1.9.9", false},
|
|
// The comparison is numeric, not lexical: 10 is after 9.
|
|
{"v1.9.0", "v1.10.0", true},
|
|
{"v1.10.0", "v1.9.0", false},
|
|
// Without the v.
|
|
{"1.0.0", "1.0.1", true},
|
|
// Pre-release suffixes compare as their base version.
|
|
{"v1.0.0-rc1", "v1.0.0", false},
|
|
{"v1.0.0", "v1.0.1-rc1", true},
|
|
|
|
// A development build is ahead of every release, not behind it. Telling
|
|
// somebody running their own tree that they are out of date every time
|
|
// they open the app teaches them to ignore the notice.
|
|
{"dev", "v1.0.0", false},
|
|
{"v1.0.0-dirty", "v2.0.0", false},
|
|
|
|
// Anything unparseable reports nothing rather than guessing.
|
|
{"", "v1.0.0", false},
|
|
{"v1.0.0", "", false},
|
|
{"v1.0.0", "latest", false},
|
|
{"v1.0", "v1.0.1", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.current+"→"+tc.latest, func(t *testing.T) {
|
|
if got := IsNewer(tc.current, tc.latest); got != tc.want {
|
|
t.Errorf("IsNewer(%q, %q) = %v, want %v", tc.current, tc.latest, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Off unless asked for, and when off it must not reach out at all.
|
|
func TestDisabledCheckerNeverCallsOut(t *testing.T) {
|
|
c := New("v1.0.0", false)
|
|
|
|
status := c.Status(context.Background())
|
|
if !status.Disabled {
|
|
t.Error("a disabled checker does not report itself as disabled")
|
|
}
|
|
if status.Available {
|
|
t.Error("a disabled checker reported an update")
|
|
}
|
|
if status.Current != "v1.0.0" {
|
|
t.Errorf("Current = %q", status.Current)
|
|
}
|
|
// CheckedAt stays empty, which is how the interface tells "no update" apart
|
|
// from "we never looked".
|
|
if status.CheckedAt != "" {
|
|
t.Errorf("CheckedAt = %q on a disabled checker", status.CheckedAt)
|
|
}
|
|
}
|