├── main.go ├── hack └── coverage.sh ├── .gitignore ├── internal ├── version │ ├── version_test.go │ └── version.go ├── mail │ ├── utils.go │ ├── utils_test.go │ ├── types.go │ ├── mail.go │ └── mail_test.go ├── prompt │ ├── prompt_test.go │ └── prompt.go ├── logging │ ├── logging_test.go │ └── logging.go ├── oreilly │ ├── test │ ├── types.go │ ├── oreilly_test.go │ └── oreilly.go ├── random │ ├── random_test.go │ └── random.go └── generator │ ├── generator.go │ └── generator_test.go ├── .github ├── dependabot.yml └── workflows │ ├── stale.yml │ ├── automerge.yml │ ├── pr.yml │ └── push.yml ├── cmd └── root │ ├── options │ ├── options_test.go │ └── options.go │ ├── root_test.go │ └── root.go ├── go.mod ├── README.md ├── Makefile ├── go.sum └── LICENSE /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 joshsagredo joshsagredo@protonmail.com 3 | */ 4 | package main 5 | 6 | import ( 7 | "github.com/joshsagredo/oreilly-trial/cmd/root" 8 | ) 9 | 10 | func main() { 11 | root.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /hack/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test -tags "unit e2e integration" ./... -race -p 1 -coverprofile=coverage.txt -covermode=atomic -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" 4 | go tool cover -html=all_coverage.txt -o all_cover.html 5 | open all_cover.html 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bin/ 3 | vendor/ 4 | **coverage.txt 5 | cover.html 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, build with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | .bin 20 | venv 21 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package version 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGet(t *testing.T) { 13 | ver := Get() 14 | assert.NotNil(t, ver) 15 | assert.Equal(t, ver.GitVersion, "none") 16 | assert.Equal(t, ver.GitCommit, "none") 17 | assert.Equal(t, ver.BuildDate, "none") 18 | } 19 | -------------------------------------------------------------------------------- /internal/mail/utils.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import "regexp" 4 | 5 | func contains(sl []string, name string) bool { 6 | for _, value := range sl { 7 | if value == name { 8 | return true 9 | } 10 | } 11 | return false 12 | } 13 | 14 | func IsValidEmail(mail string) bool { 15 | emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 16 | return emailRegex.MatchString(mail) 17 | } 18 | -------------------------------------------------------------------------------- /internal/prompt/prompt_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package prompt 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetPromptRunner(t *testing.T) { 13 | runner := GetPromptRunner() 14 | assert.NotNil(t, runner) 15 | } 16 | 17 | func TestGetSelectRunner(t *testing.T) { 18 | runner := GetSelectRunner() 19 | assert.NotNil(t, runner) 20 | } 21 | -------------------------------------------------------------------------------- /internal/logging/logging_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package logging 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // TestGetLogger function tests if GetLogger function running properly 13 | func TestGetLogger(t *testing.T) { 14 | logger := GetLogger() 15 | assert.NotNil(t, logger) 16 | } 17 | 18 | func TestEnableDebugLogging(t *testing.T) { 19 | EnableDebugLogging() 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | target-branch: "devel" 9 | labels: 10 | - "dependabot" 11 | - "gomod" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | target-branch: "devel" 17 | labels: 18 | - "dependabot" 19 | - "github-actions" 20 | -------------------------------------------------------------------------------- /internal/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | var ( 10 | logger zerolog.Logger 11 | Level = zerolog.InfoLevel 12 | ) 13 | 14 | func init() { 15 | consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} 16 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 17 | logger = zerolog.New(consoleWriter).With().Timestamp().Logger().Level(Level) 18 | } 19 | 20 | func GetLogger() zerolog.Logger { 21 | return logger 22 | } 23 | 24 | func EnableDebugLogging() { 25 | logger = logger.Level(zerolog.DebugLevel) 26 | } 27 | -------------------------------------------------------------------------------- /internal/oreilly/test: -------------------------------------------------------------------------------- 1 | abck 2 | _abck=29315AD505D2ACE73274F6B54F935D86~0~YAAQJI4hF5aeN/aKAQAAgeKzIApiZRRsGhDw8iI+Qw1Z1sFj4KoCJbUOuxOiQ8y5heTcEVXO2ykZZLGJVe33a4urup/+UeE5e6YUO7xxi4Yb1cOhHuY8IAyshLNXu3K0+9+3eqnZfBDEbHU6e+1CgsU1ftUGkbSfrDYbs/Lsa1w6F9Cz8229OqNT5dgwgH0SmnOF+02uaIiPJGazI67BzfzS3ip+Se4a7GfcAqzatpjD+4mYFII+rs7ci0FSdJ6yrvjcbV1Mku835XztmcknM4vUohC9fgU4JW95wIoclVKQ6lVDe2Wde9f9Ej2b2iSEwtZL/xhehGkhBy8uJ9SF3GS4D1XhYt6xUhF2qw3I+cL1j+VLcpVNIXcuUuncQqtExH7bzvIlcshumxuUTS+t5KKxkg22Ja/1xA==~-1~||-1||~-1 3 | comes from https://www.oreilly.com/hpjtVEdN9/XUQKv1bS/7Lv3-i7D/88/3JYYb6DkDw9LS1/bGdLUXc/ZBg/0CnJxbToB -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "runtime" 4 | 5 | var ( 6 | gitVersion = "none" 7 | gitCommit = "none" 8 | buildDate = "none" 9 | ) 10 | 11 | var ver = Version{ 12 | GoVersion: runtime.Version(), 13 | GoOs: runtime.GOOS, 14 | GoArch: runtime.GOARCH, 15 | GitVersion: gitVersion, 16 | GitCommit: gitCommit, 17 | BuildDate: buildDate, 18 | } 19 | 20 | type Version struct { 21 | GoVersion string 22 | GoOs string 23 | GoArch string 24 | GitVersion string 25 | GitCommit string 26 | BuildDate string 27 | } 28 | 29 | func Get() Version { 30 | return ver 31 | } 32 | -------------------------------------------------------------------------------- /cmd/root/options/options_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package options 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // TestGetRootOptions function tests if GetRootOptions function running properly 15 | func TestGetRootOptions(t *testing.T) { 16 | t.Log("fetching default options.RootOptions") 17 | opts := GetRootOptions() 18 | assert.NotNil(t, opts) 19 | t.Logf("fetched default options.RootOptions, %v\n", opts) 20 | } 21 | 22 | func TestRootOptions_InitFlags(t *testing.T) { 23 | opts := GetRootOptions() 24 | assert.NotNil(t, opts) 25 | cmd := &cobra.Command{} 26 | opts.InitFlags(cmd) 27 | } 28 | -------------------------------------------------------------------------------- /internal/random/random_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package random 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // TestGeneratePassword function tests if GeneratePassword function running properly 14 | func TestGeneratePassword(t *testing.T) { 15 | cases := []struct { 16 | caseName string 17 | }{ 18 | {"randomusername"}, 19 | } 20 | 21 | for _, tc := range cases { 22 | t.Run(tc.caseName, func(t *testing.T) { 23 | password, err := GeneratePassword() 24 | fmt.Println(password) 25 | assert.Nil(t, err) 26 | assert.NotEmpty(t, password) 27 | assert.Len(t, password, randomLength) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/mail/utils_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package mail 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIsValidEmail(t *testing.T) { 13 | cases := []struct { 14 | caseName, email string 15 | shouldPass bool 16 | }{ 17 | {"successCase", "nonexistedemail@gmail.com", true}, 18 | {"failCase", "nonexistedmail@gmail", false}, 19 | } 20 | 21 | for _, tc := range cases { 22 | t.Run(tc.caseName, func(t *testing.T) { 23 | t.Logf("running case %s\n", tc.caseName) 24 | valid := IsValidEmail(tc.email) 25 | switch tc.shouldPass { 26 | case true: 27 | assert.True(t, valid) 28 | case false: 29 | assert.False(t, valid) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/oreilly/types.go: -------------------------------------------------------------------------------- 1 | package oreilly 2 | 3 | const ( 4 | requestAuthority = "learning.oreilly.com" 5 | requestPragma = "no-cache" 6 | requestCacheControl = "no-cache" 7 | requestAccept = "application/json" 8 | requestUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36" 9 | requestContentType = "application/json" 10 | requestOrigin = "https://learning.oreilly.com" 11 | requestSecFetchSite = "same-origin" 12 | requestSecFetchMode = "cors" 13 | requestSecFetchDest = "empty" 14 | requestReferer = "https://learning.oreilly.com/p/register/" 15 | requestAcceptLang = "en-US,en;q=0.9" 16 | ) 17 | 18 | // successResponse is the DTO for remote API call 19 | type successResponse struct { 20 | UserID string `json:"user_id"` 21 | } 22 | -------------------------------------------------------------------------------- /internal/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | const ( 9 | chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" 10 | digits = "0123456789" 11 | specials = "=+*/!@#$?" 12 | all = chars + digits + specials 13 | randomLength = 12 14 | ) 15 | 16 | // GeneratePassword generates a random string for username or password 17 | func GeneratePassword() (string, error) { 18 | //r := mathRand.New(mathRand.NewSource(time.Now().UnixNano())) 19 | res := make([]byte, randomLength) 20 | for i := 0; i < randomLength; i++ { 21 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(all)))) 22 | if err != nil { 23 | return "", err 24 | } 25 | res[i] = all[num.Int64()] 26 | } 27 | 28 | return string(res), nil 29 | } 30 | -------------------------------------------------------------------------------- /cmd/root/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var rootOptions = &RootOptions{} 6 | 7 | // RootOptions contains frequent command line and application options. 8 | type RootOptions struct { 9 | // BannerFilePath is the relative path to the banner file 10 | BannerFilePath string 11 | // VerboseLog is the verbosity of the logging library 12 | VerboseLog bool 13 | } 14 | 15 | func (opts *RootOptions) InitFlags(cmd *cobra.Command) { 16 | cmd.Flags().StringVarP(&opts.BannerFilePath, "bannerFilePath", "", "banner.txt", 17 | "relative path of the banner file") 18 | cmd.Flags().BoolVarP(&opts.VerboseLog, "verbose", "", false, 19 | "verbose output of the logging library as 'debug' (default false)") 20 | } 21 | 22 | // GetRootOptions returns the pointer of RootOptions 23 | func GetRootOptions() *RootOptions { 24 | return rootOptions 25 | } 26 | -------------------------------------------------------------------------------- /internal/prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/joshsagredo/oreilly-trial/internal/mail" 7 | "github.com/manifoldco/promptui" 8 | ) 9 | 10 | type SelectRunner interface { 11 | Run() (int, string, error) 12 | } 13 | 14 | func GetSelectRunner() *promptui.Select { 15 | return &promptui.Select{ 16 | Label: "An error occurred while generating Oreilly account with temporary mail, would you like to provide your own valid email address?", 17 | Items: []string{"Yes please!", "No thanks!"}, 18 | } 19 | } 20 | 21 | type PromptRunner interface { 22 | Run() (string, error) 23 | } 24 | 25 | func GetPromptRunner() *promptui.Prompt { 26 | return &promptui.Prompt{ 27 | Label: "Your valid email address", 28 | Validate: func(s string) error { 29 | if !mail.IsValidEmail(s) { 30 | return errors.New("no valid email provided by user") 31 | } 32 | 33 | return nil 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joshsagredo/oreilly-trial 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dimiro1/banner v1.1.0 7 | github.com/manifoldco/promptui v0.9.0 8 | github.com/pkg/errors v0.9.1 9 | github.com/rs/zerolog v1.30.0 10 | github.com/spf13/cobra v1.7.0 11 | github.com/stretchr/testify v1.8.4 12 | ) 13 | 14 | require ( 15 | github.com/chzyer/readline v1.5.1 // indirect 16 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/kr/pretty v0.3.0 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.19 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/spf13/pflag v1.0.5 // indirect 24 | golang.org/x/sys v0.10.0 // indirect 25 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /internal/mail/types.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | const ( 4 | emailRequestQuery = "{\"query\":\"mutation introduceSession($input: IntroduceSessionInput) { " + 5 | "introduceSession(input: $input) { " + 6 | "id " + 7 | "addresses { " + 8 | "address " + 9 | "} " + 10 | "expiresAt " + 11 | "} " + 12 | "}\",\"variables\":{\"input\":{\"withAddress\":true,\"domainId\":\"%s\"}}}" 13 | domainRequestQuery = "{\"query\":\"query { domains { id name introducedAt availableVia }}\",\"variables\":{}}" 14 | hostHeader = "dropmail.p.rapidapi.com" 15 | contentType = "application/json" 16 | ) 17 | 18 | type DomainResponse struct { 19 | DomainData `json:"data"` 20 | } 21 | 22 | type DomainData struct { 23 | Domains []Domain `json:"domains"` 24 | } 25 | 26 | type Domain struct { 27 | Name string `json:"name"` 28 | ID string `json:"id"` 29 | AvailableVia []string `json:"availableVia"` 30 | } 31 | 32 | type EmailResponse struct { 33 | EmailData `json:"data"` 34 | } 35 | 36 | type EmailData struct { 37 | IntroduceSession `json:"introduceSession"` 38 | } 39 | 40 | type IntroduceSession struct { 41 | ID string `json:"id"` 42 | ExpiresAt string `json:"expiresAt"` 43 | Addresses []Address `json:"addresses"` 44 | } 45 | 46 | type Address struct { 47 | Address string `json:"address"` 48 | } 49 | -------------------------------------------------------------------------------- /internal/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "errors" 5 | "github.com/joshsagredo/oreilly-trial/internal/logging" 6 | "github.com/joshsagredo/oreilly-trial/internal/mail" 7 | "github.com/joshsagredo/oreilly-trial/internal/oreilly" 8 | "github.com/joshsagredo/oreilly-trial/internal/random" 9 | ) 10 | 11 | func RunGenerator() error { 12 | logger := logging.GetLogger() 13 | 14 | var password string 15 | var err error 16 | 17 | if password, err = random.GeneratePassword(); err != nil { 18 | logger.Error().Str("error", err.Error()).Msg("unable to generate password") 19 | return err 20 | } 21 | 22 | validDomains, err := mail.GetPossiblyValidDomains() 23 | if err != nil { 24 | logger.Error().Str("error", err.Error()).Msg("an error occurred while fetching valid domains") 25 | return err 26 | } 27 | 28 | for i, domain := range validDomains { 29 | email, err := mail.GenerateTempMail(domain) 30 | if err != nil { 31 | logger.Error(). 32 | Str("error", err.Error()). 33 | Str("domain", domain). 34 | Msg("an error occurred while generating email with specific domain") 35 | continue 36 | } 37 | 38 | if err := oreilly.Generate(email, password, logger); err != nil { 39 | logger.Warn(). 40 | Str("error", err.Error()). 41 | Str("domain", domain). 42 | Str("mail", email). 43 | Int("attempt", i+1). 44 | Msg("an error occurred while generating email with specific domain") 45 | continue 46 | } 47 | 48 | logger.Info(). 49 | Str("email", email). 50 | Str("password", password). 51 | Msg("trial account successfully created") 52 | 53 | return nil 54 | } 55 | 56 | return errors.New("all attempts failed") 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mark stale issues and pull requests 3 | 4 | on: 5 | schedule: 6 | - cron: "0 0 * * 0" 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v8 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: "This bot triages issues and PRs according to the following rules: 16 | - After 180d of inactivity, lifecycle/stale is applied. 17 | - After 90d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the issue is closed. 18 | You can: 19 | - Make a comment to remove the stale label and show your support. The 180d days reset. 20 | - If an issue has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening" 21 | stale-pr-message: "This bot triages issues and PRs according to the following rules: 22 | - After 180d of inactivity, lifecycle/stale is applied. 23 | - After 90d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed. 24 | You can: 25 | - Make a comment to remove the stale label and show your support. The 180d days reset. 26 | - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening." 27 | days-before-stale: 180 28 | days-before-close: 90 29 | stale-issue-label: 'lifecycle/stale' 30 | stale-pr-label: 'lifecycle/stale' 31 | exempt-issue-label: 'lifecycle/frozen' 32 | exempt-pr-label: 'lifecycle/frozen' 33 | close-issue-label: 'lifecycle/rotten' 34 | close-pr-label: 'lifecycle/rotten' 35 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependabot auto-merge 3 | on: pull_request 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: dependabot-metadata 16 | uses: dependabot/fetch-metadata@v1 17 | - uses: actions/checkout@v4 18 | - name: Approve a PR if not already approved 19 | run: | 20 | gh pr checkout "$PR_URL" # sets the upstream metadata for `gh pr status` 21 | if [ "$(gh pr status --json reviewDecision -q .currentBranch.reviewDecision)" != "APPROVED" ]; then 22 | gh pr review --approve "$PR_URL" 23 | else 24 | echo "PR already approved, skipping additional approvals to minimize emails/notification noise."; 25 | fi 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | - name: Enable auto-merge for Dependabot PRs for github-actions package ecosystem 30 | if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'github_actions' }} 31 | run: gh pr merge --auto --rebase "$PR_URL" 32 | env: 33 | PR_URL: ${{github.event.pull_request.html_url}} 34 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 35 | - name: Enable auto-merge for Dependabot PRs for go_modules package ecosystem 36 | if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'go_modules' && steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 37 | run: gh pr merge --auto --rebase "$PR_URL" 38 | env: 39 | PR_URL: ${{github.event.pull_request.html_url}} 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oreilly Trial 2 | 3 | As you know, you can create 10 day free trial for https://learning.oreilly.com/ for testing purposes. 4 | 5 | This tool does couple of simple steps to provide free trial account for you: 6 | - Creates temp mail with specific domains over https://dropmail.p.rapidapi.com/ 7 | - Tries to register with created temp mails to https://learning.oreilly.com/ 8 | - Prints the login information to console and then exits. 9 | 10 | ## Configuration 11 | oreilly-trial can be customized with several command line arguments: 12 | ``` 13 | Usage: 14 | oreilly-trial [flags] 15 | 16 | Flags: 17 | -h, --help help for oreilly-trial 18 | --verbose verbose output of the logging library as 'debug' (default false) 19 | -v, --version version for oreilly-trial 20 | ``` 21 | 22 | ## Installation 23 | 24 | ### Binary 25 | Binary can be downloaded from [Releases](https://github.com/joshsagredo/oreilly-trial/releases) page. 26 | 27 | After then, you can simply run binary by providing required command line arguments: 28 | ```shell 29 | $ ./oreilly-trial 30 | ``` 31 | 32 | ### Homebrew 33 | This project can be installed with [Homebrew](https://brew.sh/): 34 | ```shell 35 | $ brew tap joshsagredo/tap 36 | $ brew install joshsagredo/tap/oreilly-trial 37 | ``` 38 | 39 | Then similar to binary method, you can run it by calling below command: 40 | ```shell 41 | $ oreilly-trial 42 | ``` 43 | 44 | ### Docker 45 | You can simply run docker image with default configuration: 46 | ```shell 47 | $ docker run joshsagredo/oreilly-trial:latest 48 | ``` 49 | 50 | ## Development 51 | This project requires below tools while developing: 52 | - [Golang 1.20](https://golang.org/doc/go1.20) 53 | - [pre-commit](https://pre-commit.com/) 54 | - [golangci-lint](https://golangci-lint.run/usage/install/) - required by [pre-commit](https://pre-commit.com/) 55 | - [gocyclo](https://github.com/fzipp/gocyclo) - required by [pre-commit](https://pre-commit.com/) 56 | 57 | After you installed [pre-commit](https://pre-commit.com/), simply run below command to prepare your development environment: 58 | ```shell 59 | $ make pre-commit-setup 60 | ``` 61 | 62 | ## License 63 | Apache License 2.0 64 | -------------------------------------------------------------------------------- /internal/oreilly/oreilly_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package oreilly 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/joshsagredo/oreilly-trial/internal/logging" 13 | "github.com/joshsagredo/oreilly-trial/internal/mail" 14 | "github.com/joshsagredo/oreilly-trial/internal/random" 15 | 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | // TestGenerateError function spins up a fake httpserver and simulates a 400 bad request response 20 | func TestGenerateError(t *testing.T) { 21 | // Start a local HTTP server 22 | expectedError := "400 - bad request" 23 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | w.WriteHeader(http.StatusBadRequest) 25 | if _, err := fmt.Fprint(w, expectedError); err != nil { 26 | t.Fatalf("a fatal error occured while writing response body: %s", err.Error()) 27 | } 28 | })) 29 | 30 | defer func() { 31 | server.Close() 32 | }() 33 | 34 | apiURLOrig := apiURL 35 | apiURL = server.URL 36 | err := Generate("notreallyrequiredmail@example.com", "123123123123", logging.GetLogger()) 37 | assert.NotNil(t, err) 38 | assert.Contains(t, err.Error(), expectedError) 39 | 40 | apiURL = apiURLOrig 41 | } 42 | 43 | // TestGenerateInvalidHost function tests if Generate function fails on broken Host argument 44 | func TestGenerateInvalidHost(t *testing.T) { 45 | expectedError := "no such host" 46 | 47 | apiURLOrig := apiURL 48 | apiURL = "https://foo.example.com/" 49 | 50 | err := Generate("notreallyrequiredmail@example.com", "123123123123", logging.GetLogger()) 51 | assert.NotNil(t, err) 52 | assert.Contains(t, err.Error(), expectedError) 53 | 54 | apiURL = apiURLOrig 55 | } 56 | 57 | // TestGenerateValidArgs function tests if Generate function running properly with proper values 58 | func TestGenerateValidArgs(t *testing.T) { 59 | password, err := random.GeneratePassword() 60 | assert.NotEmpty(t, password) 61 | assert.Nil(t, err) 62 | 63 | domains, _ := mail.GetPossiblyValidDomains() 64 | 65 | for _, id := range domains { 66 | email, err := mail.GenerateTempMail(id) 67 | assert.NotEmpty(t, email) 68 | assert.Nil(t, err) 69 | 70 | err = Generate(email, password, logging.GetLogger()) 71 | 72 | if err == nil { 73 | break 74 | } 75 | } 76 | 77 | assert.Nil(t, err) 78 | } 79 | -------------------------------------------------------------------------------- /internal/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | ApiURL = "https://dropmail.p.rapidapi.com/" 14 | token = "none" 15 | PredefinedValidDomains = []string{"mailpwr.com", "mimimail.me"} 16 | ) 17 | 18 | func GetPossiblyValidDomains() ([]string, error) { 19 | var possibleValidDomains []string 20 | var domainRequestData = strings.NewReader(domainRequestQuery) 21 | domainRequest, _ := http.NewRequest("POST", ApiURL, domainRequestData) 22 | domainRequest.Header.Add("content-type", contentType) 23 | domainRequest.Header.Add("X-RapidAPI-Key", token) 24 | domainRequest.Header.Add("X-RapidAPI-Host", hostHeader) 25 | domainResp, err := http.DefaultClient.Do(domainRequest) 26 | if err != nil { 27 | return possibleValidDomains, err 28 | } 29 | 30 | defer func() { 31 | if err := domainResp.Body.Close(); err != nil { 32 | panic(err) 33 | } 34 | }() 35 | body, err := io.ReadAll(domainResp.Body) 36 | if err != nil { 37 | return possibleValidDomains, err 38 | } 39 | 40 | var domainResponse DomainResponse 41 | if err := json.Unmarshal(body, &domainResponse); err != nil { 42 | return possibleValidDomains, err 43 | } 44 | 45 | for _, v := range domainResponse.Domains { 46 | if contains(PredefinedValidDomains, v.Name) { 47 | possibleValidDomains = append(possibleValidDomains, v.ID) 48 | } 49 | } 50 | 51 | if len(possibleValidDomains) == 0 { 52 | return possibleValidDomains, errors.New("empty list of valid domains") 53 | } 54 | 55 | return possibleValidDomains, nil 56 | } 57 | 58 | func GenerateTempMail(domainID string) (string, error) { 59 | var emailRequestData = strings.NewReader(fmt.Sprintf(emailRequestQuery, domainID)) 60 | emailRequest, _ := http.NewRequest("POST", ApiURL, emailRequestData) 61 | emailRequest.Header.Add("content-type", contentType) 62 | emailRequest.Header.Add("X-RapidAPI-Key", token) 63 | emailRequest.Header.Add("X-RapidAPI-Host", hostHeader) 64 | res, err := http.DefaultClient.Do(emailRequest) 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | defer func() { 70 | if err := res.Body.Close(); err != nil { 71 | panic(err) 72 | } 73 | }() 74 | body, err := io.ReadAll(res.Body) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | var resp EmailResponse 80 | if err := json.Unmarshal(body, &resp); err != nil { 81 | return "", err 82 | } 83 | 84 | if len(resp.Addresses) == 0 { 85 | return "", errors.New("no email returned from API") 86 | } 87 | 88 | return resp.Addresses[0].Address, nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/mail/mail_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package mail 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestGetPossiblyValidDomains(t *testing.T) { 16 | domains, err := GetPossiblyValidDomains() 17 | assert.NotEmpty(t, domains) 18 | assert.Nil(t, err) 19 | } 20 | 21 | func TestGetPossiblyValidDomainsTimeoutError(t *testing.T) { 22 | apiURLOrig := ApiURL //nolint:typecheck 23 | ApiURL = "https://dropmail.p.rapidapi.co/" 24 | domains, err := GetPossiblyValidDomains() 25 | assert.Empty(t, domains) 26 | assert.NotNil(t, err) 27 | ApiURL = apiURLOrig 28 | } 29 | 30 | func TestGetPossiblyValidDomainsUnmarshalError(t *testing.T) { 31 | apiURLOrig := ApiURL //nolint:typecheck 32 | response := "non-json response" 33 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | w.WriteHeader(http.StatusOK) 35 | if _, err := fmt.Fprint(w, response); err != nil { 36 | t.Fatalf("a fatal error occured while writing response body: %s", err.Error()) 37 | } 38 | })) 39 | 40 | defer func() { 41 | server.Close() 42 | }() 43 | 44 | ApiURL = server.URL 45 | domains, err := GetPossiblyValidDomains() 46 | assert.Empty(t, domains) 47 | assert.NotNil(t, err) 48 | ApiURL = apiURLOrig 49 | } 50 | 51 | func TestGetPossiblyValidDomainsEmptyValidDomains(t *testing.T) { 52 | predefinedValidDomainsOrig := PredefinedValidDomains 53 | PredefinedValidDomains = []string{} 54 | domains, err := GetPossiblyValidDomains() 55 | assert.Empty(t, domains) 56 | assert.NotNil(t, err) 57 | PredefinedValidDomains = predefinedValidDomainsOrig 58 | } 59 | 60 | func TestGenerateTempMail(t *testing.T) { 61 | domains, err := GetPossiblyValidDomains() 62 | assert.NotEmpty(t, domains) 63 | assert.Nil(t, err) 64 | 65 | mail, err := GenerateTempMail(domains[0]) 66 | assert.NotEmpty(t, mail) 67 | assert.Nil(t, err) 68 | } 69 | 70 | func TestGenerateTempMailTimeoutError(t *testing.T) { 71 | domains, err := GetPossiblyValidDomains() 72 | assert.NotEmpty(t, domains) 73 | assert.Nil(t, err) 74 | 75 | apiURLOrig := ApiURL //nolint:typecheck 76 | ApiURL = "https://dropmail.p.rapidapi.co/" 77 | mail, err := GenerateTempMail(domains[0]) 78 | assert.Empty(t, mail) 79 | assert.NotNil(t, err) 80 | ApiURL = apiURLOrig 81 | } 82 | 83 | func TestGenerateTempMailUnmarshalError(t *testing.T) { 84 | apiURLOrig := ApiURL //nolint:typecheck 85 | response := "non-json response" 86 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 | w.WriteHeader(http.StatusOK) 88 | if _, err := fmt.Fprint(w, response); err != nil { 89 | t.Fatalf("a fatal error occured while writing response body: %s", err.Error()) 90 | } 91 | })) 92 | 93 | defer func() { 94 | server.Close() 95 | }() 96 | 97 | ApiURL = server.URL 98 | mail, err := GenerateTempMail("123456") 99 | assert.Empty(t, mail) 100 | assert.NotNil(t, err) 101 | ApiURL = apiURLOrig 102 | } 103 | -------------------------------------------------------------------------------- /internal/oreilly/oreilly.go: -------------------------------------------------------------------------------- 1 | package oreilly 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/rs/zerolog" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | var ( 14 | client *http.Client 15 | apiURL = "https://learning.oreilly.com/api/v1/registration/individual/" 16 | ) 17 | 18 | func init() { 19 | client = &http.Client{} 20 | } 21 | 22 | // Generate does the heavy lifting, communicates with the Oreilly API 23 | func Generate(mail, password string, logger zerolog.Logger) error { 24 | var ( 25 | jsonData []byte 26 | req *http.Request 27 | resp *http.Response 28 | respBody []byte 29 | err error 30 | ) 31 | 32 | // prepare json data 33 | values := map[string]string{ 34 | "email": mail, 35 | "password": password, 36 | "first_name": "John", 37 | "last_name": "Doe", 38 | "country": "US", 39 | "t_c_agreement": "true", 40 | "contact": "true", 41 | "trial_length": "10", 42 | "path": "/register/", 43 | "source": "payments-client-register", 44 | } 45 | 46 | // marshall the json body 47 | if jsonData, err = json.Marshal(values); err != nil { 48 | return errors.Wrap(err, "unable to marshal request body") 49 | } 50 | 51 | // prepare and make the request 52 | if req, err = http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)); err != nil { 53 | return errors.Wrap(err, "unable to prepare http request") 54 | } 55 | 56 | logger.Debug().Msg("trying to set request headers") 57 | setRequestHeaders(req) 58 | 59 | logger.Debug().Str("url", apiURL).Msg("sending request with http client") 60 | if resp, err = client.Do(req); err != nil { 61 | return errors.Wrapf(err, "unable to do http request to remote host %s", apiURL) 62 | } 63 | 64 | defer func(body io.ReadCloser) { 65 | err = body.Close() 66 | }(resp.Body) 67 | 68 | // read the response 69 | if respBody, err = io.ReadAll(resp.Body); err != nil { 70 | return errors.Wrap(err, "unable to read response") 71 | } 72 | 73 | if resp.StatusCode == 200 { 74 | var successResponse successResponse 75 | if err = json.Unmarshal(respBody, &successResponse); err != nil { 76 | return errors.Wrap(err, "unable to unmarshal json response") 77 | } 78 | } else { 79 | return errors.New(string(respBody)) 80 | } 81 | 82 | return err 83 | } 84 | 85 | // setRequestHeaders gets the http.Request as input and add some headers for proper API request 86 | func setRequestHeaders(req *http.Request) { 87 | req.Header.Set("authority", requestAuthority) 88 | req.Header.Set("pragma", requestPragma) 89 | req.Header.Set("cache-control", requestCacheControl) 90 | req.Header.Set("accept", requestAccept) 91 | req.Header.Set("user-agent", requestUserAgent) 92 | req.Header.Set("content-type", requestContentType) 93 | req.Header.Set("origin", requestOrigin) 94 | req.Header.Set("sec-fetch-site", requestSecFetchSite) 95 | req.Header.Set("sec-fetch-mode", requestSecFetchMode) 96 | req.Header.Set("sec-fetch-dest", requestSecFetchDest) 97 | req.Header.Set("referer", requestReferer) 98 | req.Header.Set("accept-language", requestAcceptLang) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/root/root_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | // +build e2e 3 | 4 | package root 5 | 6 | import ( 7 | "errors" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/joshsagredo/oreilly-trial/internal/mail" 12 | "github.com/joshsagredo/oreilly-trial/internal/prompt" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type promptMock struct { 18 | msg string 19 | err error 20 | } 21 | 22 | func (p promptMock) Run() (string, error) { 23 | // return expected result 24 | return p.msg, p.err 25 | } 26 | 27 | type selectMock struct { 28 | msg string 29 | err error 30 | } 31 | 32 | func (p selectMock) Run() (int, string, error) { 33 | // return expected result 34 | return 1, p.msg, p.err 35 | } 36 | 37 | func TestExecuteWithPromptsSuccessSelectFailPrompt(t *testing.T) { 38 | // get original value for valid domains 39 | predefinedValidDomainsOrg := mail.PredefinedValidDomains 40 | 41 | // override valid domains 42 | mail.PredefinedValidDomains = []string{"ssss.com"} 43 | 44 | selectRunner = selectMock{msg: "Yes please!", err: nil} 45 | promptRunner = promptMock{msg: "nonexistedemailaddress@example.com", err: errors.New("dummy error")} 46 | err := rootCmd.Execute() 47 | assert.NotNil(t, err) 48 | 49 | // revert valid domains 50 | mail.PredefinedValidDomains = predefinedValidDomainsOrg 51 | selectRunner = prompt.GetSelectRunner() 52 | promptRunner = prompt.GetPromptRunner() 53 | } 54 | 55 | func TestExecuteWithPromptsFailSelect(t *testing.T) { 56 | // get original value for valid domains 57 | predefinedValidDomainsOrg := mail.PredefinedValidDomains 58 | 59 | // override valid domains 60 | mail.PredefinedValidDomains = []string{"ssss.com"} 61 | 62 | selectRunner = selectMock{msg: "No thanks!", err: nil} 63 | err := rootCmd.Execute() 64 | assert.NotNil(t, err) 65 | 66 | // revert valid domains 67 | mail.PredefinedValidDomains = predefinedValidDomainsOrg 68 | selectRunner = prompt.GetSelectRunner() 69 | } 70 | 71 | func TestExecute(t *testing.T) { 72 | bannerFilePathOrig, _ := rootCmd.Flags().GetString("bannerFilePath") 73 | assert.NotNil(t, bannerFilePathOrig) 74 | assert.NotEmpty(t, bannerFilePathOrig) 75 | 76 | err := rootCmd.Flags().Set("bannerFilePath", "./../build/ci/banner.txt") 77 | assert.Nil(t, err) 78 | 79 | err = rootCmd.Execute() 80 | assert.Nil(t, err) 81 | 82 | _ = rootCmd.Flags().Set("bannerFilePath", bannerFilePathOrig) 83 | } 84 | 85 | func TestExecute2(t *testing.T) { 86 | Execute() 87 | } 88 | 89 | func TestExecuteMissingBannerFile(t *testing.T) { 90 | bannerFilePathOrig, _ := rootCmd.Flags().GetString("bannerFilePath") 91 | assert.NotNil(t, bannerFilePathOrig) 92 | assert.NotEmpty(t, bannerFilePathOrig) 93 | 94 | err := rootCmd.Flags().Set("bannerFilePath", "asdfasdfasdf") 95 | assert.Nil(t, err) 96 | 97 | _ = rootCmd.Execute() 98 | 99 | _ = rootCmd.Flags().Set("bannerFilePath", bannerFilePathOrig) 100 | } 101 | 102 | func TestExecuteVerbose(t *testing.T) { 103 | verboseOrig, err := rootCmd.Flags().GetBool("verbose") 104 | assert.Nil(t, err) 105 | assert.False(t, verboseOrig) 106 | 107 | err = rootCmd.Flags().Set("verbose", "true") 108 | assert.Nil(t, err) 109 | 110 | err = rootCmd.Execute() 111 | assert.Nil(t, err) 112 | 113 | _ = rootCmd.Flags().Set("verbose", strconv.FormatBool(verboseOrig)) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/root/root.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/joshsagredo/oreilly-trial/internal/prompt" 8 | 9 | "github.com/joshsagredo/oreilly-trial/cmd/root/options" 10 | "github.com/rs/zerolog" 11 | 12 | "github.com/joshsagredo/oreilly-trial/internal/generator" 13 | "github.com/joshsagredo/oreilly-trial/internal/oreilly" 14 | "github.com/joshsagredo/oreilly-trial/internal/random" 15 | "github.com/joshsagredo/oreilly-trial/internal/version" 16 | 17 | "github.com/dimiro1/banner" 18 | "github.com/joshsagredo/oreilly-trial/internal/logging" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var ( 23 | selectRunner prompt.SelectRunner = prompt.GetSelectRunner() 24 | promptRunner prompt.PromptRunner = prompt.GetPromptRunner() 25 | opts *options.RootOptions 26 | ver = version.Get() 27 | logger zerolog.Logger 28 | // rootCmd represents the base command when called without any subcommands 29 | rootCmd = &cobra.Command{ 30 | Use: "oreilly-trial", 31 | Short: "Trial account generator tool for Oreilly", 32 | Version: ver.GitVersion, 33 | SilenceUsage: true, 34 | SilenceErrors: true, 35 | Long: `As you know, you can create 10 day free trial for https://learning.oreilly.com/ for testing purposes. 36 | This tool does couple of simple steps to provide free trial account for you`, 37 | PreRunE: func(cmd *cobra.Command, args []string) error { 38 | if _, err := os.Stat(opts.BannerFilePath); err == nil { 39 | bannerBytes, _ := os.ReadFile(opts.BannerFilePath) 40 | banner.Init(os.Stdout, true, false, strings.NewReader(string(bannerBytes))) 41 | } 42 | 43 | if opts.VerboseLog { 44 | logging.EnableDebugLogging() 45 | } 46 | 47 | logger = logging.GetLogger() 48 | logger.Info().Str("appVersion", ver.GitVersion).Str("goVersion", ver.GoVersion).Str("goOS", ver.GoOs). 49 | Str("goArch", ver.GoArch).Str("gitCommit", ver.GitCommit).Str("buildDate", ver.BuildDate). 50 | Msg("oreilly-trial is started!") 51 | 52 | return nil 53 | }, 54 | RunE: func(cmd *cobra.Command, args []string) error { 55 | if err := generator.RunGenerator(); err != nil { 56 | _, result, _ := selectRunner.Run() 57 | switch result { 58 | case "Yes please!": 59 | mail, _ := promptRunner.Run() 60 | 61 | password, err := random.GeneratePassword() 62 | if err != nil { 63 | logger.Error(). 64 | Str("error", err.Error()). 65 | Msg("an error occurred while generating password") 66 | return err 67 | } 68 | 69 | if err := oreilly.Generate(mail, password, logger); err != nil { 70 | logger.Error(). 71 | Str("error", err.Error()). 72 | Msg("an error occurred while generating user with specific email") 73 | return err 74 | } 75 | 76 | logger.Info(). 77 | Str("email", mail). 78 | Str("password", password). 79 | Msg("trial account successfully created!") 80 | 81 | return nil 82 | case "No thanks!": 83 | return err 84 | } 85 | } 86 | 87 | return nil 88 | }, 89 | } 90 | ) 91 | 92 | func init() { 93 | opts = options.GetRootOptions() 94 | opts.InitFlags(rootCmd) 95 | 96 | _ = rootCmd.Flags().MarkHidden("bannerFilePath") 97 | } 98 | 99 | // Execute adds all child commands to the root command and sets flags appropriately. 100 | // This is called by main.main(). It only needs to happen once to the rootCmd. 101 | func Execute() { 102 | err := rootCmd.Execute() 103 | if err != nil { 104 | os.Exit(1) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR 3 | 4 | on: 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | 8 | env: 9 | GO111MODULE: on 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version-file: "go.mod" 20 | cache: true 21 | - name: Lint code 22 | run: make -s lint 23 | - name: Clean downloaded binaries 24 | run: make -s clean 25 | 26 | fmt: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Install Go 32 | uses: actions/setup-go@v4 33 | with: 34 | go-version-file: "go.mod" 35 | cache: true 36 | - name: Run fmt 37 | run: make -s fmt 38 | - name: Clean downloaded binaries 39 | run: make -s clean 40 | 41 | tests: 42 | strategy: 43 | matrix: 44 | name: [unit, e2e] 45 | runs-on: ubuntu-latest 46 | name: test (${{ matrix.name }}) 47 | steps: 48 | - name: Checkout code 49 | uses: actions/checkout@v4 50 | - name: Install Go 51 | uses: actions/setup-go@v4 52 | with: 53 | go-version-file: "go.mod" 54 | cache: true 55 | - name: Run ${{ matrix.name }} tests 56 | run: make -s test-${{ matrix.name }} 57 | env: 58 | API_TOKEN: ${{ secrets.API_TOKEN }} 59 | 60 | staticcheck: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v4 65 | - uses: dominikh/staticcheck-action@v1.3.0 66 | with: 67 | version: "2022.1.3" 68 | 69 | codeql: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout code 73 | uses: actions/checkout@v4 74 | - name: Initialize CodeQL 75 | uses: github/codeql-action/init@v2 76 | with: 77 | languages: go 78 | - name: Autobuild 79 | uses: github/codeql-action/autobuild@v2 80 | - name: Perform CodeQL Analysis 81 | uses: github/codeql-action/analyze@v2 82 | 83 | sonarcloud: 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v4 87 | with: 88 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 89 | - name: Install Go 90 | uses: actions/setup-go@v4 91 | with: 92 | go-version-file: "go.mod" 93 | cache: true 94 | - name: Coverage Test 95 | run: make -s test 96 | env: 97 | API_TOKEN: ${{ secrets.API_TOKEN }} 98 | - name: SonarCloud Scan 99 | uses: SonarSource/sonarcloud-github-action@master 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 103 | API_TOKEN: ${{ secrets.API_TOKEN }} 104 | with: 105 | args: > 106 | -Dproject.settings=build/ci/sonar-project.properties 107 | - name: SonarQube Quality Gate check 108 | uses: sonarsource/sonarqube-quality-gate-action@master 109 | timeout-minutes: 5 110 | env: 111 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 112 | 113 | builds: 114 | strategy: 115 | matrix: 116 | os: [ubuntu-latest, macos-latest] 117 | runs-on: ${{ matrix.os }} 118 | name: build (${{ matrix.os }}) 119 | steps: 120 | - name: Checkout code 121 | uses: actions/checkout@v4 122 | - name: Install Go 123 | uses: actions/setup-go@v4 124 | with: 125 | go-version-file: "go.mod" 126 | cache: true 127 | - name: Build on (${{ matrix.os }}) 128 | run: make -s build 129 | env: 130 | API_TOKEN: ${{ secrets.API_TOKEN }} 131 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ERRCHECK_VERSION ?= latest 2 | GOLANGCI_LINT_VERSION ?= latest 3 | REVIVE_VERSION ?= latest 4 | GOIMPORTS_VERSION ?= latest 5 | INEFFASSIGN_VERSION ?= latest 6 | 7 | LOCAL_BIN := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/.bin 8 | 9 | .PHONY: all 10 | all: clean tools lint fmt test build 11 | 12 | .PHONY: clean 13 | clean: 14 | rm -rf $(LOCAL_BIN) 15 | 16 | .PHONY: pre-commit-setup 17 | pre-commit-setup: 18 | #python3 -m venv venv 19 | #source venv/bin/activate 20 | #pip3 install pre-commit 21 | pre-commit install -c build/ci/.pre-commit-config.yaml 22 | 23 | .PHONY: tools 24 | tools: golangci-lint-install revive-install go-imports-install ineffassign-install errcheck-install 25 | go mod tidy 26 | 27 | .PHONY: golangci-lint-install 28 | golangci-lint-install: 29 | GOBIN=$(LOCAL_BIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 30 | 31 | .PHONY: revive-install 32 | revive-install: 33 | GOBIN=$(LOCAL_BIN) go install github.com/mgechev/revive@$(REVIVE_VERSION) 34 | 35 | .PHONY: ineffassign-install 36 | ineffassign-install: 37 | GOBIN=$(LOCAL_BIN) go install github.com/gordonklaus/ineffassign@$(INEFFASSIGN_VERSION) 38 | 39 | .PHONY: errcheck-install 40 | errcheck-install: 41 | GOBIN=$(LOCAL_BIN) go install github.com/kisielk/errcheck@$(ERRCHECK_VERSION) 42 | 43 | .PHONY: lint 44 | lint: tools run-lint 45 | 46 | .PHONY: run-lint 47 | run-lint: lint-golangci-lint lint-revive 48 | 49 | .PHONY: lint-golangci-lint 50 | lint-golangci-lint: 51 | $(info running golangci-lint...) 52 | $(LOCAL_BIN)/golangci-lint -v run ./... || (echo golangci-lint returned an error, exiting!; sh -c 'exit 1';) 53 | 54 | .PHONY: lint-revive 55 | lint-revive: 56 | $(info running revive...) 57 | $(LOCAL_BIN)/revive -formatter=stylish -config=build/ci/.revive.toml -exclude ./vendor/... ./... || (echo revive returned an error, exiting!; sh -c 'exit 1';) 58 | 59 | .PHONY: upgrade-direct-deps 60 | upgrade-direct-deps: tidy 61 | for item in `grep -v 'indirect' go.mod | grep '/' | cut -d ' ' -f 1`; do \ 62 | echo "trying to upgrade direct dependency $$item" ; \ 63 | go get -u $$item ; \ 64 | done 65 | go mod tidy 66 | go mod vendor 67 | 68 | .PHONY: tidy 69 | tidy: 70 | go mod tidy 71 | 72 | .PHONY: run-goimports 73 | run-goimports: go-imports-install 74 | for item in `find . -type f -name '*.go' -not -path './vendor/*'`; do \ 75 | $(LOCAL_BIN)/goimports -l -w $$item ; \ 76 | done 77 | 78 | .PHONY: go-imports-install 79 | go-imports-install: 80 | GOBIN=$(LOCAL_BIN) go install golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION) 81 | 82 | .PHONY: fmt 83 | fmt: tools run-errcheck run-fmt run-ineffassign run-vet 84 | 85 | .PHONY: run-errcheck 86 | run-errcheck: 87 | $(info running errcheck...) 88 | $(LOCAL_BIN)/errcheck ./... || (echo errcheck returned an error, exiting!; sh -c 'exit 1';) 89 | 90 | .PHONY: run-fmt 91 | run-fmt: 92 | $(info running fmt...) 93 | go fmt ./... || (echo fmt returned an error, exiting!; sh -c 'exit 1';) 94 | 95 | .PHONY: run-ineffassign 96 | run-ineffassign: 97 | $(info running ineffassign...) 98 | $(LOCAL_BIN)/ineffassign ./... || (echo ineffassign returned an error, exiting!; sh -c 'exit 1';) 99 | 100 | .PHONY: run-vet 101 | run-vet: 102 | $(info running vet...) 103 | go vet ./... || (echo vet returned an error, exiting!; sh -c 'exit 1';) 104 | 105 | .PHONY: test 106 | test: tidy 107 | $(info starting the test for whole module...) 108 | go test -tags "unit e2e integration" -failfast -vet=off -race -coverprofile=all_coverage.txt -covermode=atomic ./... -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" || (echo an error while testing, exiting!; sh -c 'exit 1';) 109 | 110 | .PHONY: test-unit 111 | test-unit: tidy 112 | $(info starting the unit test for whole module...) 113 | go test -tags "unit" -failfast -vet=off -race -coverprofile=unit_coverage.txt -covermode=atomic ./... -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" || (echo an error while testing, exiting!; sh -c 'exit 1';) 114 | 115 | .PHONY: test-e2e 116 | test-e2e: tidy 117 | $(info starting the e2e test for whole module...) 118 | go test -tags "e2e" -failfast -vet=off -race -coverprofile=e2e_coverage.txt -covermode=atomic ./... -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" || (echo an error while testing, exiting!; sh -c 'exit 1';) 119 | 120 | .PHONY: test-integration 121 | test-integration: tidy 122 | $(info starting the integration test for whole module...) 123 | go test -tags "integration" -failfast -vet=off -race -coverprofile=integration_coverage.txt -covermode=atomic ./... -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" || (echo an error while testing, exiting!; sh -c 'exit 1';) 124 | 125 | .PHONY: update 126 | update: tidy 127 | go get -u ./... 128 | 129 | .PHONY: build 130 | build: tidy 131 | $(info building binary...) 132 | go build -ldflags="-X github.com/joshsagredo/oreilly-trial/internal/mail.token=${API_TOKEN}" -o bin/main main.go || (echo an error while building binary, exiting!; sh -c 'exit 1';) 133 | 134 | .PHONY: run 135 | run: tidy 136 | go run main.go 137 | 138 | .PHONY: cross-compile 139 | cross-compile: 140 | GOOS=freebsd GOARCH=386 go build -o bin/main-freebsd-386 main.go 141 | GOOS=darwin GOARCH=386 go build -o bin/main-darwin-386 main.go 142 | GOOS=linux GOARCH=386 go build -o bin/main-linux-386 main.go 143 | GOOS=windows GOARCH=386 go build -o bin/main-windows-386 main.go 144 | GOOS=freebsd GOARCH=amd64 go build -o bin/main-freebsd-amd64 main.go 145 | GOOS=darwin GOARCH=amd64 go build -o bin/main-darwin-amd64 main.go 146 | GOOS=linux GOARCH=amd64 go build -o bin/main-linux-amd64 main.go 147 | GOOS=windows GOARCH=amd64 go build -o bin/main-windows-amd64 main.go 148 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | GO111MODULE: on 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version-file: "go.mod" 21 | cache: true 22 | - name: Lint code 23 | run: make -s lint 24 | - name: Clean downloaded binaries 25 | run: make -s clean 26 | 27 | fmt: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Install Go 33 | uses: actions/setup-go@v4 34 | with: 35 | go-version-file: "go.mod" 36 | cache: true 37 | - name: Run fmt 38 | run: make -s fmt 39 | - name: Clean downloaded binaries 40 | run: make -s clean 41 | 42 | tests: 43 | strategy: 44 | matrix: 45 | name: [unit, e2e] 46 | runs-on: ubuntu-latest 47 | name: test (${{ matrix.name }}) 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v4 51 | - name: Install Go 52 | uses: actions/setup-go@v4 53 | with: 54 | go-version-file: "go.mod" 55 | cache: true 56 | - name: Run ${{ matrix.name }} tests 57 | run: make -s test-${{ matrix.name }} 58 | env: 59 | API_TOKEN: ${{ secrets.API_TOKEN }} 60 | 61 | staticcheck: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Checkout code 65 | uses: actions/checkout@v4 66 | - uses: dominikh/staticcheck-action@v1.3.0 67 | with: 68 | version: "2022.1.3" 69 | 70 | codeql: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout code 74 | uses: actions/checkout@v4 75 | - name: Initialize CodeQL 76 | uses: github/codeql-action/init@v2 77 | with: 78 | languages: go 79 | - name: Autobuild 80 | uses: github/codeql-action/autobuild@v2 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v2 83 | 84 | sonarcloud: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v4 88 | with: 89 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 90 | - name: Install Go 91 | uses: actions/setup-go@v4 92 | with: 93 | go-version-file: "go.mod" 94 | cache: true 95 | - name: Coverage Test 96 | run: make -s test 97 | env: 98 | API_TOKEN: ${{ secrets.API_TOKEN }} 99 | - name: SonarCloud Scan 100 | uses: SonarSource/sonarcloud-github-action@master 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 104 | API_TOKEN: ${{ secrets.API_TOKEN }} 105 | with: 106 | args: > 107 | -Dproject.settings=build/ci/sonar-project.properties 108 | - name: SonarQube Quality Gate check 109 | uses: sonarsource/sonarqube-quality-gate-action@master 110 | timeout-minutes: 5 111 | env: 112 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 113 | 114 | builds: 115 | strategy: 116 | matrix: 117 | os: [ubuntu-latest, macos-latest] 118 | runs-on: ${{ matrix.os }} 119 | name: build (${{ matrix.os }}) 120 | steps: 121 | - name: Checkout code 122 | uses: actions/checkout@v4 123 | - name: Install Go 124 | uses: actions/setup-go@v4 125 | with: 126 | go-version-file: "go.mod" 127 | cache: true 128 | - name: Build on (${{ matrix.os }}) 129 | run: make -s build 130 | env: 131 | API_TOKEN: ${{ secrets.API_TOKEN }} 132 | 133 | tag: 134 | runs-on: ubuntu-latest 135 | needs: [lint, fmt, tests, codeql, sonarcloud, staticcheck, builds] 136 | steps: 137 | - name: Checkout 138 | uses: actions/checkout@v4 139 | with: 140 | fetch-depth: '0' 141 | - name: Bump version and push tag 142 | uses: anothrNick/github-tag-action@1.67.0 143 | env: 144 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 145 | WITH_V: true 146 | DEFAULT_BUMP: patch 147 | 148 | release: 149 | runs-on: ubuntu-latest 150 | needs: [tag] 151 | steps: 152 | - name: Checkout 153 | uses: actions/checkout@v4 154 | with: 155 | fetch-depth: 0 156 | - name: Set outputs 157 | id: vars 158 | run: | 159 | echo "::set-output name=latest_tag::$(git describe --tags $(git rev-list --tags --max-count=1))" 160 | echo "::set-output name=build_time::$(date -u +'%m-%d-%YT%H:%M:%SZ')" 161 | echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 162 | - name: Set up QEMU 163 | uses: docker/setup-qemu-action@v3 164 | - name: Docker Login 165 | uses: docker/login-action@v3 166 | with: 167 | registry: docker.io 168 | username: ${{ github.repository_owner }} 169 | password: ${{ secrets.DOCKER_PASSWORD }} 170 | - name: Install Go 171 | uses: actions/setup-go@v4 172 | with: 173 | go-version-file: "go.mod" 174 | cache: true 175 | - name: Run GoReleaser 176 | uses: goreleaser/goreleaser-action@v5 177 | with: 178 | version: latest 179 | args: release --clean --config build/package/.goreleaser.yaml 180 | env: 181 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 182 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 183 | API_TOKEN: ${{ secrets.API_TOKEN }} 184 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 3 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 9 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 10 | github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 11 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 12 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 13 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dimiro1/banner v1.1.0 h1:TSfy+FsPIIGLzaMPOt52KrEed/omwFO1P15VA8PMUh0= 19 | github.com/dimiro1/banner v1.1.0/go.mod h1:tbL318TJiUaHxOUNN+jnlvFSgsh/RX7iJaQrGgOiTco= 20 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 21 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 22 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 25 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 31 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 32 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 33 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 34 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 35 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 36 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 37 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 38 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 39 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 40 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 41 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 42 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 43 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 47 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 48 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 49 | github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= 50 | github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= 51 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 52 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 53 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 54 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 55 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 56 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 57 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 58 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 67 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 70 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | -------------------------------------------------------------------------------- /internal/generator/generator_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package generator 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/joshsagredo/oreilly-trial/internal/mail" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestRunGenerator(t *testing.T) { 18 | err := RunGenerator() 19 | assert.Nil(t, err) 20 | } 21 | 22 | func TestRunGeneratorDomainError(t *testing.T) { 23 | apiURLOrig := mail.ApiURL //nolint:typecheck 24 | mail.ApiURL = "https://dropmail.p.rapidapi.co/" 25 | 26 | err := RunGenerator() 27 | assert.NotNil(t, err) 28 | 29 | mail.ApiURL = apiURLOrig 30 | } 31 | 32 | func TestRunGeneratorGenerateTempMailError(t *testing.T) { 33 | apiURLOrig := mail.ApiURL //nolint:typecheck 34 | response := "{\"data\":{\"domains\":[{\"name\":\"10mail.org\",\"introducedAt\":\"2013-11-13T11:00:00.000+00:00\"," + 35 | "\"id\":\"RG9tYWluOjI\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"10mail.tk\"" + 36 | ",\"introducedAt\":\"2021-01-12T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjE2\",\"availableVia\":[\"APP\",\"API\"" + 37 | ",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"dropmail.me\",\"introducedAt\":\"2013-05-10T10:00:00.000+00:00\"" + 38 | ",\"id\":\"RG9tYWluOjE\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"emlhub.com\"" + 39 | ",\"introducedAt\":\"2017-05-14T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjg\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 40 | ",\"VIBER\",\"WEB\"]},{\"name\":\"emlpro.com\",\"introducedAt\":\"2017-05-14T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjc\"" + 41 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"emltmp.com\",\"introducedAt\"" + 42 | ":\"2016-05-20T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjY\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\"" + 43 | ",\"WEB\"]},{\"name\":\"firste.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjEy\"" + 44 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"flymail.tk\",\"introducedAt\":\"2021-09-16T21:24:30.019+00:00\"" + 45 | ",\"id\":\"RG9tYWluOjE4\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"freeml.net\"" + 46 | ",\"introducedAt\":\"2021-01-12T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjE1\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 47 | ",\"VIBER\",\"WEB\"]},{\"name\":\"laste.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjEz\"" + 48 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"mailpwr.com\",\"introducedAt\":\"2021-10-06T22:18:11.552+00:00\"" + 49 | ",\"id\":\"RG9tYWluOjIw\",\"availableVia\":[\"APP\",\"API\"]},{\"name\":\"mimimail.me\",\"introducedAt\":\"2022-01-11T01:52:41.239+00:00\"" + 50 | ",\"id\":\"RG9tYWluOjIx\",\"availableVia\":[\"APP\",\"API\"]},{\"name\":\"minimail.gq\",\"introducedAt\":\"2021-09-16T21:22:16.896+00:00\"" + 51 | ",\"id\":\"RG9tYWluOjE3\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"spymail.one\"" + 52 | ",\"introducedAt\":\"2021-10-05T11:05:59.918+00:00\",\"id\":\"RG9tYWluOjE5\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 53 | ",\"VIBER\",\"WEB\"]},{\"name\":\"yomail.info\",\"introducedAt\":\"2014-10-30T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjQ\"" + 54 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"zeroe.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\"" + 55 | ",\"id\":\"RG9tYWluOjEx\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]}]}}" 56 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | w.WriteHeader(http.StatusOK) 58 | if _, err := fmt.Fprint(w, response); err != nil { 59 | t.Fatalf("a fatal error occured while writing response body: %s", err.Error()) 60 | } 61 | })) 62 | 63 | defer func() { 64 | server.Close() 65 | }() 66 | 67 | mail.ApiURL = server.URL 68 | err := RunGenerator() 69 | assert.NotNil(t, err) 70 | 71 | mail.ApiURL = apiURLOrig 72 | } 73 | 74 | func TestRunGeneratorTrialAccountCreateError(t *testing.T) { 75 | apiURLOrig := mail.ApiURL //nolint:typecheck 76 | 77 | rr := newResponseWriter() 78 | server := httptest.NewServer(handlerResponse(rr)) 79 | defer func() { 80 | server.Close() 81 | }() 82 | 83 | mail.ApiURL = server.URL 84 | err := RunGenerator() 85 | assert.NotNil(t, err) 86 | 87 | mail.ApiURL = apiURLOrig 88 | } 89 | 90 | type responseWriter struct { 91 | resp map[int]string 92 | count int 93 | lock *sync.Mutex 94 | } 95 | 96 | func newResponseWriter() *responseWriter { 97 | r := new(responseWriter) 98 | r.lock = new(sync.Mutex) 99 | r.resp = map[int]string{ 100 | 0: "{\"data\":{\"introduceSession\":{\"id\":\"U2Vzc2lvbjqlmFu26iZORqMlWdOy3DCC\",\"expiresAt\":\"2022-11-12T13:19:17+00:00\",\"addresses\":[{\"address\":\"asdasfasfas@mailpwr.com\"}]}}}", 101 | 1: "{\"data\":{\"domains\":[{\"name\":\"10mail.org\",\"introducedAt\":\"2013-11-13T11:00:00.000+00:00\"," + 102 | "\"id\":\"RG9tYWluOjI\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"10mail.tk\"" + 103 | ",\"introducedAt\":\"2021-01-12T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjE2\",\"availableVia\":[\"APP\",\"API\"" + 104 | ",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"dropmail.me\",\"introducedAt\":\"2013-05-10T10:00:00.000+00:00\"" + 105 | ",\"id\":\"RG9tYWluOjE\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"emlhub.com\"" + 106 | ",\"introducedAt\":\"2017-05-14T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjg\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 107 | ",\"VIBER\",\"WEB\"]},{\"name\":\"emlpro.com\",\"introducedAt\":\"2017-05-14T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjc\"" + 108 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"emltmp.com\",\"introducedAt\"" + 109 | ":\"2016-05-20T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjY\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\"" + 110 | ",\"WEB\"]},{\"name\":\"firste.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjEy\"" + 111 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"flymail.tk\",\"introducedAt\":\"2021-09-16T21:24:30.019+00:00\"" + 112 | ",\"id\":\"RG9tYWluOjE4\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"freeml.net\"" + 113 | ",\"introducedAt\":\"2021-01-12T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjE1\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 114 | ",\"VIBER\",\"WEB\"]},{\"name\":\"laste.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\",\"id\":\"RG9tYWluOjEz\"" + 115 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"mailpwr.com\",\"introducedAt\":\"2021-10-06T22:18:11.552+00:00\"" + 116 | ",\"id\":\"RG9tYWluOjIw\",\"availableVia\":[\"APP\",\"API\"]},{\"name\":\"mimimail.me\",\"introducedAt\":\"2022-01-11T01:52:41.239+00:00\"" + 117 | ",\"id\":\"RG9tYWluOjIx\",\"availableVia\":[\"APP\",\"API\"]},{\"name\":\"minimail.gq\",\"introducedAt\":\"2021-09-16T21:22:16.896+00:00\"" + 118 | ",\"id\":\"RG9tYWluOjE3\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"spymail.one\"" + 119 | ",\"introducedAt\":\"2021-10-05T11:05:59.918+00:00\",\"id\":\"RG9tYWluOjE5\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\"" + 120 | ",\"VIBER\",\"WEB\"]},{\"name\":\"yomail.info\",\"introducedAt\":\"2014-10-30T11:00:00.000+00:00\",\"id\":\"RG9tYWluOjQ\"" + 121 | ",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]},{\"name\":\"zeroe.ml\",\"introducedAt\":\"2019-10-02T10:00:00.000+00:00\"" + 122 | ",\"id\":\"RG9tYWluOjEx\",\"availableVia\":[\"APP\",\"API\",\"TELEGRAM\",\"VIBER\",\"WEB\"]}]}}", 123 | 2: "{\"data\":{\"introduceSession\":{\"id\":\"U2Vzc2lvbjqlmFu26iZORqMlWdOy3DCC\",\"expiresAt\":\"2022-11-12T13:19:17+00:00\",\"addresses\":[{\"address\":\"asdasfas@mailpwr.com\"}]}}}", 124 | } 125 | r.count = 0 126 | return r 127 | } 128 | 129 | func (r *responseWriter) getResp() string { 130 | r.lock.Lock() 131 | defer r.lock.Unlock() 132 | r.count++ 133 | return r.resp[r.count%3] 134 | } 135 | 136 | func handlerResponse(rr *responseWriter) http.Handler { 137 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | w.WriteHeader(http.StatusOK) 139 | _, _ = w.Write([]byte(rr.getResp())) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------