<commit_before>package main

import (
	"bytes"
	"fmt"
	"log"
	"strings"
	"text/template"

	"github.com/google/go-github/github"
	"github.com/hashicorp/go-multierror"
	"github.com/xoebus/go-tracker"
)

var storyStateCommentTemplate = template.Must(
	template.New("story-state").Parse(
		`Hi there!

We use Pivotal Tracker to provide visibility into what our team is working on. A story for this issue has been automatically created.

The current status is as follows:

{{range .}}* [{{if eq .State "accepted"}}x{{else}} {{end}}] [#{{.ID}}]({{.URL}}) {{.Name}}
{{end}}

This comment, as well as the labels on the story, will be automatically updated as the status in Tracker changes.`,
	),
)

var issueClosedCommentTemplate = template.Must(
	template.New("issue-closed").Parse(
		`Hello again!

All stories related to this issue have been accepted, so I'm going to automatically close this issue.

At the time of writing, the following stories have been accepted:

{{range .}}* [#{{.ID}}]({{.URL}}) {{.Name}}
{{end}}

If you feel there is still more to be done or have any questions, feel free to reopen!`),
)

type Syncer struct {
	GithubClient  *github.Client
	ProjectClient tracker.ProjectClient

	OrganizationName string

	cachedUser *github.User
}

func (syncer *Syncer) SyncIssuesAndStories() error {
	repos, err := syncer.allRepos()
	if err != nil {
		return fmt.Errorf("failed to fetch repos: %s", err)
	}

	var multiErr *multierror.Error

	for _, repo := range repos {
		repoName := *repo.Owner.Login + "/" + *repo.Name

		log.Println("syncing", repoName)

		if err := syncer.syncRepoStockLabels(repo); err != nil {
			log.Printf("failed setting up labels; skipping %s: %s\n", repoName, err)
			continue
		}

		if err := syncer.processRepoIssues(repo); err != nil {
			multiErr = multierror.Append(
				multiErr,
				fmt.Errorf("errors when processing %s: %s", repoName, err),
			)
		}
	}

	return multiErr.ErrorOrNil()
}

func (syncer *Syncer) processRepoIssues(repo github.Repository) error {
	issues, err := syncer.allIssues(repo)
	if err != nil {
		return fmt.Errorf("failed to fetch issues for %s: %s", *repo.Name, err)
	}

	var multiErr *multierror.Error

	for _, issue := range issues {
		label := trackerLabelForIssue(repo, issue)

		err := syncer.ensureStoryExistsForIssue(repo, issue, label)
		if err != nil {
			multiErr = multierror.Append(
				multiErr,
				fmt.Errorf("failed to create story for issue %s: %s", label, err),
			)
		}
	}

	return multiErr.ErrorOrNil()
}

func (syncer *Syncer) syncRepoStockLabels(repo github.Repository) error {
	logName := *repo.Owner.Login + "/" + *repo.Name

	existingLabels, _, err := syncer.GithubClient.Issues.ListLabels(
		*repo.Owner.Login,
		*repo.Name,
		&github.ListOptions{},
	)
	if err != nil {
		return fmt.Errorf("failed to list labels for %s: %s", logName, err)
	}

	missingLabels := map[string]string{}
	for label, color := range stockLabels {
		missingLabels[label] = color
	}

	for _, label := range existingLabels {
		color, found := stockLabels[*label.Name]
		if !found {
			continue
		}

		delete(missingLabels, *label.Name)

		if len(color) == 0 {
			// respect existing color
			continue
		}

		if color == *label.Color {
			// color already in sync; skip
			continue
		}

		log.Printf("updating label '%s' in repo %s\n", *label.Name, logName)

		_, _, err := syncer.GithubClient.Issues.EditLabel(
			*repo.Owner.Login,
			*repo.Name,
			*label.Name,
			&github.Label{
				Name:  label.Name,
				Color: &color,
			},
		)
		if err != nil {
			return fmt.Errorf("failed to update label '%s' in %s: %s", *label.Name, logName, err)
		}
	}

	for name, color := range missingLabels {
		log.Printf("creating label '%s' in repo %s\n", name, logName)

		_, _, err := syncer.GithubClient.Issues.CreateLabel(
			*repo.Owner.Login,
			*repo.Name,
			&github.Label{
				Name:  &name,
				Color: &color,
			},
		)
		if err != nil {
			return fmt.Errorf("failed to create label '%s' in %s: %s", name, logName, err)
		}
	}

	return nil
}

func (syncer *Syncer) ensureStoryExistsForIssue(
	repo github.Repository,
	issue github.Issue,
	label string,
) error {
	var allStories StorySet

	query := tracker.StoriesQuery{
		Label: label,
	}

	for {
		stories, _, err := syncer.ProjectClient.Stories(query)
		if err != nil {
			return fmt.Errorf("failed to search for stories %s: %s", label, err)
		}

		if len(stories) == 0 {
			break
		}

		allStories = append(allStories, stories...)

		query.Offset = len(allStories)
	}

	if len(allStories) == 0 {
		// no stories for the issue yet; create an initial one

		story := choreForNewIssue(label, issue)

		createdStory, err := syncer.ProjectClient.CreateStory(story)
		if err != nil {
			return fmt.Errorf("failed to create story for %s: %s", label, err)
		}

		log.Println("created story for", label, "at", createdStory.URL)

		allStories = append(allStories, createdStory)

	} else if allStories.AllAccepted() && issue.UpdatedAt.After(allStories.LastAccepted()) {
		// issue has been reopened

		story := choreForReopenedIssue(label, issue)

		createdStory, err := syncer.ProjectClient.CreateStory(story)
		if err != nil {
			return fmt.Errorf("failed to create story for %s: %s", label, err)
		}

		log.Println("created chore for reopening of", label, "at", createdStory.URL)

		allStories = append(allStories, createdStory)
	}

	if err := syncer.ensureCommentWithStories(repo, issue, allStories); err != nil {
		return fmt.Errorf("failed to upsert comment for stories: %s", err)
	}

	if err := syncer.syncLabels(repo, issue, allStories.IssueLabels()); err != nil {
		return fmt.Errorf("failed to sync story labels: %s", err)
	}

	if allStories.AllAccepted() {
		log.Println("all stories for", label, "are accepted; closing!")

		err := syncer.closeIssue(repo, issue, allStories)
		if err != nil {
			return fmt.Errorf("failed to close issue: %s", err)
		}
	}

	return nil
}

func (syncer *Syncer) ensureCommentWithStories(
	repo github.Repository,
	issue github.Issue,
	allStories []tracker.Story,
) error {
	comments, err := syncer.allCommentsForIssue(repo, issue)
	if err != nil {
		return fmt.Errorf("failed to fetch issue comments: %s", err)
	}

	currentUser, err := syncer.currentUser()
	if err != nil {
		return fmt.Errorf("failed to get current user: %s", err)
	}

	var existingComment *github.IssueComment
	for _, comment := range comments {
		if *comment.User.ID == *currentUser.ID {
			existingComment = &comment
			break
		}
	}

	buf := new(bytes.Buffer)
	if err := storyStateCommentTemplate.Execute(buf, allStories); err != nil {
		return fmt.Errorf("error building comment body: %s", err)
	}

	commentBody := buf.String()

	if existingComment == nil {
		createdComment, _, err := syncer.GithubClient.Issues.CreateComment(
			*repo.Owner.Login,
			*repo.Name,
			*issue.Number,
			&github.IssueComment{Body: &commentBody},
		)
		if err != nil {
			return fmt.Errorf("failed to create comment: %s", err)
		}

		log.Println("created comment:", *createdComment.HTMLURL)
	} else if *existingComment.Body != commentBody {
		existingComment.Body = &commentBody

		updatedComment, _, err := syncer.GithubClient.Issues.EditComment(
			*repo.Owner.Login,
			*repo.Name,
			*existingComment.ID,
			&github.IssueComment{Body: &commentBody},
		)
		if err != nil {
			return fmt.Errorf("failed to update comment: %s", err)
		}

		log.Println("updated comment:", *updatedComment.HTMLURL)
	}

	return nil
}

func (syncer *Syncer) syncLabels(
	repo github.Repository,
	issue github.Issue,
	labels []string,
) error {
	existingLabels := map[string]bool{}
	for _, label := range issue.Labels {
		existingLabels[*label.Name] = true
	}

	labelsToAdd := []string{}
	for _, label := range labels {
		if !existingLabels[label] {
			labelsToAdd = append(labelsToAdd, label)
		}
	}

	labelsToRemove := []string{}
	for stockLabel, _ := range stockLabels {
		if !existingLabels[stockLabel] {
			continue
		}

		stillHasLabel := false
		for _, label := range labels {
			if label == stockLabel {
				stillHasLabel = true
				break
			}
		}

		if !stillHasLabel {
			labelsToRemove = append(labelsToRemove, stockLabel)
		}
	}

	if len(labelsToRemove) == 0 && len(labelsToAdd) == 0 {
		return nil
	}

	log.Println("setting issue labels:", strings.Join(labels, ", "))

	for _, label := range labelsToRemove {
		_, err := syncer.GithubClient.Issues.RemoveLabelForIssue(
			*repo.Owner.Login,
			*repo.Name,
			*issue.Number,
			label,
		)
		if err != nil && !strings.Contains(err.Error(), "404") {
			return fmt.Errorf("failed to remove label '%s': %s", label, err)
		}
	}

	_, _, err := syncer.GithubClient.Issues.AddLabelsToIssue(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		labelsToAdd,
	)
	if err != nil {
		return fmt.Errorf("failed to add labels to issue: %s", err)
	}

	return nil
}

func (syncer *Syncer) closeIssue(
	repo github.Repository,
	issue github.Issue,
	stories StorySet,
) error {
	buf := new(bytes.Buffer)
	if err := issueClosedCommentTemplate.Execute(buf, stories); err != nil {
		return fmt.Errorf("error building comment body: %s", err)
	}

	closedMessage := buf.String()

	_, _, err := syncer.GithubClient.Issues.CreateComment(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		&github.IssueComment{Body: &closedMessage},
	)
	if err != nil {
		return fmt.Errorf("failed to leave closed message: %s", err)
	}

	state := "closed"
	_, _, err = syncer.GithubClient.Issues.Edit(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		&github.IssueRequest{State: &state},
	)
	if err != nil {
		return fmt.Errorf("failed to close issue: %s", err)
	}

	return err
}

func (syncer *Syncer) currentUser() (*github.User, error) {
	if syncer.cachedUser == nil {
		user, _, err := syncer.GithubClient.Users.Get("")
		if err != nil {
			return nil, err
		}

		syncer.cachedUser = user
	}

	return syncer.cachedUser, nil
}

func trackerLabelForIssue(repo github.Repository, issue github.Issue) string {
	return fmt.Sprintf("%s/%s#%d", *repo.Owner.Login, *repo.Name, *issue.Number)
}

func choreForNewIssue(label string, issue github.Issue) tracker.Story {
	labels := []tracker.Label{
		{Name: label},
	}

	if issue.PullRequestLinks != nil {
		labels = append(labels, tracker.Label{Name: "has-pr"})
	}

	description := fmt.Sprintf(
		"[@%s](%s) opened [%s](%s) on %s:\n\n%s",
		*issue.User.Login,
		*issue.User.HTMLURL,
		label,
		*issue.HTMLURL,
		issue.CreatedAt.Format("January 2"),
		*issue.Body,
	)

	return tracker.Story{
		Name:        *issue.Title,
		Description: description,
		Type:        "chore",
		State:       "unscheduled",
		Labels:      labels,
	}
}

func choreForReopenedIssue(label string, issue github.Issue) tracker.Story {
	labels := []tracker.Label{
		{Name: label},
	}

	description := fmt.Sprintf(
		"[@%s](%s) reopened [%s](%s) on %s",
		*issue.User.Login,
		*issue.User.HTMLURL,
		label,
		*issue.HTMLURL,
		issue.UpdatedAt.Format("January 2"),
	)

	return tracker.Story{
		Name:        "reopened: " + *issue.Title,
		Description: description,
		Type:        "chore",
		State:       "unscheduled",
		Labels:      labels,
	}
}
<commit_msg>story -> issue<commit_after>package main

import (
	"bytes"
	"fmt"
	"log"
	"strings"
	"text/template"

	"github.com/google/go-github/github"
	"github.com/hashicorp/go-multierror"
	"github.com/xoebus/go-tracker"
)

var storyStateCommentTemplate = template.Must(
	template.New("story-state").Parse(
		`Hi there!

We use Pivotal Tracker to provide visibility into what our team is working on. A story for this issue has been automatically created.

The current status is as follows:

{{range .}}* [{{if eq .State "accepted"}}x{{else}} {{end}}] [#{{.ID}}]({{.URL}}) {{.Name}}
{{end}}

This comment, as well as the labels on the issue, will be automatically updated as the status in Tracker changes.`,
	),
)

var issueClosedCommentTemplate = template.Must(
	template.New("issue-closed").Parse(
		`Hello again!

All stories related to this issue have been accepted, so I'm going to automatically close this issue.

At the time of writing, the following stories have been accepted:

{{range .}}* [#{{.ID}}]({{.URL}}) {{.Name}}
{{end}}

If you feel there is still more to be done or have any questions, feel free to reopen!`),
)

type Syncer struct {
	GithubClient  *github.Client
	ProjectClient tracker.ProjectClient

	OrganizationName string

	cachedUser *github.User
}

func (syncer *Syncer) SyncIssuesAndStories() error {
	repos, err := syncer.allRepos()
	if err != nil {
		return fmt.Errorf("failed to fetch repos: %s", err)
	}

	var multiErr *multierror.Error

	for _, repo := range repos {
		repoName := *repo.Owner.Login + "/" + *repo.Name

		log.Println("syncing", repoName)

		if err := syncer.syncRepoStockLabels(repo); err != nil {
			log.Printf("failed setting up labels; skipping %s: %s\n", repoName, err)
			continue
		}

		if err := syncer.processRepoIssues(repo); err != nil {
			multiErr = multierror.Append(
				multiErr,
				fmt.Errorf("errors when processing %s: %s", repoName, err),
			)
		}
	}

	return multiErr.ErrorOrNil()
}

func (syncer *Syncer) processRepoIssues(repo github.Repository) error {
	issues, err := syncer.allIssues(repo)
	if err != nil {
		return fmt.Errorf("failed to fetch issues for %s: %s", *repo.Name, err)
	}

	var multiErr *multierror.Error

	for _, issue := range issues {
		label := trackerLabelForIssue(repo, issue)

		err := syncer.ensureStoryExistsForIssue(repo, issue, label)
		if err != nil {
			multiErr = multierror.Append(
				multiErr,
				fmt.Errorf("failed to create story for issue %s: %s", label, err),
			)
		}
	}

	return multiErr.ErrorOrNil()
}

func (syncer *Syncer) syncRepoStockLabels(repo github.Repository) error {
	logName := *repo.Owner.Login + "/" + *repo.Name

	existingLabels, _, err := syncer.GithubClient.Issues.ListLabels(
		*repo.Owner.Login,
		*repo.Name,
		&github.ListOptions{},
	)
	if err != nil {
		return fmt.Errorf("failed to list labels for %s: %s", logName, err)
	}

	missingLabels := map[string]string{}
	for label, color := range stockLabels {
		missingLabels[label] = color
	}

	for _, label := range existingLabels {
		color, found := stockLabels[*label.Name]
		if !found {
			continue
		}

		delete(missingLabels, *label.Name)

		if len(color) == 0 {
			// respect existing color
			continue
		}

		if color == *label.Color {
			// color already in sync; skip
			continue
		}

		log.Printf("updating label '%s' in repo %s\n", *label.Name, logName)

		_, _, err := syncer.GithubClient.Issues.EditLabel(
			*repo.Owner.Login,
			*repo.Name,
			*label.Name,
			&github.Label{
				Name:  label.Name,
				Color: &color,
			},
		)
		if err != nil {
			return fmt.Errorf("failed to update label '%s' in %s: %s", *label.Name, logName, err)
		}
	}

	for name, color := range missingLabels {
		log.Printf("creating label '%s' in repo %s\n", name, logName)

		_, _, err := syncer.GithubClient.Issues.CreateLabel(
			*repo.Owner.Login,
			*repo.Name,
			&github.Label{
				Name:  &name,
				Color: &color,
			},
		)
		if err != nil {
			return fmt.Errorf("failed to create label '%s' in %s: %s", name, logName, err)
		}
	}

	return nil
}

func (syncer *Syncer) ensureStoryExistsForIssue(
	repo github.Repository,
	issue github.Issue,
	label string,
) error {
	var allStories StorySet

	query := tracker.StoriesQuery{
		Label: label,
	}

	for {
		stories, _, err := syncer.ProjectClient.Stories(query)
		if err != nil {
			return fmt.Errorf("failed to search for stories %s: %s", label, err)
		}

		if len(stories) == 0 {
			break
		}

		allStories = append(allStories, stories...)

		query.Offset = len(allStories)
	}

	if len(allStories) == 0 {
		// no stories for the issue yet; create an initial one

		story := choreForNewIssue(label, issue)

		createdStory, err := syncer.ProjectClient.CreateStory(story)
		if err != nil {
			return fmt.Errorf("failed to create story for %s: %s", label, err)
		}

		log.Println("created story for", label, "at", createdStory.URL)

		allStories = append(allStories, createdStory)

	} else if allStories.AllAccepted() && issue.UpdatedAt.After(allStories.LastAccepted()) {
		// issue has been reopened

		story := choreForReopenedIssue(label, issue)

		createdStory, err := syncer.ProjectClient.CreateStory(story)
		if err != nil {
			return fmt.Errorf("failed to create story for %s: %s", label, err)
		}

		log.Println("created chore for reopening of", label, "at", createdStory.URL)

		allStories = append(allStories, createdStory)
	}

	if err := syncer.ensureCommentWithStories(repo, issue, allStories); err != nil {
		return fmt.Errorf("failed to upsert comment for stories: %s", err)
	}

	if err := syncer.syncLabels(repo, issue, allStories.IssueLabels()); err != nil {
		return fmt.Errorf("failed to sync story labels: %s", err)
	}

	if allStories.AllAccepted() {
		log.Println("all stories for", label, "are accepted; closing!")

		err := syncer.closeIssue(repo, issue, allStories)
		if err != nil {
			return fmt.Errorf("failed to close issue: %s", err)
		}
	}

	return nil
}

func (syncer *Syncer) ensureCommentWithStories(
	repo github.Repository,
	issue github.Issue,
	allStories []tracker.Story,
) error {
	comments, err := syncer.allCommentsForIssue(repo, issue)
	if err != nil {
		return fmt.Errorf("failed to fetch issue comments: %s", err)
	}

	currentUser, err := syncer.currentUser()
	if err != nil {
		return fmt.Errorf("failed to get current user: %s", err)
	}

	var existingComment *github.IssueComment
	for _, comment := range comments {
		if *comment.User.ID == *currentUser.ID {
			existingComment = &comment
			break
		}
	}

	buf := new(bytes.Buffer)
	if err := storyStateCommentTemplate.Execute(buf, allStories); err != nil {
		return fmt.Errorf("error building comment body: %s", err)
	}

	commentBody := buf.String()

	if existingComment == nil {
		createdComment, _, err := syncer.GithubClient.Issues.CreateComment(
			*repo.Owner.Login,
			*repo.Name,
			*issue.Number,
			&github.IssueComment{Body: &commentBody},
		)
		if err != nil {
			return fmt.Errorf("failed to create comment: %s", err)
		}

		log.Println("created comment:", *createdComment.HTMLURL)
	} else if *existingComment.Body != commentBody {
		existingComment.Body = &commentBody

		updatedComment, _, err := syncer.GithubClient.Issues.EditComment(
			*repo.Owner.Login,
			*repo.Name,
			*existingComment.ID,
			&github.IssueComment{Body: &commentBody},
		)
		if err != nil {
			return fmt.Errorf("failed to update comment: %s", err)
		}

		log.Println("updated comment:", *updatedComment.HTMLURL)
	}

	return nil
}

func (syncer *Syncer) syncLabels(
	repo github.Repository,
	issue github.Issue,
	labels []string,
) error {
	existingLabels := map[string]bool{}
	for _, label := range issue.Labels {
		existingLabels[*label.Name] = true
	}

	labelsToAdd := []string{}
	for _, label := range labels {
		if !existingLabels[label] {
			labelsToAdd = append(labelsToAdd, label)
		}
	}

	labelsToRemove := []string{}
	for stockLabel, _ := range stockLabels {
		if !existingLabels[stockLabel] {
			continue
		}

		stillHasLabel := false
		for _, label := range labels {
			if label == stockLabel {
				stillHasLabel = true
				break
			}
		}

		if !stillHasLabel {
			labelsToRemove = append(labelsToRemove, stockLabel)
		}
	}

	if len(labelsToRemove) == 0 && len(labelsToAdd) == 0 {
		return nil
	}

	log.Println("setting issue labels:", strings.Join(labels, ", "))

	for _, label := range labelsToRemove {
		_, err := syncer.GithubClient.Issues.RemoveLabelForIssue(
			*repo.Owner.Login,
			*repo.Name,
			*issue.Number,
			label,
		)
		if err != nil && !strings.Contains(err.Error(), "404") {
			return fmt.Errorf("failed to remove label '%s': %s", label, err)
		}
	}

	_, _, err := syncer.GithubClient.Issues.AddLabelsToIssue(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		labelsToAdd,
	)
	if err != nil {
		return fmt.Errorf("failed to add labels to issue: %s", err)
	}

	return nil
}

func (syncer *Syncer) closeIssue(
	repo github.Repository,
	issue github.Issue,
	stories StorySet,
) error {
	buf := new(bytes.Buffer)
	if err := issueClosedCommentTemplate.Execute(buf, stories); err != nil {
		return fmt.Errorf("error building comment body: %s", err)
	}

	closedMessage := buf.String()

	_, _, err := syncer.GithubClient.Issues.CreateComment(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		&github.IssueComment{Body: &closedMessage},
	)
	if err != nil {
		return fmt.Errorf("failed to leave closed message: %s", err)
	}

	state := "closed"
	_, _, err = syncer.GithubClient.Issues.Edit(
		*repo.Owner.Login,
		*repo.Name,
		*issue.Number,
		&github.IssueRequest{State: &state},
	)
	if err != nil {
		return fmt.Errorf("failed to close issue: %s", err)
	}

	return err
}

func (syncer *Syncer) currentUser() (*github.User, error) {
	if syncer.cachedUser == nil {
		user, _, err := syncer.GithubClient.Users.Get("")
		if err != nil {
			return nil, err
		}

		syncer.cachedUser = user
	}

	return syncer.cachedUser, nil
}

func trackerLabelForIssue(repo github.Repository, issue github.Issue) string {
	return fmt.Sprintf("%s/%s#%d", *repo.Owner.Login, *repo.Name, *issue.Number)
}

func choreForNewIssue(label string, issue github.Issue) tracker.Story {
	labels := []tracker.Label{
		{Name: label},
	}

	if issue.PullRequestLinks != nil {
		labels = append(labels, tracker.Label{Name: "has-pr"})
	}

	description := fmt.Sprintf(
		"[@%s](%s) opened [%s](%s) on %s:\n\n%s",
		*issue.User.Login,
		*issue.User.HTMLURL,
		label,
		*issue.HTMLURL,
		issue.CreatedAt.Format("January 2"),
		*issue.Body,
	)

	return tracker.Story{
		Name:        *issue.Title,
		Description: description,
		Type:        "chore",
		State:       "unscheduled",
		Labels:      labels,
	}
}

func choreForReopenedIssue(label string, issue github.Issue) tracker.Story {
	labels := []tracker.Label{
		{Name: label},
	}

	description := fmt.Sprintf(
		"[@%s](%s) reopened [%s](%s) on %s",
		*issue.User.Login,
		*issue.User.HTMLURL,
		label,
		*issue.HTMLURL,
		issue.UpdatedAt.Format("January 2"),
	)

	return tracker.Story{
		Name:        "reopened: " + *issue.Title,
		Description: description,
		Type:        "chore",
		State:       "unscheduled",
		Labels:      labels,
	}
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cleanup implements periodic data deletion.
package cleanup

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/google/exposure-notifications-server/pkg/logging"
	"github.com/google/exposure-notifications-verification-server/pkg/config"
	"github.com/google/exposure-notifications-verification-server/pkg/database"
	"github.com/google/exposure-notifications-verification-server/pkg/observability"
	"github.com/google/exposure-notifications-verification-server/pkg/render"
	"github.com/hashicorp/go-multierror"
	"go.opencensus.io/stats"
	"go.opencensus.io/tag"
)

// Controller is a controller for the cleanup service.
type Controller struct {
	config *config.CleanupConfig
	db     *database.Database
	h      render.Renderer
}

// New creates a new cleanup controller.
func New(ctx context.Context, config *config.CleanupConfig, db *database.Database, h render.Renderer) (*Controller, error) {
	return &Controller{
		config: config,
		db:     db,
		h:      h,
	}, nil
}

func (c *Controller) shouldCleanup(ctx context.Context) error {
	cStat, err := c.db.CreateCleanup(database.CleanupName)
	if err != nil {
		return fmt.Errorf("failed to create cleanup: %w", err)
	}

	if cStat.NotBefore.After(time.Now().UTC()) {
		return fmt.Errorf("skipping cleanup, no cleanup before %v", cStat.NotBefore)
	}

	// Attempt to advance the generation.
	if _, err = c.db.ClaimCleanup(cStat, c.config.CleanupPeriod); err != nil {
		stats.RecordWithTags(ctx, []tag.Mutator{observability.ResultNotOK()}, mClaimRequests.M(1))
		return fmt.Errorf("failed to claim cleanup: %w", err)
	}
	stats.RecordWithTags(ctx, []tag.Mutator{observability.ResultOK()}, mClaimRequests.M(1))
	return nil
}

func (c *Controller) HandleCleanup() http.Handler {
	type CleanupResult struct {
		OK     bool    `json:"ok"`
		Errors []error `json:"errors,omitempty"`
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		logger := logging.FromContext(ctx).Named("cleanup.HandleCleanup")

		var result, item tag.Mutator

		if err := c.shouldCleanup(ctx); err != nil {
			logger.Errorw("failed to run shouldCleanup", "error", err)
			c.h.RenderJSON(w, http.StatusInternalServerError, &CleanupResult{
				OK:     false,
				Errors: []error{err},
			})
			return
		}

		// Construct a multi-error. If one of the purges fails, we still want to
		// attempt the other purges.
		var merr *multierror.Error

		// API keys
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "API_KEYS")
			if count, err := c.db.PurgeAuthorizedApps(c.config.AuthorizedAppMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge authorized apps: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged authorized apps", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification codes - purge codes from database entirely.
		// Their code/long_code hmac values will have been set to "".
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_CODE")
			if count, err := c.db.PurgeVerificationCodes(c.config.VerificationCodeStatusMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge verification codes: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged verification codes", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification codes - recycle codes. Zero out the code/long_code values
		// so status can be reported, but codes couldn't be recalculated or checked.
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_CODE_RECYCLE")
			if count, err := c.db.RecycleVerificationCodes(c.config.VerificationCodeMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge verification codes: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("recycled verification codes", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification tokens
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_TOKEN")
			if count, err := c.db.PurgeTokens(c.config.VerificationTokenMaxAge); err != nil {
				result = observability.ResultError("FAILED")
				merr = multierror.Append(merr, fmt.Errorf("failed to purge tokens: %w", err))
			} else {
				logger.Infow("purged verification tokens", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Mobile apps
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "MOBILE_APP")
			if count, err := c.db.PurgeMobileApps(c.config.MobileAppMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge mobile apps: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged mobile apps", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Audit entries
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "AUDIT_ENTRY")
			if count, err := c.db.PurgeAuditEntries(c.config.AuditEntryMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge audit entries: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged audit entries", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Users
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "USER")
			if count, err := c.db.PurgeUsers(c.config.UserPurgeMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge users: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged user entries", "count", count)
				result = observability.ResultOK()
			}
		}()

		// If there are any errors, return them
		if merr != nil {
			if errs := merr.WrappedErrors(); len(errs) > 0 {
				logger.Errorw("failed to cleanup", "errors", errs)
				c.h.RenderJSON(w, http.StatusInternalServerError, &CleanupResult{
					OK:     false,
					Errors: errs,
				})
				return
			}
		}

		c.h.RenderJSON(w, http.StatusOK, &CleanupResult{
			OK: true,
		})
	})
}
<commit_msg>Only emit an error when shouldCleanup actually fails (#1520)<commit_after>// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cleanup implements periodic data deletion.
package cleanup

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/google/exposure-notifications-server/pkg/logging"
	"github.com/google/exposure-notifications-verification-server/pkg/config"
	"github.com/google/exposure-notifications-verification-server/pkg/database"
	"github.com/google/exposure-notifications-verification-server/pkg/observability"
	"github.com/google/exposure-notifications-verification-server/pkg/render"
	"github.com/hashicorp/go-multierror"
	"go.opencensus.io/stats"
	"go.opencensus.io/tag"
)

// Controller is a controller for the cleanup service.
type Controller struct {
	config *config.CleanupConfig
	db     *database.Database
	h      render.Renderer
}

// New creates a new cleanup controller.
func New(ctx context.Context, config *config.CleanupConfig, db *database.Database, h render.Renderer) (*Controller, error) {
	return &Controller{
		config: config,
		db:     db,
		h:      h,
	}, nil
}

func (c *Controller) shouldCleanup(ctx context.Context) (bool, error) {
	cStat, err := c.db.CreateCleanup(database.CleanupName)
	if err != nil {
		return false, fmt.Errorf("failed to create cleanup: %w", err)
	}

	if cStat.NotBefore.After(time.Now().UTC()) {
		return false, nil
	}

	// Attempt to advance the generation.
	if _, err = c.db.ClaimCleanup(cStat, c.config.CleanupPeriod); err != nil {
		stats.RecordWithTags(ctx, []tag.Mutator{observability.ResultNotOK()}, mClaimRequests.M(1))
		return false, fmt.Errorf("failed to claim cleanup: %w", err)
	}
	stats.RecordWithTags(ctx, []tag.Mutator{observability.ResultOK()}, mClaimRequests.M(1))
	return true, nil
}

func (c *Controller) HandleCleanup() http.Handler {
	type CleanupResult struct {
		OK     bool    `json:"ok"`
		Errors []error `json:"errors,omitempty"`
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		logger := logging.FromContext(ctx).Named("cleanup.HandleCleanup")

		var result, item tag.Mutator

		ok, err := c.shouldCleanup(ctx)
		if err != nil {
			logger.Errorw("failed to run shouldCleanup", "error", err)
			c.h.RenderJSON(w, http.StatusInternalServerError, &CleanupResult{
				OK:     false,
				Errors: []error{err},
			})
			return
		}
		if !ok {
			c.h.RenderJSON(w, http.StatusTooManyRequests, &CleanupResult{
				OK:     false,
				Errors: []error{fmt.Errorf("too early")},
			})
		}

		// Construct a multi-error. If one of the purges fails, we still want to
		// attempt the other purges.
		var merr *multierror.Error

		// API keys
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "API_KEYS")
			if count, err := c.db.PurgeAuthorizedApps(c.config.AuthorizedAppMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge authorized apps: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged authorized apps", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification codes - purge codes from database entirely.
		// Their code/long_code hmac values will have been set to "".
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_CODE")
			if count, err := c.db.PurgeVerificationCodes(c.config.VerificationCodeStatusMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge verification codes: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged verification codes", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification codes - recycle codes. Zero out the code/long_code values
		// so status can be reported, but codes couldn't be recalculated or checked.
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_CODE_RECYCLE")
			if count, err := c.db.RecycleVerificationCodes(c.config.VerificationCodeMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge verification codes: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("recycled verification codes", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Verification tokens
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "VERIFICATION_TOKEN")
			if count, err := c.db.PurgeTokens(c.config.VerificationTokenMaxAge); err != nil {
				result = observability.ResultError("FAILED")
				merr = multierror.Append(merr, fmt.Errorf("failed to purge tokens: %w", err))
			} else {
				logger.Infow("purged verification tokens", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Mobile apps
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "MOBILE_APP")
			if count, err := c.db.PurgeMobileApps(c.config.MobileAppMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge mobile apps: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged mobile apps", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Audit entries
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "AUDIT_ENTRY")
			if count, err := c.db.PurgeAuditEntries(c.config.AuditEntryMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge audit entries: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged audit entries", "count", count)
				result = observability.ResultOK()
			}
		}()

		// Users
		func() {
			defer observability.RecordLatency(ctx, time.Now(), mLatencyMs, &result, &item)
			item = tag.Upsert(itemTagKey, "USER")
			if count, err := c.db.PurgeUsers(c.config.UserPurgeMaxAge); err != nil {
				merr = multierror.Append(merr, fmt.Errorf("failed to purge users: %w", err))
				result = observability.ResultError("FAILED")
			} else {
				logger.Infow("purged user entries", "count", count)
				result = observability.ResultOK()
			}
		}()

		// If there are any errors, return them
		if merr != nil {
			if errs := merr.WrappedErrors(); len(errs) > 0 {
				logger.Errorw("failed to cleanup", "errors", errs)
				c.h.RenderJSON(w, http.StatusInternalServerError, &CleanupResult{
					OK:     false,
					Errors: errs,
				})
				return
			}
		}

		c.h.RenderJSON(w, http.StatusOK, &CleanupResult{
			OK: true,
		})
	})
}
<|endoftext|>


<commit_before>package jwtmanager

import (
	"testing"

	"github.com/vouch/vouch-proxy/pkg/cfg"
	"github.com/vouch/vouch-proxy/pkg/structs"

	"github.com/stretchr/testify/assert"
)

var (
	u1 = structs.User{
		Username: "test@testing.com",
		Name:     "Test Name",
	}

	lc VouchClaims
)

func init() {
	// log.SetLevel(log.DebugLevel)

	cfg.InitForTestPurposes()

	lc = VouchClaims{
		u1.Username,
		Sites,
		StandardClaims,
	}
}

func TestCreateUserTokenStringAndParseToUsername(t *testing.T) {

	uts := CreateUserTokenString(u1)
	assert.NotEmpty(t, uts)

	utsParsed, err := ParseTokenString(uts)
	if utsParsed == nil || err != nil {
		t.Error(err)
	} else {
		log.Debugf("test parsed token string %v", utsParsed)
		ptUsername, _ := PTokenToUsername(utsParsed)
		assert.Equal(t, u1.Username, ptUsername)
	}

}

func TestClaims(t *testing.T) {
	populateSites()
	log.Debugf("jwt config %s %d", string(cfg.Cfg.JWT.Secret), cfg.Cfg.JWT.MaxAge)
	assert.NotEmpty(t, cfg.Cfg.JWT.Secret)
	assert.NotEmpty(t, cfg.Cfg.JWT.MaxAge)

	// now := time.Now()
	// d := time.Duration(ExpiresAtMinutes) * time.Minute
	// log.Infof("lc d %s", d.String())
	// lc.StandardClaims.ExpiresAt = now.Add(time.Duration(ExpiresAtMinutes) * time.Minute).Unix()
	// log.Infof("lc expiresAt %d", now.Unix()-lc.StandardClaims.ExpiresAt)
	uts := CreateUserTokenString(u1)
	utsParsed, _ := ParseTokenString(uts)
	log.Infof("utsParsed: %+v", utsParsed)
	log.Infof("Sites: %+v", Sites)
	assert.True(t, SiteInToken(cfg.Cfg.Domains[0], utsParsed))

}
<commit_msg>Update jwt tests to support claims.<commit_after>package jwtmanager

import (
	"encoding/json"
	"github.com/vouch/vouch-proxy/pkg/cfg"
	"github.com/vouch/vouch-proxy/pkg/structs"
	"testing"

	"github.com/stretchr/testify/assert"
)

var (
	u1 = structs.User{
		Username: "test@testing.com",
		Name:     "Test Name",
	}

	lc VouchClaims

	claimjson = `{
		"sub": "f:a95afe53-60ba-4ac6-af15-fab870e72f3d:mrtester",
		"groups": ["Website Users", "Test Group"],
		"given_name": "Mister",
		"family_name": "Tester",
		"email": "mrtester@test.int"
	}`
	customClaims = structs.CustomClaims{}
)

func init() {
	// log.SetLevel(log.DebugLevel)

	cfg.InitForTestPurposes()

	lc = VouchClaims{
		u1.Username,
		Sites,
		customClaims.Claims,
		StandardClaims,
	}
	json.Unmarshal([]byte(claimjson), &customClaims.Claims)
}

func TestCreateUserTokenStringAndParseToUsername(t *testing.T) {

	uts := CreateUserTokenString(u1, customClaims)
	assert.NotEmpty(t, uts)

	utsParsed, err := ParseTokenString(uts)
	if utsParsed == nil || err != nil {
		t.Error(err)
	} else {
		log.Debugf("test parsed token string %v", utsParsed)
		ptUsername, _ := PTokenToUsername(utsParsed)
		assert.Equal(t, u1.Username, ptUsername)
	}

}

func TestClaims(t *testing.T) {
	populateSites()
	log.Debugf("jwt config %s %d", string(cfg.Cfg.JWT.Secret), cfg.Cfg.JWT.MaxAge)
	assert.NotEmpty(t, cfg.Cfg.JWT.Secret)
	assert.NotEmpty(t, cfg.Cfg.JWT.MaxAge)

	// now := time.Now()
	// d := time.Duration(ExpiresAtMinutes) * time.Minute
	// log.Infof("lc d %s", d.String())
	// lc.StandardClaims.ExpiresAt = now.Add(time.Duration(ExpiresAtMinutes) * time.Minute).Unix()
	// log.Infof("lc expiresAt %d", now.Unix()-lc.StandardClaims.ExpiresAt)
	uts := CreateUserTokenString(u1, customClaims)
	utsParsed, _ := ParseTokenString(uts)
	log.Infof("utsParsed: %+v", utsParsed)
	log.Infof("Sites: %+v", Sites)
	assert.True(t, SiteInToken(cfg.Cfg.Domains[0], utsParsed))

}
<|endoftext|>


<commit_before>package metadata

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"gopkg.in/yaml.v2"

	"github.com/calebamiles/keps/pkg/keps/states"
)

type KEP interface {
	UniqueID() string
	Authors() []string
	Title() string
	ShortID() int
	Reviewers() []string
	Approvers() []string
	Editors() []string
	State() states.Name
	DevelopmentThemes() []string
	Sections() []string // really are section paths

	// should be pointers to other KEPs
	Replaces() []string
	SupersededBy() []string

	Created() time.Time
	LastUpdated() time.Time

	// Flattened routing info
	OwningSIG() string
	AffectedSubprojects() []string
	ParticipatingSIGs() []string
	KubernetesWide() bool
	SIGWide() bool
	ContentDir() string

	// Mutators
	SetState(states.Name)
	AddSections([]string)
	AddApprovers([]string)
	AddReviewers([]string)
	Persist() error
}

type routingInfoProvider interface {
	OwningSIG() string
	AffectedSubprojects() []string
	ParticipatingSIGs() []string
	KubernetesWide() bool
	SIGWide() bool
	ContentDir() string
}

func New(authors []string, title string, routingInfo routingInfoProvider) (KEP, error) {
	k := &kep{
		AuthorsField:             authors,
		TitleField:               title,
		OwningSIGField:           routingInfo.OwningSIG(),
		AffectedSubprojectsField: routingInfo.AffectedSubprojects(),
		ParticipatingSIGsField:   routingInfo.ParticipatingSIGs(),
		KubernetesWideField:      routingInfo.KubernetesWide(),
		SIGWideField:             routingInfo.SIGWide(),
		CreatedField:             time.Now().UTC(),
		LastUpdatedField:         time.Now().UTC(),
		UniqueIDField:            uuid.New().String(), // note: will panic on error
		StateField:               states.Draft,
		contentDir:               routingInfo.ContentDir(),
		hasSectionPath:           make(map[string]bool),
		inApproversSet:           make(map[string]bool),
		inReviewersSet:           make(map[string]bool),
	}

	return k, nil
}

func Open(p string) (KEP, error) {
	kepBytes, err := ioutil.ReadFile(filepath.Join(p, metadataFilename))
	if err != nil {
		return nil, err
	}

	k, err := fromBytes(kepBytes)
	if err != nil {
		return nil, err
	}

	k.contentDir = p

	return k, nil
}

func FromBytes(b []byte) (KEP, error) {
	return fromBytes(b)
}

func fromBytes(b []byte) (*kep, error) {
	k := &kep{
		hasSectionPath: make(map[string]bool),
		inApproversSet: make(map[string]bool),
		inReviewersSet: make(map[string]bool),
	}

	err := yaml.Unmarshal(b, k)
	if err != nil {
		return nil, err
	}

	k.hasSectionPath = make(map[string]bool)

	for _, s := range k.SectionsField {
		if !k.hasSectionPath[sectionKey(s)] {
			k.hasSectionPath[sectionKey(s)] = true
			k.uniqueSections = append(k.uniqueSections, s)
		}
	}

	for _, a := range k.ApproversField {
		k.inApproversSet[a] = true
	}

	for _, r := range k.ReviewersField {
		k.inReviewersSet[r] = true
	}

	return k, nil
}

type kepSection struct {
	FilenameField string `yaml:"filename"`
	NameField     string `yaml:"name"`
}

func (s *kepSection) Name() string     { return s.NameField }
func (s *kepSection) Filename() string { return s.FilenameField }

type kep struct {
	AuthorsField           []string    `yaml:"authors,omitempty"`
	TitleField             string      `yaml:"title,omitempty"`
	ShortIDField           *int        `yaml:"kep_number",omitempty`
	ReviewersField         []string    `yaml:"reviewers,omitempty"`
	ApproversField         []string    `yaml:"approvers,omitempty"`
	EditorsField           []string    `yaml:"editors,omitempty"`
	StateField             states.Name `yaml:"state,omitempty"`
	ReplacesField          []string    `yaml:"replaces,omitempty"`
	SupersededByField      []string    `yaml:"superseded_by"`
	DevelopmentThemesField []string    `yaml:"development_themes,omitempty"`
	LastUpdatedField       time.Time   `yaml:"last_updated,omitempty"`
	CreatedField           time.Time   `yaml:"created,omitempty"`
	UniqueIDField          string      `yaml:"uuid,omitempty"`
	SectionsField          []string    `yaml:"sections,omitempty"`

	OwningSIGField           string   `yaml:"owning_sig,omitempty"`
	AffectedSubprojectsField []string `yaml:"affected_subprojects,omitempty"`
	ParticipatingSIGsField   []string `yaml:"participating_sigs,omitempty"`
	KubernetesWideField      bool     `yaml:"kubernetes_wide,omitempty"`
	SIGWideField             bool     `yaml:"sig_wide,omitempty"`

	hasSectionPath map[string]bool `yaml:"-"` // do not persist this
	uniqueSections []string        `yaml:"-"` // do not persist this
	inApproversSet map[string]bool `yaml:"-"` // do not persist this
	inReviewersSet map[string]bool `yaml:"-"` // do not persist this
	contentDir     string          `yaml:"-"` // do not persist this
	lock           sync.RWMutex    `yaml:"-"` // do not persist this
}

func (k *kep) Persist() error {
	k.lock.Lock()
	defer k.lock.Unlock()

	k.LastUpdatedField = time.Now().UTC()
	k.SectionsField = k.uniqueSections

	k.ApproversField = []string{}
	for approver := range k.inApproversSet {
		k.ApproversField = append(k.ApproversField, approver)
	}

	k.ReviewersField = []string{}
	for reviewer := range k.inReviewersSet {
		k.ReviewersField = append(k.ReviewersField, reviewer)
	}

	loc := k.contentDir
	filename := filepath.Join(loc, metadataFilename)

	metaBytes, err := yaml.Marshal(k)
	if err != nil {
		return err
	}

	err = ioutil.WriteFile(filename, metaBytes, os.ModePerm)
	if err != nil {
		return err
	}

	return nil
}

func (k *kep) AddSections(paths []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	for _, s := range paths {
		if !k.hasSectionPath[s] {
			k.hasSectionPath[sectionKey(s)] = true
			// we maintain a separate slice in order to maintain section order
			k.uniqueSections = append(k.uniqueSections, s)
		}
	}
}

func (k *kep) AddApprovers(approvers []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	// we don't currently care about the order of approvers when persisting this back to YAML
	for _, approver := range approvers {
		k.inApproversSet[approver] = true
	}
}

func (k *kep) AddReviewers(reviewers []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	// we don't currently care about the order of reviewers when persisting this back to YAML
	for _, reviewer := range reviewers {
		k.inReviewersSet[reviewer] = true
	}
}

func (k *kep) SetState(state states.Name) {
	k.lock.Lock()
	defer k.lock.Unlock()

	k.StateField = state
}

func (k *kep) Sections() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.uniqueSections
}

func (k *kep) Authors() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.AuthorsField
}

func (k *kep) Title() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.TitleField
}

func (k *kep) ShortID() int {
	k.lock.RLock()
	defer k.lock.RUnlock()

	if k.ShortIDField != nil {
		return *k.ShortIDField
	}

	return UnsetShortID
}

func (k *kep) UniqueID() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.UniqueIDField
}

func (k *kep) Reviewers() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	reviewers := []string{}
	for reviewer := range k.inReviewersSet {
		reviewers = append(reviewers, reviewer)
	}

	return reviewers
}

func (k *kep) Approvers() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	approvers := []string{}
	for approver := range k.inApproversSet {
		approvers = append(approvers, approver)
	}

	return approvers
}

func (k *kep) Editors() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.EditorsField
}

func (k *kep) State() states.Name {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.StateField
}

func (k *kep) Replaces() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.ReplacesField
}

func (k *kep) SupersededBy() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.SupersededByField
}

func (k *kep) DevelopmentThemes() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.DevelopmentThemesField
}

func (k *kep) LastUpdated() time.Time {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.LastUpdatedField
}

func (k *kep) Created() time.Time {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.CreatedField
}

func (k *kep) OwningSIG() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.OwningSIGField
}

func (k *kep) AffectedSubprojects() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.AffectedSubprojectsField
}

func (k *kep) ParticipatingSIGs() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.ParticipatingSIGsField
}

func (k *kep) KubernetesWide() bool {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.KubernetesWideField
}

func (k *kep) SIGWide() bool {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.SIGWideField
}

func (k *kep) ContentDir() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.contentDir
}

const (
	metadataFilename = "metadata.yaml"
	UnsetShortID     = -1
)

func sectionKey(s string) string {
	return strings.ToLower(strings.TrimSpace(s))
}
<commit_msg>cleanup: don't persist unset short ID in KEP metadata<commit_after>package metadata

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"gopkg.in/yaml.v2"

	"github.com/calebamiles/keps/pkg/keps/states"
)

type KEP interface {
	UniqueID() string
	Authors() []string
	Title() string
	ShortID() int
	Reviewers() []string
	Approvers() []string
	Editors() []string
	State() states.Name
	DevelopmentThemes() []string
	Sections() []string // really are section paths

	// should be pointers to other KEPs
	Replaces() []string
	SupersededBy() []string

	Created() time.Time
	LastUpdated() time.Time

	// Flattened routing info
	OwningSIG() string
	AffectedSubprojects() []string
	ParticipatingSIGs() []string
	KubernetesWide() bool
	SIGWide() bool
	ContentDir() string

	// Mutators
	SetState(states.Name)
	AddSections([]string)
	AddApprovers([]string)
	AddReviewers([]string)
	Persist() error
}

type routingInfoProvider interface {
	OwningSIG() string
	AffectedSubprojects() []string
	ParticipatingSIGs() []string
	KubernetesWide() bool
	SIGWide() bool
	ContentDir() string
}

func New(authors []string, title string, routingInfo routingInfoProvider) (KEP, error) {
	k := &kep{
		AuthorsField:             authors,
		TitleField:               title,
		OwningSIGField:           routingInfo.OwningSIG(),
		AffectedSubprojectsField: routingInfo.AffectedSubprojects(),
		ParticipatingSIGsField:   routingInfo.ParticipatingSIGs(),
		KubernetesWideField:      routingInfo.KubernetesWide(),
		SIGWideField:             routingInfo.SIGWide(),
		CreatedField:             time.Now().UTC(),
		LastUpdatedField:         time.Now().UTC(),
		UniqueIDField:            uuid.New().String(), // note: will panic on error
		StateField:               states.Draft,
		contentDir:               routingInfo.ContentDir(),
		hasSectionPath:           make(map[string]bool),
		inApproversSet:           make(map[string]bool),
		inReviewersSet:           make(map[string]bool),
	}

	return k, nil
}

func Open(p string) (KEP, error) {
	kepBytes, err := ioutil.ReadFile(filepath.Join(p, metadataFilename))
	if err != nil {
		return nil, err
	}

	k, err := fromBytes(kepBytes)
	if err != nil {
		return nil, err
	}

	k.contentDir = p

	return k, nil
}

func FromBytes(b []byte) (KEP, error) {
	return fromBytes(b)
}

func fromBytes(b []byte) (*kep, error) {
	k := &kep{
		hasSectionPath: make(map[string]bool),
		inApproversSet: make(map[string]bool),
		inReviewersSet: make(map[string]bool),
	}

	err := yaml.Unmarshal(b, k)
	if err != nil {
		return nil, err
	}

	k.hasSectionPath = make(map[string]bool)

	for _, s := range k.SectionsField {
		if !k.hasSectionPath[sectionKey(s)] {
			k.hasSectionPath[sectionKey(s)] = true
			k.uniqueSections = append(k.uniqueSections, s)
		}
	}

	for _, a := range k.ApproversField {
		k.inApproversSet[a] = true
	}

	for _, r := range k.ReviewersField {
		k.inReviewersSet[r] = true
	}

	return k, nil
}

type kepSection struct {
	FilenameField string `yaml:"filename"`
	NameField     string `yaml:"name"`
}

func (s *kepSection) Name() string     { return s.NameField }
func (s *kepSection) Filename() string { return s.FilenameField }

type kep struct {
	AuthorsField           []string    `yaml:"authors,omitempty"`
	TitleField             string      `yaml:"title,omitempty"`
	ShortIDField           *int        `yaml:"kep_number,omitempty"`
	ReviewersField         []string    `yaml:"reviewers,omitempty"`
	ApproversField         []string    `yaml:"approvers,omitempty"`
	EditorsField           []string    `yaml:"editors,omitempty"`
	StateField             states.Name `yaml:"state,omitempty"`
	ReplacesField          []string    `yaml:"replaces,omitempty"`
	SupersededByField      []string    `yaml:"superseded_by,omitempty"`
	DevelopmentThemesField []string    `yaml:"development_themes,omitempty"`
	LastUpdatedField       time.Time   `yaml:"last_updated,omitempty"`
	CreatedField           time.Time   `yaml:"created,omitempty"`
	UniqueIDField          string      `yaml:"uuid,omitempty"`
	SectionsField          []string    `yaml:"sections,omitempty"`

	OwningSIGField           string   `yaml:"owning_sig,omitempty"`
	AffectedSubprojectsField []string `yaml:"affected_subprojects,omitempty"`
	ParticipatingSIGsField   []string `yaml:"participating_sigs,omitempty"`
	KubernetesWideField      bool     `yaml:"kubernetes_wide,omitempty"`
	SIGWideField             bool     `yaml:"sig_wide,omitempty"`

	hasSectionPath map[string]bool `yaml:"-"` // do not persist this
	uniqueSections []string        `yaml:"-"` // do not persist this
	inApproversSet map[string]bool `yaml:"-"` // do not persist this
	inReviewersSet map[string]bool `yaml:"-"` // do not persist this
	contentDir     string          `yaml:"-"` // do not persist this
	lock           sync.RWMutex    `yaml:"-"` // do not persist this
}

func (k *kep) Persist() error {
	k.lock.Lock()
	defer k.lock.Unlock()

	k.LastUpdatedField = time.Now().UTC()
	k.SectionsField = k.uniqueSections

	k.ApproversField = []string{}
	for approver := range k.inApproversSet {
		k.ApproversField = append(k.ApproversField, approver)
	}

	k.ReviewersField = []string{}
	for reviewer := range k.inReviewersSet {
		k.ReviewersField = append(k.ReviewersField, reviewer)
	}

	loc := k.contentDir
	filename := filepath.Join(loc, metadataFilename)

	metaBytes, err := yaml.Marshal(k)
	if err != nil {
		return err
	}

	err = ioutil.WriteFile(filename, metaBytes, os.ModePerm)
	if err != nil {
		return err
	}

	return nil
}

func (k *kep) AddSections(paths []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	for _, s := range paths {
		if !k.hasSectionPath[s] {
			k.hasSectionPath[sectionKey(s)] = true
			// we maintain a separate slice in order to maintain section order
			k.uniqueSections = append(k.uniqueSections, s)
		}
	}
}

func (k *kep) AddApprovers(approvers []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	// we don't currently care about the order of approvers when persisting this back to YAML
	for _, approver := range approvers {
		k.inApproversSet[approver] = true
	}
}

func (k *kep) AddReviewers(reviewers []string) {
	k.lock.Lock()
	defer k.lock.Unlock()

	// we don't currently care about the order of reviewers when persisting this back to YAML
	for _, reviewer := range reviewers {
		k.inReviewersSet[reviewer] = true
	}
}

func (k *kep) SetState(state states.Name) {
	k.lock.Lock()
	defer k.lock.Unlock()

	k.StateField = state
}

func (k *kep) Sections() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.uniqueSections
}

func (k *kep) Authors() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.AuthorsField
}

func (k *kep) Title() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.TitleField
}

func (k *kep) ShortID() int {
	k.lock.RLock()
	defer k.lock.RUnlock()

	if k.ShortIDField != nil {
		return *k.ShortIDField
	}

	return UnsetShortID
}

func (k *kep) UniqueID() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.UniqueIDField
}

func (k *kep) Reviewers() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	reviewers := []string{}
	for reviewer := range k.inReviewersSet {
		reviewers = append(reviewers, reviewer)
	}

	return reviewers
}

func (k *kep) Approvers() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	approvers := []string{}
	for approver := range k.inApproversSet {
		approvers = append(approvers, approver)
	}

	return approvers
}

func (k *kep) Editors() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.EditorsField
}

func (k *kep) State() states.Name {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.StateField
}

func (k *kep) Replaces() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.ReplacesField
}

func (k *kep) SupersededBy() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.SupersededByField
}

func (k *kep) DevelopmentThemes() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.DevelopmentThemesField
}

func (k *kep) LastUpdated() time.Time {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.LastUpdatedField
}

func (k *kep) Created() time.Time {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.CreatedField
}

func (k *kep) OwningSIG() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.OwningSIGField
}

func (k *kep) AffectedSubprojects() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.AffectedSubprojectsField
}

func (k *kep) ParticipatingSIGs() []string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.ParticipatingSIGsField
}

func (k *kep) KubernetesWide() bool {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.KubernetesWideField
}

func (k *kep) SIGWide() bool {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.SIGWideField
}

func (k *kep) ContentDir() string {
	k.lock.RLock()
	defer k.lock.RUnlock()

	return k.contentDir
}

const (
	metadataFilename = "metadata.yaml"
	UnsetShortID     = -1
)

func sectionKey(s string) string {
	return strings.ToLower(strings.TrimSpace(s))
}
<|endoftext|>


<commit_before>/*
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package s3

import (
	"net/http"
	"os"
	"reflect"
	"strings"
	"testing"
)

var tc *Client

func getTestClient(t *testing.T) bool {
	accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
	secret := os.Getenv("AWS_ACCESS_KEY_SECRET")
	if accessKey == "" || secret == "" {
		t.Logf("Skipping test; no AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_SECRET set in environment")
		return false
	}
	tc = &Client{&Auth{AccessKey: accessKey, SecretAccessKey: secret}, http.DefaultClient}
	return true
}

func TestBuckets(t *testing.T) {
	if !getTestClient(t) {
		return
	}
	tc.Buckets()
}

func TestParseBuckets(t *testing.T) {
	res := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner><ID>ownerIDField</ID><DisplayName>bobDisplayName</DisplayName></Owner><Buckets><Bucket><Name>bucketOne</Name><CreationDate>2006-06-21T07:04:31.000Z</CreationDate></Bucket><Bucket><Name>bucketTwo</Name><CreationDate>2006-06-21T07:04:32.000Z</CreationDate></Bucket></Buckets></ListAllMyBucketsResult>"
	buckets, err := parseListAllMyBuckets(strings.NewReader(res))
	if err != nil {
		t.Fatal(err)
	}
	if g, w := len(buckets), 2; g != w {
		t.Errorf("num parsed buckets = %d; want %d", g, w)
	}
	want := []*Bucket{
		{Name: "bucketOne", CreationDate: "2006-06-21T07:04:31.000Z"},
		{Name: "bucketTwo", CreationDate: "2006-06-21T07:04:32.000Z"},
	}
	dump := func(v []*Bucket) {
		for i, b := range v {
			t.Logf("Bucket #%d: %#v", i, b)
		}
	}
	if !reflect.DeepEqual(buckets, want) {
		t.Error("mismatch; GOT:")
		dump(buckets)
		t.Error("WANT:")
		dump(want)
	}
}

func TestValidBucketNames(t *testing.T) {
	m := []struct {
		in   string
		want bool
	}{
		{"myawsbucket", true},
		{"my.aws.bucket", true},
		{"my-aws-bucket.1", true},
		{"my---bucket.1", true},
		{".myawsbucket", false},
		{"-myawsbucket", false},
		{"myawsbucket.", false},
		{"myawsbucket-", false},
		{"my..awsbucket", false},
	}

	for _, bt := range m {
		got := IsValidBucket(bt.in)
		if got != bt.want {
			t.Errorf("func(%q) = %v; want %v", bt.in, got, bt.want)
		}
	}
}
<commit_msg>pkg/misc/amazon/s3: Fix test broken by commit bf8d6bdb.<commit_after>/*
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package s3

import (
	"net/http"
	"os"
	"reflect"
	"strings"
	"testing"
)

var tc *Client

func getTestClient(t *testing.T) bool {
	accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
	secret := os.Getenv("AWS_ACCESS_KEY_SECRET")
	if accessKey == "" || secret == "" {
		t.Logf("Skipping test; no AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY_SECRET set in environment")
		return false
	}
	tc = &Client{&Auth{AccessKey: accessKey, SecretAccessKey: secret}, http.DefaultTransport}
	return true
}

func TestBuckets(t *testing.T) {
	if !getTestClient(t) {
		return
	}
	tc.Buckets()
}

func TestParseBuckets(t *testing.T) {
	res := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner><ID>ownerIDField</ID><DisplayName>bobDisplayName</DisplayName></Owner><Buckets><Bucket><Name>bucketOne</Name><CreationDate>2006-06-21T07:04:31.000Z</CreationDate></Bucket><Bucket><Name>bucketTwo</Name><CreationDate>2006-06-21T07:04:32.000Z</CreationDate></Bucket></Buckets></ListAllMyBucketsResult>"
	buckets, err := parseListAllMyBuckets(strings.NewReader(res))
	if err != nil {
		t.Fatal(err)
	}
	if g, w := len(buckets), 2; g != w {
		t.Errorf("num parsed buckets = %d; want %d", g, w)
	}
	want := []*Bucket{
		{Name: "bucketOne", CreationDate: "2006-06-21T07:04:31.000Z"},
		{Name: "bucketTwo", CreationDate: "2006-06-21T07:04:32.000Z"},
	}
	dump := func(v []*Bucket) {
		for i, b := range v {
			t.Logf("Bucket #%d: %#v", i, b)
		}
	}
	if !reflect.DeepEqual(buckets, want) {
		t.Error("mismatch; GOT:")
		dump(buckets)
		t.Error("WANT:")
		dump(want)
	}
}

func TestValidBucketNames(t *testing.T) {
	m := []struct {
		in   string
		want bool
	}{
		{"myawsbucket", true},
		{"my.aws.bucket", true},
		{"my-aws-bucket.1", true},
		{"my---bucket.1", true},
		{".myawsbucket", false},
		{"-myawsbucket", false},
		{"myawsbucket.", false},
		{"myawsbucket-", false},
		{"my..awsbucket", false},
	}

	for _, bt := range m {
		got := IsValidBucket(bt.in)
		if got != bt.want {
			t.Errorf("func(%q) = %v; want %v", bt.in, got, bt.want)
		}
	}
}
<|endoftext|>


<commit_before>package multiclustermanager

import (
	"context"
	"net/http"

	"github.com/rancher/apiserver/pkg/parse"

	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rancher/rancher/pkg/api/norman"
	"github.com/rancher/rancher/pkg/api/norman/customization/clusterregistrationtokens"
	"github.com/rancher/rancher/pkg/api/norman/customization/oci"
	"github.com/rancher/rancher/pkg/api/norman/customization/vsphere"
	managementapi "github.com/rancher/rancher/pkg/api/norman/server"
	"github.com/rancher/rancher/pkg/auth/providers/publicapi"
	"github.com/rancher/rancher/pkg/auth/providers/saml"
	"github.com/rancher/rancher/pkg/auth/requests"
	"github.com/rancher/rancher/pkg/auth/requests/sar"
	"github.com/rancher/rancher/pkg/auth/tokens"
	"github.com/rancher/rancher/pkg/auth/webhook"
	"github.com/rancher/rancher/pkg/channelserver"
	"github.com/rancher/rancher/pkg/clustermanager"
	rancherdialer "github.com/rancher/rancher/pkg/dialer"
	"github.com/rancher/rancher/pkg/httpproxy"
	k8sProxyPkg "github.com/rancher/rancher/pkg/k8sproxy"
	"github.com/rancher/rancher/pkg/metrics"
	"github.com/rancher/rancher/pkg/multiclustermanager/capabilities"
	"github.com/rancher/rancher/pkg/multiclustermanager/whitelist"
	"github.com/rancher/rancher/pkg/pipeline/hooks"
	"github.com/rancher/rancher/pkg/rbac"
	"github.com/rancher/rancher/pkg/rkenodeconfigserver"
	"github.com/rancher/rancher/pkg/telemetry"
	"github.com/rancher/rancher/pkg/types/config"
	"github.com/rancher/steve/pkg/auth"
)

func router(ctx context.Context, localClusterEnabled bool, scaledContext *config.ScaledContext, clusterManager *clustermanager.Manager) (func(http.Handler) http.Handler, error) {
	var (
		k8sProxy             = k8sProxyPkg.New(scaledContext, scaledContext.Dialer)
		connectHandler       = scaledContext.Dialer.(*rancherdialer.Factory).TunnelServer
		connectConfigHandler = rkenodeconfigserver.Handler(scaledContext.Dialer.(*rancherdialer.Factory).TunnelAuthorizer, scaledContext)
	)

	tokenAPI, err := tokens.NewAPIHandler(ctx, scaledContext, norman.ConfigureAPIUI)
	if err != nil {
		return nil, err
	}

	publicAPI, err := publicapi.NewHandler(ctx, scaledContext, norman.ConfigureAPIUI)
	if err != nil {
		return nil, err
	}

	managementAPI, err := managementapi.New(ctx, scaledContext, clusterManager, k8sProxy, localClusterEnabled)
	if err != nil {
		return nil, err
	}

	// Unauthenticated routes
	unauthed := mux.NewRouter()
	unauthed.UseEncodedPath()

	unauthed.Path("/").MatcherFunc(parse.MatchNotBrowser).Handler(managementAPI)
	unauthed.Handle("/v3/connect/config", connectConfigHandler)
	unauthed.Handle("/v3/connect", connectHandler)
	unauthed.Handle("/v3/connect/register", connectHandler)
	unauthed.Handle("/v3/import/{token}.yaml", http.HandlerFunc(clusterregistrationtokens.ClusterImportHandler))
	unauthed.Handle("/v3/settings/cacerts", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/first-login", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-banners", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-issues", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-pl", managementAPI).MatcherFunc(onlyGet)
	unauthed.PathPrefix("/hooks").Handler(hooks.New(scaledContext))
	unauthed.PathPrefix("/v1-k3s-release/release").Handler(channelserver.NewProxy(ctx))
	unauthed.PathPrefix("/v1-rke2-release/release").Handler(channelserver.Rke2Proxy(ctx))
	unauthed.PathPrefix("/v1-saml").Handler(saml.AuthHandler())
	unauthed.PathPrefix("/v3-public").Handler(publicAPI)

	// Authenticated routes
	authed := mux.NewRouter()
	authed.UseEncodedPath()
	authed.Use(mux.MiddlewareFunc(auth.ToMiddleware(requests.NewImpersonatingAuth(sar.NewSubjectAccessReview(clusterManager)))))
	authed.Use(mux.MiddlewareFunc(rbac.NewAccessControlHandler()))
	authed.Use(requests.NewAuthenticatedFilter)

	authed.Path("/meta/aksVersions").Handler(capabilities.NewAKSVersionsHandler())
	authed.Path("/meta/aksVirtualNetworks").Handler(capabilities.NewAKSVirtualNetworksHandler())
	authed.Path("/meta/gkeMachineTypes").Handler(capabilities.NewGKEMachineTypesHandler())
	authed.Path("/meta/gkeNetworks").Handler(capabilities.NewGKENetworksHandler())
	authed.Path("/meta/gkeServiceAccounts").Handler(capabilities.NewGKEServiceAccountsHandler())
	authed.Path("/meta/gkeSubnetworks").Handler(capabilities.NewGKESubnetworksHandler())
	authed.Path("/meta/gkeVersions").Handler(capabilities.NewGKEVersionsHandler())
	authed.Path("/meta/gkeZones").Handler(capabilities.NewGKEZonesHandler())
	authed.Path("/meta/oci/{resource}").Handler(oci.NewOCIHandler(scaledContext))
	authed.Path("/meta/vsphere/{field}").Handler(vsphere.NewVsphereHandler(scaledContext))
	authed.Path("/v3/tokenreview").Methods(http.MethodPost).Handler(&webhook.TokenReviewer{})
	authed.PathPrefix("/k8s/clusters/").Handler(k8sProxy)
	authed.PathPrefix("/meta").Handler(managementAPI)
	authed.PathPrefix("/meta/proxy").Handler(httpproxy.NewProxy("/proxy/", whitelist.Proxy.Get, scaledContext))
	authed.PathPrefix("/metrics").Handler(metrics.NewMetricsHandler(scaledContext, promhttp.Handler()))
	authed.PathPrefix("/v1-telemetry").Handler(telemetry.NewProxy())
	authed.PathPrefix("/v3/identit").Handler(tokenAPI)
	authed.PathPrefix("/v3/token").Handler(tokenAPI)
	authed.PathPrefix("/v3").Handler(managementAPI)

	unauthed.NotFoundHandler = authed
	return func(next http.Handler) http.Handler {
		authed.NotFoundHandler = next
		return unauthed
	}, nil
}

// onlyGet will match only GET but will not return a 405 like route.Methods and instead just not match
func onlyGet(req *http.Request, m *mux.RouteMatch) bool {
	return req.Method == http.MethodGet
}
<commit_msg>Fix /meta/proxy<commit_after>package multiclustermanager

import (
	"context"
	"net/http"

	"github.com/rancher/apiserver/pkg/parse"

	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rancher/rancher/pkg/api/norman"
	"github.com/rancher/rancher/pkg/api/norman/customization/clusterregistrationtokens"
	"github.com/rancher/rancher/pkg/api/norman/customization/oci"
	"github.com/rancher/rancher/pkg/api/norman/customization/vsphere"
	managementapi "github.com/rancher/rancher/pkg/api/norman/server"
	"github.com/rancher/rancher/pkg/auth/providers/publicapi"
	"github.com/rancher/rancher/pkg/auth/providers/saml"
	"github.com/rancher/rancher/pkg/auth/requests"
	"github.com/rancher/rancher/pkg/auth/requests/sar"
	"github.com/rancher/rancher/pkg/auth/tokens"
	"github.com/rancher/rancher/pkg/auth/webhook"
	"github.com/rancher/rancher/pkg/channelserver"
	"github.com/rancher/rancher/pkg/clustermanager"
	rancherdialer "github.com/rancher/rancher/pkg/dialer"
	"github.com/rancher/rancher/pkg/httpproxy"
	k8sProxyPkg "github.com/rancher/rancher/pkg/k8sproxy"
	"github.com/rancher/rancher/pkg/metrics"
	"github.com/rancher/rancher/pkg/multiclustermanager/capabilities"
	"github.com/rancher/rancher/pkg/multiclustermanager/whitelist"
	"github.com/rancher/rancher/pkg/pipeline/hooks"
	"github.com/rancher/rancher/pkg/rbac"
	"github.com/rancher/rancher/pkg/rkenodeconfigserver"
	"github.com/rancher/rancher/pkg/telemetry"
	"github.com/rancher/rancher/pkg/types/config"
	"github.com/rancher/steve/pkg/auth"
)

func router(ctx context.Context, localClusterEnabled bool, scaledContext *config.ScaledContext, clusterManager *clustermanager.Manager) (func(http.Handler) http.Handler, error) {
	var (
		k8sProxy             = k8sProxyPkg.New(scaledContext, scaledContext.Dialer)
		connectHandler       = scaledContext.Dialer.(*rancherdialer.Factory).TunnelServer
		connectConfigHandler = rkenodeconfigserver.Handler(scaledContext.Dialer.(*rancherdialer.Factory).TunnelAuthorizer, scaledContext)
	)

	tokenAPI, err := tokens.NewAPIHandler(ctx, scaledContext, norman.ConfigureAPIUI)
	if err != nil {
		return nil, err
	}

	publicAPI, err := publicapi.NewHandler(ctx, scaledContext, norman.ConfigureAPIUI)
	if err != nil {
		return nil, err
	}

	managementAPI, err := managementapi.New(ctx, scaledContext, clusterManager, k8sProxy, localClusterEnabled)
	if err != nil {
		return nil, err
	}

	// Unauthenticated routes
	unauthed := mux.NewRouter()
	unauthed.UseEncodedPath()

	unauthed.Path("/").MatcherFunc(parse.MatchNotBrowser).Handler(managementAPI)
	unauthed.Handle("/v3/connect/config", connectConfigHandler)
	unauthed.Handle("/v3/connect", connectHandler)
	unauthed.Handle("/v3/connect/register", connectHandler)
	unauthed.Handle("/v3/import/{token}.yaml", http.HandlerFunc(clusterregistrationtokens.ClusterImportHandler))
	unauthed.Handle("/v3/settings/cacerts", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/first-login", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-banners", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-issues", managementAPI).MatcherFunc(onlyGet)
	unauthed.Handle("/v3/settings/ui-pl", managementAPI).MatcherFunc(onlyGet)
	unauthed.PathPrefix("/hooks").Handler(hooks.New(scaledContext))
	unauthed.PathPrefix("/v1-k3s-release/release").Handler(channelserver.NewProxy(ctx))
	unauthed.PathPrefix("/v1-rke2-release/release").Handler(channelserver.Rke2Proxy(ctx))
	unauthed.PathPrefix("/v1-saml").Handler(saml.AuthHandler())
	unauthed.PathPrefix("/v3-public").Handler(publicAPI)

	// Authenticated routes
	authed := mux.NewRouter()
	authed.UseEncodedPath()
	authed.Use(mux.MiddlewareFunc(auth.ToMiddleware(requests.NewImpersonatingAuth(sar.NewSubjectAccessReview(clusterManager)))))
	authed.Use(mux.MiddlewareFunc(rbac.NewAccessControlHandler()))
	authed.Use(requests.NewAuthenticatedFilter)

	authed.Path("/meta/aksVersions").Handler(capabilities.NewAKSVersionsHandler())
	authed.Path("/meta/aksVirtualNetworks").Handler(capabilities.NewAKSVirtualNetworksHandler())
	authed.Path("/meta/gkeMachineTypes").Handler(capabilities.NewGKEMachineTypesHandler())
	authed.Path("/meta/gkeNetworks").Handler(capabilities.NewGKENetworksHandler())
	authed.Path("/meta/gkeServiceAccounts").Handler(capabilities.NewGKEServiceAccountsHandler())
	authed.Path("/meta/gkeSubnetworks").Handler(capabilities.NewGKESubnetworksHandler())
	authed.Path("/meta/gkeVersions").Handler(capabilities.NewGKEVersionsHandler())
	authed.Path("/meta/gkeZones").Handler(capabilities.NewGKEZonesHandler())
	authed.Path("/meta/oci/{resource}").Handler(oci.NewOCIHandler(scaledContext))
	authed.Path("/meta/vsphere/{field}").Handler(vsphere.NewVsphereHandler(scaledContext))
	authed.Path("/v3/tokenreview").Methods(http.MethodPost).Handler(&webhook.TokenReviewer{})
	authed.PathPrefix("/k8s/clusters/").Handler(k8sProxy)
	authed.PathPrefix("/meta/proxy").Handler(httpproxy.NewProxy("/proxy/", whitelist.Proxy.Get, scaledContext))
	authed.PathPrefix("/metrics").Handler(metrics.NewMetricsHandler(scaledContext, promhttp.Handler()))
	authed.PathPrefix("/v1-telemetry").Handler(telemetry.NewProxy())
	authed.PathPrefix("/v3/identit").Handler(tokenAPI)
	authed.PathPrefix("/v3/token").Handler(tokenAPI)
	authed.PathPrefix("/v3").Handler(managementAPI)

	unauthed.NotFoundHandler = authed
	return func(next http.Handler) http.Handler {
		authed.NotFoundHandler = next
		return unauthed
	}, nil
}

// onlyGet will match only GET but will not return a 405 like route.Methods and instead just not match
func onlyGet(req *http.Request, m *mux.RouteMatch) bool {
	return req.Method == http.MethodGet
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package installer

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"

	"k8s.io/helm/pkg/helm/helmpath"
)

// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")

// Debug enables verbose output.
var Debug bool

// Installer provides an interface for installing helm client plugins.
type Installer interface {
	// Install adds a plugin to $HELM_HOME.
	Install() error
	// Path is the directory of the installed plugin.
	Path() string
	// Update updates a plugin to $HELM_HOME.
	Update() error
}

// Install installs a plugin to $HELM_HOME.
func Install(i Installer) error {
	if _, pathErr := os.Stat(path.Dir(i.Path())); os.IsNotExist(pathErr) {
		return errors.New(`plugin home "$HELM_HOME/plugins" does not exists`)
	}

	if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
		return errors.New("plugin already exists")
	}

	return i.Install()
}

// Update updates a plugin in $HELM_HOME.
func Update(i Installer) error {
	if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
		return errors.New("plugin does not exist")
	}

	return i.Update()
}

// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string, home helmpath.Home) (Installer, error) {
	// Check if source is a local directory
	if isLocalReference(source) {
		return NewLocalInstaller(source, home)
	}
	return NewVCSInstaller(source, version, home)
}

// FindSource determines the correct Installer for the given source.
func FindSource(location string, home helmpath.Home) (Installer, error) {
	installer, err := existingVCSRepo(location, home)
	if err != nil && err.Error() == "Cannot detect VCS" {
		return installer, errors.New("cannot get information about plugin source")
	}
	return installer, err
}

// isLocalReference checks if the source exists on the filesystem.
func isLocalReference(source string) bool {
	_, err := os.Stat(source)
	return err == nil
}

// isPlugin checks if the directory contains a plugin.yaml file.
func isPlugin(dirname string) bool {
	_, err := os.Stat(filepath.Join(dirname, "plugin.yaml"))
	return err == nil
}

func debug(format string, args ...interface{}) {
	if Debug {
		format = fmt.Sprintf("[debug] %s\n", format)
		fmt.Printf(format, args...)
	}
}
<commit_msg>fix(plugins): exists --> exist<commit_after>/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package installer

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"

	"k8s.io/helm/pkg/helm/helmpath"
)

// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")

// Debug enables verbose output.
var Debug bool

// Installer provides an interface for installing helm client plugins.
type Installer interface {
	// Install adds a plugin to $HELM_HOME.
	Install() error
	// Path is the directory of the installed plugin.
	Path() string
	// Update updates a plugin to $HELM_HOME.
	Update() error
}

// Install installs a plugin to $HELM_HOME.
func Install(i Installer) error {
	if _, pathErr := os.Stat(path.Dir(i.Path())); os.IsNotExist(pathErr) {
		return errors.New(`plugin home "$HELM_HOME/plugins" does not exist`)
	}

	if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
		return errors.New("plugin already exists")
	}

	return i.Install()
}

// Update updates a plugin in $HELM_HOME.
func Update(i Installer) error {
	if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
		return errors.New("plugin does not exist")
	}

	return i.Update()
}

// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string, home helmpath.Home) (Installer, error) {
	// Check if source is a local directory
	if isLocalReference(source) {
		return NewLocalInstaller(source, home)
	}
	return NewVCSInstaller(source, version, home)
}

// FindSource determines the correct Installer for the given source.
func FindSource(location string, home helmpath.Home) (Installer, error) {
	installer, err := existingVCSRepo(location, home)
	if err != nil && err.Error() == "Cannot detect VCS" {
		return installer, errors.New("cannot get information about plugin source")
	}
	return installer, err
}

// isLocalReference checks if the source exists on the filesystem.
func isLocalReference(source string) bool {
	_, err := os.Stat(source)
	return err == nil
}

// isPlugin checks if the directory contains a plugin.yaml file.
func isPlugin(dirname string) bool {
	_, err := os.Stat(filepath.Join(dirname, "plugin.yaml"))
	return err == nil
}

func debug(format string, args ...interface{}) {
	if Debug {
		format = fmt.Sprintf("[debug] %s\n", format)
		fmt.Printf(format, args...)
	}
}
<|endoftext|>


<commit_before>package x

import (
	"reflect"
	"strings"
	"testing"
)

func TestConfig(t *testing.T) {
	var testCases = []struct {
		yaml     string
		expected *Config
		err      error
	}{
		{
			yaml: `android-tablets:
  sizes:
    - max_width: 500
    - max: 1000
  formats:
    - jpg
    - png
  store:
    - local: /mnt/gallery`,
			expected: &Config{
				"android-tablets": Handler{
					[]Size{
						Size{MaxWidth, 500},
						Size{Max, 1000},
					},
					[]Format{JPG, PNG},
					[]storeConfig{
						{
							"local": "/mnt/gallery",
						},
					},
				},
			},
			err: nil,
		},
	}
	for _, tc := range testCases {
		c := NewConfig()
		err := c.LoadStream(strings.NewReader(tc.yaml))
		if !reflect.DeepEqual(tc.expected, c) {
			t.Errorf("Expected %q, got %q for %q", tc.expected, c, tc.yaml)
		}
		if err != tc.err {
			t.Errorf("Expected error %q, got %q for %q", tc.err, err, tc.yaml)
		}
	}
}
<commit_msg>Wrong format test case<commit_after>package x

import (
	"reflect"
	"strings"
	"testing"
)

func TestConfig(t *testing.T) {
	var testCases = []struct {
		yaml     string
		expected *Config
		err      string
	}{
		{
			yaml: `android-tablets:
  sizes:
    - max_width: 500
    - max: 1000
  formats:
    - jpg
    - png
  store:
    - local: /mnt/gallery`,
			expected: &Config{
				"android-tablets": Handler{
					[]Size{
						Size{MaxWidth, 500},
						Size{Max, 1000},
					},
					[]Format{JPG, PNG},
					[]storeConfig{
						{
							"local": "/mnt/gallery",
						},
					},
				},
			},
			err: "",
		},
		{
			yaml:     `asd: 123`,
			expected: &Config{},
			err:      "yaml: unmarshal errors:\n  line 1: cannot unmarshal !!int `123` into x.Handler",
		},
	}
	for _, tc := range testCases {
		c := NewConfig()
		err := c.LoadStream(strings.NewReader(tc.yaml))
		if !reflect.DeepEqual(tc.expected, c) {
			t.Errorf("Expected %q, got %q for %q", tc.expected, c, tc.yaml)
		}
		if (err != nil) && (err.Error() != tc.err) {
			t.Errorf("Expected error %q, got %q for %q", tc.err, err.Error(), tc.yaml)
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package serializer

import (
	"bytes"
	"testing"
)

func TestSerializer(t *testing.T) {
	x := &X{
		Y: Y{1},
		P: &Y{2},
		A: []Y{{3}, {4}},
		B: true,
		S: "a\x09b",
		T: T1,
		F: nil,
	}
	buf := new(bytes.Buffer)
	Write(buf, x)
	t.Logf("\n%s", buf.String())
	t.Logf("\n%#v", x)
}

type X struct {
	Y Y
	P *Y
	A []Y
	B bool
	S string
	T T
	F func()
}

type Y struct {
	V int
}

type T int

const (
	_ T = iota
	T1
)
<commit_msg>pkg/serializer: test result in the test<commit_after>// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package serializer

import (
	"bytes"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestSerializer(t *testing.T) {
	x := &X{
		Y: Y{1},
		P: &Y{2},
		A: []Y{{3}, {4}},
		B: true,
		S: "a\x09b",
		T: T1,
		I: []interface{}{
			nil,
			Y{V: 42},
			new(Y),
			(*Y)(nil),
			0,
			42,
			T(0),
			T(42),
			U(96),
			false,
			B(false),
			"",
			"foo",
			S(""),
			S("foo"),
		},
		F: nil,
	}
	want := `&X{Y{1},&Y{2},[]Y{
{3},
{4},
},true,"a\tb",1,[]{
nil,
Y{},
&Y{},
nil,
0,
42,
0,
42,
96,
false,
false,
"",
"foo",
"",
"foo",
},nil}`
	buf := new(bytes.Buffer)
	Write(buf, x)
	if diff := cmp.Diff(want, buf.String()); diff != "" {
		t.Fatal(diff)
	}
}

type X struct {
	Y Y
	P *Y
	A []Y
	B bool
	S string
	T T
	I []interface{}
	F func()
}

type Y struct {
	V int
}

type (
	S string
	B bool
	T int
	U uint16
)

const (
	_ T = iota
	T1
)
<|endoftext|>


<commit_before>package config

import (
	"fmt"
	"testing"
)

func TestPrecedence(t *testing.T) {
	low := map[string]string{
		"without_override": "false",
		"with_override":    "false",
	}

	high := map[string]string{
		"with_override": "true",
	}

	c := NewConfig(
		[]Provider{
			NewStatic(low),
			NewStatic(high),
		},
	)

	without, err := c.Bool("without_override")
	if err != nil {
		t.Error(err)
	}

	if without == true {
		t.Errorf("Setting 'without_override' was true, excpected false")
	}

	with, err := c.Bool("with_override")
	if err != nil {
		t.Error(err)
	}

	if with == false {
		t.Errorf("Setting 'with_override' was 'false', excpected 'true'")
	}
}

func TestTypeLookups(t *testing.T) {
	settings := map[string]string{
		"string": "some_string",
		"bool":   "true",
		"int":    "1",
		"float":  "1.5",
	}

	c := NewConfig([]Provider{NewStatic(settings)})

	s, err := c.String("string")
	if err != nil {
		t.Error(err)
	}

	if s != "some_string" {
		t.Errorf("String setting was '%s', excpected 'some_string'", s)
	}

	b, err := c.Bool("bool")
	if err != nil {
		t.Error(err)
	}

	if b != true {
		t.Errorf("Bool setting was 'false', excpected 'true'")
	}

	i, err := c.Int("int")
	if err != nil {
		t.Error(err)
	}

	if i != 1 {
		t.Errorf("Int setting was '%d', excpected '1'", i)
	}

	f, err := c.Float("float")
	if err != nil {
		t.Error(err)
	}

	if f != 1.5 {
		t.Errorf("Float setting was '%f', excpected '1.5'", f)
	}
}

func TestTypeOrLookups(t *testing.T) {
	c := NewConfig(nil)

	s, err := c.StringOr("string", "some_string")
	if err != nil {
		t.Error(err)
	}

	if s != "some_string" {
		t.Errorf("String setting was '%s', excpected 'some_string'", s)
	}

	b, err := c.BoolOr("bool", true)
	if err != nil {
		t.Error(err)
	}

	if b != true {
		t.Errorf("Bool setting was 'false', excpected 'true'")
	}

	i, err := c.IntOr("int", 1)
	if err != nil {
		t.Error(err)
	}

	if i != 1 {
		t.Errorf("Int setting was '%d', excpected '1'", i)
	}

	f, err := c.FloatOr("float", 1.5)
	if err != nil {
		t.Error(err)
	}

	if f != 1.5 {
		t.Errorf("Float setting was '%f', excpected '1.5'", f)
	}
}

func TestValidate(t *testing.T) {
	c := NewConfig(nil)
	c.Validate = func(map[string]string) error {
		return fmt.Errorf("some error")
	}

	if err := c.Load(); err == nil {
		t.Errorf("Error was nil")
	}
}
<commit_msg>fix typos<commit_after>package config

import (
	"fmt"
	"testing"
)

func TestPrecedence(t *testing.T) {
	low := map[string]string{
		"without_override": "false",
		"with_override":    "false",
	}

	high := map[string]string{
		"with_override": "true",
	}

	c := NewConfig(
		[]Provider{
			NewStatic(low),
			NewStatic(high),
		},
	)

	without, err := c.Bool("without_override")
	if err != nil {
		t.Error(err)
	}

	if without == true {
		t.Errorf("Setting 'without_override' was true, expected false")
	}

	with, err := c.Bool("with_override")
	if err != nil {
		t.Error(err)
	}

	if with == false {
		t.Errorf("Setting 'with_override' was 'false', expected 'true'")
	}
}

func TestTypeLookups(t *testing.T) {
	settings := map[string]string{
		"string": "some_string",
		"bool":   "true",
		"int":    "1",
		"float":  "1.5",
	}

	c := NewConfig([]Provider{NewStatic(settings)})

	s, err := c.String("string")
	if err != nil {
		t.Error(err)
	}

	if s != "some_string" {
		t.Errorf("String setting was '%s', expected 'some_string'", s)
	}

	b, err := c.Bool("bool")
	if err != nil {
		t.Error(err)
	}

	if b != true {
		t.Errorf("Bool setting was 'false', expected 'true'")
	}

	i, err := c.Int("int")
	if err != nil {
		t.Error(err)
	}

	if i != 1 {
		t.Errorf("Int setting was '%d', expected '1'", i)
	}

	f, err := c.Float("float")
	if err != nil {
		t.Error(err)
	}

	if f != 1.5 {
		t.Errorf("Float setting was '%f', expected '1.5'", f)
	}
}

func TestTypeOrLookups(t *testing.T) {
	c := NewConfig(nil)

	s, err := c.StringOr("string", "some_string")
	if err != nil {
		t.Error(err)
	}

	if s != "some_string" {
		t.Errorf("String setting was '%s', expected 'some_string'", s)
	}

	b, err := c.BoolOr("bool", true)
	if err != nil {
		t.Error(err)
	}

	if b != true {
		t.Errorf("Bool setting was 'false', expected 'true'")
	}

	i, err := c.IntOr("int", 1)
	if err != nil {
		t.Error(err)
	}

	if i != 1 {
		t.Errorf("Int setting was '%d', expected '1'", i)
	}

	f, err := c.FloatOr("float", 1.5)
	if err != nil {
		t.Error(err)
	}

	if f != 1.5 {
		t.Errorf("Float setting was '%f', expected '1.5'", f)
	}
}

func TestValidate(t *testing.T) {
	c := NewConfig(nil)
	c.Validate = func(map[string]string) error {
		return fmt.Errorf("some error")
	}

	if err := c.Load(); err == nil {
		t.Errorf("Error was nil")
	}
}
<|endoftext|>