Skip to content

Commit f4c60f2

Browse files
CyberArk(identity): add tests for LoginUsernamePassword with mock and real APIs
- Introduced `identity_test.go` to validate `LoginUsernamePassword` behavior. - Added mock server support for `failureUser` to simulate bad user scenarios. - Included new test data for `failureUser` in `start_authentication_bad_user_session_id.json`. - Implemented `SkipIfNoEnv` utility to conditionally skip tests based on environment variables. Signed-off-by: Richard Wall <richard.wall@cyberark.com>
1 parent 26d6e59 commit f4c60f2

File tree

4 files changed

+201
-7
lines changed

4 files changed

+201
-7
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package identity
2+
3+
// This file contains tests for the LoginUsernamePassword function in the
4+
// identity package. The tests cover both a mock API server and the real API,
5+
// depending on the environment variables set. The tests are intended to
6+
// demonstrate that the mock API behaves the same as the real API
7+
8+
import (
9+
"net/http"
10+
"os"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"k8s.io/klog/v2"
16+
"k8s.io/klog/v2/ktesting"
17+
18+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
19+
arktesting "github.com/jetstack/preflight/pkg/internal/cyberark/testing"
20+
21+
_ "k8s.io/klog/v2/ktesting/init"
22+
)
23+
24+
// inputs holds the various input values for the tests.
25+
type inputs struct {
26+
httpClient *http.Client
27+
baseURL string
28+
subdomain string
29+
username string
30+
password string
31+
}
32+
33+
// TestLoginUsernamePassword_MockAPI tests the LoginUsernamePassword function
34+
// against a mock API server. The mock server is configured to return different
35+
// responses based on the username and password used in the request.
36+
func TestLoginUsernamePassword_MockAPI(t *testing.T) {
37+
loginUsernamePasswordTests(t, func(t testing.TB) inputs {
38+
baseURL, httpClient := MockIdentityServer(t)
39+
return inputs{
40+
httpClient: httpClient,
41+
baseURL: baseURL,
42+
subdomain: "subdomain-ignored-by-mock",
43+
username: successUser,
44+
password: successPassword,
45+
}
46+
})
47+
}
48+
49+
// TestLoginUsernamePassword_RealAPI tests the LoginUsernamePassword function
50+
// against the real API. The environment variables are used to configure the
51+
// client.
52+
func TestLoginUsernamePassword_RealAPI(t *testing.T) {
53+
arktesting.SkipIfNoEnv(t)
54+
subdomain := os.Getenv("ARK_SUBDOMAIN")
55+
httpClient := http.DefaultClient
56+
services, err := servicediscovery.New(httpClient).DiscoverServices(t.Context(), subdomain)
57+
require.NoError(t, err)
58+
59+
loginUsernamePasswordTests(t, func(t testing.TB) inputs {
60+
return inputs{
61+
httpClient: httpClient,
62+
baseURL: services.Identity.API,
63+
subdomain: subdomain,
64+
username: os.Getenv("ARK_USERNAME"),
65+
password: os.Getenv("ARK_SECRET"),
66+
}
67+
})
68+
}
69+
70+
// loginUsernamePasswordTests runs tests which are expected to pass regardless of
71+
// whether the mock or real API is used.
72+
func loginUsernamePasswordTests(t *testing.T, inputsGenerator func(t testing.TB) inputs) {
73+
type testCase struct {
74+
name string
75+
modifier func(in *inputs)
76+
expectedError string
77+
}
78+
tests := []testCase{
79+
{
80+
name: "success",
81+
},
82+
{
83+
name: "bad-username",
84+
modifier: func(in *inputs) {
85+
in.username = failureUser
86+
},
87+
expectedError: `^got a failure response from request to advance authentication: ` +
88+
`message="Authentication \(login or challenge\) has failed\. ` +
89+
`Please try again or contact your system administrator\."`,
90+
},
91+
{
92+
name: "empty-username",
93+
modifier: func(in *inputs) {
94+
in.username = ""
95+
},
96+
expectedError: `^got a failure response from request to start authentication: ` +
97+
`message="Authentication \(login or challenge\) has failed\. ` +
98+
`Please try again or contact your system administrator\."`,
99+
},
100+
{
101+
name: "bad-password",
102+
modifier: func(in *inputs) {
103+
in.password = "bad-password"
104+
},
105+
expectedError: `^got a failure response from request to advance authentication: ` +
106+
`message="Authentication \(login or challenge\) has failed\. ` +
107+
`Please try again or contact your system administrator\."`,
108+
},
109+
}
110+
for _, test := range tests {
111+
t.Run(test.name, func(t *testing.T) {
112+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
113+
ctx := klog.NewContext(t.Context(), logger)
114+
115+
in := inputsGenerator(t)
116+
if test.modifier != nil {
117+
test.modifier(&in)
118+
}
119+
cl := New(in.httpClient, in.baseURL, in.subdomain)
120+
err := cl.LoginUsernamePassword(ctx, in.username, []byte(in.password))
121+
if test.expectedError != "" {
122+
if assert.Error(t, err) {
123+
assert.Regexp(t, test.expectedError, err.Error())
124+
}
125+
return
126+
}
127+
require.NoError(t, err)
128+
})
129+
}
130+
}

pkg/internal/cyberark/identity/mock.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
const (
1919
successUser = "test@example.com"
20+
failureUser = "test-fail@example.com"
2021
successUserMultipleChallenges = "test-multiple-challenges@example.com"
2122
successUserMultipleMechanisms = "test-multiple-mechanisms@example.com"
2223
noUPMechanism = "noup@example.com"
@@ -35,6 +36,9 @@ var (
3536
//go:embed testdata/start_authentication_success.json
3637
startAuthenticationSuccessResponse string
3738

39+
//go:embed testdata/start_authentication_bad_user_session_id.json
40+
startAuthenticationBadUserResponse string
41+
3842
//go:embed testdata/start_authentication_success_multiple_challenges.json
3943
startAuthenticationSuccessMultipleChallengesResponse string
4044

@@ -133,11 +137,6 @@ func (mis *mockIdentityServer) handleStartAuthentication(w http.ResponseWriter,
133137
return
134138
}
135139

136-
// Important: Experimentally, the Identity server we generated the testdata from seems to accept
137-
// any user and provide a success response for StartAuthentication, so filtering on the user here
138-
// doesn't match the server's behavior; however, it's helpful to do so since it lets us test different
139-
// error handling capabilities in the client.
140-
141140
switch reqBody.User {
142141
case successUser:
143142
w.WriteHeader(http.StatusOK)
@@ -160,9 +159,16 @@ func (mis *mockIdentityServer) handleStartAuthentication(w http.ResponseWriter,
160159
w.WriteHeader(http.StatusOK)
161160
_, _ = w.Write([]byte(startAuthenticationFailureResponse))
162161

162+
case failureUser:
163+
// Experimentally, the real API produces a 200 response and what looks
164+
// like a success response body. but the login is rejected later by the
165+
// AdvanceAuthentication stage, perhaps by virtue of the sessionID which
166+
// is returned here and supplied to AdvanceAuthentication.
167+
w.WriteHeader(http.StatusOK)
168+
_, _ = w.Write([]byte(startAuthenticationBadUserResponse))
169+
163170
default:
164-
w.WriteHeader(http.StatusForbidden)
165-
_, _ = w.Write([]byte(`{"message":"MOCK SERVER: invalid user"}`))
171+
panic("programmer error: should not be reached")
166172
}
167173
}
168174

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"success": true,
3+
"Result": {
4+
"ClientHints": {
5+
"PersistDefault": false,
6+
"AllowPersist": true,
7+
"AllowForgotPassword": true,
8+
"EndpointAuthenticationEnabled": false
9+
},
10+
"Version": "1.0",
11+
"SessionId": "bad-user-session-id",
12+
"EventDescription": null,
13+
"RetryWaitingTime": 0,
14+
"SecurityImageName": null,
15+
"AllowLoginMfaCache": false,
16+
"Challenges": [
17+
{
18+
"Mechanisms": [
19+
{
20+
"AnswerType": "Text",
21+
"Name": "UP",
22+
"PromptMechChosen": "Enter Password",
23+
"PromptSelectMech": "Password",
24+
"MechanismId": "aaaaaaa_AAAAAAAAAAAAAAAAAAAAAAAAAAAA-1111111",
25+
"Enrolled": true
26+
}
27+
]
28+
}
29+
],
30+
"Summary": "NewPackage",
31+
"TenantId": "TENANTID"
32+
},
33+
"Message": null,
34+
"MessageID": null,
35+
"Exception": null,
36+
"ErrorID": null,
37+
"ErrorCode": null,
38+
"IsSoftError": false,
39+
"InnerExceptions": null
40+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package testing
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
// SkipIfNoEnv skips the test if the required CyberArk environment variables are not set.
9+
func SkipIfNoEnv(t testing.TB) {
10+
t.Helper()
11+
12+
if os.Getenv("ARK_SUBDOMAIN") == "" ||
13+
os.Getenv("ARK_USERNAME") == "" ||
14+
os.Getenv("ARK_SECRET") == "" {
15+
t.Skip("Skipping test because one of ARK_SUBDOMAIN, ARK_USERNAME or ARK_SECRET isn't set")
16+
}
17+
18+
}

0 commit comments

Comments
 (0)