Skip to content

Commit 11435db

Browse files
Add tests
1 parent 98efb23 commit 11435db

File tree

3 files changed

+244
-15
lines changed

3 files changed

+244
-15
lines changed

internal/cmd/ske/kubeconfig/login/login_test.go

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ package login
22

33
import (
44
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"net/http/httputil"
10+
"net/url"
11+
"strings"
512
"testing"
613
"time"
714

15+
"github.com/golang-jwt/jwt/v5"
816
"github.com/google/go-cmp/cmp"
917
"github.com/google/go-cmp/cmp/cmpopts"
1018
"github.com/google/uuid"
19+
"github.com/stackitcloud/stackit-cli/internal/pkg/cache"
1120
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
1221
"github.com/stackitcloud/stackit-sdk-go/services/ske"
1322
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -21,6 +30,10 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
2130
var testClient = &ske.APIClient{}
2231
var testProjectId = uuid.NewString()
2332
var testClusterName = "cluster"
33+
var testOrganization = uuid.NewString()
34+
var testAccessToken = "access-token-test-" + uuid.NewString()
35+
var testExchangedToken = "access-token-exchanged-" + uuid.NewString()
36+
var testTokenEndpoint = "https://accounts.stackit.cloud/test/endpoint" //nolint:gosec // Actually just a URL
2437

2538
const testRegion = "eu01"
2639

@@ -30,14 +43,15 @@ func fixtureClusterConfig(mods ...func(clusterConfig *clusterConfig)) *clusterCo
3043
ClusterName: testClusterName,
3144
cacheKey: "",
3245
Region: testRegion,
46+
OrganizationID: testOrganization,
3347
}
3448
for _, mod := range mods {
3549
mod(clusterConfig)
3650
}
3751
return clusterConfig
3852
}
3953

40-
func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest {
54+
func fixtureLoginRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest {
4155
request := testClient.CreateKubeconfig(testCtx, testProjectId, testRegion, testClusterName)
4256
request = request.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{})
4357
for _, mod := range mods {
@@ -46,6 +60,40 @@ func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.A
4660
return request
4761
}
4862

63+
func fixtureTokenExchangeRequest(tokenEndpoint string) *http.Request {
64+
form := url.Values{}
65+
form.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
66+
form.Set("client_id", "stackit-cli-0000-0000-000000000001")
67+
form.Set("subject_token_type", "urn:ietf:params:oauth:token-type:access_token")
68+
form.Set("requested_token_type", "urn:ietf:params:oauth:token-type:id_token")
69+
form.Set("scope", "openid profile email groups")
70+
form.Set("subject_token", testAccessToken)
71+
form.Set("resource", "resource://organizations/"+testOrganization+"/projects/"+testProjectId+"/regions/"+testRegion+"/ske/"+testClusterName)
72+
73+
req, _ := http.NewRequestWithContext(
74+
testCtx,
75+
http.MethodPost,
76+
tokenEndpoint,
77+
strings.NewReader(form.Encode()),
78+
)
79+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
80+
return req
81+
}
82+
83+
func fixtureTokenExchangeResponse() string {
84+
type exchangeReponse struct {
85+
AccessToken string `json:"access_token"`
86+
IssuedTokeType string `json:"issued_token_type"`
87+
TokenType string `json:"token_type"`
88+
}
89+
response, _ := json.Marshal(exchangeReponse{
90+
AccessToken: testExchangedToken,
91+
IssuedTokeType: "urn:ietf:params:oauth:token-type:id_token",
92+
TokenType: "Bearer",
93+
})
94+
return string(response)
95+
}
96+
4997
func TestBuildRequest(t *testing.T) {
5098
tests := []struct {
5199
description string
@@ -55,7 +103,7 @@ func TestBuildRequest(t *testing.T) {
55103
{
56104
description: "expiration time",
57105
clusterConfig: fixtureClusterConfig(),
58-
expectedRequest: fixtureRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{
106+
expectedRequest: fixtureLoginRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{
59107
ExpirationSeconds: utils.Ptr("1800")}),
60108
},
61109
}
@@ -134,7 +182,187 @@ zbRjZmli7cnenEnfnNoFIGbgkbjGXRUCIC5zFtWXFK7kA+B2vDxD0DlLcQodNwi4
134182
if execCredential == nil {
135183
t.Fatal("execCredential is nil")
136184
}
137-
diff := cmp.Diff(execCredential, tt.expectedExecCredentialRequest)
185+
expected, _ := json.Marshal(tt.expectedExecCredentialRequest)
186+
diff := cmp.Diff(execCredential, expected)
187+
if diff != "" {
188+
t.Fatalf("Data does not match: %s", diff)
189+
}
190+
})
191+
}
192+
}
193+
194+
func TestBuildTokenExchangeRequest(t *testing.T) {
195+
cfg := fixtureClusterConfig()
196+
expectedRequest := fixtureTokenExchangeRequest(testTokenEndpoint)
197+
req, err := buildRequestToExchangeTokens(testCtx, testTokenEndpoint, testAccessToken, cfg)
198+
if err != nil {
199+
t.Fatalf("func returned error: %s", err)
200+
}
201+
// directly using cmp.Diff is not possible, so dump the requests first
202+
expected, err := httputil.DumpRequest(expectedRequest, true)
203+
if err != nil {
204+
t.Fatalf("fail to dump expected: %s", err)
205+
}
206+
actual, err := httputil.DumpRequest(req, true)
207+
if err != nil {
208+
t.Fatalf("fail to dump actual: %s", err)
209+
}
210+
diff := cmp.Diff(actual, expected)
211+
if diff != "" {
212+
t.Fatalf("Data does not match: %s", diff)
213+
}
214+
}
215+
216+
func TestParseTokenExchangeResponse(t *testing.T) {
217+
response := fixtureTokenExchangeResponse()
218+
219+
tests := []struct {
220+
description string
221+
response string
222+
status int
223+
expectError bool
224+
}{
225+
{
226+
description: "valid response",
227+
response: response,
228+
status: http.StatusOK,
229+
},
230+
{
231+
description: "error status",
232+
response: response, // valid response to make sure the status code is checked
233+
status: http.StatusForbidden,
234+
expectError: true,
235+
},
236+
{
237+
description: "error content",
238+
response: "{}",
239+
status: http.StatusOK,
240+
expectError: true,
241+
},
242+
}
243+
244+
for _, tt := range tests {
245+
t.Run(tt.description, func(t *testing.T) {
246+
w := httptest.NewRecorder()
247+
w.WriteHeader(tt.status)
248+
_, _ = w.WriteString(tt.response)
249+
resp := w.Result()
250+
251+
defer func() {
252+
tempErr := resp.Body.Close()
253+
if tempErr != nil {
254+
t.Fatalf("failed to close response body: %v", tempErr)
255+
}
256+
}()
257+
accessToken, err := parseTokenExchangeResponse(resp)
258+
if tt.expectError {
259+
if err == nil {
260+
t.Fatal("expected error got nil")
261+
}
262+
} else {
263+
if err != nil {
264+
t.Fatalf("func returned error: %s", err)
265+
}
266+
diff := cmp.Diff(accessToken, testExchangedToken)
267+
if diff != "" {
268+
t.Fatalf("Token does not match: %s", diff)
269+
}
270+
}
271+
})
272+
}
273+
}
274+
275+
func TestExchangeToken(t *testing.T) {
276+
config := fixtureClusterConfig(func(clusterConfig *clusterConfig) {
277+
clusterConfig.cacheKey = "test-exchange-token-" + uuid.NewString()
278+
})
279+
var request *http.Request
280+
response := fixtureTokenExchangeResponse()
281+
defer cache.OverwriteCacheDir(t)()
282+
if err := cache.Init(); err != nil {
283+
t.Fatalf("cache init failed: %s", err)
284+
}
285+
286+
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
287+
// only compare body as the headers will differ
288+
expected, err := io.ReadAll(request.Body)
289+
if err != nil {
290+
t.Errorf("fail to dump expected: %s", err)
291+
}
292+
actual, err := io.ReadAll(req.Body)
293+
if err != nil {
294+
t.Errorf("fail to dump actual: %s", err)
295+
}
296+
diff := cmp.Diff(actual, expected)
297+
if diff != "" {
298+
w.WriteHeader(http.StatusBadRequest)
299+
t.Errorf("request mismatch: %v", diff)
300+
return
301+
}
302+
303+
w.Header().Set("Content-Type", "application/json")
304+
_, err = w.Write([]byte(response))
305+
if err != nil {
306+
t.Errorf("Failed to write response: %v", err)
307+
}
308+
})
309+
server := httptest.NewServer(handler)
310+
defer server.Close()
311+
312+
request = fixtureTokenExchangeRequest(server.URL)
313+
idToken, err := exchangeToken(testCtx, server.Client(), server.URL, testAccessToken, config)
314+
if err != nil {
315+
t.Fatalf("func returned error: %s", err)
316+
}
317+
diff := cmp.Diff(idToken, testExchangedToken)
318+
if diff != "" {
319+
t.Fatalf("Exchanged token does not match: %s", diff)
320+
}
321+
}
322+
323+
func TestParseTokenToExecCredential(t *testing.T) {
324+
expirationTime := time.Now().Add(30 * time.Minute)
325+
expectedTime := expirationTime.Add(-5 * time.Minute)
326+
token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
327+
ExpiresAt: jwt.NewNumericDate(expirationTime),
328+
}).SigningString()
329+
if err != nil {
330+
t.Fatalf("token generation failed: %v", err)
331+
}
332+
token += ".signatureAAA"
333+
334+
tests := []struct {
335+
description string
336+
token string
337+
expectedExecCredentialRequest *clientauthenticationv1.ExecCredential
338+
}{
339+
{
340+
description: "expiration time",
341+
token: token,
342+
expectedExecCredentialRequest: &clientauthenticationv1.ExecCredential{
343+
TypeMeta: v1.TypeMeta{
344+
APIVersion: clientauthenticationv1.SchemeGroupVersion.String(),
345+
Kind: "ExecCredential",
346+
},
347+
Status: &clientauthenticationv1.ExecCredentialStatus{
348+
ExpirationTimestamp: &v1.Time{Time: expectedTime},
349+
Token: token,
350+
},
351+
},
352+
},
353+
}
354+
355+
for _, tt := range tests {
356+
t.Run(tt.description, func(t *testing.T) {
357+
execCredential, err := parseTokenToExecCredential(tt.token)
358+
if err != nil {
359+
t.Fatalf("func returned error: %s", err)
360+
}
361+
if execCredential == nil {
362+
t.Fatal("execCredential is nil")
363+
}
364+
expected, _ := json.Marshal(tt.expectedExecCredentialRequest)
365+
diff := cmp.Diff(execCredential, expected)
138366
if diff != "" {
139367
t.Fatalf("Data does not match: %s", diff)
140368
}

internal/pkg/cache/cache.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"path/filepath"
1212
"regexp"
1313
"strconv"
14+
"testing"
1415
"time"
1516

1617
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
@@ -29,6 +30,13 @@ const (
2930
cacheKeyMaxAge = 90 * 24 * time.Hour
3031
)
3132

33+
func OverwriteCacheDir(t *testing.T) func() {
34+
cacheDirOverwrite = t.TempDir()
35+
return func() {
36+
cacheDirOverwrite = ""
37+
}
38+
}
39+
3240
func Init() error {
3341
var cacheDir string
3442
if cacheDirOverwrite == "" {

internal/pkg/cache/cache_test.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,8 @@ import (
1111
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
1212
)
1313

14-
func overwriteCacheDir(t *testing.T) func() {
15-
cacheDirOverwrite = t.TempDir()
16-
return func() {
17-
cacheDirOverwrite = ""
18-
}
19-
}
20-
2114
func TestGetObjectErrors(t *testing.T) {
22-
defer overwriteCacheDir(t)()
15+
defer OverwriteCacheDir(t)()
2316
if err := Init(); err != nil {
2417
t.Fatalf("cache init failed: %s", err)
2518
}
@@ -59,7 +52,7 @@ func TestGetObjectErrors(t *testing.T) {
5952
}
6053
}
6154
func TestPutObject(t *testing.T) {
62-
defer overwriteCacheDir(t)()
55+
defer OverwriteCacheDir(t)()
6356
if err := Init(); err != nil {
6457
t.Fatalf("cache init failed: %s", err)
6558
}
@@ -138,7 +131,7 @@ func TestPutObject(t *testing.T) {
138131
}
139132

140133
func TestDeleteObject(t *testing.T) {
141-
defer overwriteCacheDir(t)()
134+
defer OverwriteCacheDir(t)()
142135
if err := Init(); err != nil {
143136
t.Fatalf("cache init failed: %s", err)
144137
}
@@ -225,7 +218,7 @@ func TestWriteAndRead(t *testing.T) {
225218
},
226219
} {
227220
t.Run(tt.name, func(t *testing.T) {
228-
defer overwriteCacheDir(t)()
221+
defer OverwriteCacheDir(t)()
229222
if tt.clearKeys {
230223
clearKeys(t)
231224
}
@@ -254,7 +247,7 @@ func TestWriteAndRead(t *testing.T) {
254247
}
255248

256249
func TestCacheCleanup(t *testing.T) {
257-
defer overwriteCacheDir(t)()
250+
defer OverwriteCacheDir(t)()
258251
if err := Init(); err != nil {
259252
t.Fatalf("cache init failed: %s", err)
260253
}

0 commit comments

Comments
 (0)