Files
member-console/internal/workflows/oauth_token_provider_test.go
T
cgalo5758 fa169dca3b Add purchasability status to products list
- Refine tier changes ledger: fold supersession pairs, humanize reasons,
  carry grant notes
- Fix OAuth token expiry to use wall clock
2026-09-06 23:31:12 -05:00

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)
}
}