- Refine tier changes ledger: fold supersession pairs, humanize reasons, carry grant notes - Fix OAuth token expiry to use wall clock
83 lines
2.7 KiB
Go
83 lines
2.7 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package workflows
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTokenServer(t *testing.T, calls *int) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
*calls++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"access_token":"tok-` + string(rune('0'+*calls)) + `","expires_in":300,"token_type":"Bearer"}`))
|
|
}))
|
|
}
|
|
|
|
// The cached expiry must carry no monotonic clock reading, so that the
|
|
// validity check runs on the wall clock. Go's monotonic clock stops while
|
|
// the host sleeps; a monotonic comparison would keep a token past its exp
|
|
// after every resume.
|
|
func TestOAuthTokenProviderExpiryIsWallClock(t *testing.T) {
|
|
calls := 0
|
|
srv := newTokenServer(t, &calls)
|
|
defer srv.Close()
|
|
|
|
p, err := NewOAuthTokenProvider(OAuthTokenProviderConfig{TokenURL: srv.URL, ClientID: "id", ClientSecret: "secret"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := p.GetHeaders(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Round(0) strips a monotonic reading; a time that already lacks one is
|
|
// unchanged, so equality here means the stored expiry has none.
|
|
if p.expiresAt != p.expiresAt.Round(0) {
|
|
t.Fatalf("expiresAt carries a monotonic reading: %v", p.expiresAt)
|
|
}
|
|
if until := time.Until(p.expiresAt); until < 4*time.Minute || until > 5*time.Minute {
|
|
t.Fatalf("expiresAt not about five minutes out: %v", until)
|
|
}
|
|
}
|
|
|
|
// A token inside the safety buffer is refetched; one outside it is reused.
|
|
func TestOAuthTokenProviderRefreshesInsideSafetyBuffer(t *testing.T) {
|
|
calls := 0
|
|
srv := newTokenServer(t, &calls)
|
|
defer srv.Close()
|
|
|
|
p, err := NewOAuthTokenProvider(OAuthTokenProviderConfig{TokenURL: srv.URL, ClientID: "id", ClientSecret: "secret"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := p.GetHeaders(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := p.GetHeaders(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls != 1 || first[authHeaderKey] != again[authHeaderKey] {
|
|
t.Fatalf("fresh token was not reused: calls=%d", calls)
|
|
}
|
|
|
|
// Simulate the wall clock reaching the buffer (as it does after a
|
|
// suspend): the stored expiry is a wall-clock instant, so moving it is
|
|
// what the passage of wall time looks like to the check.
|
|
p.expiresAt = time.Now().Round(0).Add(p.safetyBuffer - time.Second)
|
|
third, err := p.GetHeaders(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls != 2 || third[authHeaderKey] == first[authHeaderKey] {
|
|
t.Fatalf("token inside the safety buffer was not refreshed: calls=%d", calls)
|
|
}
|
|
}
|