Skip to content

Commit f1545e0

Browse files
Bo-QiuSunrisea
authored andcommitted
fix: since the program will only log in once, there is no way to refresh the token.
1 parent 741b6ad commit f1545e0

File tree

2 files changed

+295
-2
lines changed

2 files changed

+295
-2
lines changed

common/security/nacos_auth_client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,12 @@ func (ac *NacosAuthClient) GetServerList() []constant.ServerConfig {
113113
}
114114

115115
func (ac *NacosAuthClient) login(server constant.ServerConfig) (bool, error) {
116-
if lastLoginSuccess := ac.lastRefreshTime > 0 && ac.tokenTtl > 0 && ac.tokenRefreshWindow > 0; lastLoginSuccess {
117-
return true, nil
116+
if ac.lastRefreshTime > 0 && ac.tokenTtl > 0 {
117+
// We refresh 2 windows before expiration to ensure continuous availability
118+
tokenRefreshTime := ac.lastRefreshTime + ac.tokenTtl - 2*ac.tokenRefreshWindow
119+
if time.Now().Unix() < tokenRefreshTime {
120+
return true, nil
121+
}
118122
}
119123
if ac.username == "" {
120124
ac.lastRefreshTime = time.Now().Unix()
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package security
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"testing"
11+
"time"
12+
13+
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
// MockResponseBody creates a mock response body for testing
18+
type MockResponseBody struct {
19+
*bytes.Buffer
20+
}
21+
22+
func (m *MockResponseBody) Close() error {
23+
return nil
24+
}
25+
26+
func NewMockResponseBody(data interface{}) io.ReadCloser {
27+
var buf bytes.Buffer
28+
if str, ok := data.(string); ok {
29+
buf.WriteString(str)
30+
} else {
31+
enc := json.NewEncoder(&buf)
32+
enc.SetEscapeHTML(false)
33+
enc.Encode(data)
34+
}
35+
return &MockResponseBody{&buf}
36+
}
37+
38+
// MockHttpAgent implements http_agent.IHttpAgent for testing
39+
type MockHttpAgent struct {
40+
PostFunc func(url string, header http.Header, timeoutMs uint64, params map[string]string) (response *http.Response, err error)
41+
}
42+
43+
func (m *MockHttpAgent) Request(method string, url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
44+
switch method {
45+
case http.MethodPost:
46+
return m.Post(url, header, timeoutMs, params)
47+
default:
48+
return &http.Response{
49+
StatusCode: http.StatusMethodNotAllowed,
50+
Body: NewMockResponseBody("method not allowed"),
51+
}, nil
52+
}
53+
}
54+
55+
func (m *MockHttpAgent) RequestOnlyResult(method string, url string, header http.Header, timeoutMs uint64, params map[string]string) string {
56+
resp, err := m.Request(method, url, header, timeoutMs, params)
57+
if err != nil {
58+
return ""
59+
}
60+
if resp.Body == nil {
61+
return ""
62+
}
63+
defer resp.Body.Close()
64+
data, err := io.ReadAll(resp.Body)
65+
if err != nil {
66+
return ""
67+
}
68+
return string(data)
69+
}
70+
71+
func (m *MockHttpAgent) Get(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
72+
return m.Request(http.MethodGet, url, header, timeoutMs, params)
73+
}
74+
75+
func (m *MockHttpAgent) Post(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
76+
if m.PostFunc != nil {
77+
return m.PostFunc(url, header, timeoutMs, params)
78+
}
79+
return &http.Response{
80+
StatusCode: http.StatusNotImplemented,
81+
Body: NewMockResponseBody("not implemented"),
82+
}, nil
83+
}
84+
85+
func (m *MockHttpAgent) Delete(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
86+
return m.Request(http.MethodDelete, url, header, timeoutMs, params)
87+
}
88+
89+
func (m *MockHttpAgent) Put(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
90+
return m.Request(http.MethodPut, url, header, timeoutMs, params)
91+
}
92+
93+
func TestNacosAuthClient_Login_Success(t *testing.T) {
94+
// Setup mock response
95+
mockResp := &http.Response{
96+
StatusCode: constant.RESPONSE_CODE_SUCCESS,
97+
Body: NewMockResponseBody(map[string]interface{}{
98+
constant.KEY_ACCESS_TOKEN: "test-token",
99+
constant.KEY_TOKEN_TTL: float64(10),
100+
}),
101+
}
102+
103+
mockAgent := &MockHttpAgent{
104+
PostFunc: func(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
105+
// Verify request parameters
106+
assert.Equal(t, "test-user", params["username"])
107+
assert.Equal(t, "test-pass", params["password"])
108+
contentType := header["content-type"]
109+
assert.Equal(t, []string{"application/x-www-form-urlencoded"}, contentType)
110+
return mockResp, nil
111+
},
112+
}
113+
114+
// Create client config
115+
clientConfig := constant.ClientConfig{
116+
Username: "test-user",
117+
Password: "test-pass",
118+
TimeoutMs: 10000,
119+
}
120+
121+
serverConfigs := []constant.ServerConfig{
122+
{
123+
IpAddr: "127.0.0.1",
124+
Port: 8848,
125+
ContextPath: "/nacos",
126+
},
127+
}
128+
129+
client := NewNacosAuthClient(clientConfig, serverConfigs, mockAgent)
130+
131+
// Test login
132+
success, err := client.Login()
133+
assert.NoError(t, err)
134+
assert.True(t, success)
135+
136+
// Verify token is stored
137+
assert.Equal(t, "test-token", client.GetAccessToken())
138+
}
139+
140+
func TestNacosAuthClient_Login_NoAuth(t *testing.T) {
141+
mockAgent := &MockHttpAgent{
142+
PostFunc: func(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
143+
t.Fatal("Should not make HTTP call when no username is set")
144+
return nil, nil
145+
},
146+
}
147+
148+
clientConfig := constant.ClientConfig{}
149+
serverConfigs := []constant.ServerConfig{{}}
150+
151+
client := NewNacosAuthClient(clientConfig, serverConfigs, mockAgent)
152+
153+
success, err := client.Login()
154+
assert.NoError(t, err)
155+
assert.True(t, success)
156+
assert.Empty(t, client.GetAccessToken())
157+
}
158+
159+
func TestNacosAuthClient_TokenRefresh(t *testing.T) {
160+
callCount := 0
161+
mockAgent := &MockHttpAgent{
162+
PostFunc: func(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
163+
callCount++
164+
return &http.Response{
165+
StatusCode: constant.RESPONSE_CODE_SUCCESS,
166+
Body: NewMockResponseBody(map[string]interface{}{
167+
constant.KEY_ACCESS_TOKEN: "token-" + fmt.Sprintf("%d", callCount),
168+
constant.KEY_TOKEN_TTL: float64(1), // 1 second TTL for quick testing
169+
}),
170+
}, nil
171+
},
172+
}
173+
174+
clientConfig := constant.ClientConfig{
175+
Username: "test-user",
176+
Password: "test-pass",
177+
}
178+
179+
client := NewNacosAuthClient(clientConfig, []constant.ServerConfig{{IpAddr: "localhost"}}, mockAgent)
180+
181+
// Initial login
182+
success, err := client.Login()
183+
assert.NoError(t, err)
184+
assert.True(t, success)
185+
assert.Equal(t, "token-1", client.GetAccessToken())
186+
187+
// Wait for token to require refresh (1 second TTL)
188+
time.Sleep(time.Second * 2)
189+
190+
// Second login should get new token
191+
success, err = client.Login()
192+
assert.NoError(t, err)
193+
assert.True(t, success)
194+
assert.Equal(t, "token-2", client.GetAccessToken())
195+
}
196+
197+
func TestNacosAuthClient_AutoRefresh(t *testing.T) {
198+
callCount := 0
199+
tokenChan := make(chan string, 2)
200+
mockAgent := &MockHttpAgent{
201+
PostFunc: func(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
202+
callCount++
203+
token := fmt.Sprintf("auto-token-%d", callCount)
204+
tokenChan <- token
205+
t.Logf("Mock server received request #%d, returning token: %s", callCount, token)
206+
return &http.Response{
207+
StatusCode: constant.RESPONSE_CODE_SUCCESS,
208+
Body: NewMockResponseBody(map[string]interface{}{
209+
constant.KEY_ACCESS_TOKEN: token,
210+
constant.KEY_TOKEN_TTL: float64(10), // 10 seconds TTL, resulting in 1s refresh window
211+
}),
212+
}, nil
213+
},
214+
}
215+
216+
clientConfig := constant.ClientConfig{
217+
Username: "test-user",
218+
Password: "test-pass",
219+
}
220+
221+
client := NewNacosAuthClient(clientConfig, []constant.ServerConfig{{IpAddr: "localhost"}}, mockAgent)
222+
223+
// First do a manual login
224+
t.Log("Performing initial manual login")
225+
success, err := client.Login()
226+
assert.NoError(t, err)
227+
assert.True(t, success)
228+
token1 := <-tokenChan // Get the token from the first login
229+
t.Logf("Initial login successful, token: %s", token1)
230+
231+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
232+
defer cancel()
233+
234+
// Start auto refresh
235+
t.Log("Starting auto refresh")
236+
client.AutoRefresh(ctx)
237+
238+
// Wait for token refresh (should happen after TTL-2*refreshWindow seconds = 8 seconds)
239+
// We'll wait a bit longer to account for any delays
240+
t.Log("Waiting for token refresh")
241+
var token2 string
242+
select {
243+
case token2 = <-tokenChan:
244+
t.Logf("Received refreshed token: %s", token2)
245+
case <-time.After(time.Second * 12):
246+
t.Fatal("Timeout waiting for token refresh")
247+
}
248+
249+
assert.NotEqual(t, token1, token2, "Token should have been refreshed")
250+
assert.Equal(t, "auto-token-1", token1, "First token should be auto-token-1")
251+
assert.Equal(t, "auto-token-2", token2, "Second token should be auto-token-2")
252+
}
253+
254+
func TestNacosAuthClient_GetSecurityInfo(t *testing.T) {
255+
client := NewNacosAuthClient(constant.ClientConfig{}, []constant.ServerConfig{}, nil)
256+
257+
// When no token
258+
info := client.GetSecurityInfo(RequestResource{})
259+
assert.Empty(t, info[constant.KEY_ACCESS_TOKEN])
260+
261+
// When token exists
262+
mockToken := "test-security-token"
263+
client.accessToken.Store(mockToken)
264+
265+
info = client.GetSecurityInfo(RequestResource{})
266+
assert.Equal(t, mockToken, info[constant.KEY_ACCESS_TOKEN])
267+
}
268+
269+
func TestNacosAuthClient_LoginFailure(t *testing.T) {
270+
mockAgent := &MockHttpAgent{
271+
PostFunc: func(url string, header http.Header, timeoutMs uint64, params map[string]string) (*http.Response, error) {
272+
return &http.Response{
273+
StatusCode: http.StatusUnauthorized,
274+
Body: NewMockResponseBody("Invalid credentials"),
275+
}, nil
276+
},
277+
}
278+
279+
client := NewNacosAuthClient(
280+
constant.ClientConfig{Username: "wrong-user", Password: "wrong-pass"},
281+
[]constant.ServerConfig{{IpAddr: "localhost"}},
282+
mockAgent,
283+
)
284+
285+
success, err := client.Login()
286+
assert.Error(t, err)
287+
assert.False(t, success)
288+
assert.Empty(t, client.GetAccessToken())
289+
}

0 commit comments

Comments
 (0)