Skip to content

Commit 50f6e35

Browse files
fbm3307cursoragent
andauthored
feat: add Twilio Lookup v2 API tests (#1285)
* feat: add Twilio Lookup v2 API Signed-off-by: Feny Mehta <fbm3307@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a85e1f4 commit 50f6e35

5 files changed

Lines changed: 210 additions & 6 deletions

File tree

deploy/host-operator/e2e-tests/toolchainconfig.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ spec:
2020
workatoWebHookURL: https://webhooks.testwebhook
2121
verification:
2222
enabled: true
23+
phoneLookupMode: 'disabled'
2324
excludedEmailDomains: 'redhat.com,acme.com'
2425
secret:
2526
ref: 'host-operator-secret'

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module github.com/codeready-toolchain/toolchain-e2e
22

33
require (
4-
github.com/codeready-toolchain/api v0.0.0-20260603082246-cfa3dd9db9cc
5-
github.com/codeready-toolchain/toolchain-common v0.0.0-20260603091009-6db0c02f4506
4+
github.com/codeready-toolchain/api v0.0.0-20260609071155-c8f486b1a581
5+
github.com/codeready-toolchain/toolchain-common v0.0.0-20260609073430-82d1748db579
66
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
77
github.com/fatih/color v1.18.0
88
github.com/ghodss/yaml v1.0.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
2626
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
2727
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
2828
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
29-
github.com/codeready-toolchain/api v0.0.0-20260603082246-cfa3dd9db9cc h1:Xwt43SDrCQ3cxwetRijdFrDwRp1yoam6j+36HazNivg=
30-
github.com/codeready-toolchain/api v0.0.0-20260603082246-cfa3dd9db9cc/go.mod h1:PMg6kNHuCGNlu3MOdrCisqGkBpvzB0qS1+E6nrXxPAc=
31-
github.com/codeready-toolchain/toolchain-common v0.0.0-20260603091009-6db0c02f4506 h1:FYOvK015wTBSh9J+T/yr4zjcVMpT1eKL1D/oS8yF1Lk=
32-
github.com/codeready-toolchain/toolchain-common v0.0.0-20260603091009-6db0c02f4506/go.mod h1:V3Ah7YRoTIHkU0G4Ynvj/6AVgOH3Ss0fw/B/Gurr3AA=
29+
github.com/codeready-toolchain/api v0.0.0-20260609071155-c8f486b1a581 h1:KE14RYWzMatSrwGa2wOB4SoVkbvpTm8hfnUH/nrpnfw=
30+
github.com/codeready-toolchain/api v0.0.0-20260609071155-c8f486b1a581/go.mod h1:PMg6kNHuCGNlu3MOdrCisqGkBpvzB0qS1+E6nrXxPAc=
31+
github.com/codeready-toolchain/toolchain-common v0.0.0-20260609073430-82d1748db579 h1:1qfOdNV6gRQSE0xOmJggqQxAiEjOpV7nZ6Xph75Mb1I=
32+
github.com/codeready-toolchain/toolchain-common v0.0.0-20260609073430-82d1748db579/go.mod h1:aYvTzEtTuw3O+kjWMMkH/1YgV4pgUPC3v3X2Li3ixlM=
3333
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3434
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3535
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package parallel
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"testing"
7+
8+
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
9+
"github.com/codeready-toolchain/toolchain-common/pkg/states"
10+
testconfig "github.com/codeready-toolchain/toolchain-common/pkg/test/config"
11+
. "github.com/codeready-toolchain/toolchain-e2e/testsupport"
12+
"github.com/codeready-toolchain/toolchain-e2e/testsupport/wait"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
// Twilio test credentials with magic lookup numbers return deterministic SMS Pumping Risk
18+
// responses at no cost. The API takes country_code and phone_number separately.
19+
// See: https://www.twilio.com/docs/lookup/magic-numbers-for-lookup/testing-sms-pumping-risk-with-magic-numbers
20+
//
21+
// Magic numbers used:
22+
//
23+
// +441234567890 → high risk, not blocked, score 2
24+
// +441234567891 → high risk, blocked, score 34
25+
// +911234567890 → low risk, not blocked, score 2
26+
const (
27+
twilioMagicPhoneHighRisk = "1234567890" // +44 prefix → high risk, not blocked
28+
twilioMagicPhoneHighRiskBlocked = "1234567891" // +44 prefix → high risk, blocked
29+
usTestPhone = "2025550123" // +1 prefix → fictional NANPA 555-01XX range, safe with test credentials
30+
)
31+
32+
func TestPhoneLookupMode(t *testing.T) {
33+
t.Parallel()
34+
awaitilities := WaitForDeployments(t)
35+
hostAwait := awaitilities.Host()
36+
route := hostAwait.RegistrationServiceURL
37+
38+
t.Run("log mode stores annotations and user is provisioned", func(t *testing.T) {
39+
// given
40+
hostAwait.UpdateToolchainConfig(t, testconfig.RegistrationService().Verification().PhoneLookupMode(toolchainv1alpha1.PhoneLookupModeLog))
41+
userSignup, token := signup(t, hostAwait)
42+
43+
// when
44+
NewHTTPRequest(t).
45+
InvokeEndpoint("PUT", route+"/api/v1/signup/verification", token,
46+
fmt.Sprintf(`{ "country_code":"+44", "phone_number":"%s" }`, twilioMagicPhoneHighRiskBlocked), http.StatusNoContent)
47+
48+
// then — verification code is set and lookup details are recorded
49+
userSignup, err := hostAwait.WaitForUserSignup(t, userSignup.Name,
50+
wait.UntilUserSignupHasAnnotationNotEmpty(toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey))
51+
require.NoError(t, err)
52+
53+
assert.NotEmpty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupPhoneLookupDetailsAnnotationKey])
54+
assert.NotEmpty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey])
55+
assert.False(t, states.Rejected(userSignup), "UserSignup should NOT be rejected in log mode")
56+
57+
// complete verification and confirm user is provisioned
58+
NewHTTPRequest(t).InvokeEndpoint("GET", route+fmt.Sprintf("/api/v1/signup/verification/%s",
59+
userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey]), token, "", http.StatusOK)
60+
61+
userSignup, err = wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.UserSignup{}).
62+
Update(userSignup.Name, hostAwait.Namespace,
63+
func(instance *toolchainv1alpha1.UserSignup) {
64+
states.SetApprovedManually(instance, true)
65+
})
66+
require.NoError(t, err)
67+
68+
VerifyResourcesProvisionedForSignup(t, awaitilities, userSignup)
69+
})
70+
71+
t.Run("enabled mode rejects high-risk phone number", func(t *testing.T) {
72+
// given
73+
hostAwait.UpdateToolchainConfig(t, testconfig.RegistrationService().
74+
Verification().PhoneLookupMode(toolchainv1alpha1.PhoneLookupModeEnabled).
75+
Verification().PhoneLookupExcludedCountries([]string{"US", "CA"}))
76+
userSignup, token := signup(t, hostAwait)
77+
78+
// when
79+
responseMap := NewHTTPRequest(t).
80+
InvokeEndpoint("PUT", route+"/api/v1/signup/verification", token,
81+
fmt.Sprintf(`{ "country_code":"+44", "phone_number":"%s" }`, twilioMagicPhoneHighRiskBlocked), http.StatusForbidden).
82+
UnmarshalMap()
83+
84+
// then
85+
require.NotEmpty(t, responseMap)
86+
assert.Equal(t, "Forbidden", responseMap["status"])
87+
88+
userSignup, err := hostAwait.WaitForUserSignup(t, userSignup.Name)
89+
require.NoError(t, err)
90+
assert.True(t, states.Rejected(userSignup), "UserSignup should be rejected in enabled mode with high-risk phone")
91+
assert.Empty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey])
92+
})
93+
94+
t.Run("enabled mode blocks verification for previously rejected signup", func(t *testing.T) {
95+
// given
96+
hostAwait.UpdateToolchainConfig(t, testconfig.RegistrationService().Verification().PhoneLookupMode(toolchainv1alpha1.PhoneLookupModeEnabled))
97+
userSignup, token := signup(t, hostAwait)
98+
99+
_, err := wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.UserSignup{}).
100+
Update(userSignup.Name, hostAwait.Namespace,
101+
func(us *toolchainv1alpha1.UserSignup) {
102+
states.SetRejected(us, true)
103+
})
104+
require.NoError(t, err)
105+
106+
// when
107+
responseMap := NewHTTPRequest(t).
108+
InvokeEndpoint("PUT", route+"/api/v1/signup/verification", token,
109+
fmt.Sprintf(`{ "country_code":"+91", "phone_number":"%s" }`, twilioMagicPhoneHighRisk), http.StatusForbidden).
110+
UnmarshalMap()
111+
112+
// then
113+
require.NotEmpty(t, responseMap)
114+
assert.Equal(t, "Forbidden", responseMap["status"])
115+
116+
userSignup, err = hostAwait.WaitForUserSignup(t, userSignup.Name)
117+
require.NoError(t, err)
118+
assert.True(t, states.Rejected(userSignup), "UserSignup should remain rejected")
119+
assert.Empty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey])
120+
})
121+
122+
t.Run("enabled mode skips lookup for US numbers and user is provisioned", func(t *testing.T) {
123+
// given
124+
hostAwait.UpdateToolchainConfig(t, testconfig.RegistrationService().
125+
Verification().PhoneLookupMode(toolchainv1alpha1.PhoneLookupModeEnabled).
126+
Verification().PhoneLookupExcludedCountries([]string{"US", "CA"}))
127+
userSignup, token := signup(t, hostAwait)
128+
129+
// when — US is excluded so lookup is never called
130+
NewHTTPRequest(t).
131+
InvokeEndpoint("PUT", route+"/api/v1/signup/verification", token,
132+
fmt.Sprintf(`{ "country_code":"+1", "phone_number":"%s" }`, usTestPhone), http.StatusNoContent)
133+
134+
// then
135+
userSignup, err := hostAwait.WaitForUserSignup(t, userSignup.Name,
136+
wait.UntilUserSignupHasAnnotationNotEmpty(toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey))
137+
require.NoError(t, err)
138+
139+
assert.Empty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupPhoneLookupDetailsAnnotationKey])
140+
assert.NotEmpty(t, userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey])
141+
assert.False(t, states.Rejected(userSignup), "UserSignup should NOT be rejected for excluded country")
142+
143+
// complete verification and confirm user is provisioned
144+
NewHTTPRequest(t).InvokeEndpoint("GET", route+fmt.Sprintf("/api/v1/signup/verification/%s",
145+
userSignup.Annotations[toolchainv1alpha1.UserSignupVerificationCodeAnnotationKey]), token, "", http.StatusOK)
146+
147+
userSignup, err = wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.UserSignup{}).
148+
Update(userSignup.Name, hostAwait.Namespace,
149+
func(instance *toolchainv1alpha1.UserSignup) {
150+
states.SetApprovedManually(instance, true)
151+
})
152+
require.NoError(t, err)
153+
154+
VerifyResourcesProvisionedForSignup(t, awaitilities, userSignup)
155+
})
156+
}

testsupport/wait/host.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,34 @@ func ContainsCondition(expected toolchainv1alpha1.Condition) UserSignupWaitCrite
484484
}
485485
}
486486

487+
// UntilUserSignupHasAnnotation returns a `UserSignupWaitCriterion` which checks that the given
488+
// UserSignup has an annotation with the expected key and value
489+
func UntilUserSignupHasAnnotation(key, value string) UserSignupWaitCriterion {
490+
return UserSignupWaitCriterion{
491+
Match: func(actual *toolchainv1alpha1.UserSignup) bool {
492+
actualValue, exist := actual.Annotations[key]
493+
return exist && actualValue == value
494+
},
495+
Diff: func(actual *toolchainv1alpha1.UserSignup) string {
496+
return fmt.Sprintf("expected UserSignup annotation '%s' to be '%s'\nbut it was '%s'", key, value, actual.Annotations[key])
497+
},
498+
}
499+
}
500+
501+
// UntilUserSignupHasAnnotationNotEmpty returns a `UserSignupWaitCriterion` which checks that the given
502+
// UserSignup has a non-empty annotation for the expected key
503+
func UntilUserSignupHasAnnotationNotEmpty(key string) UserSignupWaitCriterion {
504+
return UserSignupWaitCriterion{
505+
Match: func(actual *toolchainv1alpha1.UserSignup) bool {
506+
actualValue, exist := actual.Annotations[key]
507+
return exist && actualValue != ""
508+
},
509+
Diff: func(actual *toolchainv1alpha1.UserSignup) string {
510+
return fmt.Sprintf("expected UserSignup annotation '%s' to be non-empty\nbut it was '%s'", key, actual.Annotations[key])
511+
},
512+
}
513+
}
514+
487515
// UntilUserSignupHasLabel returns a `UserSignupWaitCriterion` which checks that the given
488516
// UserSignup has a `key` equal to the given `value`
489517
func UntilUserSignupHasLabel(key, value string) UserSignupWaitCriterion {
@@ -1697,6 +1725,25 @@ func UntilToolchainConfigHasVerificationEnabled(expected bool) ToolchainConfigWa
16971725
}
16981726
}
16991727

1728+
func UntilToolchainConfigHasPhoneLookupMode(expected toolchainv1alpha1.PhoneLookupMode) ToolchainConfigWaitCriterion {
1729+
return ToolchainConfigWaitCriterion{
1730+
Match: func(actual *toolchainv1alpha1.ToolchainConfig) bool {
1731+
mode := toolchainv1alpha1.PhoneLookupModeLog
1732+
if actual.Spec.Host.RegistrationService.Verification.PhoneLookupMode != nil {
1733+
mode = *actual.Spec.Host.RegistrationService.Verification.PhoneLookupMode
1734+
}
1735+
return mode == expected
1736+
},
1737+
Diff: func(actual *toolchainv1alpha1.ToolchainConfig) string {
1738+
mode := toolchainv1alpha1.PhoneLookupModeLog
1739+
if actual.Spec.Host.RegistrationService.Verification.PhoneLookupMode != nil {
1740+
mode = *actual.Spec.Host.RegistrationService.Verification.PhoneLookupMode
1741+
}
1742+
return fmt.Sprintf("expected phoneLookupMode to be '%s'.\n\tactual: '%s'", expected, mode)
1743+
},
1744+
}
1745+
}
1746+
17001747
// WaitForToolchainConfig waits until the ToolchainConfig is available with the provided criteria, if any
17011748
func (a *HostAwaitility) WaitForToolchainConfig(t *testing.T, criteria ...ToolchainConfigWaitCriterion) (*toolchainv1alpha1.ToolchainConfig, error) {
17021749
// there should only be one ToolchainConfig with the name "config"

0 commit comments

Comments
 (0)