aboutsummaryrefslogtreecommitdiffstats
path: root/fikenauth/source_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'fikenauth/source_test.go')
-rw-r--r--fikenauth/source_test.go172
1 files changed, 172 insertions, 0 deletions
diff --git a/fikenauth/source_test.go b/fikenauth/source_test.go
new file mode 100644
index 0000000..1c960d6
--- /dev/null
+++ b/fikenauth/source_test.go
@@ -0,0 +1,172 @@
+package fikenauth
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestSourceReturnsAValidTokenWithoutRefreshing(t *testing.T) {
+ server := newAuthServer(t)
+ store := tempStore(t)
+ if err := store.Save(Token{
+ AccessToken: "still-good", RefreshToken: "r1",
+ ExpiresAt: fixedNow.Add(time.Hour),
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := NewSource(server.config(), store).Token(t.Context())
+ if err != nil {
+ t.Fatalf("Token: %v", err)
+ }
+ if got != "still-good" {
+ t.Errorf("Token = %q", got)
+ }
+ if server.gotForm != nil {
+ t.Error("the token endpoint was called for a token that was still valid")
+ }
+}
+
+// An expired token is renewed and the new one stored, because Fiken invalidates
+// the refresh token once it is used: not saving would strand the next run.
+func TestSourceRefreshesAndPersists(t *testing.T) {
+ server := newAuthServer(t)
+ server.refreshOut = `{"access_token":"access-2","refresh_token":"refresh-2","expires_in":3600}`
+ store := tempStore(t)
+ if err := store.Save(Token{
+ AccessToken: "expired", RefreshToken: "refresh-1",
+ ExpiresAt: fixedNow.Add(-time.Hour),
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ source := NewSource(server.config(), store)
+ got, err := source.Token(t.Context())
+ if err != nil {
+ t.Fatalf("Token: %v", err)
+ }
+ if got != "access-2" {
+ t.Errorf("Token = %q, want the refreshed one", got)
+ }
+ if sent := server.gotForm.Get("refresh_token"); sent != "refresh-1" {
+ t.Errorf("refreshed with %q", sent)
+ }
+
+ stored, err := store.Load()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if stored.AccessToken != "access-2" || stored.RefreshToken != "refresh-2" {
+ t.Errorf("stored %+v, want the refreshed grant", stored)
+ }
+
+ // A second call within the same run reuses what it already has.
+ server.gotForm = nil
+ if _, err := source.Token(t.Context()); err != nil {
+ t.Fatalf("second Token: %v", err)
+ }
+ if server.gotForm != nil {
+ t.Error("the token was refreshed twice in one run")
+ }
+}
+
+func TestSourceWithoutAStoredToken(t *testing.T) {
+ _, err := NewSource(newAuthServer(t).config(), tempStore(t)).Token(t.Context())
+ if !errors.Is(err, ErrNoToken) {
+ t.Errorf("err = %v, want ErrNoToken", err)
+ }
+}
+
+func TestSourceCannotRenewWithoutARefreshToken(t *testing.T) {
+ store := tempStore(t)
+ if err := store.Save(Token{AccessToken: "expired", ExpiresAt: fixedNow.Add(-time.Hour)}); err != nil {
+ t.Fatal(err)
+ }
+ _, err := NewSource(newAuthServer(t).config(), store).Token(t.Context())
+ if !errors.Is(err, ErrNoToken) {
+ t.Errorf("err = %v, want ErrNoToken", err)
+ }
+}
+
+// A revoked grant should tell the operator what to do, not report a raw
+// OAuth code at them.
+func TestSourceExplainsARevokedGrant(t *testing.T) {
+ server := newAuthServer(t)
+ server.replyCode = http.StatusBadRequest
+ server.reply = `{"error":"invalid_grant","error_description":"revoked"}`
+ store := tempStore(t)
+ if err := store.Save(Token{
+ AccessToken: "expired", RefreshToken: "r1",
+ ExpiresAt: fixedNow.Add(-time.Hour),
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ // A caller that says what its login command is gets named in the error,
+ // because every program embedding this spells it differently.
+ source := NewSource(server.config(), store)
+ source.LoginCommand = "bill fiken login"
+ _, err := source.Token(t.Context())
+ if err == nil || !strings.Contains(err.Error(), "run `bill fiken login` again") {
+ t.Fatalf("err = %v, want it to name the caller's login command", err)
+ }
+
+ // One that does not still has to say what to do, without inventing a
+ // command that does not exist.
+ _, err = NewSource(server.config(), store).Token(t.Context())
+ switch {
+ case err == nil || !strings.Contains(err.Error(), "log in to Fiken again"):
+ t.Fatalf("err = %v, want it to say how to recover", err)
+ case strings.Contains(err.Error(), "`"):
+ t.Errorf("err = %v, want no command quoted when none was given", err)
+ }
+}
+
+func TestLogoutRevokesAndForgets(t *testing.T) {
+ server := newAuthServer(t)
+ store := tempStore(t)
+ if err := store.Save(Token{
+ AccessToken: "access-1", RefreshToken: "r1",
+ ExpiresAt: fixedNow.Add(time.Hour),
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ source := NewSource(server.config(), store)
+ if err := source.Logout(t.Context()); err != nil {
+ t.Fatalf("Logout: %v", err)
+ }
+ if server.gotRevokeAuth != "Bearer access-1" {
+ t.Errorf("revoke Authorization = %q", server.gotRevokeAuth)
+ }
+ if _, err := store.Load(); !errors.Is(err, ErrNoToken) {
+ t.Errorf("the token file survived logout: %v", err)
+ }
+}
+
+// A token that cannot be revoked is exactly the one not to keep on disk.
+func TestLogoutForgetsEvenWhenRevokeFails(t *testing.T) {
+ server := newAuthServer(t)
+ server.replyCode = http.StatusInternalServerError
+ store := tempStore(t)
+ if err := store.Save(Token{AccessToken: "access-1", ExpiresAt: fixedNow.Add(time.Hour)}); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := NewSource(server.config(), store).Logout(t.Context()); err == nil {
+ t.Error("Logout hid the revoke failure")
+ }
+ if _, err := store.Load(); !errors.Is(err, ErrNoToken) {
+ t.Errorf("the token file survived logout: %v", err)
+ }
+}
+
+func TestLogoutWithoutATokenIsNoToken(t *testing.T) {
+ err := NewSource(newAuthServer(t).config(), tempStore(t)).Logout(t.Context())
+ if !errors.Is(err, ErrNoToken) {
+ t.Errorf("err = %v, want ErrNoToken", err)
+ }
+}