azuredevops: Add client method ListAuthorizedUserOrganizations (#48572)

This commit is contained in:
Indradhanush Gupta 2023-03-03 15:09:43 +05:30 committed by GitHub
parent 4549d045cb
commit 7d2d2a0094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 342 additions and 1 deletions

View File

@ -5728,6 +5728,10 @@ type MockAzureDevOpsClient struct {
// IsAzureDevOpsServicesFunc is an instance of a mock function object
// controlling the behavior of the method IsAzureDevOpsServices.
IsAzureDevOpsServicesFunc *AzureDevOpsClientIsAzureDevOpsServicesFunc
// ListAuthorizedUserOrganizationsFunc is an instance of a mock function
// object controlling the behavior of the method
// ListAuthorizedUserOrganizations.
ListAuthorizedUserOrganizationsFunc *AzureDevOpsClientListAuthorizedUserOrganizationsFunc
// ListRepositoriesByProjectOrOrgFunc is an instance of a mock function
// object controlling the behavior of the method
// ListRepositoriesByProjectOrOrg.
@ -5814,6 +5818,11 @@ func NewMockAzureDevOpsClient() *MockAzureDevOpsClient {
return
},
},
ListAuthorizedUserOrganizationsFunc: &AzureDevOpsClientListAuthorizedUserOrganizationsFunc{
defaultHook: func(context.Context, azuredevops.Profile) (r0 []azuredevops.Org, r1 error) {
return
},
},
ListRepositoriesByProjectOrOrgFunc: &AzureDevOpsClientListRepositoriesByProjectOrOrgFunc{
defaultHook: func(context.Context, azuredevops.ListRepositoriesByProjectOrOrgArgs) (r0 []azuredevops.Repository, r1 error) {
return
@ -5906,6 +5915,11 @@ func NewStrictMockAzureDevOpsClient() *MockAzureDevOpsClient {
panic("unexpected invocation of MockAzureDevOpsClient.IsAzureDevOpsServices")
},
},
ListAuthorizedUserOrganizationsFunc: &AzureDevOpsClientListAuthorizedUserOrganizationsFunc{
defaultHook: func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error) {
panic("unexpected invocation of MockAzureDevOpsClient.ListAuthorizedUserOrganizations")
},
},
ListRepositoriesByProjectOrOrgFunc: &AzureDevOpsClientListRepositoriesByProjectOrOrgFunc{
defaultHook: func(context.Context, azuredevops.ListRepositoriesByProjectOrOrgArgs) ([]azuredevops.Repository, error) {
panic("unexpected invocation of MockAzureDevOpsClient.ListRepositoriesByProjectOrOrg")
@ -5971,6 +5985,9 @@ func NewMockAzureDevOpsClientFrom(i azuredevops.Client) *MockAzureDevOpsClient {
IsAzureDevOpsServicesFunc: &AzureDevOpsClientIsAzureDevOpsServicesFunc{
defaultHook: i.IsAzureDevOpsServices,
},
ListAuthorizedUserOrganizationsFunc: &AzureDevOpsClientListAuthorizedUserOrganizationsFunc{
defaultHook: i.ListAuthorizedUserOrganizations,
},
ListRepositoriesByProjectOrOrgFunc: &AzureDevOpsClientListRepositoriesByProjectOrOrgFunc{
defaultHook: i.ListRepositoriesByProjectOrOrg,
},
@ -7522,6 +7539,118 @@ func (c AzureDevOpsClientIsAzureDevOpsServicesFuncCall) Results() []interface{}
return []interface{}{c.Result0}
}
// AzureDevOpsClientListAuthorizedUserOrganizationsFunc describes the
// behavior when the ListAuthorizedUserOrganizations method of the parent
// MockAzureDevOpsClient instance is invoked.
type AzureDevOpsClientListAuthorizedUserOrganizationsFunc struct {
defaultHook func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error)
hooks []func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error)
history []AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall
mutex sync.Mutex
}
// ListAuthorizedUserOrganizations delegates to the next hook function in
// the queue and stores the parameter and result values of this invocation.
func (m *MockAzureDevOpsClient) ListAuthorizedUserOrganizations(v0 context.Context, v1 azuredevops.Profile) ([]azuredevops.Org, error) {
r0, r1 := m.ListAuthorizedUserOrganizationsFunc.nextHook()(v0, v1)
m.ListAuthorizedUserOrganizationsFunc.appendCall(AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the
// ListAuthorizedUserOrganizations method of the parent
// MockAzureDevOpsClient instance is invoked and the hook queue is empty.
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) SetDefaultHook(hook func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// ListAuthorizedUserOrganizations method of the parent
// MockAzureDevOpsClient instance invokes the hook at the front of the queue
// and discards it. After the queue is empty, the default hook function is
// invoked for any future action.
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) PushHook(hook func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) SetDefaultReturn(r0 []azuredevops.Org, r1 error) {
f.SetDefaultHook(func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) PushReturn(r0 []azuredevops.Org, r1 error) {
f.PushHook(func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error) {
return r0, r1
})
}
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) nextHook() func(context.Context, azuredevops.Profile) ([]azuredevops.Org, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) appendCall(r0 AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall objects
// describing the invocations of this function.
func (f *AzureDevOpsClientListAuthorizedUserOrganizationsFunc) History() []AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall {
f.mutex.Lock()
history := make([]AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall is an object
// that describes an invocation of method ListAuthorizedUserOrganizations on
// an instance of MockAzureDevOpsClient.
type AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 azuredevops.Profile
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []azuredevops.Org
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AzureDevOpsClientListAuthorizedUserOrganizationsFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// AzureDevOpsClientListRepositoriesByProjectOrOrgFunc describes the
// behavior when the ListRepositoriesByProjectOrOrg method of the parent
// MockAzureDevOpsClient instance is invoked.

View File

@ -46,6 +46,7 @@ type Client interface {
GetRepositoryBranch(ctx context.Context, args OrgProjectRepoArgs, branchName string) (Ref, error)
GetProject(ctx context.Context, org, project string) (Project, error)
GetAuthorizedProfile(ctx context.Context) (Profile, error)
ListAuthorizedUserOrganizations(ctx context.Context, profile Profile) ([]Org, error)
}
type client struct {

View File

@ -4,6 +4,7 @@ import (
"flag"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
@ -33,7 +34,15 @@ func NewTestClient(t testing.TB, name string, update bool) (Client, func()) {
t.Fatal(err)
}
cli, err := NewClient("urn", "https://dev.azure.com", &auth.BasicAuth{Username: "testuser", Password: "testtoken"}, hc)
cli, err := NewClient(
"urn",
AzureDevOpsAPIURL,
&auth.BasicAuth{
Username: os.Getenv("AZURE_DEV_OPS_USERNAME"),
Password: os.Getenv("AZURE_DEV_OPS_TOKEN"),
},
hc,
)
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,22 @@
[
{
"accountId": "3b958f8e-017e-4f06-95ce-1b6cdd242b36",
"accountUri": "https://vssps.dev.azure.com:443/sgtestazure/",
"accountName": "sgtestazure"
},
{
"accountId": "65fd1acc-c89e-43e5-9069-395214eb0c25",
"accountUri": "https://vssps.dev.azure.com:443/repo-management/",
"accountName": "repo-management"
},
{
"accountId": "e8899f60-b66c-4106-8176-8c75b1710c97",
"accountUri": "https://vssps.dev.azure.com:443/indra-foobar/",
"accountName": "indra-foobar"
},
{
"accountId": "0b807719-5c78-49cd-a851-b24b694db41c",
"accountUri": "https://vssps.dev.azure.com:443/sgtestazure2/",
"accountName": "sgtestazure2"
}
]

View File

@ -0,0 +1,113 @@
---
version: 1
interactions:
- request:
body: ""
form: {}
headers: {}
url: https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.0
method: GET
response:
body: '{"displayName":"idan.varsano","publicAlias":"473dec3e-03d7-6147-b106-0b0a7f766a92","emailAddress":"idan.varsano@sourcegraph.com","coreRevision":411938451,"timeStamp":"2023-01-20T19:20:51.6433333+00:00","id":"473dec3e-03d7-6147-b106-0b0a7f766a92","revision":411938451}'
headers:
Access-Control-Expose-Headers:
- Request-Context
Activityid:
- d2cf0ab4-6956-4880-bffa-eebdf9f92550
Cache-Control:
- no-cache, no-store, must-revalidate
Content-Type:
- application/json; charset=utf-8; api-version=7.0
Date:
- Thu, 02 Mar 2023 18:36:45 GMT
Etag:
- '"0"'
Expires:
- "-1"
Last-Modified:
- Fri, 20 Jan 2023 19:20:51 GMT
P3p:
- CP="CAO DSP COR ADMa DEV CONo TELo CUR PSA PSD TAI IVDo OUR SAMi BUS DEM NAV
STA UNI COM INT PHY ONL FIN PUR LOC CNT"
Pragma:
- no-cache
Request-Context:
- appId=cid-v1:20b3930f-73dc-453a-b660-e3891d782eef
Strict-Transport-Security:
- max-age=31536000; includeSubDomains
Vary:
- Accept-Encoding
X-Cache:
- CONFIG_NOCACHE
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Msedge-Ref:
- 'Ref A: 5C3F7F96F5674231B1EC5F1F62DC3144 Ref B: MAA01EDGE1111 Ref C: 2023-03-02T18:36:45Z'
X-Tfs-Processid:
- 820ab1ae-fa5e-4b54-bcd3-c19228bd810b
X-Tfs-Session:
- d2cf0ab4-6956-4880-bffa-eebdf9f92550
X-Vss-E2eid:
- d2cf0ab4-6956-4880-bffa-eebdf9f92550
X-Vss-Senderdeploymentid:
- a5ca35eb-148e-4ccd-bbb3-d31576d75958
X-Vss-Userdata:
- 473dec3e-03d7-6147-b106-0b0a7f766a92:idan.varsano@sourcegraph.com
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers: {}
url: https://app.vssps.visualstudio.com/_apis/accounts?api-version=7.0&memberId=473dec3e-03d7-6147-b106-0b0a7f766a92
method: GET
response:
body: '{"count":4,"value":[{"accountId":"3b958f8e-017e-4f06-95ce-1b6cdd242b36","accountUri":"https://vssps.dev.azure.com:443/sgtestazure/","accountName":"sgtestazure","properties":{}},{"accountId":"65fd1acc-c89e-43e5-9069-395214eb0c25","accountUri":"https://vssps.dev.azure.com:443/repo-management/","accountName":"repo-management","properties":{}},{"accountId":"e8899f60-b66c-4106-8176-8c75b1710c97","accountUri":"https://vssps.dev.azure.com:443/indra-foobar/","accountName":"indra-foobar","properties":{}},{"accountId":"0b807719-5c78-49cd-a851-b24b694db41c","accountUri":"https://vssps.dev.azure.com:443/sgtestazure2/","accountName":"sgtestazure2","properties":{}}]}'
headers:
Access-Control-Expose-Headers:
- Request-Context
Activityid:
- e2fa6c27-6b16-4f2d-83e4-bfc6a8cbcc87
Cache-Control:
- no-cache, no-store, must-revalidate
Content-Type:
- application/json; charset=utf-8; api-version=7.0
Date:
- Thu, 02 Mar 2023 18:36:45 GMT
Expires:
- "-1"
P3p:
- CP="CAO DSP COR ADMa DEV CONo TELo CUR PSA PSD TAI IVDo OUR SAMi BUS DEM NAV
STA UNI COM INT PHY ONL FIN PUR LOC CNT"
Pragma:
- no-cache
Request-Context:
- appId=cid-v1:20b3930f-73dc-453a-b660-e3891d782eef
Strict-Transport-Security:
- max-age=31536000; includeSubDomains
Vary:
- Accept-Encoding
X-Cache:
- CONFIG_NOCACHE
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Msedge-Ref:
- 'Ref A: BB7D52F5D4D94054936DD2D1686AD864 Ref B: MAA01EDGE1111 Ref C: 2023-03-02T18:36:45Z'
X-Tfs-Processid:
- 7079e5b3-fcdc-4e60-bc32-c76d7829ecec
X-Tfs-Session:
- e2fa6c27-6b16-4f2d-83e4-bfc6a8cbcc87
X-Vss-E2eid:
- e2fa6c27-6b16-4f2d-83e4-bfc6a8cbcc87
X-Vss-Senderdeploymentid:
- a5ca35eb-148e-4ccd-bbb3-d31576d75958
X-Vss-Userdata:
- 473dec3e-03d7-6147-b106-0b0a7f766a92:idan.varsano@sourcegraph.com
status: 200 OK
code: 200
duration: ""

View File

@ -28,6 +28,17 @@ var (
PullRequestMergeStrategyNoFastForward PullRequestMergeStrategy = "notFastForward"
)
type Org struct {
ID string `json:"accountId"`
URI string `json:"accountUri"`
Name string `json:"accountName"`
}
type ListAuthorizedUserOrgsResponse struct {
Count int `json:"count"`
Value []Org `json:"value"`
}
type OrgProjectRepoArgs struct {
Org string
Project string

View File

@ -9,6 +9,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/encryption"
"github.com/sourcegraph/sourcegraph/internal/extsvc"
"github.com/sourcegraph/sourcegraph/lib/errors"
"golang.org/x/oauth2"
)
@ -41,6 +42,35 @@ func (c *client) GetAuthorizedProfile(ctx context.Context) (Profile, error) {
return p, nil
}
func (c *client) ListAuthorizedUserOrganizations(ctx context.Context, profile Profile) ([]Org, error) {
if MockVisualStudioAppURL == "" && !c.IsAzureDevOpsServices() {
return nil, errors.New("ListAuthorizedUserOrganizations can only be used with Azure DevOps Services")
}
reqURL := url.URL{Path: "_apis/accounts"}
req, err := http.NewRequest("GET", reqURL.String(), nil)
if err != nil {
return nil, err
}
queryParams := req.URL.Query()
queryParams.Set("memberId", profile.PublicAlias)
req.URL.RawQuery = queryParams.Encode()
apiURL := VisualStudioAppURL
if MockVisualStudioAppURL != "" {
apiURL = MockVisualStudioAppURL
}
response := ListAuthorizedUserOrgsResponse{}
if _, err := c.do(ctx, req, apiURL, &response); err != nil {
return nil, err
}
return response.Value, nil
}
// SetExternalAccountData sets the user and token into the external account data blob.
func SetExternalAccountData(data *extsvc.AccountData, user *Profile, token *oauth2.Token) error {
serializedUser, err := json.Marshal(user)

View File

@ -18,3 +18,29 @@ func TestClient_AzureServicesProfile(t *testing.T) {
testutil.AssertGolden(t, "testdata/golden/AzureServicesProfile.json", *update, resp)
}
// To update this test run:
// 1. Set the env AZURE_DEV_OPS_USERNAME and AZURE_DEV_OPS_TOKEN (the secrets can be found in 1Password if you search for Azure test credentials)
// 2. Run the test with the -update flag:
// `go test -run='TestClient_ListAuthorizedUserOrganizations' -update=true`
func TestClient_ListAuthorizedUserOrganizations(t *testing.T) {
cli, save := NewTestClient(
t,
"ListAuthorizedUserOrganizations",
*update,
)
t.Cleanup(save)
ctx := context.Background()
profile, err := cli.GetAuthorizedProfile(ctx)
if err != nil {
t.Fatalf("failed to get authorized profile: %v", err)
}
orgs, err := cli.ListAuthorizedUserOrganizations(ctx, profile)
if err != nil {
t.Fatalf("failed to list authorized user origanizations: %v", err)
}
testutil.AssertGolden(t, "testdata/golden/ListAuthorizedUserOrganizations.json", *update, orgs)
}