github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/branchprotector/protect_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  	"fmt"
    22  	"reflect"
    23  	"sort"
    24  	"strings"
    25  	"testing"
    26  
    27  	"k8s.io/apimachinery/pkg/util/diff"
    28  	"sigs.k8s.io/yaml"
    29  
    30  	"k8s.io/test-infra/prow/config"
    31  	"k8s.io/test-infra/prow/flagutil"
    32  	"k8s.io/test-infra/prow/github"
    33  )
    34  
    35  func TestOptions_Validate(t *testing.T) {
    36  	var testCases = []struct {
    37  		name        string
    38  		opt         options
    39  		expectedErr bool
    40  	}{
    41  		{
    42  			name: "all ok",
    43  			opt: options{
    44  				config: "dummy",
    45  				github: flagutil.GitHubOptions{TokenPath: "fake"},
    46  			},
    47  			expectedErr: false,
    48  		},
    49  		{
    50  			name: "no config",
    51  			opt: options{
    52  				config: "",
    53  				github: flagutil.GitHubOptions{TokenPath: "fake"},
    54  			},
    55  			expectedErr: true,
    56  		},
    57  		{
    58  			name: "no token, allow",
    59  			opt: options{
    60  				config: "dummy",
    61  			},
    62  			expectedErr: false,
    63  		},
    64  	}
    65  
    66  	for _, testCase := range testCases {
    67  		err := testCase.opt.Validate()
    68  		if testCase.expectedErr && err == nil {
    69  			t.Errorf("%s: expected an error but got none", testCase.name)
    70  		}
    71  		if !testCase.expectedErr && err != nil {
    72  			t.Errorf("%s: expected no error but got one: %v", testCase.name, err)
    73  		}
    74  	}
    75  }
    76  
    77  type fakeClient struct {
    78  	repos    map[string][]github.Repo
    79  	branches map[string][]github.Branch
    80  	deleted  map[string]bool
    81  	updated  map[string]github.BranchProtectionRequest
    82  }
    83  
    84  func (c fakeClient) GetRepo(org string, repo string) (github.Repo, error) {
    85  	r, ok := c.repos[org]
    86  	if !ok {
    87  		return github.Repo{}, fmt.Errorf("Unknown org: %s", org)
    88  	}
    89  	for _, item := range r {
    90  		if item.Name == repo {
    91  			return item, nil
    92  		}
    93  	}
    94  	return github.Repo{}, fmt.Errorf("Unknown repo: %s", repo)
    95  }
    96  
    97  func (c fakeClient) GetRepos(org string, user bool) ([]github.Repo, error) {
    98  	r, ok := c.repos[org]
    99  	if !ok {
   100  		return nil, fmt.Errorf("Unknown org: %s", org)
   101  	}
   102  	return r, nil
   103  }
   104  
   105  func (c fakeClient) GetBranches(org, repo string, onlyProtected bool) ([]github.Branch, error) {
   106  	b, ok := c.branches[org+"/"+repo]
   107  	if !ok {
   108  		return nil, fmt.Errorf("Unknown repo: %s/%s", org, repo)
   109  	}
   110  	var out []github.Branch
   111  	if onlyProtected {
   112  		for _, item := range b {
   113  			if !item.Protected {
   114  				continue
   115  			}
   116  			out = append(out, item)
   117  		}
   118  	} else {
   119  		// when !onlyProtected, github does not set Protected
   120  		// match that behavior here to ensure we handle this correctly
   121  		for _, item := range b {
   122  			item.Protected = false
   123  			out = append(out, item)
   124  		}
   125  	}
   126  	return b, nil
   127  }
   128  
   129  func (c *fakeClient) UpdateBranchProtection(org, repo, branch string, config github.BranchProtectionRequest) error {
   130  	if branch == "error" {
   131  		return errors.New("failed to update branch protection")
   132  	}
   133  	if c.updated == nil {
   134  		c.updated = map[string]github.BranchProtectionRequest{}
   135  	}
   136  	ctx := org + "/" + repo + "=" + branch
   137  	c.updated[ctx] = config
   138  	return nil
   139  }
   140  
   141  func (c *fakeClient) RemoveBranchProtection(org, repo, branch string) error {
   142  	if branch == "error" {
   143  		return errors.New("failed to remove branch protection")
   144  	}
   145  	if c.deleted == nil {
   146  		c.deleted = map[string]bool{}
   147  	}
   148  	ctx := org + "/" + repo + "=" + branch
   149  	c.deleted[ctx] = true
   150  	return nil
   151  }
   152  
   153  func TestConfigureBranches(t *testing.T) {
   154  	yes := true
   155  
   156  	prot := github.BranchProtectionRequest{}
   157  	diffprot := github.BranchProtectionRequest{
   158  		EnforceAdmins: &yes,
   159  	}
   160  
   161  	cases := []struct {
   162  		name    string
   163  		updates []requirements
   164  		deletes map[string]bool
   165  		sets    map[string]github.BranchProtectionRequest
   166  		errors  int
   167  	}{
   168  		{
   169  			name: "remove-protection",
   170  			updates: []requirements{
   171  				{Org: "one", Repo: "1", Branch: "delete", Request: nil},
   172  				{Org: "one", Repo: "1", Branch: "remove", Request: nil},
   173  				{Org: "two", Repo: "2", Branch: "remove", Request: nil},
   174  			},
   175  			deletes: map[string]bool{
   176  				"one/1=delete": true,
   177  				"one/1=remove": true,
   178  				"two/2=remove": true,
   179  			},
   180  		},
   181  		{
   182  			name: "error-remove-protection",
   183  			updates: []requirements{
   184  				{Org: "one", Repo: "1", Branch: "error", Request: nil},
   185  			},
   186  			errors: 1,
   187  		},
   188  		{
   189  			name: "update-protection-context",
   190  			updates: []requirements{
   191  				{
   192  					Org:     "one",
   193  					Repo:    "1",
   194  					Branch:  "master",
   195  					Request: &prot,
   196  				},
   197  				{
   198  					Org:     "one",
   199  					Repo:    "1",
   200  					Branch:  "other",
   201  					Request: &diffprot,
   202  				},
   203  			},
   204  			sets: map[string]github.BranchProtectionRequest{
   205  				"one/1=master": prot,
   206  				"one/1=other":  diffprot,
   207  			},
   208  		},
   209  		{
   210  			name: "complex",
   211  			updates: []requirements{
   212  				{Org: "update", Repo: "1", Branch: "master", Request: &prot},
   213  				{Org: "update", Repo: "2", Branch: "error", Request: &prot},
   214  				{Org: "remove", Repo: "3", Branch: "master", Request: nil},
   215  				{Org: "remove", Repo: "4", Branch: "error", Request: nil},
   216  			},
   217  			errors: 2, // four and five
   218  			deletes: map[string]bool{
   219  				"remove/3=master": true,
   220  			},
   221  			sets: map[string]github.BranchProtectionRequest{
   222  				"update/1=master": prot,
   223  			},
   224  		},
   225  	}
   226  
   227  	for _, tc := range cases {
   228  		fc := fakeClient{}
   229  		p := protector{
   230  			client:  &fc,
   231  			updates: make(chan requirements),
   232  			done:    make(chan []error),
   233  		}
   234  		go p.configureBranches()
   235  		for _, u := range tc.updates {
   236  			p.updates <- u
   237  		}
   238  		close(p.updates)
   239  		errs := <-p.done
   240  		if len(errs) != tc.errors {
   241  			t.Errorf("%s: %d errors != expected %d: %v", tc.name, len(errs), tc.errors, errs)
   242  		}
   243  		if !reflect.DeepEqual(fc.deleted, tc.deletes) {
   244  			t.Errorf("%s: deletes %v != expected %v", tc.name, fc.deleted, tc.deletes)
   245  		}
   246  		if !reflect.DeepEqual(fc.updated, tc.sets) {
   247  			t.Errorf("%s: updates %v != expected %v", tc.name, fc.updated, tc.sets)
   248  		}
   249  
   250  	}
   251  }
   252  
   253  func split(branch string) (string, string, string) {
   254  	parts := strings.Split(branch, "=")
   255  	b := parts[1]
   256  	parts = strings.Split(parts[0], "/")
   257  	return parts[0], parts[1], b
   258  }
   259  
   260  func TestProtect(t *testing.T) {
   261  	yes := true
   262  
   263  	cases := []struct {
   264  		name             string
   265  		branches         []string
   266  		startUnprotected bool
   267  		config           string
   268  		archived         string
   269  		expected         []requirements
   270  		errors           int
   271  	}{
   272  		{
   273  			name: "nothing",
   274  		},
   275  		{
   276  			name: "unknown org",
   277  			config: `
   278  branch-protection:
   279    protect: true
   280    orgs:
   281      unknown:
   282  `,
   283  			errors: 1,
   284  		},
   285  		{
   286  			name:     "protect org via config default",
   287  			branches: []string{"cfgdef/repo1=master", "cfgdef/repo1=branch", "cfgdef/repo2=master"},
   288  			config: `
   289  branch-protection:
   290    protect: true
   291    orgs:
   292      cfgdef:
   293  `,
   294  			expected: []requirements{
   295  				{
   296  					Org:     "cfgdef",
   297  					Repo:    "repo1",
   298  					Branch:  "master",
   299  					Request: &github.BranchProtectionRequest{},
   300  				},
   301  				{
   302  					Org:     "cfgdef",
   303  					Repo:    "repo1",
   304  					Branch:  "branch",
   305  					Request: &github.BranchProtectionRequest{},
   306  				},
   307  				{
   308  					Org:     "cfgdef",
   309  					Repo:    "repo2",
   310  					Branch:  "master",
   311  					Request: &github.BranchProtectionRequest{},
   312  				},
   313  			},
   314  		},
   315  		{
   316  			name:     "protect this but not that org",
   317  			branches: []string{"this/yes=master", "that/no=master"},
   318  			config: `
   319  branch-protection:
   320    protect: false
   321    orgs:
   322      this:
   323        protect: true
   324      that:
   325  `,
   326  			expected: []requirements{
   327  				{
   328  					Org:     "this",
   329  					Repo:    "yes",
   330  					Branch:  "master",
   331  					Request: &github.BranchProtectionRequest{},
   332  				},
   333  				{
   334  					Org:     "that",
   335  					Repo:    "no",
   336  					Branch:  "master",
   337  					Request: nil,
   338  				},
   339  			},
   340  		},
   341  		{
   342  			name:     "protect all repos when protection configured at org level",
   343  			branches: []string{"kubernetes/test-infra=master", "kubernetes/publishing-bot=master"},
   344  			config: `
   345  branch-protection:
   346    orgs:
   347      kubernetes:
   348        protect: true
   349        repos:
   350          test-infra:
   351            required_status_checks:
   352              contexts:
   353              - hello-world
   354  `,
   355  			expected: []requirements{
   356  				{
   357  					Org:    "kubernetes",
   358  					Repo:   "test-infra",
   359  					Branch: "master",
   360  					Request: &github.BranchProtectionRequest{
   361  						RequiredStatusChecks: &github.RequiredStatusChecks{
   362  							Contexts: []string{"hello-world"},
   363  						},
   364  					},
   365  				},
   366  				{
   367  					Org:     "kubernetes",
   368  					Repo:    "publishing-bot",
   369  					Branch:  "master",
   370  					Request: &github.BranchProtectionRequest{},
   371  				},
   372  			},
   373  		},
   374  		{
   375  			name:     "require a defined branch to make a protection decision",
   376  			branches: []string{"org/repo=branch"},
   377  			config: `
   378  branch-protection:
   379    orgs:
   380      org:
   381        repos:
   382          repo:
   383            branches:
   384              branch: # empty
   385  `,
   386  			errors: 1,
   387  		},
   388  		{
   389  			name:     "require pushers to set protection",
   390  			branches: []string{"org/repo=push"},
   391  			config: `
   392  branch-protection:
   393    protect: false
   394    restrictions:
   395      teams:
   396      - oncall
   397    orgs:
   398      org:
   399  `,
   400  			errors: 1,
   401  		},
   402  		{
   403  			name:     "required contexts must set protection",
   404  			branches: []string{"org/repo=context"},
   405  			config: `
   406  branch-protection:
   407    protect: false
   408    required_status_checks:
   409      contexts:
   410      - test-foo
   411    orgs:
   412      org:
   413  `,
   414  			errors: 1,
   415  		},
   416  		{
   417  			name:     "protect org but skip a repo",
   418  			branches: []string{"org/repo1=master", "org/repo1=branch", "org/skip=master"},
   419  			config: `
   420  branch-protection:
   421    protect: false
   422    orgs:
   423      org:
   424        protect: true
   425        repos:
   426          skip:
   427            protect: false
   428  `,
   429  			expected: []requirements{
   430  				{
   431  					Org:     "org",
   432  					Repo:    "repo1",
   433  					Branch:  "master",
   434  					Request: &github.BranchProtectionRequest{},
   435  				},
   436  				{
   437  					Org:     "org",
   438  					Repo:    "repo1",
   439  					Branch:  "branch",
   440  					Request: &github.BranchProtectionRequest{},
   441  				},
   442  				{
   443  					Org:     "org",
   444  					Repo:    "skip",
   445  					Branch:  "master",
   446  					Request: nil,
   447  				},
   448  			},
   449  		},
   450  		{
   451  			name:     "protect org but skip a repo due to archival",
   452  			branches: []string{"org/repo1=master", "org/repo1=branch", "org/skip=master"},
   453  			config: `
   454  branch-protection:
   455    protect: false
   456    orgs:
   457      org:
   458        protect: true
   459  `,
   460  			archived: "skip",
   461  			expected: []requirements{
   462  				{
   463  					Org:     "org",
   464  					Repo:    "repo1",
   465  					Branch:  "master",
   466  					Request: &github.BranchProtectionRequest{},
   467  				},
   468  				{
   469  					Org:     "org",
   470  					Repo:    "repo1",
   471  					Branch:  "branch",
   472  					Request: &github.BranchProtectionRequest{},
   473  				},
   474  			},
   475  		},
   476  		{
   477  			name:     "collapse duplicated contexts",
   478  			branches: []string{"org/repo=master"},
   479  			config: `
   480  branch-protection:
   481    protect: true
   482    required_status_checks:
   483      contexts:
   484      - hello-world
   485      - duplicate-context
   486      - duplicate-context
   487      - hello-world
   488    orgs:
   489      org:
   490  `,
   491  			expected: []requirements{
   492  				{
   493  					Org:    "org",
   494  					Repo:   "repo",
   495  					Branch: "master",
   496  					Request: &github.BranchProtectionRequest{
   497  						RequiredStatusChecks: &github.RequiredStatusChecks{
   498  							Contexts: []string{"duplicate-context", "hello-world"},
   499  						},
   500  					},
   501  				},
   502  			},
   503  		},
   504  		{
   505  			name:     "append contexts",
   506  			branches: []string{"org/repo=master"},
   507  			config: `
   508  branch-protection:
   509    protect: true
   510    required_status_checks:
   511      contexts:
   512      - config-presubmit
   513    orgs:
   514      org:
   515        required_status_checks:
   516          contexts:
   517          - org-presubmit
   518        repos:
   519          repo:
   520            required_status_checks:
   521              contexts:
   522              - repo-presubmit
   523            branches:
   524              master:
   525                required_status_checks:
   526                  contexts:
   527                  - branch-presubmit
   528  `,
   529  			expected: []requirements{
   530  				{
   531  					Org:    "org",
   532  					Repo:   "repo",
   533  					Branch: "master",
   534  					Request: &github.BranchProtectionRequest{
   535  						RequiredStatusChecks: &github.RequiredStatusChecks{
   536  							Contexts: []string{"config-presubmit", "org-presubmit", "repo-presubmit", "branch-presubmit"},
   537  						},
   538  					},
   539  				},
   540  			},
   541  		},
   542  		{
   543  			name:     "append pushers",
   544  			branches: []string{"org/repo=master"},
   545  			config: `
   546  branch-protection:
   547    protect: true
   548    restrictions:
   549      teams:
   550      - config-team
   551    orgs:
   552      org:
   553        restrictions:
   554          teams:
   555          - org-team
   556        repos:
   557          repo:
   558            restrictions:
   559              teams:
   560              - repo-team
   561            branches:
   562              master:
   563                restrictions:
   564                  teams:
   565                  - branch-team
   566  `,
   567  			expected: []requirements{
   568  				{
   569  					Org:    "org",
   570  					Repo:   "repo",
   571  					Branch: "master",
   572  					Request: &github.BranchProtectionRequest{
   573  						Restrictions: &github.Restrictions{
   574  							Users: &[]string{},
   575  							Teams: &[]string{"config-team", "org-team", "repo-team", "branch-team"},
   576  						},
   577  					},
   578  				},
   579  			},
   580  		},
   581  		{
   582  			name:     "all modern fields",
   583  			branches: []string{"all/modern=master"},
   584  			config: `
   585  branch-protection:
   586    protect: true
   587    enforce_admins: true
   588    required_status_checks:
   589      contexts:
   590      - config-presubmit
   591      strict: true
   592    required_pull_request_reviews:
   593      required_approving_review_count: 3
   594      dismiss_stale: false
   595      require_code_owner_reviews: true
   596      dismissal_restrictions:
   597        users:
   598        - bob
   599        - jane
   600        teams:
   601        - oncall
   602        - sres
   603    restrictions:
   604      teams:
   605      - config-team
   606      users:
   607      - cindy
   608    orgs:
   609      all:
   610        required_status_checks:
   611          contexts:
   612          - org-presubmit
   613        restrictions:
   614          teams:
   615          - org-team
   616  `,
   617  			expected: []requirements{
   618  				{
   619  					Org:    "all",
   620  					Repo:   "modern",
   621  					Branch: "master",
   622  					Request: &github.BranchProtectionRequest{
   623  						EnforceAdmins: &yes,
   624  						RequiredStatusChecks: &github.RequiredStatusChecks{
   625  							Strict:   true,
   626  							Contexts: []string{"config-presubmit", "org-presubmit"},
   627  						},
   628  						RequiredPullRequestReviews: &github.RequiredPullRequestReviews{
   629  							DismissStaleReviews:          false,
   630  							RequireCodeOwnerReviews:      true,
   631  							RequiredApprovingReviewCount: 3,
   632  							DismissalRestrictions: github.Restrictions{
   633  								Users: &[]string{"bob", "jane"},
   634  								Teams: &[]string{"oncall", "sres"},
   635  							},
   636  						},
   637  						Restrictions: &github.Restrictions{
   638  							Users: &[]string{"cindy"},
   639  							Teams: &[]string{"config-team", "org-team"},
   640  						},
   641  					},
   642  				},
   643  			},
   644  		},
   645  		{
   646  			name:     "child cannot disable parent policy by default",
   647  			branches: []string{"parent/child=unprotected"},
   648  			config: `
   649  branch-protection:
   650    protect: true
   651    enforce_admins: true
   652    orgs:
   653      parent:
   654        protect: false
   655  `,
   656  			errors: 1,
   657  		},
   658  		{
   659  			name:     "child disables parent",
   660  			branches: []string{"parent/child=unprotected"},
   661  			config: `
   662  branch-protection:
   663    allow_disabled_policies: true
   664    protect: true
   665    enforce_admins: true
   666    orgs:
   667      parent:
   668        protect: false
   669  `,
   670  			expected: []requirements{
   671  				{
   672  					Org:    "parent",
   673  					Repo:   "child",
   674  					Branch: "unprotected",
   675  				},
   676  			},
   677  		},
   678  		{
   679  			name:     "do not unprotect unprotected",
   680  			branches: []string{"protect/update=master", "unprotected/skip=master"},
   681  			config: `
   682  branch-protection:
   683    protect: true
   684    orgs:
   685      protect:
   686        protect: true
   687      unprotected:
   688        protect: false
   689  `,
   690  			startUnprotected: true,
   691  			expected: []requirements{
   692  				{
   693  					Org:     "protect",
   694  					Repo:    "update",
   695  					Branch:  "master",
   696  					Request: &github.BranchProtectionRequest{},
   697  				},
   698  			},
   699  		},
   700  	}
   701  
   702  	for _, tc := range cases {
   703  		t.Run(tc.name, func(t *testing.T) {
   704  			repos := map[string]map[string]bool{}
   705  			branches := map[string][]github.Branch{}
   706  			for _, b := range tc.branches {
   707  				org, repo, branch := split(b)
   708  				k := org + "/" + repo
   709  				branches[k] = append(branches[k], github.Branch{
   710  					Name:      branch,
   711  					Protected: !tc.startUnprotected,
   712  				})
   713  				r := repos[org]
   714  				if r == nil {
   715  					repos[org] = make(map[string]bool)
   716  				}
   717  				repos[org][repo] = true
   718  			}
   719  			fc := fakeClient{
   720  				branches: branches,
   721  				repos:    map[string][]github.Repo{},
   722  			}
   723  			for org, r := range repos {
   724  				for rname := range r {
   725  					fc.repos[org] = append(fc.repos[org], github.Repo{Name: rname, FullName: org + "/" + rname, Archived: rname == tc.archived})
   726  				}
   727  			}
   728  
   729  			var cfg config.Config
   730  			if err := yaml.Unmarshal([]byte(tc.config), &cfg); err != nil {
   731  				t.Fatalf("failed to parse config: %v", err)
   732  			}
   733  			p := protector{
   734  				client:         &fc,
   735  				cfg:            &cfg,
   736  				errors:         Errors{},
   737  				updates:        make(chan requirements),
   738  				done:           make(chan []error),
   739  				completedRepos: make(map[string]bool),
   740  			}
   741  			go func() {
   742  				p.protect()
   743  				close(p.updates)
   744  			}()
   745  
   746  			var actual []requirements
   747  			for r := range p.updates {
   748  				actual = append(actual, r)
   749  			}
   750  			errors := p.errors.errs
   751  			if len(errors) != tc.errors {
   752  				t.Errorf("actual errors %d != expected %d: %v", len(errors), tc.errors, errors)
   753  			}
   754  			switch {
   755  			case len(actual) != len(tc.expected):
   756  				t.Errorf("%+v %+v", cfg.BranchProtection, actual)
   757  				t.Errorf("actual updates %v != expected %v", actual, tc.expected)
   758  			default:
   759  				for _, a := range actual {
   760  					found := false
   761  					for _, e := range tc.expected {
   762  						if e.Org == a.Org && e.Repo == a.Repo && e.Branch == a.Branch {
   763  							found = true
   764  							fixup(&a)
   765  							fixup(&e)
   766  							if !reflect.DeepEqual(e, a) {
   767  								t.Errorf("actual != expected: %s", diff.ObjectDiff(a.Request, e.Request))
   768  							}
   769  							break
   770  						}
   771  					}
   772  					if !found {
   773  						t.Errorf("actual updates %v not in expected %v", a, tc.expected)
   774  					}
   775  				}
   776  			}
   777  		})
   778  	}
   779  }
   780  
   781  func fixup(r *requirements) {
   782  	if r == nil || r.Request == nil {
   783  		return
   784  	}
   785  	req := r.Request
   786  	if req.RequiredStatusChecks != nil {
   787  		sort.Strings(req.RequiredStatusChecks.Contexts)
   788  	}
   789  	if restr := req.Restrictions; restr != nil {
   790  		sort.Strings(*restr.Teams)
   791  		sort.Strings(*restr.Users)
   792  	}
   793  }
   794  
   795  func TestIgnoreArchivedRepos(t *testing.T) {
   796  	repos := map[string]map[string]bool{}
   797  	branches := map[string][]github.Branch{}
   798  	org, repo, branch := "organization", "repository", "branch"
   799  	k := org + "/" + repo
   800  	branches[k] = append(branches[k], github.Branch{
   801  		Name: branch,
   802  	})
   803  	r := repos[org]
   804  	if r == nil {
   805  		repos[org] = make(map[string]bool)
   806  	}
   807  	repos[org][repo] = true
   808  	fc := fakeClient{
   809  		branches: branches,
   810  		repos:    map[string][]github.Repo{},
   811  	}
   812  	for org, r := range repos {
   813  		for rname := range r {
   814  			fc.repos[org] = append(fc.repos[org], github.Repo{Name: rname, FullName: org + "/" + rname, Archived: true})
   815  		}
   816  	}
   817  
   818  	var cfg config.Config
   819  	if err := yaml.Unmarshal([]byte(`
   820  branch-protection:
   821    protect-by-default: true
   822    orgs:
   823      organization:
   824  `), &cfg); err != nil {
   825  		t.Fatalf("failed to parse config: %v", err)
   826  	}
   827  	p := protector{
   828  		client:         &fc,
   829  		cfg:            &cfg,
   830  		errors:         Errors{},
   831  		updates:        make(chan requirements),
   832  		done:           make(chan []error),
   833  		completedRepos: make(map[string]bool),
   834  	}
   835  	go func() {
   836  		p.protect()
   837  		close(p.updates)
   838  	}()
   839  
   840  	protectionErrors := p.errors.errs
   841  	if len(protectionErrors) != 0 {
   842  		t.Errorf("expected no errors, got %d errors: %v", len(protectionErrors), protectionErrors)
   843  	}
   844  	var actual []requirements
   845  	for r := range p.updates {
   846  		actual = append(actual, r)
   847  	}
   848  	if len(actual) != 0 {
   849  		t.Errorf("expected no updates, got: %v", actual)
   850  	}
   851  }