feat: oauth2_auth_url (pure), oauth2_exchange, oauth2_refresh
Fase 4 del issue 0010 — cliente OAuth2 sin golang.org/x/oauth2. - Oauth2AuthURL es pura: solo construye la URL con net/url - Oauth2Exchange/Refresh hacen POST application/x-www-form-urlencoded - ExpiresAt calculado como now + expires_in del proveedor - Refresh conserva el token original si el proveedor no devuelve uno nuevo - Tests con httptest.NewServer como mock del proveedor
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Oauth2AuthURL construye la URL de autorizacion OAuth2 a partir de la config.
|
||||
// Funcion pura — no hace I/O, solo concatenacion de strings.
|
||||
// La URL resultante redirige al usuario al proveedor para que autorice el acceso.
|
||||
func Oauth2AuthURL(config OAuthConfig, state string) string {
|
||||
q := url.Values{}
|
||||
q.Set("client_id", config.ClientID)
|
||||
q.Set("redirect_uri", config.RedirectURL)
|
||||
q.Set("response_type", "code")
|
||||
if len(config.Scopes) > 0 {
|
||||
q.Set("scope", strings.Join(config.Scopes, " "))
|
||||
}
|
||||
if state != "" {
|
||||
q.Set("state", state)
|
||||
}
|
||||
sep := "?"
|
||||
if strings.Contains(config.AuthURL, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
return config.AuthURL + sep + q.Encode()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: oauth2_auth_url
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func Oauth2AuthURL(config OAuthConfig, state string) string"
|
||||
description: "Construye la URL de autorizacion OAuth2 a partir de la config. Funcion pura que concatena el AuthURL del proveedor con los query params (client_id, redirect_uri, response_type=code, scope, state)."
|
||||
tags: [oauth, oauth2, auth, url, infra]
|
||||
uses_functions: []
|
||||
uses_types: [OAuthConfig_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [net/url, strings]
|
||||
params:
|
||||
- name: config
|
||||
desc: "OAuthConfig del proveedor (Google, GitHub, etc.) con ClientID, AuthURL, RedirectURL y Scopes"
|
||||
- name: state
|
||||
desc: "valor aleatorio anti-CSRF que debe validarse en el callback. Si es vacio no se añade"
|
||||
output: "URL completa a la que redirigir al usuario para iniciar el flujo OAuth2"
|
||||
tested: true
|
||||
tests: ["genera URL con todos los params basicos", "concatena scopes con espacio", "añade state si no es vacio", "detecta si AuthURL ya trae query y usa & en vez de ?"]
|
||||
test_file_path: "functions/infra/oauth2_auth_url_test.go"
|
||||
file_path: "functions/infra/oauth2_auth_url.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
google := OAuthConfig{
|
||||
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
RedirectURL: "http://localhost:8080/callback",
|
||||
Scopes: []string{"openid", "email", "profile"},
|
||||
}
|
||||
state := "random-anti-csrf-token" // guardar en cookie/session
|
||||
url := Oauth2AuthURL(google, state)
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Pura — solo hace string building con `net/url.Values.Encode()` (ordena params alfabeticamente y hace URL-encoding). No lee env, ni toca I/O, ni `time.Now()`. El state es critico para prevenir CSRF: debe ser aleatorio por sesion, guardarse server-side (cookie firmada, session, etc.) y validarse en el callback antes de hacer Oauth2Exchange. Un state vacio significa sin proteccion CSRF y no se incluye en la URL — solo apto para pruebas locales.
|
||||
@@ -0,0 +1,68 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOauth2AuthURL_BuildsBasicParams(t *testing.T) {
|
||||
cfg := OAuthConfig{
|
||||
ClientID: "abc-client",
|
||||
AuthURL: "https://example.com/authorize",
|
||||
RedirectURL: "http://localhost/callback",
|
||||
Scopes: []string{"openid", "email"},
|
||||
}
|
||||
got := Oauth2AuthURL(cfg, "state-xyz")
|
||||
u, err := url.Parse(got)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if u.Scheme != "https" || u.Host != "example.com" || u.Path != "/authorize" {
|
||||
t.Fatalf("base URL incorrecta: %s", got)
|
||||
}
|
||||
q := u.Query()
|
||||
if q.Get("client_id") != "abc-client" {
|
||||
t.Errorf("client_id = %q", q.Get("client_id"))
|
||||
}
|
||||
if q.Get("redirect_uri") != "http://localhost/callback" {
|
||||
t.Errorf("redirect_uri = %q", q.Get("redirect_uri"))
|
||||
}
|
||||
if q.Get("response_type") != "code" {
|
||||
t.Errorf("response_type = %q", q.Get("response_type"))
|
||||
}
|
||||
if q.Get("scope") != "openid email" {
|
||||
t.Errorf("scope = %q", q.Get("scope"))
|
||||
}
|
||||
if q.Get("state") != "state-xyz" {
|
||||
t.Errorf("state = %q", q.Get("state"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2AuthURL_OmitsEmptyState(t *testing.T) {
|
||||
cfg := OAuthConfig{ClientID: "c", AuthURL: "https://x.test/a", RedirectURL: "http://r"}
|
||||
got := Oauth2AuthURL(cfg, "")
|
||||
if strings.Contains(got, "state=") {
|
||||
t.Errorf("state deberia estar ausente: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2AuthURL_HandlesExistingQueryString(t *testing.T) {
|
||||
cfg := OAuthConfig{
|
||||
ClientID: "c",
|
||||
AuthURL: "https://example.com/authorize?hd=domain.com",
|
||||
RedirectURL: "http://r",
|
||||
}
|
||||
got := Oauth2AuthURL(cfg, "s")
|
||||
u, err := url.Parse(got)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
q := u.Query()
|
||||
if q.Get("hd") != "domain.com" {
|
||||
t.Errorf("param pre-existente se perdio")
|
||||
}
|
||||
if q.Get("client_id") != "c" {
|
||||
t.Errorf("client_id no agregado")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// oauth2TokenResponse es la respuesta JSON estandar del endpoint token de OAuth2.
|
||||
type oauth2TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
// oauth2DoTokenRequest hace POST application/x-www-form-urlencoded al TokenURL
|
||||
// con el body indicado, parsea la respuesta JSON y construye OAuthTokens.
|
||||
func oauth2DoTokenRequest(tokenURL string, form url.Values) (OAuthTokens, error) {
|
||||
var zero OAuthTokens
|
||||
req, err := http.NewRequest(http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
|
||||
var parsed oauth2TokenResponse
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return zero, fmt.Errorf("parse json: %w (body=%s)", err, string(body))
|
||||
}
|
||||
if parsed.Error != "" {
|
||||
return zero, fmt.Errorf("oauth provider error: %s: %s", parsed.Error, parsed.ErrorDescription)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return zero, fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
if parsed.AccessToken == "" {
|
||||
return zero, fmt.Errorf("respuesta sin access_token")
|
||||
}
|
||||
|
||||
var expiresAt int64
|
||||
if parsed.ExpiresIn > 0 {
|
||||
expiresAt = time.Now().Unix() + parsed.ExpiresIn
|
||||
}
|
||||
|
||||
return OAuthTokens{
|
||||
AccessToken: parsed.AccessToken,
|
||||
RefreshToken: parsed.RefreshToken,
|
||||
TokenType: parsed.TokenType,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Oauth2Exchange intercambia un authorization code por tokens OAuth2.
|
||||
// Hace POST al TokenURL con grant_type=authorization_code y las credenciales
|
||||
// del cliente. Retorna OAuthTokens con AccessToken, RefreshToken y ExpiresAt.
|
||||
func Oauth2Exchange(config OAuthConfig, code string) (OAuthTokens, error) {
|
||||
var zero OAuthTokens
|
||||
if code == "" {
|
||||
return zero, fmt.Errorf("oauth2_exchange: code vacio")
|
||||
}
|
||||
if config.TokenURL == "" {
|
||||
return zero, fmt.Errorf("oauth2_exchange: token_url vacio")
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "authorization_code")
|
||||
form.Set("code", code)
|
||||
form.Set("client_id", config.ClientID)
|
||||
form.Set("client_secret", config.ClientSecret)
|
||||
form.Set("redirect_uri", config.RedirectURL)
|
||||
|
||||
tokens, err := oauth2DoTokenRequest(config.TokenURL, form)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("oauth2_exchange: %w", err)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: oauth2_exchange
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func Oauth2Exchange(config OAuthConfig, code string) (OAuthTokens, error)"
|
||||
description: "Intercambia un authorization code por tokens OAuth2. POST al TokenURL del proveedor con grant_type=authorization_code y las credenciales del cliente. Retorna OAuthTokens con AccessToken, RefreshToken y ExpiresAt calculado."
|
||||
tags: [oauth, oauth2, auth, token, exchange, http, infra]
|
||||
uses_functions: []
|
||||
uses_types: [OAuthConfig_go_infra, OAuthTokens_go_infra]
|
||||
returns: [OAuthTokens_go_infra]
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [encoding/json, fmt, io, net/http, net/url, strings, time]
|
||||
params:
|
||||
- name: config
|
||||
desc: "OAuthConfig del proveedor con ClientID, ClientSecret, TokenURL y RedirectURL"
|
||||
- name: code
|
||||
desc: "authorization code recibido en el callback tras redirigir al usuario a la URL de Oauth2AuthURL"
|
||||
output: "OAuthTokens con access/refresh tokens. ExpiresAt = now + expires_in del proveedor"
|
||||
tested: true
|
||||
tests: ["intercambia code por tokens contra mock server", "rechaza code vacio", "propaga error si proveedor devuelve error"]
|
||||
test_file_path: "functions/infra/oauth2_exchange_test.go"
|
||||
file_path: "functions/infra/oauth2_exchange.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
// Validar state contra el guardado en cookie/session...
|
||||
|
||||
tokens, err := Oauth2Exchange(googleConfig, code)
|
||||
if err != nil {
|
||||
HTTPErrorResponse(w, HTTPError{Status: 500, Code: "oauth_error", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
// Usar tokens.AccessToken para llamar a APIs del proveedor
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — hace POST HTTP al TokenURL con timeout de 30s, y usa `time.Now()` para calcular ExpiresAt. El body es application/x-www-form-urlencoded (estandar OAuth2). Si el proveedor retorna JSON con campo `error` se wrappea en un error descriptivo. El ClientSecret se envia en el body (no en header Authorization Basic) para compatibilidad amplia — la mayoria de proveedores aceptan ambos. NO valida el state anti-CSRF: eso debe hacerlo el handler del callback antes de llamar a Oauth2Exchange.
|
||||
@@ -0,0 +1,74 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOauth2Exchange_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("metodo = %s", r.Method)
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
t.Fatalf("ParseForm: %v", err)
|
||||
}
|
||||
if got := r.PostForm.Get("grant_type"); got != "authorization_code" {
|
||||
t.Errorf("grant_type = %q", got)
|
||||
}
|
||||
if got := r.PostForm.Get("code"); got != "abc-code" {
|
||||
t.Errorf("code = %q", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "at-123",
|
||||
"refresh_token": "rt-456",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := OAuthConfig{
|
||||
ClientID: "c",
|
||||
ClientSecret: "s",
|
||||
TokenURL: srv.URL,
|
||||
RedirectURL: "http://r",
|
||||
}
|
||||
tokens, err := Oauth2Exchange(cfg, "abc-code")
|
||||
if err != nil {
|
||||
t.Fatalf("Oauth2Exchange: %v", err)
|
||||
}
|
||||
if tokens.AccessToken != "at-123" || tokens.RefreshToken != "rt-456" || tokens.TokenType != "Bearer" {
|
||||
t.Errorf("tokens = %+v", tokens)
|
||||
}
|
||||
if tokens.ExpiresAt == 0 {
|
||||
t.Error("ExpiresAt no deberia ser 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2Exchange_EmptyCode(t *testing.T) {
|
||||
cfg := OAuthConfig{TokenURL: "http://x", ClientID: "c"}
|
||||
if _, err := Oauth2Exchange(cfg, ""); err == nil {
|
||||
t.Fatal("esperaba error con code vacio")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2Exchange_ProviderError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "code expired",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := OAuthConfig{TokenURL: srv.URL, ClientID: "c"}
|
||||
if _, err := Oauth2Exchange(cfg, "code"); err == nil {
|
||||
t.Fatal("esperaba error del proveedor")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Oauth2Refresh renueva un access token OAuth2 usando el refresh token.
|
||||
// POST al TokenURL con grant_type=refresh_token. Retorna OAuthTokens con
|
||||
// el nuevo AccessToken (y posiblemente un nuevo RefreshToken segun el proveedor).
|
||||
func Oauth2Refresh(config OAuthConfig, refreshToken string) (OAuthTokens, error) {
|
||||
var zero OAuthTokens
|
||||
if refreshToken == "" {
|
||||
return zero, fmt.Errorf("oauth2_refresh: refresh_token vacio")
|
||||
}
|
||||
if config.TokenURL == "" {
|
||||
return zero, fmt.Errorf("oauth2_refresh: token_url vacio")
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "refresh_token")
|
||||
form.Set("refresh_token", refreshToken)
|
||||
form.Set("client_id", config.ClientID)
|
||||
form.Set("client_secret", config.ClientSecret)
|
||||
|
||||
tokens, err := oauth2DoTokenRequest(config.TokenURL, form)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("oauth2_refresh: %w", err)
|
||||
}
|
||||
// Algunos proveedores no devuelven refresh_token al renovar — conservar el original
|
||||
if tokens.RefreshToken == "" {
|
||||
tokens.RefreshToken = refreshToken
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: oauth2_refresh
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func Oauth2Refresh(config OAuthConfig, refreshToken string) (OAuthTokens, error)"
|
||||
description: "Renueva un access token OAuth2 usando el refresh token. POST al TokenURL con grant_type=refresh_token. Conserva el refresh token original si el proveedor no devuelve uno nuevo."
|
||||
tags: [oauth, oauth2, auth, token, refresh, http, infra]
|
||||
uses_functions: []
|
||||
uses_types: [OAuthConfig_go_infra, OAuthTokens_go_infra]
|
||||
returns: [OAuthTokens_go_infra]
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [fmt, net/url]
|
||||
params:
|
||||
- name: config
|
||||
desc: "OAuthConfig del proveedor con ClientID, ClientSecret y TokenURL"
|
||||
- name: refreshToken
|
||||
desc: "refresh token obtenido previamente de Oauth2Exchange"
|
||||
output: "OAuthTokens con nuevo AccessToken. Si el proveedor no devuelve RefreshToken se conserva el original"
|
||||
tested: true
|
||||
tests: ["renueva tokens contra mock server", "conserva refresh token si el proveedor no devuelve uno nuevo", "rechaza refresh vacio"]
|
||||
test_file_path: "functions/infra/oauth2_refresh_test.go"
|
||||
file_path: "functions/infra/oauth2_refresh.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
tokens, err := Oauth2Refresh(googleConfig, storedRefreshToken)
|
||||
if err != nil {
|
||||
// El refresh token tambien puede haber caducado — forzar relogin
|
||||
HTTPErrorResponse(w, HTTPError{Status: 401, Code: "refresh_failed", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
saveTokens(tokens) // actualizar tokens en BD/cookie
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — reutiliza oauth2DoTokenRequest para el POST. Algunos proveedores (Google) no devuelven un nuevo RefreshToken al renovar — en ese caso se conserva el original. Otros (Microsoft) pueden rotar el refresh token en cada renovacion: el campo tokens.RefreshToken siempre trae el que hay que guardar para la proxima renovacion. Si el refresh token expiro (el usuario revoco acceso o paso demasiado tiempo) el proveedor retorna 400 con `error: invalid_grant` y se propaga como error.
|
||||
@@ -0,0 +1,69 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOauth2Refresh_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
|
||||
t.Errorf("grant_type = %q", got)
|
||||
}
|
||||
if got := r.PostForm.Get("refresh_token"); got != "rt-old" {
|
||||
t.Errorf("refresh_token = %q", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "at-new",
|
||||
"refresh_token": "rt-new",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := OAuthConfig{TokenURL: srv.URL, ClientID: "c", ClientSecret: "s"}
|
||||
tokens, err := Oauth2Refresh(cfg, "rt-old")
|
||||
if err != nil {
|
||||
t.Fatalf("Oauth2Refresh: %v", err)
|
||||
}
|
||||
if tokens.AccessToken != "at-new" {
|
||||
t.Errorf("AccessToken = %q", tokens.AccessToken)
|
||||
}
|
||||
if tokens.RefreshToken != "rt-new" {
|
||||
t.Errorf("RefreshToken = %q", tokens.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2Refresh_PreservesRefreshTokenIfProviderOmits(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "at-new",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
// no refresh_token
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := OAuthConfig{TokenURL: srv.URL}
|
||||
tokens, err := Oauth2Refresh(cfg, "rt-keep")
|
||||
if err != nil {
|
||||
t.Fatalf("Oauth2Refresh: %v", err)
|
||||
}
|
||||
if tokens.RefreshToken != "rt-keep" {
|
||||
t.Errorf("esperaba conservar rt-keep, got %q", tokens.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2Refresh_EmptyToken(t *testing.T) {
|
||||
cfg := OAuthConfig{TokenURL: "http://x"}
|
||||
if _, err := Oauth2Refresh(cfg, ""); err == nil {
|
||||
t.Fatal("esperaba error con refresh vacio")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user