github.com/abayer/test-infra@v0.0.5/prow/cmd/peribolos/main_test.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"reflect"
    24  	"sort"
    25  	"testing"
    26  
    27  	"k8s.io/test-infra/prow/config/org"
    28  	"k8s.io/test-infra/prow/flagutil"
    29  	"k8s.io/test-infra/prow/github"
    30  
    31  	"github.com/ghodss/yaml"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  )
    34  
    35  func TestOptions(t *testing.T) {
    36  	weirdFlags := flagutil.NewStrings(defaultEndpoint)
    37  	weirdFlags.Set("weird://url") // no error possible
    38  	cases := []struct {
    39  		name     string
    40  		args     []string
    41  		expected *options
    42  	}{
    43  		{
    44  			name: "missing --config",
    45  			args: []string{"--github-token-path=fake"},
    46  		},
    47  		{
    48  			name: "missing --github-token-path",
    49  			args: []string{"--config-path=fake"},
    50  		},
    51  		{
    52  			name: "bad --github-endpoint",
    53  			args: []string{"--config-path=foo", "--github-token-path=bar", "--github-endpoint=ht!tp://:dumb"},
    54  		},
    55  		{
    56  			name: "--minAdmins too low",
    57  			args: []string{"--config-path=foo", "--github-token-path=bar", "--min-admins=1"},
    58  		},
    59  		{
    60  			name: "--maximum-removal-delta too high",
    61  			args: []string{"--config-path=foo", "--github-token-path=bar", "--maximum-removal-delta=1.1"},
    62  		},
    63  		{
    64  			name: "--maximum-removal-delta too low",
    65  			args: []string{"--config-path=foo", "--github-token-path=bar", "--maximum-removal-delta=-0.1"},
    66  		},
    67  		{
    68  			name: "maximal delta",
    69  			args: []string{"--config-path=foo", "--github-token-path=bar", "--maximum-removal-delta=1"},
    70  			expected: &options{
    71  				config:        "foo",
    72  				token:         "bar",
    73  				endpoint:      flagutil.NewStrings(defaultEndpoint),
    74  				minAdmins:     defaultMinAdmins,
    75  				requireSelf:   true,
    76  				maximumDelta:  1,
    77  				tokensPerHour: defaultTokens,
    78  				tokenBurst:    defaultBurst,
    79  			},
    80  		},
    81  		{
    82  			name: "minimal delta",
    83  			args: []string{"--config-path=foo", "--github-token-path=bar", "--maximum-removal-delta=0"},
    84  			expected: &options{
    85  				config:        "foo",
    86  				token:         "bar",
    87  				endpoint:      flagutil.NewStrings(defaultEndpoint),
    88  				minAdmins:     defaultMinAdmins,
    89  				requireSelf:   true,
    90  				maximumDelta:  0,
    91  				tokensPerHour: defaultTokens,
    92  				tokenBurst:    defaultBurst,
    93  			},
    94  		},
    95  		{
    96  			name: "minimal admins",
    97  			args: []string{"--config-path=foo", "--github-token-path=bar", "--min-admins=2"},
    98  			expected: &options{
    99  				config:        "foo",
   100  				token:         "bar",
   101  				endpoint:      flagutil.NewStrings(defaultEndpoint),
   102  				minAdmins:     2,
   103  				requireSelf:   true,
   104  				maximumDelta:  defaultDelta,
   105  				tokensPerHour: defaultTokens,
   106  				tokenBurst:    defaultBurst,
   107  			},
   108  		},
   109  		{
   110  			name: "reject burst > tokens",
   111  			args: []string{"--config-path=foo", "--github-token-path=bar", "--tokens=10", "--token-burst=11"},
   112  		},
   113  		{
   114  			name: "reject dump and confirm",
   115  			args: []string{"--github-token-path=bar", "--confirm", "--dump=frogger"},
   116  		},
   117  		{
   118  			name: "reject dump and config-path",
   119  			args: []string{"--github-token-path=bar", "--config-path=foo", "--dump=frogger"},
   120  		},
   121  		{
   122  			name: "reject --fix-team-members without --fix-teams",
   123  			args: []string{"--github-token-path=bar", "--config-path=foo", "--fix-team-members"},
   124  		},
   125  		{
   126  			name: "allow disabled throttle",
   127  			args: []string{"--config-path=foo", "--github-token-path=bar", "--tokens=0"},
   128  			expected: &options{
   129  				config:        "foo",
   130  				token:         "bar",
   131  				endpoint:      flagutil.NewStrings(defaultEndpoint),
   132  				minAdmins:     defaultMinAdmins,
   133  				requireSelf:   true,
   134  				maximumDelta:  defaultDelta,
   135  				tokensPerHour: 0,
   136  				tokenBurst:    defaultBurst,
   137  			},
   138  		},
   139  		{
   140  			name: "allow dump without config",
   141  			args: []string{"--github-token-path=bar", "--dump=frogger"},
   142  			expected: &options{
   143  				token:         "bar",
   144  				endpoint:      flagutil.NewStrings(defaultEndpoint),
   145  				minAdmins:     defaultMinAdmins,
   146  				requireSelf:   true,
   147  				maximumDelta:  defaultDelta,
   148  				tokensPerHour: defaultTokens,
   149  				tokenBurst:    defaultBurst,
   150  				dump:          "frogger",
   151  			},
   152  		},
   153  		{
   154  			name: "minimal",
   155  			args: []string{"--config-path=foo", "--github-token-path=bar"},
   156  			expected: &options{
   157  				config:        "foo",
   158  				token:         "bar",
   159  				endpoint:      flagutil.NewStrings(defaultEndpoint),
   160  				minAdmins:     defaultMinAdmins,
   161  				requireSelf:   true,
   162  				maximumDelta:  defaultDelta,
   163  				tokensPerHour: defaultTokens,
   164  				tokenBurst:    defaultBurst,
   165  			},
   166  		},
   167  		{
   168  			name: "full",
   169  			args: []string{"--config-path=foo", "--github-token-path=bar", "--github-endpoint=weird://url", "--confirm=true", "--require-self=false", "--tokens=5", "--token-burst=2", "--dump=", "--fix-org", "--fix-org-members", "--fix-teams", "--fix-team-members"},
   170  			expected: &options{
   171  				config:         "foo",
   172  				token:          "bar",
   173  				endpoint:       weirdFlags,
   174  				confirm:        true,
   175  				requireSelf:    false,
   176  				minAdmins:      defaultMinAdmins,
   177  				maximumDelta:   defaultDelta,
   178  				tokensPerHour:  5,
   179  				tokenBurst:     2,
   180  				fixOrg:         true,
   181  				fixOrgMembers:  true,
   182  				fixTeams:       true,
   183  				fixTeamMembers: true,
   184  			},
   185  		},
   186  	}
   187  
   188  	for _, tc := range cases {
   189  		flags := flag.NewFlagSet(tc.name, flag.ContinueOnError)
   190  		var actual options
   191  		err := actual.parseArgs(flags, tc.args)
   192  		switch {
   193  		case err == nil && tc.expected == nil:
   194  			t.Errorf("%s: failed to return an error", tc.name)
   195  		case err != nil && tc.expected != nil:
   196  			t.Errorf("%s: unexpected error: %v", tc.name, err)
   197  		case tc.expected != nil && !reflect.DeepEqual(*tc.expected, actual):
   198  			t.Errorf("%s: actual %v != expected %v", tc.name, actual, *tc.expected)
   199  		}
   200  	}
   201  }
   202  
   203  type fakeClient struct {
   204  	orgMembers sets.String
   205  	admins     sets.String
   206  	invitees   sets.String
   207  	members    sets.String
   208  	removed    sets.String
   209  	newAdmins  sets.String
   210  	newMembers sets.String
   211  }
   212  
   213  func (c *fakeClient) BotName() (string, error) {
   214  	return "me", nil
   215  }
   216  
   217  func (c fakeClient) makeMembers(people sets.String) []github.TeamMember {
   218  	var ret []github.TeamMember
   219  	for p := range people {
   220  		ret = append(ret, github.TeamMember{Login: p})
   221  	}
   222  	return ret
   223  }
   224  
   225  func (c *fakeClient) ListOrgMembers(org, role string) ([]github.TeamMember, error) {
   226  	switch role {
   227  	case github.RoleMember:
   228  		return c.makeMembers(c.members), nil
   229  	case github.RoleAdmin:
   230  		return c.makeMembers(c.admins), nil
   231  	default:
   232  		// RoleAll: implmenent when/if necessary
   233  		return nil, fmt.Errorf("bad role: %s", role)
   234  	}
   235  }
   236  
   237  func (c *fakeClient) ListOrgInvitations(org string) ([]github.OrgInvitation, error) {
   238  	var ret []github.OrgInvitation
   239  	for p := range c.invitees {
   240  		if p == "fail" {
   241  			return nil, errors.New("injected list org invitations failure")
   242  		}
   243  		ret = append(ret, github.OrgInvitation{
   244  			TeamMember: github.TeamMember{
   245  				Login: p,
   246  			},
   247  		})
   248  	}
   249  	return ret, nil
   250  }
   251  
   252  func (c *fakeClient) RemoveOrgMembership(org, user string) error {
   253  	if user == "fail" {
   254  		return errors.New("injected remove org membership failure")
   255  	}
   256  	c.removed.Insert(user)
   257  	c.admins.Delete(user)
   258  	c.members.Delete(user)
   259  	return nil
   260  }
   261  
   262  func (c *fakeClient) UpdateOrgMembership(org, user string, admin bool) (*github.OrgMembership, error) {
   263  	if user == "fail" {
   264  		return nil, errors.New("injected update org failure")
   265  	}
   266  	var state string
   267  	if c.members.Has(user) || c.admins.Has(user) {
   268  		state = github.StateActive
   269  	} else {
   270  		state = github.StatePending
   271  	}
   272  	var role string
   273  	if admin {
   274  		c.newAdmins.Insert(user)
   275  		c.admins.Insert(user)
   276  		role = github.RoleAdmin
   277  	} else {
   278  		c.newMembers.Insert(user)
   279  		c.members.Insert(user)
   280  		role = github.RoleMember
   281  	}
   282  	return &github.OrgMembership{
   283  		Membership: github.Membership{
   284  			Role:  role,
   285  			State: state,
   286  		},
   287  	}, nil
   288  }
   289  
   290  func (c *fakeClient) ListTeamMembers(id int, role string) ([]github.TeamMember, error) {
   291  	if id != teamID {
   292  		return nil, fmt.Errorf("only team 66 supported, not %d", id)
   293  	}
   294  	switch role {
   295  	case github.RoleMember:
   296  		return c.makeMembers(c.members), nil
   297  	case github.RoleMaintainer:
   298  		return c.makeMembers(c.admins), nil
   299  	default:
   300  		return nil, fmt.Errorf("fake does not support: %v", role)
   301  	}
   302  }
   303  
   304  const teamID = 66
   305  
   306  func (c *fakeClient) UpdateTeamMembership(id int, user string, maintainer bool) (*github.TeamMembership, error) {
   307  	if id != teamID {
   308  		return nil, fmt.Errorf("only team %d supported, not %d", teamID, id)
   309  	}
   310  	if user == "fail" {
   311  		return nil, fmt.Errorf("injected failure for %s", user)
   312  	}
   313  	var state string
   314  	if c.orgMembers.Has(user) || len(c.orgMembers) == 0 {
   315  		state = github.StateActive
   316  	} else {
   317  		state = github.StatePending
   318  	}
   319  	var role string
   320  	if maintainer {
   321  		c.newAdmins.Insert(user)
   322  		c.admins.Insert(user)
   323  		role = github.RoleMaintainer
   324  	} else {
   325  		c.newMembers.Insert(user)
   326  		c.members.Insert(user)
   327  		role = github.RoleMember
   328  	}
   329  	return &github.TeamMembership{
   330  		Membership: github.Membership{
   331  			Role:  role,
   332  			State: state,
   333  		},
   334  	}, nil
   335  }
   336  
   337  func (c *fakeClient) RemoveTeamMembership(id int, user string) error {
   338  	if id != teamID {
   339  		return fmt.Errorf("only team %d supported, not %d", teamID, id)
   340  	}
   341  	if user == "fail" {
   342  		return fmt.Errorf("injected failure for %s", user)
   343  	}
   344  	c.removed.Insert(user)
   345  	c.admins.Delete(user)
   346  	c.members.Delete(user)
   347  	return nil
   348  }
   349  
   350  func TestConfigureMembers(t *testing.T) {
   351  	cases := []struct {
   352  		name    string
   353  		want    memberships
   354  		have    memberships
   355  		remove  sets.String
   356  		members sets.String
   357  		supers  sets.String
   358  		err     bool
   359  	}{
   360  		{
   361  			name: "forgot to remove duplicate entry",
   362  			want: memberships{
   363  				members: sets.NewString("me"),
   364  				super:   sets.NewString("me"),
   365  			},
   366  			err: true,
   367  		},
   368  		{
   369  			name: "removal fails",
   370  			have: memberships{
   371  				members: sets.NewString("fail"),
   372  			},
   373  			err: true,
   374  		},
   375  		{
   376  			name: "adding admin fails",
   377  			want: memberships{
   378  				super: sets.NewString("fail"),
   379  			},
   380  			err: true,
   381  		},
   382  		{
   383  			name: "adding member fails",
   384  			want: memberships{
   385  				members: sets.NewString("fail"),
   386  			},
   387  			err: true,
   388  		},
   389  		{
   390  			name: "promote to admin",
   391  			have: memberships{
   392  				members: sets.NewString("promote"),
   393  			},
   394  			want: memberships{
   395  				super: sets.NewString("promote"),
   396  			},
   397  			supers: sets.NewString("promote"),
   398  		},
   399  		{
   400  			name: "downgrade to member",
   401  			have: memberships{
   402  				super: sets.NewString("downgrade"),
   403  			},
   404  			want: memberships{
   405  				members: sets.NewString("downgrade"),
   406  			},
   407  			members: sets.NewString("downgrade"),
   408  		},
   409  		{
   410  			name: "some of everything",
   411  			have: memberships{
   412  				super:   sets.NewString("keep-admin", "drop-admin"),
   413  				members: sets.NewString("keep-member", "drop-member"),
   414  			},
   415  			want: memberships{
   416  				members: sets.NewString("keep-member", "new-member"),
   417  				super:   sets.NewString("keep-admin", "new-admin"),
   418  			},
   419  			remove:  sets.NewString("drop-admin", "drop-member"),
   420  			members: sets.NewString("new-member"),
   421  			supers:  sets.NewString("new-admin"),
   422  		},
   423  		{
   424  			name: "ensure case insensitivity",
   425  			have: memberships{
   426  				super:   sets.NewString("lower"),
   427  				members: sets.NewString("UPPER"),
   428  			},
   429  			want: memberships{
   430  				super:   sets.NewString("Lower"),
   431  				members: sets.NewString("UpPeR"),
   432  			},
   433  		},
   434  	}
   435  
   436  	for _, tc := range cases {
   437  		t.Run(tc.name, func(t *testing.T) {
   438  			removed := sets.String{}
   439  			members := sets.String{}
   440  			supers := sets.String{}
   441  			adder := func(user string, super bool) error {
   442  				if user == "fail" {
   443  					return fmt.Errorf("injected adder failure for %s", user)
   444  				}
   445  				if super {
   446  					supers.Insert(user)
   447  				} else {
   448  					members.Insert(user)
   449  				}
   450  				return nil
   451  			}
   452  
   453  			remover := func(user string) error {
   454  				if user == "fail" {
   455  					return fmt.Errorf("injected remover failure for %s", user)
   456  				}
   457  				removed.Insert(user)
   458  				return nil
   459  			}
   460  
   461  			err := configureMembers(tc.have, tc.want, adder, remover)
   462  			switch {
   463  			case err != nil:
   464  				if !tc.err {
   465  					t.Errorf("Unexpected error: %v", err)
   466  				}
   467  			case tc.err:
   468  				t.Errorf("Failed to receive error")
   469  			default:
   470  				if err := cmpLists(tc.remove.List(), removed.List()); err != nil {
   471  					t.Errorf("Wrong users removed: %v", err)
   472  				} else if err := cmpLists(tc.members.List(), members.List()); err != nil {
   473  					t.Errorf("Wrong members added: %v", err)
   474  				} else if err := cmpLists(tc.supers.List(), supers.List()); err != nil {
   475  					t.Errorf("Wrong supers added: %v", err)
   476  				}
   477  			}
   478  		})
   479  	}
   480  }
   481  
   482  func TestConfigureOrgMembers(t *testing.T) {
   483  	cases := []struct {
   484  		name        string
   485  		opt         options
   486  		config      org.Config
   487  		admins      []string
   488  		members     []string
   489  		invitations []string
   490  		err         bool
   491  		remove      []string
   492  		addAdmins   []string
   493  		addMembers  []string
   494  	}{
   495  		{
   496  			name: "too few admins",
   497  			opt: options{
   498  				minAdmins: 5,
   499  			},
   500  			config: org.Config{
   501  				Admins: []string{"joe"},
   502  			},
   503  			err: true,
   504  		},
   505  		{
   506  			name: "remove too many admins",
   507  			opt: options{
   508  				maximumDelta: 0.3,
   509  			},
   510  			config: org.Config{
   511  				Admins: []string{"keep", "me"},
   512  			},
   513  			admins: []string{"a", "b", "c", "keep"},
   514  			err:    true,
   515  		},
   516  		{
   517  			name: "forgot to add self",
   518  			opt: options{
   519  				requireSelf: true,
   520  			},
   521  			config: org.Config{
   522  				Admins: []string{"other"},
   523  			},
   524  			err: true,
   525  		},
   526  		{
   527  			name: "forgot to add required admins",
   528  			opt: options{
   529  				requiredAdmins: flagutil.NewStrings("francis"),
   530  			},
   531  			err: true,
   532  		},
   533  		{
   534  			name:   "can remove self with flag",
   535  			config: org.Config{},
   536  			opt: options{
   537  				maximumDelta: 1,
   538  				requireSelf:  false,
   539  			},
   540  			admins: []string{"me"},
   541  			remove: []string{"me"},
   542  		},
   543  		{
   544  			name: "reject same person with both roles",
   545  			config: org.Config{
   546  				Admins:  []string{"me"},
   547  				Members: []string{"me"},
   548  			},
   549  			err: true,
   550  		},
   551  		{
   552  			name:   "github remove rpc fails",
   553  			admins: []string{"fail"},
   554  			err:    true,
   555  		},
   556  		{
   557  			name: "github add rpc fails",
   558  			config: org.Config{
   559  				Admins: []string{"fail"},
   560  			},
   561  			err: true,
   562  		},
   563  		{
   564  			name: "require team member to be org member",
   565  			config: org.Config{
   566  				Teams: map[string]org.Team{
   567  					"group": {
   568  						Members: []string{"non-member"},
   569  					},
   570  				},
   571  			},
   572  			err: true,
   573  		},
   574  		{
   575  			name: "require team maintainer to be org member",
   576  			config: org.Config{
   577  				Teams: map[string]org.Team{
   578  					"group": {
   579  						Maintainers: []string{"non-member"},
   580  					},
   581  				},
   582  			},
   583  			err: true,
   584  		},
   585  		{
   586  			name: "disallow duplicate names",
   587  			config: org.Config{
   588  				Teams: map[string]org.Team{
   589  					"duplicate": {},
   590  					"other": {
   591  						Previously: []string{"duplicate"},
   592  					},
   593  				},
   594  			},
   595  			err: true,
   596  		},
   597  		{
   598  			name: "disallow duplicate names (single team)",
   599  			config: org.Config{
   600  				Teams: map[string]org.Team{
   601  					"foo": {
   602  						Previously: []string{"foo"},
   603  					},
   604  				},
   605  			},
   606  			err: true,
   607  		},
   608  		{
   609  			name: "trival case works",
   610  		},
   611  		{
   612  			name: "some of everything",
   613  			config: org.Config{
   614  				Admins:  []string{"keep-admin", "new-admin"},
   615  				Members: []string{"keep-member", "new-member"},
   616  			},
   617  			opt: options{
   618  				maximumDelta: 0.5,
   619  			},
   620  			admins:     []string{"keep-admin", "drop-admin"},
   621  			members:    []string{"keep-member", "drop-member"},
   622  			remove:     []string{"drop-admin", "drop-member"},
   623  			addMembers: []string{"new-member"},
   624  			addAdmins:  []string{"new-admin"},
   625  		},
   626  		{
   627  			name: "do not reinvite",
   628  			config: org.Config{
   629  				Admins:  []string{"invited-admin"},
   630  				Members: []string{"invited-member"},
   631  			},
   632  			invitations: []string{"invited-admin", "invited-member"},
   633  		},
   634  		{
   635  			name:        "github list invitation rpc fails",
   636  			invitations: []string{"fail"},
   637  			err:         true,
   638  		},
   639  	}
   640  
   641  	for _, tc := range cases {
   642  		t.Run(tc.name, func(t *testing.T) {
   643  			fc := &fakeClient{
   644  				admins:     sets.NewString(tc.admins...),
   645  				members:    sets.NewString(tc.members...),
   646  				invitees:   sets.NewString(tc.invitations...),
   647  				removed:    sets.String{},
   648  				newAdmins:  sets.String{},
   649  				newMembers: sets.String{},
   650  			}
   651  			err := configureOrgMembers(tc.opt, fc, fakeOrg, tc.config)
   652  			switch {
   653  			case err != nil:
   654  				if !tc.err {
   655  					t.Errorf("Unexpected error: %v", err)
   656  				}
   657  			case tc.err:
   658  				t.Errorf("Failed to receive error")
   659  			default:
   660  				if err := cmpLists(tc.remove, fc.removed.List()); err != nil {
   661  					t.Errorf("Wrong users removed: %v", err)
   662  				} else if err := cmpLists(tc.addMembers, fc.newMembers.List()); err != nil {
   663  					t.Errorf("Wrong members added: %v", err)
   664  				} else if err := cmpLists(tc.addAdmins, fc.newAdmins.List()); err != nil {
   665  					t.Errorf("Wrong admins added: %v", err)
   666  				}
   667  			}
   668  		})
   669  	}
   670  }
   671  
   672  type fakeTeamClient struct {
   673  	teams map[int]github.Team
   674  	max   int
   675  }
   676  
   677  func makeFakeTeamClient(teams ...github.Team) *fakeTeamClient {
   678  	fc := fakeTeamClient{
   679  		teams: map[int]github.Team{},
   680  	}
   681  	for _, t := range teams {
   682  		fc.teams[t.ID] = t
   683  		if t.ID >= fc.max {
   684  			fc.max = t.ID + 1
   685  		}
   686  	}
   687  	return &fc
   688  }
   689  
   690  const fakeOrg = "random-org"
   691  
   692  func (c *fakeTeamClient) CreateTeam(org string, team github.Team) (*github.Team, error) {
   693  	if org != fakeOrg {
   694  		return nil, fmt.Errorf("org must be %s, not %s", fakeOrg, org)
   695  	}
   696  	if team.Name == "fail" {
   697  		return nil, errors.New("injected CreateTeam error")
   698  	}
   699  	c.max++
   700  	team.ID = c.max
   701  	c.teams[team.ID] = team
   702  	return &team, nil
   703  
   704  }
   705  
   706  func (c *fakeTeamClient) ListTeams(name string) ([]github.Team, error) {
   707  	if name == "fail" {
   708  		return nil, errors.New("injected ListTeams error")
   709  	}
   710  	var teams []github.Team
   711  	for _, t := range c.teams {
   712  		teams = append(teams, t)
   713  	}
   714  	return teams, nil
   715  }
   716  
   717  func (c *fakeTeamClient) DeleteTeam(id int) error {
   718  	switch _, ok := c.teams[id]; {
   719  	case !ok:
   720  		return fmt.Errorf("not found %d", id)
   721  	case id < 0:
   722  		return errors.New("injected DeleteTeam error")
   723  	}
   724  	delete(c.teams, id)
   725  	return nil
   726  }
   727  
   728  func (c *fakeTeamClient) EditTeam(team github.Team) (*github.Team, error) {
   729  	id := team.ID
   730  	t, ok := c.teams[id]
   731  	if !ok {
   732  		return nil, fmt.Errorf("team %d does not exist", id)
   733  	}
   734  	switch {
   735  	case team.Description == "fail":
   736  		return nil, errors.New("injected description failure")
   737  	case team.Name == "fail":
   738  		return nil, errors.New("injected name failure")
   739  	case team.Privacy == "fail":
   740  		return nil, errors.New("injected privacy failure")
   741  	}
   742  	if team.Description != "" {
   743  		t.Description = team.Description
   744  	}
   745  	if team.Name != "" {
   746  		t.Name = team.Name
   747  	}
   748  	if team.Privacy != "" {
   749  		t.Privacy = team.Privacy
   750  	}
   751  	if team.ParentTeamID != nil {
   752  		t.Parent = &github.Team{
   753  			ID: *team.ParentTeamID,
   754  		}
   755  	} else {
   756  		t.Parent = nil
   757  	}
   758  	c.teams[id] = t
   759  	return &t, nil
   760  }
   761  
   762  func TestFindTeam(t *testing.T) {
   763  	cases := []struct {
   764  		name     string
   765  		teams    map[string]github.Team
   766  		current  string
   767  		previous []string
   768  		expected int
   769  	}{
   770  		{
   771  			name: "will find current team",
   772  			teams: map[string]github.Team{
   773  				"hello": {ID: 17},
   774  			},
   775  			current:  "hello",
   776  			expected: 17,
   777  		},
   778  		{
   779  			name: "team does not exist returns nil",
   780  			teams: map[string]github.Team{
   781  				"unrelated": {ID: 5},
   782  			},
   783  			current: "hypothetical",
   784  		},
   785  		{
   786  			name: "will find previous name",
   787  			teams: map[string]github.Team{
   788  				"deprecated name": {ID: 1},
   789  			},
   790  			current:  "current name",
   791  			previous: []string{"archaic name", "deprecated name"},
   792  			expected: 1,
   793  		},
   794  		{
   795  			name: "prioritize current when previous also exists",
   796  			teams: map[string]github.Team{
   797  				"deprecated": {ID: 1},
   798  				"current":    {ID: 2},
   799  			},
   800  			current:  "current",
   801  			previous: []string{"deprecated"},
   802  			expected: 2,
   803  		},
   804  	}
   805  
   806  	for _, tc := range cases {
   807  		t.Run(tc.name, func(t *testing.T) {
   808  			actual := findTeam(tc.teams, tc.current, tc.previous...)
   809  			switch {
   810  			case actual == nil:
   811  				if tc.expected != 0 {
   812  					t.Errorf("failed to find team %d", tc.expected)
   813  				}
   814  			case tc.expected == 0:
   815  				t.Errorf("unexpected team returned: %v", *actual)
   816  			case actual.ID != tc.expected:
   817  				t.Errorf("team %v != expected ID %d", actual, tc.expected)
   818  			}
   819  		})
   820  	}
   821  }
   822  
   823  func TestConfigureTeams(t *testing.T) {
   824  	desc := "so interesting"
   825  	priv := org.Secret
   826  	cases := []struct {
   827  		name            string
   828  		err             bool
   829  		orgNameOverride string
   830  		config          org.Config
   831  		teams           []github.Team
   832  		expected        map[string]github.Team
   833  		deleted         []int
   834  		delta           float64
   835  	}{
   836  		{
   837  			name: "do nothing without error",
   838  		},
   839  		{
   840  			name: "reject duplicated team names (different teams)",
   841  			err:  true,
   842  			config: org.Config{
   843  				Teams: map[string]org.Team{
   844  					"hello": {},
   845  					"there": {Previously: []string{"hello"}},
   846  				},
   847  			},
   848  		},
   849  		{
   850  			name: "reject duplicated team names (single team)",
   851  			err:  true,
   852  			config: org.Config{
   853  				Teams: map[string]org.Team{
   854  					"hello": {Previously: []string{"hello"}},
   855  				},
   856  			},
   857  		},
   858  		{
   859  			name:            "fail to list teams",
   860  			orgNameOverride: "fail",
   861  			err:             true,
   862  		},
   863  		{
   864  			name: "fail to create team",
   865  			config: org.Config{
   866  				Teams: map[string]org.Team{
   867  					"fail": {},
   868  				},
   869  			},
   870  			err: true,
   871  		},
   872  		{
   873  			name: "fail to delete team",
   874  			teams: []github.Team{
   875  				{Name: "fail", ID: -55},
   876  			},
   877  			err: true,
   878  		},
   879  		{
   880  			name: "create missing team",
   881  			teams: []github.Team{
   882  				{Name: "old", ID: 1},
   883  			},
   884  			config: org.Config{
   885  				Teams: map[string]org.Team{
   886  					"new": {},
   887  					"old": {},
   888  				},
   889  			},
   890  			expected: map[string]github.Team{
   891  				"old": {Name: "old", ID: 1},
   892  				"new": {Name: "new", ID: 3},
   893  			},
   894  		},
   895  		{
   896  			name: "reuse existing teams",
   897  			teams: []github.Team{
   898  				{Name: "current", ID: 1},
   899  				{Name: "deprecated", ID: 5},
   900  			},
   901  			config: org.Config{
   902  				Teams: map[string]org.Team{
   903  					"current": {},
   904  					"updated": {Previously: []string{"deprecated"}},
   905  				},
   906  			},
   907  			expected: map[string]github.Team{
   908  				"current": {Name: "current", ID: 1},
   909  				"updated": {Name: "deprecated", ID: 5},
   910  			},
   911  		},
   912  		{
   913  			name: "delete unused teams",
   914  			teams: []github.Team{
   915  				{
   916  					Name: "unused",
   917  					ID:   1,
   918  				},
   919  				{
   920  					Name: "used",
   921  					ID:   2,
   922  				},
   923  			},
   924  			config: org.Config{
   925  				Teams: map[string]org.Team{
   926  					"used": {},
   927  				},
   928  			},
   929  			expected: map[string]github.Team{
   930  				"used": {ID: 2, Name: "used"},
   931  			},
   932  			deleted: []int{1},
   933  		},
   934  		{
   935  			name: "create team with metadata",
   936  			config: org.Config{
   937  				Teams: map[string]org.Team{
   938  					"new": {
   939  						TeamMetadata: org.TeamMetadata{
   940  							Description: &desc,
   941  							Privacy:     &priv,
   942  						},
   943  					},
   944  				},
   945  			},
   946  			expected: map[string]github.Team{
   947  				"new": {ID: 1, Name: "new", Description: desc, Privacy: string(priv)},
   948  			},
   949  		},
   950  		{
   951  			name: "allow deleting many teams",
   952  			teams: []github.Team{
   953  				{
   954  					Name: "unused",
   955  					ID:   1,
   956  				},
   957  				{
   958  					Name: "used",
   959  					ID:   2,
   960  				},
   961  			},
   962  			config: org.Config{
   963  				Teams: map[string]org.Team{
   964  					"used": {},
   965  				},
   966  			},
   967  			expected: map[string]github.Team{
   968  				"used": {ID: 2, Name: "used"},
   969  			},
   970  			delta: 0.6,
   971  		},
   972  		{
   973  			name: "refuse to delete too many teams",
   974  			teams: []github.Team{
   975  				{
   976  					Name: "unused",
   977  					ID:   1,
   978  				},
   979  				{
   980  					Name: "used",
   981  					ID:   2,
   982  				},
   983  			},
   984  			config: org.Config{
   985  				Teams: map[string]org.Team{
   986  					"used": {},
   987  				},
   988  			},
   989  			err:   true,
   990  			delta: 0.1,
   991  		},
   992  	}
   993  
   994  	for _, tc := range cases {
   995  		t.Run(tc.name, func(t *testing.T) {
   996  			fc := makeFakeTeamClient(tc.teams...)
   997  			orgName := tc.orgNameOverride
   998  			if orgName == "" {
   999  				orgName = fakeOrg
  1000  			}
  1001  			if tc.expected == nil {
  1002  				tc.expected = map[string]github.Team{}
  1003  			}
  1004  			if tc.delta == 0 {
  1005  				tc.delta = 1
  1006  			}
  1007  			actual, err := configureTeams(fc, orgName, tc.config, tc.delta)
  1008  			switch {
  1009  			case err != nil:
  1010  				if !tc.err {
  1011  					t.Errorf("unexpected error: %v", err)
  1012  				}
  1013  			case tc.err:
  1014  				t.Errorf("failed to receive error")
  1015  			case !reflect.DeepEqual(actual, tc.expected):
  1016  				t.Errorf("%#v != actual %#v", tc.expected, actual)
  1017  			}
  1018  			for _, id := range tc.deleted {
  1019  				if team, ok := fc.teams[id]; ok {
  1020  					t.Errorf("%d still present: %#v", id, team)
  1021  				}
  1022  			}
  1023  		})
  1024  	}
  1025  }
  1026  
  1027  func TestConfigureTeam(t *testing.T) {
  1028  	old := "old value"
  1029  	cur := "current value"
  1030  	fail := "fail"
  1031  	pfail := org.Privacy(fail)
  1032  	whatev := "whatever"
  1033  	secret := org.Secret
  1034  	parent := 2
  1035  	cases := []struct {
  1036  		name     string
  1037  		err      bool
  1038  		teamName string
  1039  		parent   *int
  1040  		config   org.Team
  1041  		github   github.Team
  1042  		expected github.Team
  1043  	}{
  1044  		{
  1045  			name:     "patch team when name changes",
  1046  			teamName: cur,
  1047  			config: org.Team{
  1048  				Previously: []string{old},
  1049  			},
  1050  			github: github.Team{
  1051  				ID:   1,
  1052  				Name: old,
  1053  			},
  1054  			expected: github.Team{
  1055  				ID:   1,
  1056  				Name: cur,
  1057  			},
  1058  		},
  1059  		{
  1060  			name:     "patch team when description changes",
  1061  			teamName: whatev,
  1062  			parent:   nil,
  1063  			config: org.Team{
  1064  				TeamMetadata: org.TeamMetadata{
  1065  					Description: &cur,
  1066  				},
  1067  			},
  1068  			github: github.Team{
  1069  				ID:          2,
  1070  				Name:        whatev,
  1071  				Description: old,
  1072  			},
  1073  			expected: github.Team{
  1074  				ID:          2,
  1075  				Name:        whatev,
  1076  				Description: cur,
  1077  			},
  1078  		},
  1079  		{
  1080  			name:     "patch team when privacy changes",
  1081  			teamName: whatev,
  1082  			parent:   nil,
  1083  			config: org.Team{
  1084  				TeamMetadata: org.TeamMetadata{
  1085  					Privacy: &secret,
  1086  				},
  1087  			},
  1088  			github: github.Team{
  1089  				ID:      3,
  1090  				Name:    whatev,
  1091  				Privacy: string(org.Closed),
  1092  			},
  1093  			expected: github.Team{
  1094  				ID:      3,
  1095  				Name:    whatev,
  1096  				Privacy: string(secret),
  1097  			},
  1098  		},
  1099  		{
  1100  			name:     "patch team when parent changes",
  1101  			teamName: whatev,
  1102  			parent:   &parent,
  1103  			config:   org.Team{},
  1104  			github: github.Team{
  1105  				ID:   3,
  1106  				Name: whatev,
  1107  				Parent: &github.Team{
  1108  					ID: 4,
  1109  				},
  1110  			},
  1111  			expected: github.Team{
  1112  				ID:   3,
  1113  				Name: whatev,
  1114  				Parent: &github.Team{
  1115  					ID: 2,
  1116  				},
  1117  				Privacy: string(org.Closed),
  1118  			},
  1119  		},
  1120  		{
  1121  			name:     "patch team when parent removed",
  1122  			teamName: whatev,
  1123  			parent:   nil,
  1124  			config:   org.Team{},
  1125  			github: github.Team{
  1126  				ID:   3,
  1127  				Name: whatev,
  1128  				Parent: &github.Team{
  1129  					ID: 2,
  1130  				},
  1131  			},
  1132  			expected: github.Team{
  1133  				ID:     3,
  1134  				Name:   whatev,
  1135  				Parent: nil,
  1136  			},
  1137  		},
  1138  		{
  1139  			name:     "do not patch team when values are the same",
  1140  			teamName: fail,
  1141  			parent:   &parent,
  1142  			config: org.Team{
  1143  				TeamMetadata: org.TeamMetadata{
  1144  					Description: &fail,
  1145  					Privacy:     &pfail,
  1146  				},
  1147  			},
  1148  			github: github.Team{
  1149  				ID:          4,
  1150  				Name:        fail,
  1151  				Description: fail,
  1152  				Privacy:     fail,
  1153  				Parent: &github.Team{
  1154  					ID: 2,
  1155  				},
  1156  			},
  1157  			expected: github.Team{
  1158  				ID:          4,
  1159  				Name:        fail,
  1160  				Description: fail,
  1161  				Privacy:     fail,
  1162  				Parent: &github.Team{
  1163  					ID: 2,
  1164  				},
  1165  			},
  1166  		},
  1167  		{
  1168  			name:     "fail to patch team",
  1169  			teamName: "team",
  1170  			parent:   nil,
  1171  			config: org.Team{
  1172  				TeamMetadata: org.TeamMetadata{
  1173  					Description: &fail,
  1174  				},
  1175  			},
  1176  			github: github.Team{
  1177  				ID:          1,
  1178  				Name:        "team",
  1179  				Description: whatev,
  1180  			},
  1181  			err: true,
  1182  		},
  1183  	}
  1184  
  1185  	for _, tc := range cases {
  1186  		t.Run(tc.name, func(t *testing.T) {
  1187  			fc := makeFakeTeamClient(tc.github)
  1188  			err := configureTeam(fc, fakeOrg, tc.teamName, tc.config, tc.github, tc.parent)
  1189  			switch {
  1190  			case err != nil:
  1191  				if !tc.err {
  1192  					t.Errorf("unexpected error: %v", err)
  1193  				}
  1194  			case tc.err:
  1195  				t.Errorf("failed to receive expected error")
  1196  			case !reflect.DeepEqual(fc.teams[tc.expected.ID], tc.expected):
  1197  				t.Errorf("actual %+v != expected %+v", fc.teams[tc.expected.ID], tc.expected)
  1198  			}
  1199  		})
  1200  	}
  1201  }
  1202  
  1203  func TestConfigureTeamMembers(t *testing.T) {
  1204  	cases := []struct {
  1205  		name           string
  1206  		err            bool
  1207  		members        sets.String
  1208  		maintainers    sets.String
  1209  		remove         sets.String
  1210  		addMembers     sets.String
  1211  		addMaintainers sets.String
  1212  		team           org.Team
  1213  		id             int
  1214  	}{
  1215  		{
  1216  			name: "fail when listing fails",
  1217  			id:   teamID ^ 0xff,
  1218  			err:  true,
  1219  		},
  1220  		{
  1221  			name:    "fail when removal fails",
  1222  			members: sets.NewString("fail"),
  1223  			err:     true,
  1224  		},
  1225  		{
  1226  			name: "fail when add fails",
  1227  			team: org.Team{
  1228  				Maintainers: []string{"fail"},
  1229  			},
  1230  			err: true,
  1231  		},
  1232  		{
  1233  			name: "some of everything",
  1234  			team: org.Team{
  1235  				Maintainers: []string{"keep-maintainer", "new-maintainer"},
  1236  				Members:     []string{"keep-member", "new-member"},
  1237  			},
  1238  			maintainers:    sets.NewString("keep-maintainer", "drop-maintainer"),
  1239  			members:        sets.NewString("keep-member", "drop-member"),
  1240  			remove:         sets.NewString("drop-maintainer", "drop-member"),
  1241  			addMembers:     sets.NewString("new-member"),
  1242  			addMaintainers: sets.NewString("new-maintainer"),
  1243  		},
  1244  	}
  1245  
  1246  	for _, tc := range cases {
  1247  		if tc.id == 0 {
  1248  			tc.id = teamID
  1249  		}
  1250  		t.Run(tc.name, func(t *testing.T) {
  1251  			fc := &fakeClient{
  1252  				admins:     sets.StringKeySet(tc.maintainers),
  1253  				members:    sets.StringKeySet(tc.members),
  1254  				removed:    sets.String{},
  1255  				newAdmins:  sets.String{},
  1256  				newMembers: sets.String{},
  1257  			}
  1258  			err := configureTeamMembers(fc, tc.id, tc.team)
  1259  			switch {
  1260  			case err != nil:
  1261  				if !tc.err {
  1262  					t.Errorf("Unexpected error: %v", err)
  1263  				}
  1264  			case tc.err:
  1265  				t.Errorf("Failed to receive error")
  1266  			default:
  1267  				if err := cmpLists(tc.remove.List(), fc.removed.List()); err != nil {
  1268  					t.Errorf("Wrong users removed: %v", err)
  1269  				} else if err := cmpLists(tc.addMembers.List(), fc.newMembers.List()); err != nil {
  1270  					t.Errorf("Wrong members added: %v", err)
  1271  				} else if err := cmpLists(tc.addMaintainers.List(), fc.newAdmins.List()); err != nil {
  1272  					t.Errorf("Wrong admins added: %v", err)
  1273  				}
  1274  			}
  1275  
  1276  		})
  1277  	}
  1278  }
  1279  
  1280  func cmpLists(a, b []string) error {
  1281  	if a == nil {
  1282  		a = []string{}
  1283  	}
  1284  	if b == nil {
  1285  		b = []string{}
  1286  	}
  1287  	sort.Strings(a)
  1288  	sort.Strings(b)
  1289  	if !reflect.DeepEqual(a, b) {
  1290  		return fmt.Errorf("%v != %v", a, b)
  1291  	}
  1292  	return nil
  1293  }
  1294  
  1295  type fakeOrgClient struct {
  1296  	current github.Organization
  1297  	changed bool
  1298  }
  1299  
  1300  func (o *fakeOrgClient) GetOrg(name string) (*github.Organization, error) {
  1301  	if name == "fail" {
  1302  		return nil, errors.New("injected GetOrg error")
  1303  	}
  1304  	return &o.current, nil
  1305  }
  1306  
  1307  func (o *fakeOrgClient) EditOrg(name string, org github.Organization) (*github.Organization, error) {
  1308  	if org.Description == "fail" {
  1309  		return nil, errors.New("injected EditOrg error")
  1310  	}
  1311  	o.current = org
  1312  	o.changed = true
  1313  	return &o.current, nil
  1314  }
  1315  
  1316  func TestUpdateBool(t *testing.T) {
  1317  	yes := true
  1318  	no := false
  1319  	cases := []struct {
  1320  		name string
  1321  		have *bool
  1322  		want *bool
  1323  		end  bool
  1324  		ret  *bool
  1325  	}{
  1326  		{
  1327  			name: "panic on nil have",
  1328  			want: &no,
  1329  		},
  1330  		{
  1331  			name: "never change on nil want",
  1332  			want: nil,
  1333  			have: &yes,
  1334  			end:  yes,
  1335  			ret:  &no,
  1336  		},
  1337  		{
  1338  			name: "do not change if same",
  1339  			want: &yes,
  1340  			have: &yes,
  1341  			end:  yes,
  1342  			ret:  &no,
  1343  		},
  1344  		{
  1345  			name: "change if different",
  1346  			want: &no,
  1347  			have: &yes,
  1348  			end:  no,
  1349  			ret:  &yes,
  1350  		},
  1351  	}
  1352  
  1353  	for _, tc := range cases {
  1354  		t.Run(tc.name, func(t *testing.T) {
  1355  			defer func() {
  1356  				wantPanic := tc.ret == nil
  1357  				r := recover()
  1358  				gotPanic := r != nil
  1359  				switch {
  1360  				case gotPanic && !wantPanic:
  1361  					t.Errorf("unexpected panic: %v", r)
  1362  				case wantPanic && !gotPanic:
  1363  					t.Errorf("failed to receive panic")
  1364  				}
  1365  			}()
  1366  			if tc.have != nil { // prevent overwriting what tc.have points to for next test case
  1367  				have := *tc.have
  1368  				tc.have = &have
  1369  			}
  1370  			ret := updateBool(tc.have, tc.want)
  1371  			switch {
  1372  			case ret != *tc.ret:
  1373  				t.Errorf("return value %t != expected %t", ret, *tc.ret)
  1374  			case *tc.have != tc.end:
  1375  				t.Errorf("end value %t != expected %t", *tc.have, tc.end)
  1376  			}
  1377  		})
  1378  	}
  1379  }
  1380  
  1381  func TestUpdateString(t *testing.T) {
  1382  	no := false
  1383  	yes := true
  1384  	hello := "hello"
  1385  	world := "world"
  1386  	empty := ""
  1387  	cases := []struct {
  1388  		name     string
  1389  		have     *string
  1390  		want     *string
  1391  		expected string
  1392  		ret      *bool
  1393  	}{
  1394  		{
  1395  			name: "panic on nil have",
  1396  			want: &hello,
  1397  		},
  1398  		{
  1399  			name:     "never change on nil want",
  1400  			want:     nil,
  1401  			have:     &hello,
  1402  			expected: hello,
  1403  			ret:      &no,
  1404  		},
  1405  		{
  1406  			name:     "do not change if same",
  1407  			want:     &world,
  1408  			have:     &world,
  1409  			expected: world,
  1410  			ret:      &no,
  1411  		},
  1412  		{
  1413  			name:     "change if different",
  1414  			want:     &empty,
  1415  			have:     &hello,
  1416  			expected: empty,
  1417  			ret:      &yes,
  1418  		},
  1419  	}
  1420  
  1421  	for _, tc := range cases {
  1422  		t.Run(tc.name, func(t *testing.T) {
  1423  			defer func() {
  1424  				wantPanic := tc.ret == nil
  1425  				r := recover()
  1426  				gotPanic := r != nil
  1427  				switch {
  1428  				case gotPanic && !wantPanic:
  1429  					t.Errorf("unexpected panic: %v", r)
  1430  				case wantPanic && !gotPanic:
  1431  					t.Errorf("failed to receive panic")
  1432  				}
  1433  			}()
  1434  			if tc.have != nil { // prevent overwriting what tc.have points to for next test case
  1435  				have := *tc.have
  1436  				tc.have = &have
  1437  			}
  1438  			ret := updateString(tc.have, tc.want)
  1439  			switch {
  1440  			case ret != *tc.ret:
  1441  				t.Errorf("return value %t != expected %t", ret, *tc.ret)
  1442  			case *tc.have != tc.expected:
  1443  				t.Errorf("end value %s != expected %s", *tc.have, tc.expected)
  1444  			}
  1445  		})
  1446  	}
  1447  }
  1448  
  1449  func TestConfigureOrgMeta(t *testing.T) {
  1450  	filled := github.Organization{
  1451  		BillingEmail:                 "be",
  1452  		Company:                      "co",
  1453  		Email:                        "em",
  1454  		Location:                     "lo",
  1455  		Name:                         "na",
  1456  		Description:                  "de",
  1457  		HasOrganizationProjects:      true,
  1458  		HasRepositoryProjects:        true,
  1459  		DefaultRepositoryPermission:  "not-a-real-value",
  1460  		MembersCanCreateRepositories: true,
  1461  	}
  1462  	yes := true
  1463  	no := false
  1464  	str := "random-letters"
  1465  	fail := "fail"
  1466  	read := org.Read
  1467  
  1468  	cases := []struct {
  1469  		name     string
  1470  		orgName  string
  1471  		want     org.Metadata
  1472  		have     github.Organization
  1473  		expected github.Organization
  1474  		err      bool
  1475  		change   bool
  1476  	}{
  1477  		{
  1478  			name:     "no want means no change",
  1479  			have:     filled,
  1480  			expected: filled,
  1481  			change:   false,
  1482  		},
  1483  		{
  1484  			name:    "fail if GetOrg fails",
  1485  			orgName: fail,
  1486  			err:     true,
  1487  		},
  1488  		{
  1489  			name: "fail if EditOrg fails",
  1490  			want: org.Metadata{Description: &fail},
  1491  			err:  true,
  1492  		},
  1493  		{
  1494  			name: "billing diff causes change",
  1495  			want: org.Metadata{BillingEmail: &str},
  1496  			expected: github.Organization{
  1497  				BillingEmail: str,
  1498  			},
  1499  			change: true,
  1500  		},
  1501  		{
  1502  			name: "company diff causes change",
  1503  			want: org.Metadata{Company: &str},
  1504  			expected: github.Organization{
  1505  				Company: str,
  1506  			},
  1507  			change: true,
  1508  		},
  1509  		{
  1510  			name: "email diff causes change",
  1511  			want: org.Metadata{Email: &str},
  1512  			expected: github.Organization{
  1513  				Email: str,
  1514  			},
  1515  			change: true,
  1516  		},
  1517  		{
  1518  			name: "location diff causes change",
  1519  			want: org.Metadata{Location: &str},
  1520  			expected: github.Organization{
  1521  				Location: str,
  1522  			},
  1523  			change: true,
  1524  		},
  1525  		{
  1526  			name: "name diff causes change",
  1527  			want: org.Metadata{Name: &str},
  1528  			expected: github.Organization{
  1529  				Name: str,
  1530  			},
  1531  			change: true,
  1532  		},
  1533  		{
  1534  			name: "org projects diff causes change",
  1535  			want: org.Metadata{HasOrganizationProjects: &yes},
  1536  			expected: github.Organization{
  1537  				HasOrganizationProjects: yes,
  1538  			},
  1539  			change: true,
  1540  		},
  1541  		{
  1542  			name: "repo projects diff causes change",
  1543  			want: org.Metadata{HasRepositoryProjects: &yes},
  1544  			expected: github.Organization{
  1545  				HasRepositoryProjects: yes,
  1546  			},
  1547  			change: true,
  1548  		},
  1549  		{
  1550  			name: "default permission diff causes change",
  1551  			want: org.Metadata{DefaultRepositoryPermission: &read},
  1552  			expected: github.Organization{
  1553  				DefaultRepositoryPermission: string(read),
  1554  			},
  1555  			change: true,
  1556  		},
  1557  		{
  1558  			name: "members can create diff causes change",
  1559  			want: org.Metadata{MembersCanCreateRepositories: &yes},
  1560  			expected: github.Organization{
  1561  				MembersCanCreateRepositories: yes,
  1562  			},
  1563  			change: true,
  1564  		},
  1565  		{
  1566  			name: "change all values at once",
  1567  			have: filled,
  1568  			want: org.Metadata{
  1569  				BillingEmail:                 &str,
  1570  				Company:                      &str,
  1571  				Email:                        &str,
  1572  				Location:                     &str,
  1573  				Name:                         &str,
  1574  				Description:                  &str,
  1575  				HasOrganizationProjects:      &no,
  1576  				HasRepositoryProjects:        &no,
  1577  				MembersCanCreateRepositories: &no,
  1578  				DefaultRepositoryPermission:  &read,
  1579  			},
  1580  			expected: github.Organization{
  1581  				BillingEmail:                 str,
  1582  				Company:                      str,
  1583  				Email:                        str,
  1584  				Location:                     str,
  1585  				Name:                         str,
  1586  				Description:                  str,
  1587  				HasOrganizationProjects:      no,
  1588  				HasRepositoryProjects:        no,
  1589  				MembersCanCreateRepositories: no,
  1590  				DefaultRepositoryPermission:  string(read),
  1591  			},
  1592  			change: true,
  1593  		},
  1594  	}
  1595  
  1596  	for _, tc := range cases {
  1597  		t.Run(tc.name, func(t *testing.T) {
  1598  			if tc.orgName == "" {
  1599  				tc.orgName = "whatever"
  1600  			}
  1601  			fc := fakeOrgClient{
  1602  				current: tc.have,
  1603  			}
  1604  			err := configureOrgMeta(&fc, tc.orgName, tc.want)
  1605  			switch {
  1606  			case err != nil:
  1607  				if !tc.err {
  1608  					t.Errorf("unexpected error: %v", err)
  1609  				}
  1610  			case tc.err:
  1611  				t.Errorf("failed to receive error")
  1612  			case tc.change != fc.changed:
  1613  				t.Errorf("changed %t != expected %t", fc.changed, tc.change)
  1614  			case !reflect.DeepEqual(fc.current, tc.expected):
  1615  				t.Errorf("current %#v != expected %#v", fc.current, tc.expected)
  1616  			}
  1617  		})
  1618  	}
  1619  }
  1620  
  1621  func TestDumpOrgConfig(t *testing.T) {
  1622  	empty := ""
  1623  	hello := "Hello"
  1624  	details := "wise and brilliant exemplary human specimens"
  1625  	yes := true
  1626  	no := false
  1627  	perm := org.Write
  1628  	pub := org.Privacy("")
  1629  	cases := []struct {
  1630  		name        string
  1631  		orgOverride string
  1632  		meta        github.Organization
  1633  		members     []string
  1634  		admins      []string
  1635  		teams       []github.Team
  1636  		teamMembers map[int][]string
  1637  		maintainers map[int][]string
  1638  		expected    org.Config
  1639  		err         bool
  1640  	}{
  1641  		{
  1642  			name:        "fails if GetOrg fails",
  1643  			orgOverride: "fail",
  1644  			err:         true,
  1645  		},
  1646  		{
  1647  			name:    "fails if ListOrgMembers fails",
  1648  			err:     true,
  1649  			members: []string{"hello", "fail"},
  1650  		},
  1651  		{
  1652  			name: "fails if ListTeams fails",
  1653  			err:  true,
  1654  			teams: []github.Team{
  1655  				{
  1656  					Name: "fail",
  1657  					ID:   3,
  1658  				},
  1659  			},
  1660  		},
  1661  		{
  1662  			name: "fails if ListTeamMembersFails",
  1663  			err:  true,
  1664  			teams: []github.Team{
  1665  				{
  1666  					Name: "fred",
  1667  					ID:   -1,
  1668  				},
  1669  			},
  1670  		},
  1671  		{
  1672  			name: "basically works",
  1673  			meta: github.Organization{
  1674  				Name: hello,
  1675  				MembersCanCreateRepositories: yes,
  1676  				DefaultRepositoryPermission:  string(perm),
  1677  			},
  1678  			members: []string{"george", "jungle", "banana"},
  1679  			admins:  []string{"james", "giant", "peach"},
  1680  			teams: []github.Team{
  1681  				{
  1682  					ID:          5,
  1683  					Name:        "friends",
  1684  					Description: details,
  1685  				},
  1686  				{
  1687  					ID:   6,
  1688  					Name: "enemies",
  1689  				},
  1690  				{
  1691  					ID:   7,
  1692  					Name: "archenemies",
  1693  					Parent: &github.Team{
  1694  						ID:   6,
  1695  						Name: "enemies",
  1696  					},
  1697  				},
  1698  			},
  1699  			teamMembers: map[int][]string{
  1700  				5: {"george", "james"},
  1701  				6: {"george"},
  1702  				7: {},
  1703  			},
  1704  			maintainers: map[int][]string{
  1705  				5: {},
  1706  				6: {"giant", "jungle"},
  1707  				7: {"banana"},
  1708  			},
  1709  			expected: org.Config{
  1710  				Metadata: org.Metadata{
  1711  					Name:                         &hello,
  1712  					BillingEmail:                 &empty,
  1713  					Company:                      &empty,
  1714  					Email:                        &empty,
  1715  					Description:                  &empty,
  1716  					Location:                     &empty,
  1717  					HasOrganizationProjects:      &no,
  1718  					HasRepositoryProjects:        &no,
  1719  					DefaultRepositoryPermission:  &perm,
  1720  					MembersCanCreateRepositories: &yes,
  1721  				},
  1722  				Teams: map[string]org.Team{
  1723  					"friends": {
  1724  						TeamMetadata: org.TeamMetadata{
  1725  							Description: &details,
  1726  							Privacy:     &pub,
  1727  						},
  1728  						Members:     []string{"george", "james"},
  1729  						Maintainers: []string{},
  1730  						Children:    map[string]org.Team{},
  1731  					},
  1732  					"enemies": {
  1733  						TeamMetadata: org.TeamMetadata{
  1734  							Description: &empty,
  1735  							Privacy:     &pub,
  1736  						},
  1737  						Members:     []string{"george"},
  1738  						Maintainers: []string{"giant", "jungle"},
  1739  						Children: map[string]org.Team{
  1740  							"archenemies": {
  1741  								TeamMetadata: org.TeamMetadata{
  1742  									Description: &empty,
  1743  									Privacy:     &pub,
  1744  								},
  1745  								Members:     []string{},
  1746  								Maintainers: []string{"banana"},
  1747  								Children:    map[string]org.Team{},
  1748  							},
  1749  						},
  1750  					},
  1751  				},
  1752  				Members: []string{"george", "jungle", "banana"},
  1753  				Admins:  []string{"james", "giant", "peach"},
  1754  			},
  1755  		},
  1756  	}
  1757  
  1758  	for _, tc := range cases {
  1759  		t.Run(tc.name, func(t *testing.T) {
  1760  			orgName := "random-org"
  1761  			if tc.orgOverride != "" {
  1762  				orgName = tc.orgOverride
  1763  			}
  1764  			fc := fakeDumpClient{
  1765  				name:        orgName,
  1766  				members:     tc.members,
  1767  				admins:      tc.admins,
  1768  				meta:        tc.meta,
  1769  				teams:       tc.teams,
  1770  				teamMembers: tc.teamMembers,
  1771  				maintainers: tc.maintainers,
  1772  			}
  1773  			actual, err := dumpOrgConfig(fc, orgName)
  1774  			switch {
  1775  			case err != nil:
  1776  				if !tc.err {
  1777  					t.Errorf("unexpected error: %v", err)
  1778  				}
  1779  			case tc.err:
  1780  				t.Errorf("failed to receive error")
  1781  			default:
  1782  				fixup(actual)
  1783  				fixup(&tc.expected)
  1784  				if !reflect.DeepEqual(actual, &tc.expected) {
  1785  					a, _ := yaml.Marshal(*actual)
  1786  					e, _ := yaml.Marshal(tc.expected)
  1787  					t.Errorf("actual:\n%s != expected:\n%s", string(a), string(e))
  1788  				}
  1789  
  1790  			}
  1791  		})
  1792  	}
  1793  }
  1794  
  1795  type fakeDumpClient struct {
  1796  	name        string
  1797  	members     []string
  1798  	admins      []string
  1799  	meta        github.Organization
  1800  	teams       []github.Team
  1801  	teamMembers map[int][]string
  1802  	maintainers map[int][]string
  1803  }
  1804  
  1805  func (c fakeDumpClient) GetOrg(name string) (*github.Organization, error) {
  1806  	if name != c.name {
  1807  		return nil, errors.New("bad name")
  1808  	}
  1809  	if name == "fail" {
  1810  		return nil, errors.New("injected GetOrg error")
  1811  	}
  1812  	return &c.meta, nil
  1813  }
  1814  
  1815  func (c fakeDumpClient) makeMembers(people []string) ([]github.TeamMember, error) {
  1816  	var ret []github.TeamMember
  1817  	for _, p := range people {
  1818  		if p == "fail" {
  1819  			return nil, errors.New("injected makeMembers error")
  1820  		}
  1821  		ret = append(ret, github.TeamMember{Login: p})
  1822  	}
  1823  	return ret, nil
  1824  }
  1825  
  1826  func (c fakeDumpClient) ListOrgMembers(name, role string) ([]github.TeamMember, error) {
  1827  	switch {
  1828  	case name != c.name:
  1829  		return nil, fmt.Errorf("bad org: %s", name)
  1830  	case role == github.RoleAdmin:
  1831  		return c.makeMembers(c.admins)
  1832  	case role == github.RoleMember:
  1833  		return c.makeMembers(c.members)
  1834  	}
  1835  	return nil, fmt.Errorf("bad role: %s", role)
  1836  }
  1837  
  1838  func (c fakeDumpClient) ListTeams(name string) ([]github.Team, error) {
  1839  	if name != c.name {
  1840  		return nil, fmt.Errorf("bad org: %s", name)
  1841  	}
  1842  
  1843  	for _, t := range c.teams {
  1844  		if t.Name == "fail" {
  1845  			return nil, errors.New("injected ListTeams error")
  1846  		}
  1847  	}
  1848  	return c.teams, nil
  1849  }
  1850  
  1851  func (c fakeDumpClient) ListTeamMembers(id int, role string) ([]github.TeamMember, error) {
  1852  	var mapping map[int][]string
  1853  	switch {
  1854  	case id < 0:
  1855  		return nil, errors.New("injected ListTeamMembers error")
  1856  	case role == github.RoleMaintainer:
  1857  		mapping = c.maintainers
  1858  	case role == github.RoleMember:
  1859  		mapping = c.teamMembers
  1860  	default:
  1861  		return nil, fmt.Errorf("bad role: %s", role)
  1862  	}
  1863  	people, ok := mapping[id]
  1864  	if !ok {
  1865  		return nil, fmt.Errorf("team does not exist: %d", id)
  1866  	}
  1867  	return c.makeMembers(people)
  1868  }
  1869  
  1870  func fixup(ret *org.Config) {
  1871  	if ret == nil {
  1872  		return
  1873  	}
  1874  	sort.Strings(ret.Members)
  1875  	sort.Strings(ret.Admins)
  1876  	for name, team := range ret.Teams {
  1877  		sort.Strings(team.Members)
  1878  		sort.Strings(team.Maintainers)
  1879  		sort.Strings(team.Previously)
  1880  		ret.Teams[name] = team
  1881  	}
  1882  }